import { FuzzyFormula, FuzzySet } from '../types.js';
import { Loggers } from '../utils/logger.js';
const logger = Loggers.validators.fuzzy;
export class FuzzyValidator {
// Safety limits for fuzzy computations
private static readonly MAX_VISUALIZATION_RESOLUTION = 100;
private static readonly MAX_INFERENCE_RULES = 100;
private static readonly MAX_OPERANDS = 20;
private membershipFunctions: Map<string, (x: number) => number>;
constructor() {
this.membershipFunctions = new Map();
this.initializeStandardSets();
}
/**
* Initialize standard fuzzy sets
*/
private initializeStandardSets() {
// Temperature sets
this.membershipFunctions.set('cold', (x: number) => {
if (x <= 0) return 1;
if (x >= 20) return 0;
return (20 - x) / 20;
});
this.membershipFunctions.set('warm', (x: number) => {
if (x <= 10 || x >= 30) return 0;
if (x >= 15 && x <= 25) return 1;
if (x < 15) return (x - 10) / 5;
return (30 - x) / 5;
});
this.membershipFunctions.set('hot', (x: number) => {
if (x <= 20) return 0;
if (x >= 40) return 1;
return (x - 20) / 20;
});
// Speed sets
this.membershipFunctions.set('slow', (x: number) => {
if (x <= 0) return 1;
if (x >= 60) return 0;
return (60 - x) / 60;
});
this.membershipFunctions.set('moderate', (x: number) => {
if (x <= 30 || x >= 90) return 0;
if (x >= 50 && x <= 70) return 1;
if (x < 50) return (x - 30) / 20;
return (90 - x) / 20;
});
this.membershipFunctions.set('fast', (x: number) => {
if (x <= 60) return 0;
if (x >= 120) return 1;
return (x - 60) / 60;
});
// Height sets
this.membershipFunctions.set('short', (x: number) => {
if (x <= 150) return 1;
if (x >= 170) return 0;
return (170 - x) / 20;
});
this.membershipFunctions.set('medium', (x: number) => {
if (x <= 160 || x >= 190) return 0;
if (x >= 170 && x <= 180) return 1;
if (x < 170) return (x - 160) / 10;
return (190 - x) / 10;
});
this.membershipFunctions.set('tall', (x: number) => {
if (x <= 175) return 0;
if (x >= 195) return 1;
return (x - 175) / 20;
});
}
/**
* Evaluate fuzzy formula with given inputs
*/
evaluate(formula: FuzzyFormula, inputs: Map<string, number> = new Map()): number {
switch (formula.type) {
case 'atom':
if (formula.degree !== undefined) {
return formula.degree;
}
// Check for membership expression
if (formula.atom && formula.atom.includes('_IS_')) {
const [variable, setName] = formula.atom.split('_IS_');
const value = inputs.get(variable);
const membershipFn = this.membershipFunctions.get(setName.toLowerCase());
if (value !== undefined && membershipFn) {
return membershipFn(value);
}
}
// Direct atom lookup
return inputs.get(formula.atom || '') || 0;
case 'hedged':
if (!formula.operands || formula.operands.length === 0) return 0;
const baseValue = this.evaluate(formula.operands[0], inputs);
return this.applyHedge(baseValue, formula.hedge || 'very');
case 'compound':
if (!formula.operator || !formula.operands) return 0;
return this.evaluateCompound(formula.operator, formula.operands, inputs);
default:
logger.error('Unknown fuzzy formula type');
return 0;
}
}
/**
* Apply linguistic hedge to membership degree
*/
private applyHedge(value: number, hedge: string): number {
switch (hedge) {
case 'very':
return Math.pow(value, 2);
case 'somewhat':
return Math.sqrt(value);
case 'slightly':
return Math.pow(value, 0.75);
case 'extremely':
return Math.pow(value, 3);
default:
return value;
}
}
/**
* Evaluate compound fuzzy expressions
*/
private evaluateCompound(
operator: string,
operands: FuzzyFormula[],
inputs: Map<string, number>
): number {
// Safety check on operand count
if (operands.length > FuzzyValidator.MAX_OPERANDS) {
logger.warn(
`Too many operands (${operands.length}). Capping at ${FuzzyValidator.MAX_OPERANDS}.`
);
operands = operands.slice(0, FuzzyValidator.MAX_OPERANDS);
}
switch (operator) {
case 'AND':
// Fuzzy AND: minimum
return Math.min(...operands.map(op => this.evaluate(op, inputs)));
case 'OR':
// Fuzzy OR: maximum
return Math.max(...operands.map(op => this.evaluate(op, inputs)));
case 'NOT':
// Fuzzy NOT: complement
if (operands.length > 0) {
return 1 - this.evaluate(operands[0], inputs);
}
return 0;
case 'IMPLIES':
// Fuzzy implication: Lukasiewicz implication
if (operands.length >= 2) {
const a = this.evaluate(operands[0], inputs);
const b = this.evaluate(operands[1], inputs);
return Math.min(1, 1 - a + b);
}
return 1;
default:
logger.error(`Unknown fuzzy operator: ${operator}`);
return 0;
}
}
/**
* Generate fuzzy set visualization
* @throws Error if resolution exceeds maximum
*/
visualizeFuzzySet(
setName: string,
domain: [number, number] = [0, 100],
resolution: number = 20
): string {
const membershipFn = this.membershipFunctions.get(setName.toLowerCase());
if (!membershipFn) {
return `Fuzzy set '${setName}' not found`;
}
// Safety check on resolution
if (resolution > FuzzyValidator.MAX_VISUALIZATION_RESOLUTION) {
logger.warn(
`Resolution ${resolution} exceeds maximum ${FuzzyValidator.MAX_VISUALIZATION_RESOLUTION}. Capping.`
);
resolution = FuzzyValidator.MAX_VISUALIZATION_RESOLUTION;
}
let viz = `Fuzzy Set: ${setName}\n`;
viz += '1.0 |';
const [min, max] = domain;
const step = (max - min) / resolution;
// Create ASCII plot
for (let row = 10; row >= 0; row--) {
const threshold = row / 10;
let line = '';
for (let i = 0; i <= resolution; i++) {
const x = min + i * step;
const membership = membershipFn(x);
if (Math.abs(membership - threshold) < 0.05) {
line += '*';
} else if (membership > threshold) {
line += '▓';
} else {
line += ' ';
}
}
if (row === 10) {
viz += line + '\n';
} else if (row === 5) {
viz += `0.5 |${line}\n`;
} else if (row === 0) {
viz += `0.0 |${line}\n`;
viz += ' +' + '─'.repeat(resolution) + '\n';
viz += ` ${min}` + ' '.repeat(resolution - 5) + `${max}\n`;
} else {
viz += ' |' + line + '\n';
}
}
return viz;
}
/**
* Perform fuzzy inference
* @throws Error if too many rules
*/
inference(
rules: Array<{ condition: FuzzyFormula; consequence: FuzzyFormula }>,
inputs: Map<string, number>
): Map<string, number> {
// Safety check on number of rules
if (rules.length > FuzzyValidator.MAX_INFERENCE_RULES) {
throw new Error(
`Too many inference rules (${rules.length}). Maximum allowed is ${FuzzyValidator.MAX_INFERENCE_RULES}. ` +
`Processing this many rules could cause performance degradation.`
);
}
const outputs = new Map<string, number>();
for (const rule of rules) {
// Evaluate condition (antecedent)
const conditionDegree = this.evaluate(rule.condition, inputs);
// Apply to consequence (implication)
if (rule.consequence.type === 'atom' && rule.consequence.atom) {
const currentValue = outputs.get(rule.consequence.atom) || 0;
// Use max aggregation
outputs.set(rule.consequence.atom, Math.max(currentValue, conditionDegree));
}
}
return outputs;
}
/**
* Add custom membership function
*/
addMembershipFunction(name: string, fn: (x: number) => number) {
this.membershipFunctions.set(name.toLowerCase(), fn);
}
/**
* Get all available fuzzy sets
*/
getAvailableSets(): string[] {
return Array.from(this.membershipFunctions.keys());
}
}