import Package from "../package.json";
/**
* @class HillClimbing
* @classdesc A entry point for the Hill Climbing algorithm.
*
* @example
* const targets = [
* {
* name: "myValue1", // The name of the value
* value: 50, // The initial value
* min: 0, // The minimum value that the value can be
* max: 100, // The maximum value that the value can be
* precision: 0, // The number of decimal places to round to
* },
* { name: "myValue2", value: -2, min: -100, max: 10 },
* { name: "myValue3", value: 764, min: 100, max: 1250, precision: 10 },
* ];
*
* const options = {startScore: -100, numberOfMutations:2};
*
* const myHillClimbing = new HillClimbing(targets); // Create a new instance and pass the initial data (targets)
*
* @param {Object[]} targets - a list of targets
* @param {Object} options - options for the algorithm
* @param {number} [options.startScore=-Infinity] - the starting score for the algorithm
* @param {number} [options.numberOfMutations=1] - number of mutations per run (not targets)
* @constructor
* @see {@link https://en.wikipedia.org/wiki/Hill_climbing}
*/
class HillClimbing {
constructor(targets, options = { startScore: -Infinity, numberOfMutations: 1 }) {
if (targets === undefined) throw new Error("You must pass a list of targets");
else if (targets.length === 0) throw new Error("You must pass at least one target");
targets.forEach(target => {
if (typeof target !== "object") throw new Error("You must pass a list of targets");
else if (target.name === undefined || typeof target.name !== "string") throw new Error("You must pass a name (String) for each target");
else if (target.value === undefined || typeof target.value !== "number") throw new Error("You must pass a value (Number) for each target");
else if (target.min === undefined || typeof target.min !== "number") throw new Error("You must pass a minimum (Number) value for each target");
else if (target.max === undefined || typeof target.max !== "number") throw new Error("You must pass a maximum (Number) value for each target");
});
if (typeof options !== "object") throw new Error("You must pass an options object");
this.targets = targets.map(target => ({ ...target }));
this.bestSolution = targets.map(target => ({ ...target }));
this.currentSolution = targets.map(target => ({ ...target }));
this.lastTargetsChanged = [];
this.lastScore = options.startScore;
this.numberOfIterations = 0;
this.bestScore = options.startScore;
this.numberOfMutations = options.numberOfMutations;
this.iterationsData = [{
iteration: this.numberOfIterations,
score: options.startScore,
changedTargets: this.lastTargetsChanged,
solution: this.currentSolution,
}];
this._startScore = options.startScore;
}
/**
* @description
* Add a new target to the list of targets
*
* @example
* myHillClimbing.addTarget({ name: "myNewValue", value: 50, min: 0, max: 100 });
*
* @param {Object} target - A new target to calculate in the new solution
* @returns {void}
* @memberof HillClimbing
*/
addTarget(target) {
if (target === undefined) throw new Error("You must pass a target");
else if (typeof target !== "object") throw new Error("You must pass a target");
else if (target.name === undefined || typeof target.name !== "string") throw new Error("You must pass a name with the type String");
else if (target.value === undefined || typeof target.value !== "number") throw new Error("You must pass a value with the type Number");
else if (target.min === undefined || typeof target.min !== "number") throw new Error("You must pass a minimum with the type Number");
else if (target.max === undefined || typeof target.max !== "number") throw new Error("You must pass a maximum with the type Number");
this.targets.push(target);
this.bestSolution.push(target);
this.currentSolution.push(target);
}
/**
* @description
* Change all targets to the new targets
*
* @example
* myHillClimbing.setAllTargets([
* { name: "myNewValue1", value: 0, min: 0, max: 5 },
* { name: "myNewValue2", value: -100, min: -500, max: -100 },
* { name: "myNewValue3", value: 0, min: 0, max: 1 },
* ]);
*
* @param {Object[]} targets - A list of new targets
* @returns {void}
* @memberof HillClimbing
*/
setAllTargets(targets) {
if (targets === undefined) throw new Error("You must pass a list of targets");
else if (targets.length === 0) throw new Error("You must pass at least one target");
targets.forEach(target => {
if (typeof target !== "object") throw new Error("You must pass a list of targets");
else if (target.name === undefined || typeof target.name !== "string") throw new Error("You must pass a name (String) for each target");
else if (target.value === undefined || typeof target.value !== "number") throw new Error("You must pass a value (Number) for each target");
else if (target.min === undefined || typeof target.min !== "number") throw new Error("You must pass a minimum (Number) value for each target");
else if (target.max === undefined || typeof target.max !== "number") throw new Error("You must pass a maximum (Number) value for each target");
});
this.targets = targets;
this.bestSolution = targets;
this.currentSolution = targets;
}
/**
* @description
* Remove a target from the list of targets
*
* @example
* myHillClimbing.removeTarget("myValue1");
*
* @param {String} name - The name of the target to remove
* @returns {void}
* @memberof HillClimbing
*/
removeTarget(name) {
if (name === undefined) throw new Error("You must pass a name");
else if (typeof name !== "string") throw new Error("You must pass a name with the type String");
const index = this.targets.findIndex(target => target.name === name);
if (index > -1) {
this.targets.splice(index, 1);
this.bestSolution.splice(index, 1);
this.currentSolution.splice(index, 1);
}
}
/**
* @description
* Change a target property (name, min, max, precision)
*
* @example myHillClimbing.setTargetProperty("myTargetName", "name", "myNewName");
* @example myHillClimbing.setTargetProperty("myNewName", "min", -100);
*
* @param {string} targetName - The name of the target to change
* @param {string} property - The property to change
* @param {string|number} value - The new value
* @returns {void}
* @memberof HillClimbing
*/
setTargetProperty(targetName, property, value) {
if (targetName === undefined) throw new Error("You must pass a target name");
else if (property === undefined) throw new Error("You must pass a property name to change");
else if (value === undefined) throw new Error("You must pass a value to change");
const targetIndex = this.targets.findIndex(target => target.name === targetName);
if (targetIndex < 0) throw new Error(`Target ${targetName} not found`);
if (targetIndex > -1) {
this.targets[targetIndex][property] = value;
this.bestSolution[targetIndex][property] = value;
this.currentSolution[targetIndex][property] = value;
}
}
/**
* @description
* Change a target name
*
* @example
* myHillClimbing.setTargetName("myTargetName", "myNewName");
*
* @param {string} oldName - The name of the target to change
* @param {string} newName - The new name of the target
* @returns {void}
* @memberof HillClimbing
*/
setTargetName(oldName, newName) { this.setTargetProperty(oldName, "name", newName); }
/**
* @description
* Change a target minimum value
*
* @example
* myHillClimbing.setTargetMin("myTargetName", -100);
*
* @param {string} targetName - The name of the target to change
* @param {number} min - The new minimum value
* @returns {void}
* @memberof HillClimbing
*/
setTargetMin(targetName, min) { this.setTargetProperty(targetName, "min", min); }
/**
* @description
* Change a target maximum value
*
* @example
* myHillClimbing.setTargetMax("myTargetName", 100);
*
* @param {string} targetName - The name of the target to change
* @param {number} max - The new maximum value
* @returns {void}
* @memberof HillClimbing
*/
setTargetMax(targetName, max) { this.setTargetProperty(targetName, "max", max); }
/**
* @description
* Change a target precision
*
* @example
* myHillClimbing.setTargetPrecision("myTargetName", 5);
*
* @param {string} targetName - The name of the target to change
* @param {number} precision - The new precision value
* @returns {void}
* @memberof HillClimbing
*/
setTargetPrecision(targetName, precision) { this.setTargetProperty(targetName, "precision", precision); }
/**
* @description
* Returns the number of iterations that have been run
*
* @example
* console.log(myHillClimbing.getNumberOfIterations());
*
* @returns {Number} The number of iterations that have been run
* @memberof HillClimbing
*/
getNumberOfIterations() { return this.numberOfIterations; }
/**
* @description
* Get the best score that has been found so far
*
* @example
* console.log(myHillClimbing.getBestScore());
*
* @returns {Number} The best score that has been found so far
* @memberof HillClimbing
*/
getBestScore() { return this.bestScore; }
/**
* @description
* Returns all targets from the list
*
* @example
* console.log(myHillClimbing.getTargets());
*
* @returns {Object[]} The list of targets
* @memberof HillClimbing
*/
getTargets() { return this.targets.map(target => ({ ...target })); }
/**
* @description
* Returns the current best solution. The best solution is the solution that has the highest score
*
* @example
* console.log(myHillClimbing.getBestSolution());
*
* @returns {Object[]} The best solution
* @memberof HillClimbing
*/
getBestSolution() { return this.bestSolution.map(target => ({ ...target })); }
/**
* @description
* Returns a array with the values of the targets with the best solution
*
* @example
* console.log(myHillClimbing.getBestSolutionValues());
*
* @returns {number[]} The values of the targets with the best solution
* @memberof HillClimbing
*/
getBestSolutionValues() { return this.bestSolution.map(target => target.value); }
/**
* @description
* Returns the current solution
*
* @example
* console.log(myHillClimbing.getCurrentSolution());
*
* @returns {Object[]} The current solution
* @memberof HillClimbing
*/
getCurrentSolution() { return this.currentSolution.map(target => ({ ...target })); }
/**
* @description
* Returns a array with the values of the targets with the current solution
*
* @example
* console.log(myHillClimbing.getCurrentSolutionValues());
*
* @returns {number[]} The values of the targets with the current solution
* @memberof HillClimbing
*/
getCurrentSolutionValues() { return this.currentSolution.map(target => target.value); }
/**
* @description
* Returns the current solution value of the given target name
*
* @example
* console.log(myHillClimbing.getCurrentSolutionValue("myValue1"));
*
* @param {String} name - The name of the target
* @returns {Number} The current solution value of the given target name
* @memberof HillClimbing
*/
getCurrentTargetValueSolutionByName(name) {
if (name === undefined) throw new Error("You must pass a target name");
else if (typeof name !== "string") throw new Error("The target name must be a string");
return this.currentSolution.find(target => target.name === name).value;
}
/**
* @description
* Returns the best solution value of the given target name
*
* @example
* console.log(myHillClimbing.getBestSolutionValue("myValue1"));
*
* @param {String} name - The name of the target
* @returns {Number} The best solution value of the given target name
* @memberof HillClimbing
*/
getBestTargetValueSolutionByName(name) {
if (name === undefined) throw new Error("You must pass a target name");
else if (typeof name !== "string") throw new Error("The target name must be a string");
return this.bestSolution.find(target => target.name === name).value;
}
/**
* @description
* Returns the last target that has been changed
*
* @example
* console.log(myHillClimbing.getLastTargetsChanged()); // [Target]
*
* @returns {Object[]} The last target that has been changed
* @memberof HillClimbing
*/
getLastTargetsChanged() { return this.lastTargetsChanged.map(target => ({ ...target })); }
/**
* @description
* This function its the main function of the algorithm.
* This function will calculate a new solution based on the best solution and will return the new solution.
*
* Based in the given score, the algorithm will change randomly the value of a random target.
*
* @example
* const myNewScore = 10;
* myHillClimbing.run(myNewScore);
*
* @param {Number} score - The score that will be used to calculate the new solution
* @returns {Object[]} The new current solution
* @memberof HillClimbing
*/
run(score = -Infinity) {
this.numberOfIterations++;
this.lastTargetsChanged = [];
this.lastScore = score;
// If the current solution is better than the bestSolution, update the best solution
if (score > this.bestScore) {
this.bestScore = score;
this.bestSolution = this.currentSolution.map(target => ({ ...target }));
} else this.currentSolution = this.bestSolution.map(target => ({ ...target }));
for (let i = 0; i < this.numberOfMutations; i++) {
// Get a new random target to change
let targetIndex = this.randomNumber(0, this.bestSolution.length - 1);
const target = { ...this.bestSolution[targetIndex] };
// Change the value of the target
this.lastTargetsChanged.push(target);
this.currentSolution[targetIndex].value = this.randomNumber(target.min, target.max, target.precision);
}
const newSolution = this.currentSolution.map(target => ({ ...target }));
this.iterationsData.push({
iteration: this.numberOfIterations,
score: score,
changedTarget: this.lastTargetsChanged,
solution: newSolution,
});
return newSolution;
}
/**
* @description
* Returns the data of all iterations
*
* @example
* console.log(myHillClimbing.exportData());
*
* @param {boolean} json - true if the exported data should be in JSON format
* @returns {Object[]|string} The data of all iterations
* @memberof HillClimbing
*/
exportData(json = false) {
if (json) return JSON.stringify(this.iterationsData);
return this.iterationsData;
}
/**
* @description
* Resets the algorithm
*
* @example
* myHillClimbing.reset();
*
* @returns {void}
* @memberof HillClimbing
*/
reset() {
this.numberOfIterations = 0;
this.bestSolution = this.targets.map(target => ({ ...target }));
this.currentSolution = this.targets.map(target => ({ ...target }));
this.lastTargetsChanged = [];
this.bestScore = this._startScore;
this.lastScore = this._startScore;
this.iterationsData = [];
}
/**
* @description
* Get a random number between two numbers
*
* @example
* myHillClimbing.randomNumber(0, 100);
*
* @param {Number} min - The minimum number
* @param {Number} max - The maximum number
* @returns {Number} The random number
* @memberof HillClimbing
*/
randomNumber(min = 0, max = 1, precision = 0) {
if (typeof min !== "number") throw new Error("The minimum number must be a number");
else if (typeof max !== "number") throw new Error("The maximum number must be a number");
else if (typeof precision !== "number") throw new Error("The precision number must be a number");
else if (min > max) throw new Error("The minimum number must be less than the maximum number");
return parseFloat((Math.random() * (max - min) + min).toFixed(precision))
}
/**
* @description
* Returns the current version of the library
*
* @example
* console.log(HillClimbing.getVersion()); // "0.0.2"
*
* @returns {String}
* @memberof HillClimbing
* @static
*/
static getVersion() { return Package.version; }
}
export default HillClimbing;