consys-solver
Version:
consys-solver is a tool to find feasible model assignments for consys constraint systems.
475 lines (474 loc) • 16.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Domain_1 = __importDefault(require("./Domain"));
const RandomUtils_1 = __importDefault(require("./ignoreCoverage/RandomUtils"));
/**
* Solver class, used to find solutions for a specific set of constraints and
* domains.
*/
class Solver {
/**
* Create a new solver instance for a given constraint system and config.
*
* @param system constraint system
* @param config configuration
*/
constructor(system, config) {
// default configuration
this.config = {
maxIterations: 10000,
retryIterations: 2000,
lookAheadModels: -1,
randomnessFactor: 0.3,
preferenceFactor: 0.1,
};
this.system = system;
if (!!config) {
this.initConfig(config);
}
}
/**
* Initializes parameters based on the given config.
*
* @param config configuration
* @private
*/
initConfig(config) {
if (config.maxIterations !== undefined) {
this.config.maxIterations = Math.max(0, config.maxIterations);
}
if (config.retryIterations !== undefined) {
this.config.retryIterations = Math.max(0, config.retryIterations);
}
if (config.lookAheadModels !== undefined) {
this.config.lookAheadModels = Math.max(0, config.lookAheadModels);
}
else {
this.config.lookAheadModels = -1;
}
if (config.randomnessFactor !== undefined) {
this.config.randomnessFactor = Math.max(0, Math.min(config.randomnessFactor, 1));
}
if (config.preferenceFactor !== undefined) {
this.config.preferenceFactor = Math.max(0, Math.min(config.preferenceFactor, 1));
}
}
/**
* Flatten a model domain object to have keys as strings, seperated by a dot
* if the key is nested.
*
* @param modelDomain initial domain object
* @param parent parent key
* @param res result map
* @private
*/
static flattenModelDomain(modelDomain, parent, res = {}) {
for (let key in modelDomain) {
let propertyName = parent ? parent + '.' + key : key;
if (modelDomain[key]['kind'] !== 'Domain') {
Solver.flattenModelDomain(modelDomain[key], propertyName, res);
}
else {
res[propertyName] = modelDomain[key];
}
}
return res;
}
/**
* Creates a model domains object with domain values and current search index.
*
* @param modelDomain model domain
* @private
*/
static getModelDomains(modelDomain) {
let flattened = Solver.flattenModelDomain(modelDomain);
let res = {};
Object.keys(flattened).forEach(key => {
res[key] = {
index: 0,
values: flattened[key].getValues(),
preference: (element) => {
return flattened[key].getPreferenceValue(element);
},
};
});
return res;
}
/**
* Inserts a value into an object. Key is a dot separated string, which will
* result in a nested value.
*
* @param object object where value should be inserted
* @param key key of the value
* @param value value to be inserted
* @private
*/
static insertValue(object, key, value) {
let keys = key.split('.');
let obj = object;
for (let i = 0; i < keys.length - 1; i++) {
let currentKey = keys[i];
if (!obj[currentKey]) {
obj[currentKey] = {};
}
obj = obj[currentKey];
}
let lastKey = keys[keys.length - 1];
obj[lastKey] = value;
}
/**
* Randomizes the current search index of each domain.
*
* @param modelDomains model domains
* @private
*/
static randomizeModel(modelDomains) {
for (let key of Object.keys(modelDomains)) {
let domain = modelDomains[key];
domain.index = Math.floor(RandomUtils_1.default.unsignedFloat() * domain.values.length);
}
}
/**
* Creates a new model object from the values of the current search indices.
*
* @param modelDomains model domains
* @private
*/
getCurrentModel(modelDomains) {
let res = {};
for (let key of Object.keys(modelDomains)) {
let domain = modelDomains[key];
Solver.insertValue(res, key, domain.values[domain.index]);
}
return res;
}
/**
* Calculates a logarithmic score between 0 (bad) and 1 (perfect) for a given
* model and state.
*
* @param model model instance
* @param state state instance
* @private
*/
getLogScore(model, state) {
return (1.0 / (1.0 + this.system.getNumInconsistentConstraints(model, state)));
}
/**
* Returns a normalized preference score from 0 to 1
*
* @param preference preference of the domain value
* @private
*/
getPreferenceScore(preference) {
return (1 -
this.config.preferenceFactor +
(preference / Domain_1.default.maxPreference) * this.config.preferenceFactor);
}
/**
* Calculates the harmonic mean of the log score and preference score. This
* penalizes low values for the log score, as well as low values for the
* preference score. Only if both values are high, the harmonic mean will be
* high as well.
*
* @param logScore log score
* @param prefScore preference score
* @private
*/
static getHarmonicMean(logScore, prefScore) {
return (2 * logScore * prefScore) / (logScore + prefScore);
}
/**
* Calculates the total score of a given model and state.
*
* @param instance model with preference value
* @param state state
* @private
*/
getScore(instance, state) {
let logScore = this.getLogScore(instance.model, state);
let prefScore = this.getPreferenceScore(instance.preference);
return Solver.getHarmonicMean(logScore, prefScore);
}
/**
* Checks if a model is consistent with a given state.
*
* @param model model to be checked
* @param state state to be checked
* @private
*/
isModelFeasible(model, state) {
return this.system.getNumInconsistentConstraints(model, state) === 0;
}
/**
* Shuffles an array of values.
*
* @param array array to be shuffled
* @private
*/
static shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(RandomUtils_1.default.unsignedFloat() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
/**
* Randomly chooses a key based on their counts as weights.
*
* @param domains key domains
* @param keyCounts key counts
* @private
*/
static chooseKey(domains, keyCounts) {
// apply laplace smoothing, since counts can be 0
const laplaceAlpha = 0.1;
let keys = Object.keys(domains);
Solver.shuffle(keys);
let totalCount = laplaceAlpha * keys.length;
for (let key of keys) {
totalCount += !!keyCounts[key] ? keyCounts[key] : 0;
}
// random value between 1 and total amount of keys
let target = RandomUtils_1.default.unsignedFloat() * (totalCount - 1) + 1;
for (let key of keys) {
let count = keyCounts[key] + laplaceAlpha;
if (target <= count) {
return key;
}
target -= count;
}
// in theory, this can not happen
return Solver.chooseRandom(keys);
}
/**
* Returns an array of values to be considered as the next value for a model
* domain.
*
* @param domains model domains
* @param key key to be searched for values
* @private
*/
getNextValuesForKey(domains, key) {
let currentIndex = domains[key].index;
let currentValues = domains[key].values;
let start = Math.max(0, currentIndex - Math.floor(this.config.lookAheadModels / 2));
let end = Math.min(currentValues.length, currentIndex + Math.floor(this.config.lookAheadModels / 2));
return currentValues.slice(start, end);
}
/**
* Checks if two models have equal keys and values.
*
* @param model0 first model
* @param model1 second model
* @private
*/
static modelsEqual(model0, model1) {
for (let key of Object.keys(model0)) {
if (typeof model0[key] == 'object') {
if (!Solver.modelsEqual(model0[key], model1[key])) {
return false;
}
}
else if (model0[key] !== model1[key]) {
return false;
}
}
return true;
}
/**
* Checks if a model is already in an array of solutions.
*
* @param solutions solutions
* @param model model
* @private
*/
isModelInSolutions(solutions, model) {
for (let solution of solutions) {
if (Solver.modelsEqual(solution, model)) {
return true;
}
}
return false;
}
/**
* Returns an array of models to be considered as the next best model, along
* with their preference value.
*
* @param solutions current solutions
* @param domains model domains
* @param key key to be changed for the next models
* @param nextValues array of possible next values for the key
* @private
*/
getNextModels(solutions, domains, key, nextValues) {
let res = [];
for (let nextValue of nextValues) {
let model = this.getCurrentModel(domains);
Solver.insertValue(model, key, nextValue);
if (!this.isModelInSolutions(solutions, model)) {
let preference = domains[key].preference(nextValue);
res.push({
preference: preference,
model: model,
});
}
}
return res;
}
/**
* Returns the next best model given a current model, state and solutions.
*
* @param solutions current solutions
* @param domains model domains
* @param currentModel current model
* @param state state
* @private
*/
getNextBestModel(solutions, domains, currentModel, state) {
// for each variable value the amount of inconsistent constraints it is in
let statisticsReport = this.system.evaluateStatistics(currentModel, state);
let keyInfluences = statisticsReport.inconsistent.model;
// choose next variable to generate values for
let nextKey = Solver.chooseKey(domains, keyInfluences);
// generate the next values for the variable
let nextValuesForKey = this.getNextValuesForKey(domains, nextKey);
// from the next values, generate new models
let nextModels = this.getNextModels(solutions, domains, nextKey, nextValuesForKey);
// from the new models, get the one with the minimum conflicts
let bestModel = this.minConflicts(nextModels, state);
if (!bestModel) {
return null;
}
domains[nextKey].index = nextModels.indexOf(bestModel);
return bestModel.model;
}
/**
* Chooses a random element from array.
*
* @param values array
* @private
*/
static chooseRandom(values) {
return values[Math.floor(RandomUtils_1.default.unsignedFloat() * values.length)];
}
/**
* Returns the model with the minimum amount of conflicts (heuristic) and
* factors in the preference value.
*
* @param models models to be considered
* @param state state
* @private
*/
minConflicts(models, state) {
// To avoid local maximums and plateaus, choose completely random sometimes
if (RandomUtils_1.default.unsignedFloat() < this.config.randomnessFactor) {
return Solver.chooseRandom(models);
}
let bestModel = null;
let bestScore = 0;
// choose the model with the best score
for (let instance of models) {
// calculate score based on number of conflicts
let score = this.getScore(instance, state);
if (score > bestScore) {
bestScore = score;
bestModel = instance;
}
}
return bestModel;
}
/**
* Returns the maximum look ahead value based on the size of the largest
* domain.
*
* @param domains model domains
* @private
*/
static getMaxLookAhead(domains) {
let maxLength = 0;
for (let key of Object.keys(domains)) {
let values = domains[key].values;
if (values.length > maxLength) {
maxLength = values.length;
}
}
return maxLength * 2;
}
static initializePreferredDomains(domains) {
for (let key of Object.keys(domains)) {
let domain = domains[key];
let bestIndex = 0;
let bestPreference = Number.MIN_VALUE;
for (let i = 0; i < domain.values.length; i++) {
const value = domain.values[i];
const preference = domain.preference(value);
if (preference > bestPreference) {
bestPreference = preference;
bestIndex = i;
}
}
domain.index = bestIndex;
}
}
/**
* Searches for solutions with a given configuration. Returns an array of
* solutions as well as the number of iterations it took to find them.
*
* @param maxSolutions maximum number of solutions to be found before stopping
* @param modelDomain model domain to be searched
* @param state state
* @param config solver configuration
*/
solve(maxSolutions, modelDomain, state, config) {
if (!!config) {
this.initConfig(config);
}
let domains = Solver.getModelDomains(modelDomain);
// nothing configured, so use the largest domain size as default
if (this.config.lookAheadModels < 0) {
this.config.lookAheadModels = Solver.getMaxLookAhead(domains);
}
let max = Math.max(1, maxSolutions);
let res = [];
// start with the preferred model
Solver.initializePreferredDomains(domains);
let currentModel = this.getCurrentModel(domains);
let iterations = 1;
for (let i = 0; i < this.config.maxIterations && res.length < max; i++) {
if (iterations % this.config.retryIterations === 0) {
Solver.randomizeModel(domains);
}
if (!!currentModel) {
if (this.isModelFeasible(currentModel, state)) {
res.push(currentModel);
// when we found a solution, start again with preferred values
Solver.initializePreferredDomains(domains);
currentModel = this.getCurrentModel(domains);
}
else {
currentModel = this.getNextBestModel(res, domains, currentModel, state);
}
}
iterations++;
}
return {
iterations: iterations,
solutions: res,
};
}
/**
* Searches for solutions with a given configuration. Returns an array of
* solutions.
*
* @param maxSolutions maximum number of solutions to be found before stopping
* @param modelDomain model domain to be searched
* @param state state
* @param config solver configuration
*/
find(maxSolutions, modelDomain, state, config) {
return this.solve(maxSolutions, modelDomain, state, config).solutions;
}
}
exports.default = Solver;