import {FuzzySet, UnitInterval, NumericalDomain, trap, up, down, constant} from "vaguely"; import { CasesExprContext, FuzzyExprContext } from "../antlr/CertusParser"; import { Artifact, Indicator, IndicatorCategory, NodeType } from "./Argument"; import { EvaluationError } from "../parser/Evaluator"; import { Macro } from "../macro/Macro"; import { SyntaxIssue } from "../parser/CertusErrorListener"; export type FZ = FuzzySet | null; export type Valuation = {id: string, valuation: FZ, type: NodeType}; export class ContextError extends Error { constructor(msg) { super(msg); } } export class CertusContext { private readonly UI: NumericalDomain; private _root: Valuation; private _children: Record; private _childrenTypes: Record; private canonicalNames: Record = {}; private readonly canonicalSets: FZ[] = []; // Sorted, lowest to highest rank private readonly namedSets: Record = {}; private readonly namedOps: Record = {}; private readonly macros: Record = {}; private readonly indicators: Record = {}; private readonly indicatorCategories: Record< string, IndicatorCategory> = {}; private readonly artifacts: Record = {}; private syntaxErrors: SyntaxIssue[] = []; private evaluationErrors: (EvaluationError | RangeError)[] = []; constructor() { this.UI = new NumericalDomain("Belief", -1.0, 1.0, 0.01); this._children = {}; this._childrenTypes = {}; this.initCanonicalSets(); } getRoot(): Valuation { return this._root; } setRoot(id: string, valuation: FZ = null, type: NodeType = NodeType.ANY) { const k = this.sanitizeNodeId(id); this._root = {id: k, valuation, type}; } setRootValuation(valuation: FZ) { if (this._root) { this._root.valuation = valuation; } else { throw new ContextError(`Cannot assign valuation of null-ish context root: ${this._root}`); } } hasRoot(id: string): boolean { return !!this._root && !!id && this.sanitizeNodeId(this._root.id) == this.sanitizeNodeId(id); } get children(): string[] { return Object.keys(this._children); } addChild(c: string, t: NodeType, valuation: FZ = null) { const k = this.sanitizeNodeId(c); this._children[k] = valuation; this._childrenTypes[k] = t; } hasChild(id: string): boolean { const k = this.sanitizeNodeId(id); return this._children[k] !== undefined && this._childrenTypes[k] !== undefined; } getChildValuation(id: string): FZ { const k = this.sanitizeNodeId(id); return this._children[k]; } getChildType(id: string): NodeType { const k = this.sanitizeNodeId(id); return this._childrenTypes[k]; } removeAllChildren(): void { this._children = {}; this._childrenTypes = {}; } removeChild(id: string): void { if (this.hasChild(id)) { const k = this.sanitizeNodeId(id); delete this._children[k]; delete this._childrenTypes[k]; } else { throw new ContextError(`Cannot remove child with identifier '${id}', not found in context`); } } addNamedSet(name: string, set: FZ = null) { if (this.hasNamedSet(name)) { throw new ContextError(`Re-declaration of named set: ${name}`); } else { this.namedSets[name.toLowerCase()] = set; } } addCanonicalSet(set: FZ, sort = true) { this.canonicalSets.push(set); if (sort) { this.sortCanonical(); } } hasCanonicalSet(name: string): boolean { return !!this.canonicalNames[name.toLowerCase()]; } getCanonicalSet(name: string): FZ | null { const idx = this.canonicalNames[name.toLowerCase()]; if (idx != undefined) { return this.canonicalSets[idx]; } else { return null; } } getCanonicalSets(): FZ[] { return this.canonicalSets; } getDefaultCanonical(t: NodeType): FZ { if (NodeType.checkSubType(NodeType.PREMISE, t)) { return this.getCanonicalSet('uncert') } else if (NodeType.checkSubType(NodeType.DEFEATER, t)) { return this.getCanonicalSet('certain') } else { throw new EvaluationError(`Unknown alternative node type '${t}' while getting default canonical set`) } } private sortCanonical() { this.canonicalSets.sort((a, b) => { if (a.lt(b)) { return -1; } else if (a.gt(b)) { return 1; } else { return 0; } }); this.recomputeCanonicalNames(); } private recomputeCanonicalNames() { this.canonicalNames = {}; for(let i = 0; i < this.canonicalSets.length; i++) { const s = this.canonicalSets[i]; this.canonicalNames[s.name] = i; } } getNamedSets(): FZ[] { return Object.values(this.namedSets); } getNamedSet(name: string): FZ { const ret = this.namedSets[name.toLowerCase()]; const can = this.getCanonicalSet(name.toLowerCase()); if (can) { return can; } else if (ret) { return ret; } else { return null; } } hasNamedSet(name: string): boolean { return !!this.namedSets[name.toLowerCase()] || !!this.getCanonicalSet(name.toLowerCase()); } removeNamedSet(name: string): void { if (this.hasNamedSet(name)) { delete this.namedSets[name.toLowerCase()] } else { throw new ContextError(`Named set ${name} not found in context, cannot remove`) } } addNamedOp(op: Operation) { if (this.hasNamedOp(op.name)) { throw new ContextError(`Re-declaration of operation ${op.name}`) } else { this.namedOps[op.name.toLowerCase()] = op; } } getNamedOps(): Operation[] { return Object.values(this.namedOps); } getNamedOp(name: string): Operation | null { const ret = this.namedOps[name.toLowerCase()]; if (ret) { return ret; } else { return null; } } hasNamedOp(name: string): boolean { return !!this.namedOps[name.toLowerCase()]; } removeNamedOp(name: string): void { if(this.hasNamedOp(name)) { delete this.namedOps[name.toLowerCase()]; } else { throw new ContextError(`Named operation ${name} not found in context, cannot remove`); } } hasMacro(name: string): boolean { return !!this.macros[name.toLowerCase()]; } addMacro(m: Macro) { if (!this.hasMacro(m.name)) { this.macros[m.name.toLowerCase()] = m; } else { throw new ContextError(`Macro with name ${m.name} already exists in context.`); } } getMacro(name: string): Macro | null { return this.macros[name.toLowerCase()]; } getMacros(): Macro[] { return Object.values(this.macros); } hasIndicator(id: string): boolean { const k = this.sanitizeIndicator(id); return !!this.indicators[k]; } addIndicator(ind: Indicator): void { const k = this.sanitizeIndicator(ind.id); if (this.hasIndicator(k)) { throw new ContextError(`Indicator with name ${ind.id} already exists in context.`); } else { this.indicators[k] = ind; } } getIndicator(id: string): Indicator | null { const ret = this.indicators[this.sanitizeIndicator(id)]; if (ret) { return ret; } else { return null; } } getIndicators(): Indicator[] { return Object.values(this.indicators); } removeIndicator(id: string): void { if (!this.hasIndicator(id)) { throw new ContextError(`Indicator with ID ${id} not found, cannot remove.`); } else { delete this.indicators[this.sanitizeIndicator[id]]; } } hasIndicatorCategory(id: string): boolean { return !!this.indicatorCategories[this.sanitizeIndicator(id)]; } addIndicatorCategory(cat: IndicatorCategory): void { if (this.hasIndicatorCategory(cat.id)) { throw new ContextError(`Indicator category with ID ${cat.id} already exists in context.`); } else { this.indicatorCategories[this.sanitizeIndicator(cat.id)] = cat; } } getIndicatorCategory(id: string): IndicatorCategory | null { const ret = this.indicatorCategories[this.sanitizeIndicator(id)]; return ret ? ret : null; } removeIndicatorCategory(id: string): void { if (!this.hasIndicatorCategory(id)) { throw new ContextError(`Indicator category with ID ${id} not found, cannot remove.`); } else { delete this.indicatorCategories[this.sanitizeIndicator(id)]; } } getIndicatorCategories(): IndicatorCategory[] { return Object.values(this.indicatorCategories); } indicatorIsCategory(ind: Indicator, cat: IndicatorCategory): boolean { return this.sanitizeIndicator(ind.valuation.id) == this.sanitizeIndicator(cat.id); } hasArtifact(id: string): boolean { return !!this.artifacts[this.sanitizeArtifact(id)]; } addArtifact(artifact: Artifact): void { const k = this.sanitizeArtifact(artifact.id); if (this.hasArtifact(k)) { throw new ContextError(`Artifact with ID ${artifact.id} already exists in context.`); } else { this.artifacts[k] = artifact; } } getArtifact(id: string): Artifact | null { const ret = this.artifacts[this.sanitizeArtifact(id)]; return ret ? ret : null; } getArtifacts(): Artifact[] { return Object.values(this.artifacts); } removeArtifact(id: string): void { if (!this.hasArtifact(id)) { throw new ContextError(`Artifact with ID ${id} not found, cannot remove.`); } else { delete this.artifacts[this.sanitizeArtifact(id)]; } } addSyntaxErrors(...e: SyntaxIssue[]) { this.syntaxErrors.push(...e); } clearSyntaxErrors(): void { this.syntaxErrors = []; } getSyntaxErrors(): SyntaxIssue[] { return this.syntaxErrors; } addEvaluationErrors(...e: (EvaluationError | RangeError)[]) { this.evaluationErrors.push(...e); } clearEvaluationErrors(): void { this.evaluationErrors = []; } getEvaluationErrors(): (EvaluationError | RangeError)[] { return this.evaluationErrors; } public clone(): CertusContext { const ctx = new CertusContext(); ctx.setRoot(this._root.id, this._root.valuation ? this._root.valuation.clone() : null); for (const c of this.children) { ctx.addChild(c, this._childrenTypes[c], this._children[c].clone()); } for (const s in this.namedSets) { ctx.addNamedSet(s, this.namedSets[s].clone()); } for (const s in this.namedOps) { ctx.addNamedOp(this.namedOps[s].clone()); } for (const m in this.macros) { ctx.addMacro(this.macros[m]); } return ctx; } public buildTrap(a, b, c, d: number): FZ { return trap(this.UI, a, b, c, d); } public buildTri(a, b, c: number): FZ { return trap(this.UI, a, b, b, c); } public buildUp(a, b: number): FZ { return up(this.UI, a,b); } public buildDown(a, b: number): FZ { return down(this.UI, a, b); } public buildConstant(a: number): FZ { return constant(this.UI, a); } /** * Computes the fuzzy set inversion (mirror image) of the provided set * preferring to return a mirror image based on a canonical * set if it matches (e.g., vhigh -> vopp). Otherwise, directly * computes the mirror image. * * @param fz fuzzy set to compute complement for * @returns Complement of the set */ public computeSetInversion(fz: FZ): FZ { switch(fz.name.toLowerCase()){ case 'reject': return this.getCanonicalSet('certain'); case 'vopp': return this.getCanonicalSet('vhigh'); case 'opp': return this.getCanonicalSet('high'); case 'skep': return this.getCanonicalSet('low'); case 'uncert': return this.getCanonicalSet('uncert'); case 'low': return this.getCanonicalSet('skep'); case 'high': return this.getCanonicalSet('opp'); case 'vhigh': return this.getCanonicalSet('vopp'); case 'certain': return this.getCanonicalSet('reject'); default: // No matching canonical set, compute mirror directly return fz.mirror(); } } /** * Performs an efficient ranking of fuzzy sets. Checks for canonical sets * and then compares them directly. If either set is not a canonical set, * then uses the FuzzySet.rank() function from Vaguely. * * @param A the first fuzzy set to compare * @param B the second fuzzy set to compare * @return +1 if A > B, -1 if A < B, and 0 otherwise. */ public rankSets(A: FZ, B: FZ): number { if (this.hasCanonicalSet(A.name) && this.hasCanonicalSet(B.name)) { const c1 = this.canonicalNames[A.name.toLowerCase()]; const c2 = this.canonicalNames[B.name.toLowerCase()]; if (c1 > c2) return 1; else if (c1 < c2) return -1; else return 0; } else { if (A.gt(B)) return 1; else if (A.lt(B)) return -1; else return 0; } } /** * Canonical sets are: * - reject * - vopp * - opp * - uncert * - low * - high * - vhigh * - certain */ private initCanonicalSets() { // Add sets in order (lowest to highest rank), do not sort for speed const fz_reject = constant(this.UI, 0); fz_reject.assign(-1, 1); fz_reject.name = 'reject'; this.addCanonicalSet(fz_reject, false); const fz_vopp = down(this.UI, -0.8, -0.7); fz_vopp.name = 'vopp'; this.addCanonicalSet(fz_vopp, false); const fz_opp = trap(this.UI, -0.9, -0.8, -0.5, -0.4); fz_opp.name = 'opp'; this.addCanonicalSet(fz_opp, false); const fz_skep = trap(this.UI, -0.6, -0.5, -0.2, -0.1); fz_skep.name = 'skep'; this.addCanonicalSet(fz_skep, false); const fz_uncert = trap(this.UI, -0.3, -0.2, 0.2, 0.3); fz_uncert.name = 'uncert'; this.addCanonicalSet(fz_uncert, false); const fz_low = trap(this.UI, 0.1, 0.2, 0.5, 0.6); fz_low.name = 'low'; this.addCanonicalSet(fz_low, false); const fz_high = trap(this.UI, 0.4, 0.5, 0.8, 0.9); fz_high.name = 'high'; this.addCanonicalSet(fz_high, false); const fz_vhigh = up(this.UI, 0.7, 0.8); fz_vhigh.name = 'vhigh'; this.addCanonicalSet(fz_vhigh, false); const fz_certain = constant(this.UI, 0); fz_certain.assign(1, 1); fz_certain.name = 'certain'; this.addCanonicalSet(fz_certain, false); // Ensure list of names is updated for O(1) access later this.recomputeCanonicalNames(); } private sanitizeNodeId(id: string): string { if (!id) { throw new ContextError(`Cannot sanitize node ID with value: ${id}`); } else { return id.replace(/^([A-Za-z]+)0*(\d+)/, '$1$2').toLowerCase(); } } private sanitizeIndicator(id: string): string { if (!id) { throw new ContextError(`Cannot sanitize indicator ID with value: ${id}`); } else { return id.toLowerCase().replaceAll(/( |\t)+/g, "_"); } } private sanitizeArtifact(id: string): string { if (!id) { throw new ContextError(`Cannot sanitize artifact ID with value: ${id}`); } else { return id.toLowerCase().replaceAll(/( |\t)+/g, "_"); } } } export class Operation { constructor(public readonly name, public readonly params: TypedParameter[], public readonly expr: FuzzyExprContext | CasesExprContext) { /* intentionally empty */ } public check(): void { this.checkSelfRecursion(); this.checkDuplicateParams(); } private checkSelfRecursion() { const re = new RegExp(`${this.name.toLowerCase()}\s*\\(.*\\)`); if (this.expr.text.toLowerCase().match(re)) { throw new EvaluationError(`Recursion is not permitted in operator ${this.name}`); } } private checkDuplicateParams() { for(let i = 0; i < this.params.length; i++) { for (let j = 0; j < this.params.length; j++) { const a = this.params[i].name.toLowerCase(); const b = this.params[j].name.toLowerCase(); if (i != j && a == b) { throw new EvaluationError(`Duplicate parameters appear in operator definition ${this.name}: ${a} and ${b}`); } } } } public clone(): Operation { return new Operation(this.name, this.params.map(t => t.clone()), this.expr); } } export class TypedParameter { constructor(public readonly name, public readonly type) { /* intentionally empty */ } public clone(): TypedParameter { return new TypedParameter(this.name, this.type); } }