// https://github.com/wangchen/Programming-Game-AI-by-Example-src/blob/master/Common/fuzzy/FuzzyRule.h
// FUZZY SETS
// Definition of the base fuzzy set class.
class FuzzySet {
constructor(peakPoint, leftOffset, rightOffset, representativeValue) {
// Members that define the shape.
this.peakPoint = peakPoint;
this.leftOffset = leftOffset;
this.rightOffset = rightOffset;
// representativeValue - the maximum of the set's membership function.
// For instance, if the set is triangular then this will be the peak
// point of the triangular. If the set has a plateau then this value
// will be the mid point of the plateau. This value is set in creation
// to avoid run-time calculation of mid-point values.
this.representativeValue = representativeValue;
// DOM - holds the degree of membership of a given value in this set.
this.DOM = 0.0;
}
// calculateDOM - calculates and returns the degree of membership for a
// particular value. NOTE, this does not set DOM to the value passed as
// the parameter. This is because the centroid defuzzification method also
// uses this method to determine the degree of memberships of the values
// it uses as its sample points.
calculateDOM(val) {
}
clearDOM() { this.DOM = 0.0; }
getDOM() { return this.DOM; }
// ORwithDOM - If this fuzzy set is part of a consequent variable, and
// it is fired by a rule then this method sets the DOM (in this context,
// the DOM represents a confidence level) to the maximum of the parameter
// value or the set's existing member DOM value.
ORwithDOM(val) {
if (val > this.DOM)
this.DOM = val;
}
// Fuzzify a value by calculating its degree of membership.
fuzzify(val) {
this.DOM = this.calculateDOM(val);
return this;
}
}
// Definition of a fuzzy set that has a left shoulder shape.
class FuzzySet_LeftShoulder extends FuzzySet {
constructor(peakPoint, leftOffset, rightOffset) {
super(peakPoint, leftOffset, rightOffset, peakPoint - leftOffset / 2);
}
calculateDOM(val) {
// Test for the case where the left or right offsets are zero.
// (to prevent divide by zero errors below).
if (isEqual(this.peakPoint, val) &&
(isEqual(this.leftOffset, 0.0) || isEqual(this.rightOffset, 0.0)))
return 1.0;
// Find DOM if right of center.
if ((val >= this.peakPoint) && (val < (this.peakPoint + this.rightOffset)))
return (1.0 / -this.rightOffset) * (val - this.peakPoint) + 1.0;
// Find DOM if left of center.
if ((val < this.peakPoint) && (val >= this.peakPoint - this.leftOffset))
return 1.0;
// Out of range.
return 0.0;
}
}
// Definition of a fuzzy set that has a right shoulder shape.
class FuzzySet_RightShoulder extends FuzzySet {
constructor(peakPoint, leftOffset, rightOffset) {
super(peakPoint, leftOffset, rightOffset, peakPoint + rightOffset / 2);
}
calculateDOM(val) {
// Test for the case where the left or right offsets are zero
// (to prevent divide by zero errors below).
if (isEqual(this.peakPoint, val) &&
(isEqual(this.leftOffset, 0.0) || isEqual(this.rightOffset, 0.0)))
return 1.0;
// Find DOM if left of center.
if ((val <= this.peakPoint) && (val > (this.peakPoint - this.leftOffset)))
return (1.0 / this.leftOffset) * (val - (this.peakPoint - this.leftOffset));
// Find DOM if right of center and less than center + right offset.
if ((val > this.peakPoint) && (val <= this.peakPoint + this.rightOffset))
return 1.0;
// Out of range.
return 0.0;
}
}
// This defines a fuzzy set that is a singleton (a range
// over which the DOM is always 1.0).
class FuzzySet_Singleton extends FuzzySet {
constructor(peakPoint, leftOffset, rightOffset) {
super(peakPoint, leftOffset, rightOffset, peakPoint);
}
calculateDOM(val) {
if ((val >= this.peakPoint - this.leftOffset)
&& (val <= this.peakPoint + this.rightOffset))
return 1.0;
// Out of range.
return 0.0;
}
}
// This is a simple class to define fuzzy sets that have a triangular
// shape and can be defined by a mid point, a left displacement and a
// right displacement.
class FuzzySet_Triangle extends FuzzySet {
constructor(peakPoint, leftOffset, rightOffset) {
super(peakPoint, leftOffset, rightOffset, peakPoint);
}
calculateDOM(val) {
// Test for the case where the left or right offsets are zero
// (to prevent divide by zero errors below).
if (isEqual(this.peakPoint, val) &&
(isEqual(this.leftOffset, 0.0) || isEqual(this.rightOffset, 0.0)))
return 1.0;
// Find DOM if left of center.
if ((val <= this.peakPoint) && (val >= (this.peakPoint - this.leftOffset)))
return (1.0 / this.leftOffset) * (val - (this.peakPoint - this.leftOffset));
// Find DOM if right of center.
if ((val > this.peakPoint) && (val < (this.peakPoint + this.rightOffset)))
return (1.0 / -this.rightOffset) * (val - this.peakPoint) + 1.0;
// Out of range.
return 0.0;
}
}
// FUZZY VARIABLES
class FuzzyVariable {
constructor() {
// A map of the fuzzy sets that comprise this variable.
this.sets = {};
// The minimum and maximum value of the range of this variable.
this.minRange = 0.0;
this.maxRange = 0.0;
}
// This method is called with the upper and lower bound of a set each time a
// new set is added to adjust the upper and lower range values accordingly.
adjustRangeToFit(minBound, maxBound) {
if (minBound < this.minRange)
this.minRange = minBound;
if (maxBound > this.maxRange)
this.maxRange = maxBound;
}
// The following methods create instances of the sets named in the method
// name and add them to the member set map. Each time a set of any type is
// added the minRange and maxRange are adjusted accordingly.
// Adds a left shoulder type set.
addLeftShoulderSet(name, minBound, peak, maxBound) {
this.sets[name] = new FuzzySet_LeftShoulder(peak, peak - minBound, maxBound - peak);
this.adjustRangeToFit(minBound, maxBound);
return this;
}
// Adds a right shoulder type set.
addRightShoulderSet(name, minBound, peak, maxBound) {
this.sets[name] = new FuzzySet_RightShoulder(peak, peak - minBound, maxBound - peak);
this.adjustRangeToFit(minBound, maxBound);
return this;
}
// Adds a triangular shaped fuzzy set to the variable.
addTriangularSet(name, minBound, peak, maxBound) {
this.sets[name] = new FuzzySet_Triangle(peak, peak - minBound, maxBound - peak);
this.adjustRangeToFit(minBound, maxBound);
return this;
}
// Adds a singleton to the variable.
addSingletonSet(name, minBound, peak, maxBound) {
this.sets[name] = new FuzzySet_Singleton(peak, peak - minBound, maxBound - peak);
this.adjustRangeToFit(minBound, maxBound);
return this;
}
// Fuzzify a value by calculating its degree of membership in each of this variable's subsets
// takes a crisp value and calculates its degree of membership for each set
// in the variable.
fuzzify(val) {
// For each set in the flv calculate the degree of membership for the
// given value.
for (let set in this.sets)
this.sets[set].DOM = this.sets[set].calculateDOM(val);
return this;
}
// Method for updating the DOM of a consequent when a rule fires.
ORwithDOM(val) {
for (let name in this.sets)
this.sets[name].ORwithDOM(val);
}
// Defuzzifies the value by averaging the maxima of the sets that have fired.
// Returns sum (maxima * degree of membership) / sum (degree of memberships).
deFuzzifyMaxAv() {
let bottom = 0.0;
let top = 0.0;
for (let set in this.sets) {
bottom += this.sets[set].DOM;
top += this.sets[set].representativeValue * this.sets[set].DOM;
}
// Make sure bottom is not equal to zero.
if (isEqual(0, bottom))
return 0.0;
return top / bottom;
}
// Defuzzify the variable using the centroid method.
deFuzzifyCentroid(numSamples) {
if (numSamples <= 0)
throw new Error("Number of samples must be greater than zero");
// Calculate the step size.
let stepSize = (this.maxRange - this.minRange) / numSamples;
let totalArea = 0.0;
let sumOfMoments = 0.0;
// Step through the range of this variable in increments equal to stepSize
// adding up the contribution (lower of calculateDOM
// or the actual degree of membership of this
// variable's fuzzified value) for each subset. This gives an approximation of
// the total area of the fuzzy manifold.(This is similar to how the area under
// a curve is calculated using calculus... the heights of lots of 'slices' are
// summed to give the total area.)
// In addition the moment of each slice is calculated and summed. Dividing
// the total area by the sum of the moments gives the centroid. (Just like
// calculating the center of mass of an object.)
for (let samp = 1; samp <= numSamples; ++samp) {
// For each set get the contribution to the area. This is the lower of the
// value returned from calculateDOM or
// the actual degree of membership of the fuzzified value itself.
for (let set in this.sets) {
let contribution = Math.min(
this.sets[set].calculateDOM(this.minRange + samp * stepSize),
this.sets[set].DOM);
totalArea += contribution;
sumOfMoments += (this.minRange + samp * stepSize) * contribution;
}
}
// Make sure total area is not equal to zero.
if (isEqual(0, totalArea))
return 0.0;
return sumOfMoments / totalArea;
}
}
// FUZZY OPERATORS
// Fuzzy logic works with membership values in a way that mimics Boolean logic.
// Replacements for basic logic operators AND and OR are defined here.
class FuzzyOperator {
constructor(sets = []) {
this.sets = sets;
}
clearDOM() {
for (let set of this.sets)
set.clearDOM();
}
ORwithDOM(val) {
for (let set of this.sets)
set.ORwithDOM(val);
}
}
class FuzzyAND extends FuzzyOperator {
constructor(sets = []) {
super(sets);
}
// Returns the minimum DOM of the sets it is operating on.
getDOM() {
let smallest = Number.MAX_VALUE;
for (let set of this.sets) {
let cur = set.getDOM();
if (cur < smallest)
smallest = cur;
}
return smallest == Number.MAX_VALUE ? 0.0 : smallest;
}
}
class FuzzyOR extends FuzzyOperator {
constructor(sets = []) {
super(sets);
}
// Returns the maximum DOM of the sets it is operating on.
getDOM() {
let largest = -Number.MAX_VALUE;
for (let set of this.sets) {
let cur = set.getDOM();
if (cur > largest)
largest = cur;
}
return largest == -Number.MAX_VALUE ? 0.0 : largest;
}
}
// FUZZY RULES
class FuzzyRule {
constructor(antecedent, consequence) {
// Antecedent (usually a composite of several fuzzy sets and operators).
this.antecedent = antecedent;
// Consequence (usually a single fuzzy set, but can be several
// ANDed together).
this.consequence = consequence;
}
// Updates the DOM (the confidence) of the consequent set with
// the DOM of the antecedent set.
calculate() {
this.consequence.ORwithDOM(this.antecedent.getDOM());
return this.consequence;
}
setConfidenceOfConsequentToZero() {
this.consequence.clearDOM();
}
}
// FUZZY MODULE
class FuzzyModule {
// Defuzzify methods supported by this module.
static DefuzzifyMethod = { max_av: 0, centroid: 1 };
constructor() {
// When calculating the centroid of the fuzzy manifold this value is used
// to determine how many cross-sections should be sampled.
this.numSamples = 15;
// A map of all the fuzzy variables this module uses.
this.variables = {};
// An array containing all the fuzzy rules.
this.rules = [];
}
// Zeros the DOMs of the consequents of each rule. Used by Defuzzify().
setConfidencesOfConsequentsToZero() {
for (let rule of this.rules)
rule.setConfidenceOfConsequentToZero();
}
// Creates and return a new 'empty' fuzzy variable.
createVariable(varName) {
this.variables[varName] = new FuzzyVariable();
return this.variables[varName];
}
// Adds a rule to the module.
addRule(antecedent, consequence) {
this.rules.push(new FuzzyRule(antecedent, consequence));
}
// Calls the Fuzzify method of the variable with the same name.
fuzzify(varName, val) {
this.variables[varName].fuzzify(val);
}
// Given a fuzzy variable and a defuzzification method
// this returns a crisp value.
deFuzzify(varName, method) {
// Clear the DOMs of all the consequents of all the rules.
this.setConfidencesOfConsequentsToZero();
// Process the rules.
for (let rule of this.rules)
rule.calculate();
// Defuzzify the resultant conclusion using the specified method.
switch (method) {
case FuzzyModule.DefuzzifyMethod.centroid:
return this.variables[varName].deFuzzifyCentroid(this.numSamples);
case FuzzyModule.DefuzzifyMethod.max_av:
return this.variables[varName].deFuzzifyMaxAv();
}
return 0.0;
}
}