initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* Bilattice Orderings for Evidential Reasoning
|
||||
*
|
||||
* Implements the bilattice orderings (≥ᵢ, ≥ₜ) for comparing the epistemic status
|
||||
* of propositions in qualitative capacity systems as described in the research paper.
|
||||
*
|
||||
* These orderings are essential for evidential reasoning where we need to compare
|
||||
* the strength of evidence for different propositions.
|
||||
*/
|
||||
|
||||
import { QualitativeCapacity } from './QualitativeCapacity.js';
|
||||
import { QualitativeScale } from './QualitativeScale.js';
|
||||
import { getSetKey } from './SetUtils.js';
|
||||
|
||||
export class BilatticeOrderings {
|
||||
|
||||
/**
|
||||
* Information Ordering (≥ᵢ)
|
||||
*
|
||||
* Compares the information content of two epistemic pairs.
|
||||
* (c₁, c₁') ≥ᵢ (c₂, c₂') ⟺ c₁ ≥ c₂ and c₁' ≥ c₂'
|
||||
*
|
||||
* This ordering captures the idea that one epistemic state is more informative
|
||||
* than another if it provides higher confidence in both the proposition and its negation.
|
||||
*
|
||||
* @param {Object} epistemic1 - First epistemic pair {belief: number, disbelief: number}
|
||||
* @param {Object} epistemic2 - Second epistemic pair {belief: number, disbelief: number}
|
||||
* @param {QualitativeScale} scale - The qualitative scale to use for comparison
|
||||
* @returns {boolean} True if epistemic1 ≥ᵢ epistemic2
|
||||
*/
|
||||
static informationOrdering(epistemic1, epistemic2, scale) {
|
||||
const { belief: c1, disbelief: c1Prime } = epistemic1;
|
||||
const { belief: c2, disbelief: c2Prime } = epistemic2;
|
||||
|
||||
// (c₁, c₁') ≥ᵢ (c₂, c₂') ⟺ c₁ ≥ c₂ and c₁' ≥ c₂'
|
||||
const beliefComparison = scale.compare(c1, c2) >= 0;
|
||||
const disbeliefComparison = scale.compare(c1Prime, c2Prime) >= 0;
|
||||
|
||||
return beliefComparison && disbeliefComparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truth Ordering (≥ₜ)
|
||||
*
|
||||
* Compares the truth content of two propositions with respect to a capacity.
|
||||
* A ≥ₜ B ⟺ γ(A) ≥ γ(B) and γ(Bᶜ) ≥ γ(Aᶜ)
|
||||
*
|
||||
* This ordering captures the idea that proposition A is "more true" than B
|
||||
* if A has higher capacity value and its complement has lower capacity value.
|
||||
*
|
||||
* @param {Set|Array} propositionA - First proposition (subset of state space)
|
||||
* @param {Set|Array} propositionB - Second proposition (subset of state space)
|
||||
* @param {QualitativeCapacity} capacity - The capacity function
|
||||
* @returns {boolean} True if A ≥ₜ B
|
||||
*/
|
||||
static truthOrdering(propositionA, propositionB, capacity) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
|
||||
// Convert to Sets if needed
|
||||
const setA = propositionA instanceof Set ? propositionA : new Set(propositionA);
|
||||
const setB = propositionB instanceof Set ? propositionB : new Set(propositionB);
|
||||
|
||||
// Get capacity values using canonical keys
|
||||
const gammaA = capacity.getCapacity(setA);
|
||||
const gammaB = capacity.getCapacity(setB);
|
||||
|
||||
// Compute complements
|
||||
const complementA = new Set([...stateSpace].filter(x => !setA.has(x)));
|
||||
const complementB = new Set([...stateSpace].filter(x => !setB.has(x)));
|
||||
|
||||
const gammaComplementA = capacity.getCapacity(complementA);
|
||||
const gammaComplementB = capacity.getCapacity(complementB);
|
||||
|
||||
// A ≥ₜ B ⟺ γ(A) ≥ γ(B) and γ(Bᶜ) ≥ γ(Aᶜ)
|
||||
const capacityComparison = scale.compare(gammaA, gammaB) >= 0;
|
||||
const complementComparison = scale.compare(gammaComplementB, gammaComplementA) >= 0;
|
||||
|
||||
return capacityComparison && complementComparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare epistemic status of two propositions
|
||||
*
|
||||
* This is a comprehensive comparison that considers both information and truth orderings.
|
||||
* It returns a detailed comparison result indicating the relationship between the propositions.
|
||||
*
|
||||
* @param {Set|Array} propositionA - First proposition
|
||||
* @param {Set|Array} propositionB - Second proposition
|
||||
* @param {QualitativeCapacity} capacity - The capacity function
|
||||
* @returns {Object} Comparison result with detailed analysis
|
||||
*/
|
||||
static compareEpistemicStatus(propositionA, propositionB, capacity) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
|
||||
// Convert to Sets if needed
|
||||
const setA = propositionA instanceof Set ? propositionA : new Set(propositionA);
|
||||
const setB = propositionB instanceof Set ? propositionB : new Set(propositionB);
|
||||
|
||||
// Get capacity values and complements
|
||||
const gammaA = capacity.getCapacity(setA);
|
||||
const gammaB = capacity.getCapacity(setB);
|
||||
|
||||
const complementA = new Set([...stateSpace].filter(x => !setA.has(x)));
|
||||
const complementB = new Set([...stateSpace].filter(x => !setB.has(x)));
|
||||
|
||||
const gammaComplementA = capacity.getCapacity(complementA);
|
||||
const gammaComplementB = capacity.getCapacity(complementB);
|
||||
|
||||
// Create epistemic pairs
|
||||
const epistemicA = { belief: gammaA, disbelief: gammaComplementA };
|
||||
const epistemicB = { belief: gammaB, disbelief: gammaComplementB };
|
||||
|
||||
// Apply orderings
|
||||
const informationOrdering = this.informationOrdering(epistemicA, epistemicB, scale);
|
||||
const truthOrdering = this.truthOrdering(setA, setB, capacity);
|
||||
|
||||
// Determine relationship
|
||||
let relationship = 'incomparable';
|
||||
if (informationOrdering && truthOrdering) {
|
||||
relationship = 'A dominates B';
|
||||
} else if (this.informationOrdering(epistemicB, epistemicA, scale) &&
|
||||
this.truthOrdering(setB, setA, capacity)) {
|
||||
relationship = 'B dominates A';
|
||||
} else if (informationOrdering) {
|
||||
relationship = 'A more informative than B';
|
||||
} else if (truthOrdering) {
|
||||
relationship = 'A more true than B';
|
||||
} else if (this.informationOrdering(epistemicB, epistemicA, scale)) {
|
||||
relationship = 'B more informative than A';
|
||||
} else if (this.truthOrdering(setB, setA, capacity)) {
|
||||
relationship = 'B more true than A';
|
||||
}
|
||||
|
||||
return {
|
||||
propositionA: {
|
||||
set: setA,
|
||||
capacity: gammaA,
|
||||
complement: gammaComplementA,
|
||||
epistemic: epistemicA
|
||||
},
|
||||
propositionB: {
|
||||
set: setB,
|
||||
capacity: gammaB,
|
||||
complement: gammaComplementB,
|
||||
epistemic: epistemicB
|
||||
},
|
||||
informationOrdering,
|
||||
truthOrdering,
|
||||
relationship,
|
||||
analysis: this._analyzeComparison(epistemicA, epistemicB, scale)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most informative proposition from a set of propositions
|
||||
*
|
||||
* @param {Array<Set|Array>} propositions - Array of propositions to compare
|
||||
* @param {QualitativeCapacity} capacity - The capacity function
|
||||
* @returns {Object} The most informative proposition and its analysis
|
||||
*/
|
||||
static findMostInformative(propositions, capacity) {
|
||||
if (propositions.length === 0) {
|
||||
throw new Error('Cannot find most informative from empty set');
|
||||
}
|
||||
|
||||
if (propositions.length === 1) {
|
||||
return {
|
||||
proposition: propositions[0],
|
||||
epistemic: this._getEpistemicPair(propositions[0], capacity),
|
||||
rank: 1,
|
||||
total: 1
|
||||
};
|
||||
}
|
||||
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
|
||||
// Convert all propositions to Sets and compute epistemic pairs
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const epistemic = this._getEpistemicPair(set, capacity);
|
||||
return { set, epistemic, original: prop };
|
||||
});
|
||||
|
||||
// Find the most informative using information ordering
|
||||
let mostInformative = propositionData[0];
|
||||
|
||||
for (let i = 1; i < propositionData.length; i++) {
|
||||
const current = propositionData[i];
|
||||
|
||||
// Check if current is more informative than current best
|
||||
if (this.informationOrdering(current.epistemic, mostInformative.epistemic, scale)) {
|
||||
mostInformative = current;
|
||||
}
|
||||
}
|
||||
|
||||
// The most informative proposition has rank 1
|
||||
const rank = 1;
|
||||
|
||||
return {
|
||||
proposition: mostInformative.original,
|
||||
epistemic: mostInformative.epistemic,
|
||||
rank,
|
||||
total: propositions.length,
|
||||
analysis: `Most informative proposition with belief=${mostInformative.epistemic.belief}, disbelief=${mostInformative.epistemic.disbelief}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most true proposition from a set of propositions
|
||||
*
|
||||
* @param {Array<Set|Array>} propositions - Array of propositions to compare
|
||||
* @param {QualitativeCapacity} capacity - The capacity function
|
||||
* @returns {Object} The most true proposition and its analysis
|
||||
*/
|
||||
static findMostTrue(propositions, capacity) {
|
||||
if (propositions.length === 0) {
|
||||
throw new Error('Cannot find most true from empty set');
|
||||
}
|
||||
|
||||
if (propositions.length === 1) {
|
||||
return {
|
||||
proposition: propositions[0],
|
||||
capacity: capacity.getCapacity(propositions[0] instanceof Set ? propositions[0] : new Set(propositions[0])),
|
||||
rank: 1,
|
||||
total: 1
|
||||
};
|
||||
}
|
||||
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
|
||||
// Convert all propositions to Sets and compute capacity values
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const capacityValue = capacity.getCapacity(set);
|
||||
return { set, capacityValue, original: prop };
|
||||
});
|
||||
|
||||
// Find the most true using truth ordering
|
||||
let mostTrue = propositionData[0];
|
||||
|
||||
for (let i = 1; i < propositionData.length; i++) {
|
||||
const current = propositionData[i];
|
||||
|
||||
// Check if current is more true than current best
|
||||
if (this.truthOrdering(current.set, mostTrue.set, capacity)) {
|
||||
mostTrue = current;
|
||||
}
|
||||
}
|
||||
|
||||
// The most true proposition has rank 1
|
||||
const rank = 1;
|
||||
|
||||
return {
|
||||
proposition: mostTrue.original,
|
||||
capacity: mostTrue.capacityValue,
|
||||
rank,
|
||||
total: propositions.length,
|
||||
analysis: `Most true proposition with capacity=${mostTrue.capacityValue}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank propositions by information content
|
||||
*
|
||||
* @param {Array<Set|Array>} propositions - Array of propositions to rank
|
||||
* @param {QualitativeCapacity} capacity - The capacity function
|
||||
* @returns {Array} Ranked propositions with epistemic analysis
|
||||
*/
|
||||
static rankByInformation(propositions, capacity) {
|
||||
const scale = capacity.scale;
|
||||
|
||||
// Convert to proposition data with epistemic pairs
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const epistemic = this._getEpistemicPair(set, capacity);
|
||||
return { set, epistemic, original: prop };
|
||||
});
|
||||
|
||||
// Sort by information content (descending)
|
||||
propositionData.sort((a, b) => {
|
||||
const aMoreInformative = this.informationOrdering(a.epistemic, b.epistemic, scale);
|
||||
const bMoreInformative = this.informationOrdering(b.epistemic, a.epistemic, scale);
|
||||
|
||||
if (aMoreInformative && !bMoreInformative) return -1;
|
||||
if (bMoreInformative && !aMoreInformative) return 1;
|
||||
return 0; // Incomparable or equal
|
||||
});
|
||||
|
||||
return propositionData.map((data, index) => ({
|
||||
rank: index + 1,
|
||||
proposition: data.original,
|
||||
epistemic: data.epistemic,
|
||||
analysis: `Rank ${index + 1}: belief=${data.epistemic.belief}, disbelief=${data.epistemic.disbelief}`
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank propositions by truth content
|
||||
*
|
||||
* @param {Array<Set|Array>} propositions - Array of propositions to rank
|
||||
* @param {QualitativeCapacity} capacity - The capacity function
|
||||
* @returns {Array} Ranked propositions with truth analysis
|
||||
*/
|
||||
static rankByTruth(propositions, capacity) {
|
||||
const scale = capacity.scale;
|
||||
|
||||
// Convert to proposition data with capacity values
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const capacityValue = capacity.getCapacity(set);
|
||||
return { set, capacityValue, original: prop };
|
||||
});
|
||||
|
||||
// Sort by truth content (descending)
|
||||
propositionData.sort((a, b) => {
|
||||
const aMoreTrue = this.truthOrdering(a.set, b.set, capacity);
|
||||
const bMoreTrue = this.truthOrdering(b.set, a.set, capacity);
|
||||
|
||||
if (aMoreTrue && !bMoreTrue) return -1;
|
||||
if (bMoreTrue && !aMoreTrue) return 1;
|
||||
return 0; // Incomparable or equal
|
||||
});
|
||||
|
||||
return propositionData.map((data, index) => ({
|
||||
rank: index + 1,
|
||||
proposition: data.original,
|
||||
capacity: data.capacityValue,
|
||||
analysis: `Rank ${index + 1}: capacity=${data.capacityValue}`
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get epistemic pair for a proposition
|
||||
* @private
|
||||
*/
|
||||
static _getEpistemicPair(proposition, capacity) {
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
const set = proposition instanceof Set ? proposition : new Set(proposition);
|
||||
|
||||
const belief = capacity.getCapacity(set);
|
||||
const complement = new Set([...stateSpace].filter(x => !set.has(x)));
|
||||
const disbelief = capacity.getCapacity(complement);
|
||||
|
||||
return { belief, disbelief };
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze comparison between two epistemic pairs
|
||||
* @private
|
||||
*/
|
||||
static _analyzeComparison(epistemicA, epistemicB, scale) {
|
||||
const { belief: c1, disbelief: c1Prime } = epistemicA;
|
||||
const { belief: c2, disbelief: c2Prime } = epistemicB;
|
||||
|
||||
const beliefDiff = scale.compare(c1, c2);
|
||||
const disbeliefDiff = scale.compare(c1Prime, c2Prime);
|
||||
|
||||
let analysis = [];
|
||||
|
||||
if (beliefDiff > 0) {
|
||||
analysis.push('A has higher belief than B');
|
||||
} else if (beliefDiff < 0) {
|
||||
analysis.push('B has higher belief than A');
|
||||
} else {
|
||||
analysis.push('A and B have equal belief');
|
||||
}
|
||||
|
||||
if (disbeliefDiff > 0) {
|
||||
analysis.push('A has higher disbelief than B');
|
||||
} else if (disbeliefDiff < 0) {
|
||||
analysis.push('B has higher disbelief than A');
|
||||
} else {
|
||||
analysis.push('A and B have equal disbelief');
|
||||
}
|
||||
|
||||
return analysis.join('; ');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* Evidence Aggregation System
|
||||
*
|
||||
* This module provides a unified system for aggregating evidence using OWA (Ordered Weighted Averaging)
|
||||
* operators, separate from reconciliation logic. It supports both qualitative and quantitative modes
|
||||
* with the same aggregation lexicon for consistency.
|
||||
*
|
||||
* Key Features:
|
||||
* - Unified aggregation lexicon for both qualitative and quantitative modes
|
||||
* - OWA weight generation for sophisticated aggregation
|
||||
* - Support for custom weights and reliability weighting
|
||||
* - Integration with both qualitative and quantitative fusion systems
|
||||
*/
|
||||
|
||||
import { OWAFusion } from '../utils/OWAFusion.js';
|
||||
import { OWAQualitativeFusion, getOWAQualitativeWeights } from './OWAQualitativeFusion.js';
|
||||
import { QualitativeScale } from './QualitativeScale.js';
|
||||
|
||||
export class EvidenceAggregation {
|
||||
/**
|
||||
* Aggregate evidence using OWA operators
|
||||
* @param {Array} values - Array of values to aggregate
|
||||
* @param {Array} metas - Array of metadata for each value
|
||||
* @param {Object} options - Aggregation options
|
||||
* @returns {Object} Aggregation result
|
||||
*/
|
||||
static aggregate(values, metas = [], options = {}) {
|
||||
const {
|
||||
mode = 'quantitative', // 'qualitative' or 'quantitative'
|
||||
aggregator = 'max', // OWA aggregation method
|
||||
weights = null, // Custom weights (optional)
|
||||
reliabilityWeighting = false, // Whether to weight by reliability
|
||||
scale = null, // QualitativeScale for qualitative mode
|
||||
customWeights = null // Custom OWA weights
|
||||
} = options;
|
||||
|
||||
if (!values || values.length === 0) {
|
||||
return {
|
||||
value: mode === 'qualitative' ? (scale?.bottom || 0) : 0,
|
||||
possibility: mode === 'qualitative' ? (scale?.bottom || 0) : 0,
|
||||
hasValue: false,
|
||||
aggregationMethod: aggregator,
|
||||
mode
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'qualitative') {
|
||||
return this._aggregateQualitative(values, metas, {
|
||||
aggregator,
|
||||
weights,
|
||||
reliabilityWeighting,
|
||||
scale: scale || QualitativeScale.fivePoint(),
|
||||
customWeights
|
||||
});
|
||||
} else {
|
||||
return this._aggregateQuantitative(values, metas, {
|
||||
aggregator,
|
||||
weights,
|
||||
reliabilityWeighting,
|
||||
customWeights
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate qualitative evidence using OWA operators
|
||||
* @private
|
||||
*/
|
||||
static _aggregateQualitative(values, metas, options) {
|
||||
const {
|
||||
aggregator,
|
||||
weights,
|
||||
reliabilityWeighting,
|
||||
scale,
|
||||
customWeights
|
||||
} = options;
|
||||
|
||||
// Generate OWA weights
|
||||
let owaWeights;
|
||||
if (customWeights) {
|
||||
owaWeights = customWeights;
|
||||
} else {
|
||||
owaWeights = getOWAQualitativeWeights(aggregator, values.length, null, scale);
|
||||
}
|
||||
|
||||
// Apply reliability weighting if requested
|
||||
let weightedValues = values;
|
||||
if (reliabilityWeighting && metas.length > 0) {
|
||||
weightedValues = values.map((value, index) => {
|
||||
const meta = metas[index] || {};
|
||||
const reliability = meta.reliability || 1.0;
|
||||
// In qualitative mode, we use min operation for reliability weighting
|
||||
return scale.min(value, reliability);
|
||||
});
|
||||
}
|
||||
|
||||
// Perform qualitative OWA fusion
|
||||
const result = OWAQualitativeFusion.fuseWithMeta(
|
||||
weightedValues,
|
||||
owaWeights,
|
||||
owaWeights,
|
||||
aggregator,
|
||||
scale
|
||||
);
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
possibility: result.value, // In qualitative mode, value and possibility are the same
|
||||
hasValue: true,
|
||||
aggregationMethod: aggregator,
|
||||
mode: 'qualitative',
|
||||
weights: owaWeights,
|
||||
reliabilityWeighted: reliabilityWeighting
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate quantitative evidence using OWA operators
|
||||
* @private
|
||||
*/
|
||||
static _aggregateQuantitative(values, metas, options) {
|
||||
const {
|
||||
aggregator,
|
||||
weights,
|
||||
reliabilityWeighting,
|
||||
customWeights
|
||||
} = options;
|
||||
|
||||
// Generate OWA weights
|
||||
let owaWeights;
|
||||
if (customWeights) {
|
||||
owaWeights = customWeights;
|
||||
} else {
|
||||
owaWeights = this._generateQuantitativeOWAWeights(aggregator, values.length, metas);
|
||||
}
|
||||
|
||||
// Apply reliability weighting if requested
|
||||
let weightedValues = values;
|
||||
if (reliabilityWeighting && metas.length > 0) {
|
||||
weightedValues = values.map((value, index) => {
|
||||
const meta = metas[index] || {};
|
||||
const reliability = meta.reliability || 1.0;
|
||||
return value * reliability;
|
||||
});
|
||||
}
|
||||
|
||||
// Perform quantitative OWA fusion
|
||||
const result = OWAFusion.fuseWithMeta(
|
||||
weightedValues,
|
||||
metas,
|
||||
owaWeights,
|
||||
aggregator
|
||||
);
|
||||
|
||||
return {
|
||||
value: result.value,
|
||||
possibility: result.value, // In quantitative mode, we use the aggregated value
|
||||
hasValue: true,
|
||||
aggregationMethod: aggregator,
|
||||
mode: 'quantitative',
|
||||
weights: owaWeights,
|
||||
reliabilityWeighted: reliabilityWeighting
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OWA weights for quantitative aggregation
|
||||
* @private
|
||||
*/
|
||||
static _generateQuantitativeOWAWeights(aggregator, count, metas = []) {
|
||||
const weights = new Array(count).fill(0);
|
||||
|
||||
switch (aggregator.toLowerCase()) {
|
||||
case 'max':
|
||||
weights[0] = 1.0; // First (highest) value gets full weight
|
||||
break;
|
||||
|
||||
case 'min':
|
||||
weights[count - 1] = 1.0; // Last (lowest) value gets full weight
|
||||
break;
|
||||
|
||||
case 'average':
|
||||
case 'avg':
|
||||
case 'mean':
|
||||
// Equal weights for all values
|
||||
const equalWeight = 1.0 / count;
|
||||
weights.fill(equalWeight);
|
||||
break;
|
||||
|
||||
case 'sum':
|
||||
// All values get full weight (additive)
|
||||
weights.fill(1.0);
|
||||
break;
|
||||
|
||||
case 'majority':
|
||||
// Focus on top 60% of values
|
||||
const majorityCount = Math.ceil(count * 0.6);
|
||||
const majorityWeight = 1.0 / majorityCount;
|
||||
for (let i = 0; i < majorityCount; i++) {
|
||||
weights[i] = majorityWeight;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'median':
|
||||
// Middle value(s) get full weight
|
||||
if (count % 2 === 1) {
|
||||
weights[Math.floor(count / 2)] = 1.0;
|
||||
} else {
|
||||
const mid1 = count / 2 - 1;
|
||||
const mid2 = count / 2;
|
||||
weights[mid1] = 0.5;
|
||||
weights[mid2] = 0.5;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'optimistic':
|
||||
// Exponential decay favoring higher values
|
||||
for (let i = 0; i < count; i++) {
|
||||
weights[i] = Math.exp(-i * 0.5);
|
||||
}
|
||||
// Normalize
|
||||
const optimisticSum = weights.reduce((sum, w) => sum + w, 0);
|
||||
weights.forEach((w, i) => weights[i] = w / optimisticSum);
|
||||
break;
|
||||
|
||||
case 'pessimistic':
|
||||
// Exponential decay favoring lower values
|
||||
for (let i = 0; i < count; i++) {
|
||||
weights[count - 1 - i] = Math.exp(-i * 0.5);
|
||||
}
|
||||
// Normalize
|
||||
const pessimisticSum = weights.reduce((sum, w) => sum + w, 0);
|
||||
weights.forEach((w, i) => weights[i] = w / pessimisticSum);
|
||||
break;
|
||||
|
||||
case 'top2':
|
||||
// Equal weight on top 2 values
|
||||
const top2Weight = 0.5;
|
||||
weights[0] = top2Weight;
|
||||
weights[1] = top2Weight;
|
||||
break;
|
||||
|
||||
case 'top3':
|
||||
// Equal weight on top 3 values
|
||||
const top3Weight = 1.0 / 3;
|
||||
weights[0] = top3Weight;
|
||||
weights[1] = top3Weight;
|
||||
weights[2] = top3Weight;
|
||||
break;
|
||||
|
||||
case 'priority':
|
||||
// Weight by priority values in metadata
|
||||
if (metas.length > 0) {
|
||||
const priorities = metas.map(meta => meta.priority || 1);
|
||||
const totalPriority = priorities.reduce((sum, p) => sum + p, 0);
|
||||
priorities.forEach((priority, i) => {
|
||||
weights[i] = priority / totalPriority;
|
||||
});
|
||||
} else {
|
||||
// Fallback to equal weights
|
||||
weights.fill(1.0 / count);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
// Custom weights should be provided via customWeights parameter
|
||||
weights.fill(1.0 / count); // Fallback to equal weights
|
||||
break;
|
||||
|
||||
default:
|
||||
// Default to max
|
||||
weights[0] = 1.0;
|
||||
break;
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available aggregation methods
|
||||
* @returns {Array} List of available aggregation methods
|
||||
*/
|
||||
static getAvailableMethods() {
|
||||
return [
|
||||
'max', 'min', 'average', 'avg', 'mean', 'sum',
|
||||
'majority', 'median', 'optimistic', 'pessimistic',
|
||||
'top2', 'top3', 'priority', 'custom'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate aggregation method
|
||||
* @param {string} method - Aggregation method to validate
|
||||
* @returns {boolean} True if method is valid
|
||||
*/
|
||||
static isValidMethod(method) {
|
||||
return this.getAvailableMethods().includes(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method description
|
||||
* @param {string} method - Aggregation method
|
||||
* @returns {string} Description of the method
|
||||
*/
|
||||
static getMethodDescription(method) {
|
||||
const descriptions = {
|
||||
'max': 'Maximum value (optimistic OR)',
|
||||
'min': 'Minimum value (pessimistic AND)',
|
||||
'average': 'Equal weight average',
|
||||
'avg': 'Equal weight average',
|
||||
'mean': 'Equal weight average',
|
||||
'sum': 'Additive evidence (each contributes full weight)',
|
||||
'majority': 'Focus on consensus (top 60% of values)',
|
||||
'median': 'Pure median value',
|
||||
'optimistic': 'Exponential decay favoring higher values',
|
||||
'pessimistic': 'Exponential decay favoring lower values',
|
||||
'top2': 'Equal weight on top 2 values',
|
||||
'top3': 'Equal weight on top 3 values',
|
||||
'priority': 'Weight by rule priority values',
|
||||
'custom': 'User-defined OWA weights'
|
||||
};
|
||||
|
||||
return descriptions[method] || 'Unknown aggregation method';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare aggregation results between qualitative and quantitative modes
|
||||
* @param {Array} values - Array of values to aggregate
|
||||
* @param {Array} metas - Array of metadata
|
||||
* @param {string} aggregator - Aggregation method
|
||||
* @param {Object} options - Additional options
|
||||
* @returns {Object} Comparison result
|
||||
*/
|
||||
static compareModes(values, metas, aggregator, options = {}) {
|
||||
const qualitativeResult = this.aggregate(values, metas, {
|
||||
...options,
|
||||
mode: 'qualitative',
|
||||
aggregator
|
||||
});
|
||||
|
||||
const quantitativeResult = this.aggregate(values, metas, {
|
||||
...options,
|
||||
mode: 'quantitative',
|
||||
aggregator
|
||||
});
|
||||
|
||||
return {
|
||||
aggregator,
|
||||
qualitative: qualitativeResult,
|
||||
quantitative: quantitativeResult,
|
||||
comparison: {
|
||||
valueDifference: Math.abs(qualitativeResult.value - quantitativeResult.value),
|
||||
sameResult: qualitativeResult.value === quantitativeResult.value,
|
||||
qualitativeAdvantage: qualitativeResult.value > quantitativeResult.value,
|
||||
quantitativeAdvantage: quantitativeResult.value > qualitativeResult.value
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
/**
|
||||
* Evidence Reconciliation System
|
||||
*
|
||||
* This module provides a unified system for reconciling conflicting evidence
|
||||
* using different theoretical frameworks:
|
||||
*
|
||||
* 1. Qualitative Mode: Bilattice orderings for qualitative scales
|
||||
* 2. Quantitative Mode: Dempster-Shafer/Subjective Logic for numeric evidence
|
||||
*
|
||||
* The system separates aggregation logic (OWA) from reconciliation logic,
|
||||
* allowing for sophisticated evidence fusion that handles epistemic uncertainty
|
||||
* and conflicting information appropriately.
|
||||
*/
|
||||
|
||||
import { BilatticeOrderings } from './BilatticeOrderings.js';
|
||||
import { NumericBilatticeOrderings } from './NumericBilatticeOrderings.js';
|
||||
import { QualitativeCapacity } from './QualitativeCapacity.js';
|
||||
import { QualitativeScale } from './QualitativeScale.js';
|
||||
|
||||
export class EvidenceReconciliation {
|
||||
/**
|
||||
* Reconcile evidence using appropriate theoretical framework
|
||||
* @param {Array} collectedValues - Array of collected evidence values
|
||||
* @param {Object} options - Reconciliation options
|
||||
* @returns {Object} Reconciliation result
|
||||
*/
|
||||
static reconcile(collectedValues, options = {}) {
|
||||
const {
|
||||
mode = 'qualitative', // 'qualitative' or 'quantitative'
|
||||
reconciliationMethod = 'bilattice', // 'bilattice', 'dempster_shafer', 'subjective_logic'
|
||||
epistemicMode = 'hybrid', // 'information', 'truth', 'hybrid'
|
||||
capacityType = 'simple_support', // 'simple_support', 'possibility', 'necessity'
|
||||
scale = null, // QualitativeScale for qualitative mode
|
||||
aggregationMethod = 'max' // OWA aggregation method
|
||||
} = options;
|
||||
|
||||
if (!collectedValues || collectedValues.length === 0) {
|
||||
return {
|
||||
value: 0,
|
||||
possibility: 0,
|
||||
hasValue: false,
|
||||
reconciliationMethod: 'none',
|
||||
epistemicAnalysis: null
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'qualitative') {
|
||||
return this._reconcileQualitative(collectedValues, {
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
scale: scale || QualitativeScale.fivePoint(),
|
||||
aggregationMethod
|
||||
});
|
||||
} else {
|
||||
return this._reconcileQuantitative(collectedValues, {
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
aggregationMethod
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile qualitative evidence using bilattice orderings
|
||||
* @private
|
||||
*/
|
||||
static _reconcileQualitative(collectedValues, options) {
|
||||
const {
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
scale,
|
||||
aggregationMethod
|
||||
} = options;
|
||||
|
||||
// Create qualitative capacity from collected values
|
||||
const capacity = this._createQualitativeCapacity(collectedValues, scale, capacityType);
|
||||
|
||||
if (!capacity) {
|
||||
return {
|
||||
value: scale.bottom,
|
||||
possibility: scale.bottom,
|
||||
hasValue: false,
|
||||
reconciliationMethod: 'none',
|
||||
epistemicAnalysis: null
|
||||
};
|
||||
}
|
||||
|
||||
// Create propositions for bilattice analysis
|
||||
const propositions = collectedValues.map((cv, index) => [`evidence_${index}`]);
|
||||
|
||||
let selectedValue;
|
||||
let epistemicAnalysis;
|
||||
|
||||
switch (reconciliationMethod) {
|
||||
case 'bilattice':
|
||||
epistemicAnalysis = this._performBilatticeReconciliation(
|
||||
propositions, capacity, epistemicMode, scale
|
||||
);
|
||||
selectedValue = epistemicAnalysis.selectedValue;
|
||||
break;
|
||||
|
||||
case 'dempster_shafer':
|
||||
epistemicAnalysis = this._performDempsterShaferReconciliation(
|
||||
propositions, capacity, epistemicMode, scale
|
||||
);
|
||||
selectedValue = epistemicAnalysis.selectedValue;
|
||||
break;
|
||||
|
||||
case 'subjective_logic':
|
||||
epistemicAnalysis = this._performSubjectiveLogicReconciliation(
|
||||
propositions, capacity, epistemicMode, scale
|
||||
);
|
||||
selectedValue = epistemicAnalysis.selectedValue;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Fallback to simple aggregation
|
||||
selectedValue = this._simpleQualitativeAggregation(collectedValues, aggregationMethod, scale);
|
||||
epistemicAnalysis = {
|
||||
method: 'simple_aggregation',
|
||||
selectedValue,
|
||||
reasoning: 'Fallback to simple aggregation'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: selectedValue,
|
||||
possibility: selectedValue, // In qualitative mode, value and possibility are the same
|
||||
hasValue: true,
|
||||
reconciliationMethod,
|
||||
epistemicAnalysis
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile quantitative evidence using Dempster-Shafer/Subjective Logic
|
||||
* @private
|
||||
*/
|
||||
static _reconcileQuantitative(collectedValues, options) {
|
||||
const {
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
aggregationMethod
|
||||
} = options;
|
||||
|
||||
// Create numeric capacity from collected values
|
||||
const capacity = this._createNumericCapacity(collectedValues, capacityType);
|
||||
|
||||
if (!capacity) {
|
||||
return {
|
||||
value: 0,
|
||||
possibility: 0,
|
||||
hasValue: false,
|
||||
reconciliationMethod: 'none',
|
||||
epistemicAnalysis: null
|
||||
};
|
||||
}
|
||||
|
||||
// Create propositions for analysis
|
||||
const propositions = collectedValues.map((cv, index) => [`evidence_${index}`]);
|
||||
|
||||
let selectedValue;
|
||||
let epistemicAnalysis;
|
||||
|
||||
switch (reconciliationMethod) {
|
||||
case 'dempster_shafer':
|
||||
epistemicAnalysis = this._performNumericDempsterShaferReconciliation(
|
||||
propositions, capacity, epistemicMode
|
||||
);
|
||||
selectedValue = epistemicAnalysis.selectedValue;
|
||||
break;
|
||||
|
||||
case 'subjective_logic':
|
||||
epistemicAnalysis = this._performNumericSubjectiveLogicReconciliation(
|
||||
propositions, capacity, epistemicMode
|
||||
);
|
||||
selectedValue = epistemicAnalysis.selectedValue;
|
||||
break;
|
||||
|
||||
case 'bilattice':
|
||||
epistemicAnalysis = this._performNumericBilatticeReconciliation(
|
||||
propositions, capacity, epistemicMode
|
||||
);
|
||||
selectedValue = epistemicAnalysis.selectedValue;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Fallback to simple aggregation
|
||||
selectedValue = this._simpleNumericAggregation(collectedValues, aggregationMethod);
|
||||
epistemicAnalysis = {
|
||||
method: 'simple_aggregation',
|
||||
selectedValue,
|
||||
reasoning: 'Fallback to simple aggregation'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: selectedValue,
|
||||
possibility: selectedValue, // In quantitative mode, we use the reconciled value
|
||||
hasValue: true,
|
||||
reconciliationMethod,
|
||||
epistemicAnalysis
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform bilattice reconciliation for qualitative evidence
|
||||
* @private
|
||||
*/
|
||||
static _performBilatticeReconciliation(propositions, capacity, epistemicMode, scale) {
|
||||
let selectedProposition;
|
||||
let ranking;
|
||||
|
||||
switch (epistemicMode) {
|
||||
case 'information':
|
||||
selectedProposition = BilatticeOrderings.findMostInformative(propositions, capacity);
|
||||
ranking = BilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
break;
|
||||
|
||||
case 'truth':
|
||||
selectedProposition = BilatticeOrderings.findMostTrue(propositions, capacity);
|
||||
ranking = BilatticeOrderings.rankByTruth(propositions, capacity);
|
||||
break;
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
// Use information ordering as primary, truth as tie-breaker
|
||||
const infoRanking = BilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
const truthRanking = BilatticeOrderings.rankByTruth(propositions, capacity);
|
||||
|
||||
// Find best proposition considering both orderings
|
||||
let bestScore = -1;
|
||||
let bestProposition = null;
|
||||
|
||||
for (let i = 0; i < propositions.length; i++) {
|
||||
const infoRank = infoRanking.find(r => r.proposition === propositions[i])?.rank || propositions.length;
|
||||
const truthRank = truthRanking.find(r => r.proposition === propositions[i])?.rank || propositions.length;
|
||||
|
||||
// Combined score (lower rank is better)
|
||||
const score = 1 / (infoRank + truthRank);
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestProposition = propositions[i];
|
||||
}
|
||||
}
|
||||
|
||||
selectedProposition = {
|
||||
proposition: bestProposition,
|
||||
epistemic: BilatticeOrderings._getEpistemicPair(new Set(bestProposition), capacity),
|
||||
rank: 1
|
||||
};
|
||||
ranking = infoRanking;
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract value from selected proposition
|
||||
const selectedIndex = propositions.findIndex(p => p === selectedProposition.proposition);
|
||||
const selectedValue = selectedIndex >= 0 ?
|
||||
this._extractValueFromProposition(selectedProposition.proposition, capacity, scale) :
|
||||
scale.bottom;
|
||||
|
||||
return {
|
||||
method: 'bilattice',
|
||||
epistemicMode,
|
||||
selectedValue,
|
||||
selectedProposition,
|
||||
ranking,
|
||||
reasoning: `Selected based on ${epistemicMode} ordering`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Dempster-Shafer reconciliation for qualitative evidence
|
||||
* @private
|
||||
*/
|
||||
static _performDempsterShaferReconciliation(propositions, capacity, epistemicMode, scale) {
|
||||
// Calculate Dempster-Shafer measures for each proposition
|
||||
const dsMeasures = propositions.map(prop => {
|
||||
const set = new Set(prop);
|
||||
const belief = BilatticeOrderings._getEpistemicPair(set, capacity).belief;
|
||||
const plausibility = 1 - BilatticeOrderings._getEpistemicPair(set, capacity).disbelief;
|
||||
const uncertainty = plausibility - belief;
|
||||
|
||||
return {
|
||||
proposition: prop,
|
||||
belief,
|
||||
plausibility,
|
||||
uncertainty,
|
||||
expectation: belief + uncertainty / 2
|
||||
};
|
||||
});
|
||||
|
||||
// Select based on epistemic mode
|
||||
let selectedMeasure;
|
||||
switch (epistemicMode) {
|
||||
case 'information':
|
||||
// Select based on uncertainty (lower uncertainty = more informative)
|
||||
selectedMeasure = dsMeasures.reduce((best, current) =>
|
||||
current.uncertainty < best.uncertainty ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'truth':
|
||||
// Select based on belief (higher belief = more true)
|
||||
selectedMeasure = dsMeasures.reduce((best, current) =>
|
||||
current.belief > best.belief ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
// Select based on expectation value
|
||||
selectedMeasure = dsMeasures.reduce((best, current) =>
|
||||
current.expectation > best.expectation ? current : best
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const selectedValue = this._extractValueFromProposition(selectedMeasure.proposition, capacity, scale);
|
||||
|
||||
return {
|
||||
method: 'dempster_shafer',
|
||||
epistemicMode,
|
||||
selectedValue,
|
||||
selectedMeasure,
|
||||
allMeasures: dsMeasures,
|
||||
reasoning: `Selected based on ${epistemicMode} Dempster-Shafer analysis`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform Subjective Logic reconciliation for qualitative evidence
|
||||
* @private
|
||||
*/
|
||||
static _performSubjectiveLogicReconciliation(propositions, capacity, epistemicMode, scale) {
|
||||
// Calculate Subjective Logic opinions for each proposition
|
||||
const opinions = propositions.map(prop => {
|
||||
const set = new Set(prop);
|
||||
const epistemic = BilatticeOrderings._getEpistemicPair(set, capacity);
|
||||
const opinion = {
|
||||
b: epistemic.belief, // Belief
|
||||
d: epistemic.disbelief, // Disbelief
|
||||
u: Math.max(0, 1 - epistemic.belief - epistemic.disbelief) // Uncertainty
|
||||
};
|
||||
const expectation = opinion.b + opinion.u / 2;
|
||||
|
||||
return {
|
||||
proposition: prop,
|
||||
opinion,
|
||||
expectation
|
||||
};
|
||||
});
|
||||
|
||||
// Select based on epistemic mode
|
||||
let selectedOpinion;
|
||||
switch (epistemicMode) {
|
||||
case 'information':
|
||||
// Select based on uncertainty (lower uncertainty = more informative)
|
||||
selectedOpinion = opinions.reduce((best, current) =>
|
||||
current.opinion.u < best.opinion.u ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'truth':
|
||||
// Select based on belief (higher belief = more true)
|
||||
selectedOpinion = opinions.reduce((best, current) =>
|
||||
current.opinion.b > best.opinion.b ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
// Select based on expectation value
|
||||
selectedOpinion = opinions.reduce((best, current) =>
|
||||
current.expectation > best.expectation ? current : best
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const selectedValue = this._extractValueFromProposition(selectedOpinion.proposition, capacity, scale);
|
||||
|
||||
return {
|
||||
method: 'subjective_logic',
|
||||
epistemicMode,
|
||||
selectedValue,
|
||||
selectedOpinion,
|
||||
allOpinions: opinions,
|
||||
reasoning: `Selected based on ${epistemicMode} Subjective Logic analysis`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform numeric Dempster-Shafer reconciliation
|
||||
* @private
|
||||
*/
|
||||
static _performNumericDempsterShaferReconciliation(propositions, capacity, epistemicMode) {
|
||||
// Similar to qualitative but with numeric values
|
||||
const dsMeasures = propositions.map(prop => {
|
||||
const set = new Set(prop);
|
||||
const belief = NumericBilatticeOrderings.dempsterShaferBelief(set, capacity);
|
||||
const plausibility = NumericBilatticeOrderings.dempsterShaferPlausibility(set, capacity);
|
||||
const uncertainty = NumericBilatticeOrderings.dempsterShaferUncertainty(set, capacity);
|
||||
|
||||
return {
|
||||
proposition: prop,
|
||||
belief,
|
||||
plausibility,
|
||||
uncertainty,
|
||||
expectation: belief + uncertainty / 2
|
||||
};
|
||||
});
|
||||
|
||||
// Select based on epistemic mode (same logic as qualitative)
|
||||
let selectedMeasure;
|
||||
switch (epistemicMode) {
|
||||
case 'information':
|
||||
selectedMeasure = dsMeasures.reduce((best, current) =>
|
||||
current.uncertainty < best.uncertainty ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'truth':
|
||||
selectedMeasure = dsMeasures.reduce((best, current) =>
|
||||
current.belief > best.belief ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
selectedMeasure = dsMeasures.reduce((best, current) =>
|
||||
current.expectation > best.expectation ? current : best
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const selectedValue = this._extractNumericValueFromProposition(selectedMeasure.proposition, capacity);
|
||||
|
||||
return {
|
||||
method: 'dempster_shafer',
|
||||
epistemicMode,
|
||||
selectedValue,
|
||||
selectedMeasure,
|
||||
allMeasures: dsMeasures,
|
||||
reasoning: `Selected based on ${epistemicMode} numeric Dempster-Shafer analysis`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform numeric Subjective Logic reconciliation
|
||||
* @private
|
||||
*/
|
||||
static _performNumericSubjectiveLogicReconciliation(propositions, capacity, epistemicMode) {
|
||||
// Similar to qualitative but with numeric values
|
||||
const opinions = propositions.map(prop => {
|
||||
const set = new Set(prop);
|
||||
const epistemic = NumericBilatticeOrderings._getEpistemicPair(set, capacity);
|
||||
const opinion = NumericBilatticeOrderings.subjectiveLogicOpinion(epistemic);
|
||||
const expectation = NumericBilatticeOrderings.subjectiveLogicExpectation(opinion);
|
||||
|
||||
return {
|
||||
proposition: prop,
|
||||
opinion,
|
||||
expectation
|
||||
};
|
||||
});
|
||||
|
||||
// Select based on epistemic mode (same logic as qualitative)
|
||||
let selectedOpinion;
|
||||
switch (epistemicMode) {
|
||||
case 'information':
|
||||
selectedOpinion = opinions.reduce((best, current) =>
|
||||
current.opinion.u < best.opinion.u ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'truth':
|
||||
selectedOpinion = opinions.reduce((best, current) =>
|
||||
current.opinion.b > best.opinion.b ? current : best
|
||||
);
|
||||
break;
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
selectedOpinion = opinions.reduce((best, current) =>
|
||||
current.expectation > best.expectation ? current : best
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const selectedValue = this._extractNumericValueFromProposition(selectedOpinion.proposition, capacity);
|
||||
|
||||
return {
|
||||
method: 'subjective_logic',
|
||||
epistemicMode,
|
||||
selectedValue,
|
||||
selectedOpinion,
|
||||
allOpinions: opinions,
|
||||
reasoning: `Selected based on ${epistemicMode} numeric Subjective Logic analysis`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform numeric bilattice reconciliation
|
||||
* @private
|
||||
*/
|
||||
static _performNumericBilatticeReconciliation(propositions, capacity, epistemicMode) {
|
||||
let selectedProposition;
|
||||
let ranking;
|
||||
|
||||
switch (epistemicMode) {
|
||||
case 'information':
|
||||
selectedProposition = NumericBilatticeOrderings.findMostInformative(propositions, capacity);
|
||||
ranking = NumericBilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
break;
|
||||
|
||||
case 'truth':
|
||||
selectedProposition = NumericBilatticeOrderings.findMostTrue(propositions, capacity);
|
||||
ranking = NumericBilatticeOrderings.rankByTruth(propositions, capacity);
|
||||
break;
|
||||
|
||||
case 'hybrid':
|
||||
default:
|
||||
// Use information ordering as primary, truth as tie-breaker
|
||||
const infoRanking = NumericBilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
const truthRanking = NumericBilatticeOrderings.rankByTruth(propositions, capacity);
|
||||
|
||||
// Find best proposition considering both orderings
|
||||
let bestScore = -1;
|
||||
let bestProposition = null;
|
||||
|
||||
for (let i = 0; i < propositions.length; i++) {
|
||||
const infoRank = infoRanking.find(r => r.proposition === propositions[i])?.rank || propositions.length;
|
||||
const truthRank = truthRanking.find(r => r.proposition === propositions[i])?.rank || propositions.length;
|
||||
|
||||
// Combined score (lower rank is better)
|
||||
const score = 1 / (infoRank + truthRank);
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestProposition = propositions[i];
|
||||
}
|
||||
}
|
||||
|
||||
selectedProposition = {
|
||||
proposition: bestProposition,
|
||||
epistemic: NumericBilatticeOrderings._getEpistemicPair(new Set(bestProposition), capacity),
|
||||
rank: 1
|
||||
};
|
||||
ranking = infoRanking;
|
||||
break;
|
||||
}
|
||||
|
||||
const selectedValue = this._extractNumericValueFromProposition(selectedProposition.proposition, capacity);
|
||||
|
||||
return {
|
||||
method: 'bilattice',
|
||||
epistemicMode,
|
||||
selectedValue,
|
||||
selectedProposition,
|
||||
ranking,
|
||||
reasoning: `Selected based on ${epistemicMode} numeric bilattice ordering`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create qualitative capacity from collected values
|
||||
* @private
|
||||
*/
|
||||
static _createQualitativeCapacity(collectedValues, scale, capacityType) {
|
||||
if (!collectedValues || collectedValues.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create state space from unique values
|
||||
const uniqueValues = [...new Set(collectedValues.map(cv => cv.value))];
|
||||
const stateSpace = uniqueValues.map((_, index) => `evidence_${index}`);
|
||||
|
||||
// Create QMT based on capacity type
|
||||
const qmt = new Map();
|
||||
|
||||
switch (capacityType) {
|
||||
case 'simple_support':
|
||||
collectedValues.forEach((cv, index) => {
|
||||
const evidenceSet = new Set([`evidence_${index}`]);
|
||||
qmt.set(evidenceSet, cv.possibility);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'possibility':
|
||||
collectedValues.forEach((cv, index) => {
|
||||
const singletonSet = new Set([`evidence_${index}`]);
|
||||
qmt.set(singletonSet, cv.possibility);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'necessity':
|
||||
const sortedValues = collectedValues
|
||||
.map((cv, index) => ({ value: cv.value, possibility: cv.possibility, index }))
|
||||
.sort((a, b) => b.possibility - a.possibility);
|
||||
|
||||
sortedValues.forEach((item, rank) => {
|
||||
const nestedSet = new Set(sortedValues.slice(0, rank + 1).map(sv => `evidence_${sv.index}`));
|
||||
qmt.set(nestedSet, item.possibility);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown capacity type: ${capacityType}`);
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create numeric capacity from collected values
|
||||
* @private
|
||||
*/
|
||||
static _createNumericCapacity(collectedValues, capacityType) {
|
||||
if (!collectedValues || collectedValues.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a simple numeric capacity function
|
||||
const stateSpace = collectedValues.map((_, index) => `evidence_${index}`);
|
||||
|
||||
return {
|
||||
stateSpace,
|
||||
getCapacity: (set) => {
|
||||
if (set.size === 0) return 0;
|
||||
|
||||
// For numeric capacity, we use the maximum possibility of included evidence
|
||||
let maxPossibility = 0;
|
||||
for (const element of set) {
|
||||
const index = parseInt(element.replace('evidence_', ''));
|
||||
if (index >= 0 && index < collectedValues.length) {
|
||||
maxPossibility = Math.max(maxPossibility, collectedValues[index].possibility);
|
||||
}
|
||||
}
|
||||
return maxPossibility;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract value from qualitative proposition
|
||||
* @private
|
||||
*/
|
||||
static _extractValueFromProposition(proposition, capacity, scale) {
|
||||
const index = parseInt(proposition[0].replace('evidence_', ''));
|
||||
if (index >= 0 && index < capacity.stateSpace.length) {
|
||||
// Return the possibility value from the capacity
|
||||
const set = new Set([`evidence_${index}`]);
|
||||
return capacity.getCapacity(set);
|
||||
}
|
||||
return scale.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract value from numeric proposition
|
||||
* @private
|
||||
*/
|
||||
static _extractNumericValueFromProposition(proposition, capacity) {
|
||||
const index = parseInt(proposition[0].replace('evidence_', ''));
|
||||
if (index >= 0 && index < capacity.stateSpace.length) {
|
||||
// Return the possibility value from the capacity
|
||||
const set = new Set([`evidence_${index}`]);
|
||||
return capacity.getCapacity(set);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple qualitative aggregation fallback
|
||||
* @private
|
||||
*/
|
||||
static _simpleQualitativeAggregation(collectedValues, aggregationMethod, scale) {
|
||||
const values = collectedValues.map(cv => cv.possibility);
|
||||
|
||||
switch (aggregationMethod) {
|
||||
case 'max':
|
||||
return scale.maxAll(values);
|
||||
case 'min':
|
||||
return scale.minAll(values);
|
||||
case 'avg':
|
||||
case 'average':
|
||||
return scale.at(Math.floor(values.length / 2)); // Median
|
||||
default:
|
||||
return scale.maxAll(values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple numeric aggregation fallback
|
||||
* @private
|
||||
*/
|
||||
static _simpleNumericAggregation(collectedValues, aggregationMethod) {
|
||||
const values = collectedValues.map(cv => cv.possibility);
|
||||
|
||||
switch (aggregationMethod) {
|
||||
case 'max':
|
||||
return Math.max(...values);
|
||||
case 'min':
|
||||
return Math.min(...values);
|
||||
case 'avg':
|
||||
case 'average':
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
case 'sum':
|
||||
return values.reduce((sum, val) => sum + val, 0);
|
||||
default:
|
||||
return Math.max(...values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,614 @@
|
||||
/**
|
||||
* HybridFusion - Fusion system that handles mixed possibilistic and qualitative values
|
||||
*
|
||||
* This module provides fusion capabilities that can seamlessly work with:
|
||||
* - Pure possibilistic values [0,1]
|
||||
* - Pure qualitative scale values
|
||||
* - Mixed arrays of both types
|
||||
* - Linguistic expressions
|
||||
*
|
||||
* The system automatically converts between representations as needed for fusion operations.
|
||||
*/
|
||||
|
||||
import { OWAFusion } from '../utils/OWAFusion.js';
|
||||
import { OWAQualitativeFusion } from './OWAQualitativeFusion.js';
|
||||
import { QualitativeFusion } from './QualitativeFusion.js';
|
||||
import { PossibilisticConverter, LINGUISTIC_MAPPING } from './PossibilisticConverter.js';
|
||||
import { QualitativeScale } from './QualitativeScale.js';
|
||||
|
||||
export class HybridFusion {
|
||||
/**
|
||||
* Fuse mixed possibilistic and qualitative values
|
||||
* @param {Array} values - Array of values (mixed types)
|
||||
* @param {Object} options - Fusion options
|
||||
* @param {QualitativeScale} options.scale - Target qualitative scale (default: five-point)
|
||||
* @param {string} options.strategy - Conversion strategy: 'downgrade', 'upgrade', 'hybrid'
|
||||
* @param {string} options.method - Fusion method: 'max', 'min', 'average', 'majority', etc.
|
||||
* @param {Array} options.weights - Custom OWA weights
|
||||
* @param {boolean} options.preserveType - Whether to preserve original value types in result
|
||||
* @returns {Object} Fusion result with both possibilistic and qualitative representations
|
||||
*/
|
||||
static fuse(values, options = {}) {
|
||||
const {
|
||||
scale = QualitativeScale.fivePoint(),
|
||||
strategy = 'downgrade',
|
||||
method = 'max',
|
||||
weights = null,
|
||||
preserveType = false
|
||||
} = options;
|
||||
|
||||
if (!values || values.length === 0) {
|
||||
return this._createEmptyResult(scale);
|
||||
}
|
||||
|
||||
// Analyze input types
|
||||
const analysis = this._analyzeValues(values);
|
||||
|
||||
// Convert all values to a common representation
|
||||
const converted = this._convertValues(values, scale, strategy, analysis);
|
||||
|
||||
// Perform fusion based on the common representation
|
||||
const result = this._performFusion(converted, method, weights, scale);
|
||||
|
||||
// Create hybrid result
|
||||
return this._createHybridResult(result, values, scale, strategy, preserveType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse possibilistic values using qualitative fusion methods
|
||||
* @param {number[]} possibilities - Array of possibility values
|
||||
* @param {Object} options - Fusion options
|
||||
* @returns {Object} Fusion result
|
||||
*/
|
||||
static fusePossibilities(possibilities, options = {}) {
|
||||
const {
|
||||
scale = QualitativeScale.fivePoint(),
|
||||
method = 'max',
|
||||
weights = null
|
||||
} = options;
|
||||
|
||||
// Convert to qualitative values
|
||||
const qualitativeValues = PossibilisticConverter.downgradePossibilities(
|
||||
possibilities, scale, 'closest'
|
||||
);
|
||||
|
||||
// Use qualitative fusion
|
||||
const result = OWAQualitativeFusion.fuseWithMeta(
|
||||
qualitativeValues,
|
||||
this._createDefaultMetas(qualitativeValues),
|
||||
weights,
|
||||
method,
|
||||
scale
|
||||
);
|
||||
|
||||
return {
|
||||
possibilistic: result.value,
|
||||
qualitative: result.value,
|
||||
scale: scale,
|
||||
method: method,
|
||||
meta: result.meta,
|
||||
originalValues: possibilities,
|
||||
convertedValues: qualitativeValues
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse qualitative values using possibilistic fusion methods
|
||||
* @param {number[]} qualitativeValues - Array of qualitative scale values
|
||||
* @param {QualitativeScale} scale - Source qualitative scale
|
||||
* @param {Object} options - Fusion options
|
||||
* @returns {Object} Fusion result
|
||||
*/
|
||||
static fuseQualitative(qualitativeValues, scale, options = {}) {
|
||||
const {
|
||||
method = 'max',
|
||||
weights = null
|
||||
} = options;
|
||||
|
||||
// Convert to possibilistic values
|
||||
const possibilities = PossibilisticConverter.upgradePossibilities(
|
||||
qualitativeValues, scale, 'direct'
|
||||
);
|
||||
|
||||
// Use possibilistic fusion
|
||||
const result = OWAFusion.fuseWithMeta(
|
||||
possibilities,
|
||||
this._createDefaultMetas(possibilities),
|
||||
weights,
|
||||
method
|
||||
);
|
||||
|
||||
return {
|
||||
possibilistic: result.value,
|
||||
qualitative: result.value,
|
||||
scale: scale,
|
||||
method: method,
|
||||
meta: result.meta,
|
||||
originalValues: qualitativeValues,
|
||||
convertedValues: possibilities
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse linguistic expressions
|
||||
* @param {string[]} linguistics - Array of linguistic expressions
|
||||
* @param {Object} options - Fusion options
|
||||
* @returns {Object} Fusion result
|
||||
*/
|
||||
static fuseLinguistic(linguistics, options = {}) {
|
||||
const {
|
||||
scale = QualitativeScale.fivePoint(),
|
||||
method = 'majority',
|
||||
weights = null,
|
||||
linguisticStrategy = 'median'
|
||||
} = options;
|
||||
|
||||
// Convert linguistic expressions to qualitative values
|
||||
const qualitativeValues = linguistics.map(ling =>
|
||||
PossibilisticConverter.linguisticToQualitative(ling, scale, linguisticStrategy)
|
||||
);
|
||||
|
||||
// Use qualitative fusion
|
||||
const result = OWAQualitativeFusion.fuseWithMeta(
|
||||
qualitativeValues,
|
||||
this._createLinguisticMetas(linguistics),
|
||||
weights,
|
||||
method,
|
||||
scale
|
||||
);
|
||||
|
||||
// Convert back to linguistic
|
||||
const resultLinguistic = PossibilisticConverter.qualitativeToLinguistic(
|
||||
result.value, scale
|
||||
);
|
||||
|
||||
return {
|
||||
possibilistic: result.value,
|
||||
qualitative: result.value,
|
||||
linguistic: resultLinguistic,
|
||||
scale: scale,
|
||||
method: method,
|
||||
meta: result.meta,
|
||||
originalValues: linguistics,
|
||||
convertedValues: qualitativeValues
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse intervals (possibilistic or qualitative)
|
||||
* @param {Array} intervals - Array of intervals
|
||||
* @param {Object} options - Fusion options
|
||||
* @returns {Object} Fusion result
|
||||
*/
|
||||
static fuseIntervals(intervals, options = {}) {
|
||||
const {
|
||||
scale = QualitativeScale.fivePoint(),
|
||||
strategy = 'downgrade',
|
||||
method = 'union'
|
||||
} = options;
|
||||
|
||||
// Analyze interval types
|
||||
const analysis = this._analyzeIntervals(intervals);
|
||||
|
||||
// Convert to common representation
|
||||
const converted = this._convertIntervals(intervals, scale, strategy, analysis);
|
||||
|
||||
// Perform interval fusion
|
||||
const result = this._performIntervalFusion(converted, method, scale);
|
||||
|
||||
return {
|
||||
possibilistic: result.possibilistic,
|
||||
qualitative: result.qualitative,
|
||||
scale: scale,
|
||||
method: method,
|
||||
originalIntervals: intervals,
|
||||
convertedIntervals: converted
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a capacity from mixed possibilistic and qualitative values
|
||||
* @param {Array} values - Array of values (mixed types)
|
||||
* @param {Object} options - Options
|
||||
* @returns {Object} Capacity representation
|
||||
*/
|
||||
static createCapacity(values, options = {}) {
|
||||
const {
|
||||
scale = QualitativeScale.fivePoint(),
|
||||
strategy = 'downgrade',
|
||||
method = 'max'
|
||||
} = options;
|
||||
|
||||
// Convert to qualitative values
|
||||
const converted = this._convertValues(values, scale, strategy, this._analyzeValues(values));
|
||||
|
||||
// Create simple support capacity
|
||||
const capacity = QualitativeFusion.createSimpleSupport(
|
||||
converted.qualitativeValues,
|
||||
scale
|
||||
);
|
||||
|
||||
return {
|
||||
capacity: capacity,
|
||||
scale: scale,
|
||||
originalValues: values,
|
||||
convertedValues: converted.qualitativeValues,
|
||||
method: method
|
||||
};
|
||||
}
|
||||
|
||||
// ========== PRIVATE HELPER METHODS ==========
|
||||
|
||||
/**
|
||||
* Analyze the types of values in the input array
|
||||
* @private
|
||||
*/
|
||||
static _analyzeValues(values) {
|
||||
const analysis = {
|
||||
hasPossibilistic: false,
|
||||
hasQualitative: false,
|
||||
hasLinguistic: false,
|
||||
hasIntervals: false,
|
||||
types: new Set()
|
||||
};
|
||||
|
||||
for (const value of values) {
|
||||
if (typeof value === 'number') {
|
||||
if (value >= 0 && value <= 1) {
|
||||
analysis.hasPossibilistic = true;
|
||||
analysis.types.add('possibilistic');
|
||||
} else {
|
||||
analysis.hasQualitative = true;
|
||||
analysis.types.add('qualitative');
|
||||
}
|
||||
} else if (typeof value === 'string') {
|
||||
if (LINGUISTIC_MAPPING[value]) {
|
||||
analysis.hasLinguistic = true;
|
||||
analysis.types.add('linguistic');
|
||||
}
|
||||
} else if (typeof value === 'object' && (value.min !== undefined || value.lower !== undefined)) {
|
||||
analysis.hasIntervals = true;
|
||||
analysis.types.add('interval');
|
||||
}
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze the types of intervals in the input array
|
||||
* @private
|
||||
*/
|
||||
static _analyzeIntervals(intervals) {
|
||||
const analysis = {
|
||||
hasPossibilistic: false,
|
||||
hasQualitative: false
|
||||
};
|
||||
|
||||
for (const interval of intervals) {
|
||||
if (interval.min !== undefined && interval.max !== undefined) {
|
||||
analysis.hasPossibilistic = true;
|
||||
} else if (interval.lower !== undefined && interval.upper !== undefined) {
|
||||
analysis.hasQualitative = true;
|
||||
}
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert values to a common representation
|
||||
* @private
|
||||
*/
|
||||
static _convertValues(values, scale, strategy, analysis) {
|
||||
const possibilisticValues = [];
|
||||
const qualitativeValues = [];
|
||||
|
||||
for (const value of values) {
|
||||
if (typeof value === 'number') {
|
||||
if (value >= 0 && value <= 1) {
|
||||
// Possibilistic value
|
||||
possibilisticValues.push(value);
|
||||
if (strategy === 'downgrade') {
|
||||
qualitativeValues.push(PossibilisticConverter.downgradePossibility(value, scale, 'closest'));
|
||||
} else {
|
||||
qualitativeValues.push(value);
|
||||
}
|
||||
} else {
|
||||
// Qualitative value
|
||||
qualitativeValues.push(value);
|
||||
if (strategy === 'upgrade') {
|
||||
possibilisticValues.push(PossibilisticConverter.upgradePossibility(value, scale, 'direct'));
|
||||
} else {
|
||||
possibilisticValues.push(value);
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'string' && LINGUISTIC_MAPPING[value]) {
|
||||
// Linguistic expression
|
||||
const possibilistic = LINGUISTIC_MAPPING[value].median;
|
||||
const qualitative = PossibilisticConverter.linguisticToQualitative(value, scale, 'median');
|
||||
possibilisticValues.push(possibilistic);
|
||||
qualitativeValues.push(qualitative);
|
||||
}
|
||||
}
|
||||
|
||||
return { possibilisticValues, qualitativeValues };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert intervals to a common representation
|
||||
* @private
|
||||
*/
|
||||
static _convertIntervals(intervals, scale, strategy, analysis) {
|
||||
const possibilisticIntervals = [];
|
||||
const qualitativeIntervals = [];
|
||||
|
||||
for (const interval of intervals) {
|
||||
if (interval.min !== undefined && interval.max !== undefined) {
|
||||
// Possibilistic interval
|
||||
possibilisticIntervals.push(interval);
|
||||
if (strategy === 'downgrade') {
|
||||
qualitativeIntervals.push(PossibilisticConverter.downgradeInterval(interval, scale, 'closest'));
|
||||
} else {
|
||||
qualitativeIntervals.push(interval);
|
||||
}
|
||||
} else if (interval.lower !== undefined && interval.upper !== undefined) {
|
||||
// Qualitative interval
|
||||
qualitativeIntervals.push(interval);
|
||||
if (strategy === 'upgrade') {
|
||||
possibilisticIntervals.push(PossibilisticConverter.upgradeInterval(interval, scale, 'direct'));
|
||||
} else {
|
||||
possibilisticIntervals.push(interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { possibilisticIntervals, qualitativeIntervals };
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform fusion on converted values
|
||||
* @private
|
||||
*/
|
||||
static _performFusion(converted, method, weights, scale) {
|
||||
const { possibilisticValues, qualitativeValues } = converted;
|
||||
|
||||
// Use qualitative fusion as the primary method
|
||||
const qualitativeResult = OWAQualitativeFusion.fuseWithMeta(
|
||||
qualitativeValues,
|
||||
this._createDefaultMetas(qualitativeValues),
|
||||
weights,
|
||||
method,
|
||||
scale
|
||||
);
|
||||
|
||||
// Use possibilistic fusion for comparison
|
||||
const possibilisticResult = OWAFusion.fuseWithMeta(
|
||||
possibilisticValues,
|
||||
this._createDefaultMetas(possibilisticValues),
|
||||
weights,
|
||||
method
|
||||
);
|
||||
|
||||
return {
|
||||
qualitative: qualitativeResult,
|
||||
possibilistic: possibilisticResult
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform interval fusion
|
||||
* @private
|
||||
*/
|
||||
static _performIntervalFusion(converted, method, scale) {
|
||||
const { possibilisticIntervals, qualitativeIntervals } = converted;
|
||||
|
||||
// Use OWAFusion's interval fusion for possibilistic intervals
|
||||
const possibilisticResult = OWAFusion.fuseIntervalsWithMeta(
|
||||
possibilisticIntervals,
|
||||
this._createDefaultMetas(possibilisticIntervals),
|
||||
null,
|
||||
method
|
||||
);
|
||||
|
||||
// For qualitative intervals, perform fusion directly on qualitative scale
|
||||
const qualitativeResult = this._fuseQualitativeIntervals(qualitativeIntervals, method, scale);
|
||||
|
||||
return {
|
||||
possibilistic: possibilisticResult.interval,
|
||||
qualitative: qualitativeResult
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse qualitative intervals using qualitative scale operations
|
||||
* @private
|
||||
*/
|
||||
static _fuseQualitativeIntervals(intervals, method, scale) {
|
||||
if (intervals.length === 0) {
|
||||
return { lower: scale.bottom, upper: scale.bottom };
|
||||
}
|
||||
|
||||
if (intervals.length === 1) {
|
||||
return intervals[0];
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case 'union':
|
||||
// Union: [min of lowers, max of uppers]
|
||||
const unionLower = scale.minAll(intervals.map(i => i.lower));
|
||||
const unionUpper = scale.maxAll(intervals.map(i => i.upper));
|
||||
return { lower: unionLower, upper: unionUpper };
|
||||
|
||||
case 'intersection':
|
||||
// Intersection: [max of lowers, min of uppers]
|
||||
const intersectionLower = scale.maxAll(intervals.map(i => i.lower));
|
||||
const intersectionUpper = scale.minAll(intervals.map(i => i.upper));
|
||||
|
||||
// Ensure valid interval (lower <= upper)
|
||||
if (scale.compare(intersectionLower, intersectionUpper) > 0) {
|
||||
// No intersection, return empty interval
|
||||
return { lower: scale.bottom, upper: scale.bottom };
|
||||
}
|
||||
return { lower: intersectionLower, upper: intersectionUpper };
|
||||
|
||||
case 'max':
|
||||
// Max: take the interval with highest upper bound
|
||||
const maxInterval = intervals.reduce((max, current) =>
|
||||
scale.compare(current.upper, max.upper) > 0 ? current : max
|
||||
);
|
||||
return maxInterval;
|
||||
|
||||
case 'min':
|
||||
// Min: take the interval with lowest lower bound
|
||||
const minInterval = intervals.reduce((min, current) =>
|
||||
scale.compare(current.lower, min.lower) < 0 ? current : min
|
||||
);
|
||||
return minInterval;
|
||||
|
||||
case 'average':
|
||||
case 'avg':
|
||||
case 'mean':
|
||||
// Average: average the lower and upper bounds separately
|
||||
const avgLower = this._averageQualitativeValues(intervals.map(i => i.lower), scale);
|
||||
const avgUpper = this._averageQualitativeValues(intervals.map(i => i.upper), scale);
|
||||
return { lower: avgLower, upper: avgUpper };
|
||||
|
||||
case 'sum':
|
||||
// Sum: in qualitative bag algebra, sum means "all intervals contribute"
|
||||
// This is equivalent to taking the maximum interval (strongest evidence dominates)
|
||||
const maxSumInterval = intervals.reduce((max, current) =>
|
||||
scale.compare(current.upper, max.upper) > 0 ? current : max
|
||||
);
|
||||
return maxSumInterval;
|
||||
|
||||
case 'majority':
|
||||
// Majority: take the interval that appears most frequently or has the most overlap
|
||||
if (intervals.length === 1) return intervals[0];
|
||||
// For simplicity, return the interval with the highest upper bound
|
||||
return intervals.reduce((max, current) =>
|
||||
scale.compare(current.upper, max.upper) > 0 ? current : max
|
||||
);
|
||||
|
||||
case 'median':
|
||||
// Median: take the middle interval when sorted by lower bound
|
||||
const sortedIntervals = [...intervals].sort((a, b) =>
|
||||
scale.compare(a.lower, b.lower)
|
||||
);
|
||||
const midIndex = Math.floor(sortedIntervals.length / 2);
|
||||
return sortedIntervals[midIndex];
|
||||
|
||||
case 'optimistic':
|
||||
// Optimistic: take the interval with the highest upper bound
|
||||
return intervals.reduce((max, current) =>
|
||||
scale.compare(current.upper, max.upper) > 0 ? current : max
|
||||
);
|
||||
|
||||
case 'pessimistic':
|
||||
// Pessimistic: take the interval with the lowest lower bound
|
||||
return intervals.reduce((min, current) =>
|
||||
scale.compare(current.lower, min.lower) < 0 ? current : min
|
||||
);
|
||||
|
||||
case 'top2':
|
||||
// Top2: take the two intervals with highest upper bounds and union them
|
||||
const top2Intervals = [...intervals]
|
||||
.sort((a, b) => scale.compare(b.upper, a.upper))
|
||||
.slice(0, 2);
|
||||
return this._fuseQualitativeIntervals(top2Intervals, 'union', scale);
|
||||
|
||||
case 'top3':
|
||||
// Top3: take the three intervals with highest upper bounds and union them
|
||||
const top3Intervals = [...intervals]
|
||||
.sort((a, b) => scale.compare(b.upper, a.upper))
|
||||
.slice(0, 3);
|
||||
return this._fuseQualitativeIntervals(top3Intervals, 'union', scale);
|
||||
|
||||
default:
|
||||
// Default to union
|
||||
return this._fuseQualitativeIntervals(intervals, 'union', scale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Average qualitative values using scale operations
|
||||
* @private
|
||||
*/
|
||||
static _averageQualitativeValues(values, scale) {
|
||||
if (values.length === 0) return scale.bottom;
|
||||
if (values.length === 1) return values[0];
|
||||
|
||||
// Find the median value on the scale
|
||||
const sortedValues = [...values].sort((a, b) => scale.compare(a, b));
|
||||
const midIndex = Math.floor(sortedValues.length / 2);
|
||||
|
||||
if (sortedValues.length % 2 === 0) {
|
||||
// Even number of values, return the lower of the two middle values
|
||||
return sortedValues[midIndex - 1];
|
||||
} else {
|
||||
// Odd number of values, return the middle value
|
||||
return sortedValues[midIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create hybrid result object
|
||||
* @private
|
||||
*/
|
||||
static _createHybridResult(result, originalValues, scale, strategy, preserveType) {
|
||||
return {
|
||||
possibilistic: result.qualitative.value,
|
||||
qualitative: result.qualitative.value,
|
||||
scale: scale,
|
||||
strategy: strategy,
|
||||
method: result.qualitative.meta?.method || 'max',
|
||||
meta: {
|
||||
qualitative: result.qualitative.meta,
|
||||
possibilistic: result.possibilistic.meta
|
||||
},
|
||||
originalValues: originalValues,
|
||||
convertedValues: {
|
||||
possibilistic: result.possibilistic.meta?.allValues || [],
|
||||
qualitative: result.qualitative.meta?.allValues || []
|
||||
},
|
||||
preserveType: preserveType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create empty result
|
||||
* @private
|
||||
*/
|
||||
static _createEmptyResult(scale) {
|
||||
return {
|
||||
possibilistic: 0,
|
||||
qualitative: scale.bottom,
|
||||
scale: scale,
|
||||
meta: { reason: 'no_values' }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default metadata for fusion
|
||||
* @private
|
||||
*/
|
||||
static _createDefaultMetas(values) {
|
||||
return values.map((value, index) => ({
|
||||
index: index,
|
||||
value: value,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create linguistic metadata for fusion
|
||||
* @private
|
||||
*/
|
||||
static _createLinguisticMetas(linguistics) {
|
||||
return linguistics.map((linguistic, index) => ({
|
||||
index: index,
|
||||
linguistic: linguistic,
|
||||
timestamp: Date.now()
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* Numeric Bilattice Orderings for Quantitative Evidence Fusion
|
||||
*
|
||||
* This module provides bilattice orderings for numeric (quantitative) evidence,
|
||||
* complementing the qualitative bilattice orderings. It supports both traditional
|
||||
* bilattice theory and Dempster-Shafer/Subjective Logic approaches for handling
|
||||
* epistemic uncertainty in quantitative domains.
|
||||
*
|
||||
* Key Concepts:
|
||||
* - Information Ordering: (c₁, c₁') ≥ᵢ (c₂, c₂') ⟺ c₁ ≥ c₂ and c₁' ≥ c₂'
|
||||
* - Truth Ordering: A ≥ₜ B ⟺ γ(A) ≥ γ(B) and γ(Bᶜ) ≥ γ(Aᶜ)
|
||||
* - Dempster-Shafer: Belief, Plausibility, and Uncertainty measures
|
||||
* - Subjective Logic: Opinion space with belief, disbelief, and uncertainty
|
||||
*/
|
||||
|
||||
export class NumericBilatticeOrderings {
|
||||
/**
|
||||
* Information ordering for numeric epistemic pairs
|
||||
* Compares the information content of two epistemic states.
|
||||
* (c₁, c₁') ≥ᵢ (c₂, c₂') ⟺ c₁ ≥ c₂ and c₁' ≥ c₂'
|
||||
*
|
||||
* @param {Object} epistemic1 - First epistemic pair {belief: number, disbelief: number}
|
||||
* @param {Object} epistemic2 - Second epistemic pair {belief: number, disbelief: number}
|
||||
* @returns {boolean} True if epistemic1 ≥ᵢ epistemic2
|
||||
*/
|
||||
static informationOrdering(epistemic1, epistemic2) {
|
||||
const { belief: c1, disbelief: c1Prime } = epistemic1;
|
||||
const { belief: c2, disbelief: c2Prime } = epistemic2;
|
||||
|
||||
// (c₁, c₁') ≥ᵢ (c₂, c₂') ⟺ c₁ ≥ c₂ and c₁' ≥ c₂'
|
||||
const beliefComparison = c1 >= c2;
|
||||
const disbeliefComparison = c1Prime >= c2Prime;
|
||||
|
||||
return beliefComparison && disbeliefComparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truth ordering for numeric propositions
|
||||
* Compares the truth content of two propositions with respect to a capacity.
|
||||
* A ≥ₜ B ⟺ γ(A) ≥ γ(B) and γ(Bᶜ) ≥ γ(Aᶜ)
|
||||
*
|
||||
* @param {Array|Set} propositionA - First proposition (subset of state space)
|
||||
* @param {Array|Set} propositionB - Second proposition (subset of state space)
|
||||
* @param {Object} capacity - Capacity function with getCapacity method
|
||||
* @returns {boolean} True if propositionA ≥ₜ propositionB
|
||||
*/
|
||||
static truthOrdering(propositionA, propositionB, capacity) {
|
||||
const setA = propositionA instanceof Set ? propositionA : new Set(propositionA);
|
||||
const setB = propositionB instanceof Set ? propositionB : new Set(propositionB);
|
||||
|
||||
const gammaA = capacity.getCapacity(setA);
|
||||
const gammaB = capacity.getCapacity(setB);
|
||||
|
||||
// Get complements
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
const complementA = new Set([...stateSpace].filter(x => !setA.has(x)));
|
||||
const complementB = new Set([...stateSpace].filter(x => !setB.has(x)));
|
||||
|
||||
const gammaComplementA = capacity.getCapacity(complementA);
|
||||
const gammaComplementB = capacity.getCapacity(complementB);
|
||||
|
||||
// A ≥ₜ B ⟺ γ(A) ≥ γ(B) and γ(Bᶜ) ≥ γ(Aᶜ)
|
||||
const capacityComparison = gammaA >= gammaB;
|
||||
const complementComparison = gammaComplementB >= gammaComplementA;
|
||||
|
||||
return capacityComparison && complementComparison;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare epistemic status between two propositions
|
||||
* @param {Array|Set} propositionA - First proposition
|
||||
* @param {Array|Set} propositionB - Second proposition
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {Object} Comparison result with information and truth orderings
|
||||
*/
|
||||
static compareEpistemicStatus(propositionA, propositionB, capacity) {
|
||||
const epistemicA = this._getEpistemicPair(propositionA, capacity);
|
||||
const epistemicB = this._getEpistemicPair(propositionB, capacity);
|
||||
|
||||
return {
|
||||
informationOrdering: this.informationOrdering(epistemicA, epistemicB),
|
||||
truthOrdering: this.truthOrdering(propositionA, propositionB, capacity),
|
||||
epistemicA,
|
||||
epistemicB
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most informative proposition from a list
|
||||
* @param {Array} propositions - Array of propositions
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {Object} Most informative proposition with epistemic analysis
|
||||
*/
|
||||
static findMostInformative(propositions, capacity) {
|
||||
if (propositions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert all propositions to Sets and compute epistemic pairs
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const epistemic = this._getEpistemicPair(set, capacity);
|
||||
return { set, epistemic, original: prop };
|
||||
});
|
||||
|
||||
// Find the most informative using information ordering
|
||||
let mostInformative = propositionData[0];
|
||||
|
||||
for (let i = 1; i < propositionData.length; i++) {
|
||||
const current = propositionData[i];
|
||||
|
||||
// Check if current is more informative than current best
|
||||
if (this.informationOrdering(current.epistemic, mostInformative.epistemic)) {
|
||||
mostInformative = current;
|
||||
}
|
||||
}
|
||||
|
||||
// The most informative proposition has rank 1
|
||||
return {
|
||||
proposition: mostInformative.original,
|
||||
epistemic: mostInformative.epistemic,
|
||||
rank: 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most true proposition from a list
|
||||
* @param {Array} propositions - Array of propositions
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {Object} Most true proposition with epistemic analysis
|
||||
*/
|
||||
static findMostTrue(propositions, capacity) {
|
||||
if (propositions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert all propositions to Sets and compute epistemic pairs
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const epistemic = this._getEpistemicPair(set, capacity);
|
||||
return { set, epistemic, original: prop };
|
||||
});
|
||||
|
||||
// Find the most true using truth ordering
|
||||
let mostTrue = propositionData[0];
|
||||
|
||||
for (let i = 1; i < propositionData.length; i++) {
|
||||
const current = propositionData[i];
|
||||
|
||||
// Check if current is more true than current best
|
||||
if (this.truthOrdering(current.original, mostTrue.original, capacity)) {
|
||||
mostTrue = current;
|
||||
}
|
||||
}
|
||||
|
||||
// The most true proposition has rank 1
|
||||
return {
|
||||
proposition: mostTrue.original,
|
||||
epistemic: mostTrue.epistemic,
|
||||
rank: 1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank propositions by information content
|
||||
* @param {Array} propositions - Array of propositions
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {Array} Ranked propositions with epistemic analysis
|
||||
*/
|
||||
static rankByInformation(propositions, capacity) {
|
||||
if (propositions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert all propositions to Sets and compute epistemic pairs
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const epistemic = this._getEpistemicPair(set, capacity);
|
||||
return { set, epistemic, original: prop };
|
||||
});
|
||||
|
||||
// Sort by information ordering (most informative first)
|
||||
propositionData.sort((a, b) => {
|
||||
if (this.informationOrdering(a.epistemic, b.epistemic)) return -1;
|
||||
if (this.informationOrdering(b.epistemic, a.epistemic)) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Assign ranks
|
||||
return propositionData.map((item, index) => ({
|
||||
proposition: item.original,
|
||||
epistemic: item.epistemic,
|
||||
rank: index + 1
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rank propositions by truth content
|
||||
* @param {Array} propositions - Array of propositions
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {Array} Ranked propositions with epistemic analysis
|
||||
*/
|
||||
static rankByTruth(propositions, capacity) {
|
||||
if (propositions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Convert all propositions to Sets and compute epistemic pairs
|
||||
const propositionData = propositions.map(prop => {
|
||||
const set = prop instanceof Set ? prop : new Set(prop);
|
||||
const epistemic = this._getEpistemicPair(set, capacity);
|
||||
return { set, epistemic, original: prop };
|
||||
});
|
||||
|
||||
// Sort by truth ordering (most true first)
|
||||
propositionData.sort((a, b) => {
|
||||
if (this.truthOrdering(a.original, b.original, capacity)) return -1;
|
||||
if (this.truthOrdering(b.original, a.original, capacity)) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Assign ranks
|
||||
return propositionData.map((item, index) => ({
|
||||
proposition: item.original,
|
||||
epistemic: item.epistemic,
|
||||
rank: index + 1
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Dempster-Shafer belief function
|
||||
* @param {Array|Set} proposition - Proposition to evaluate
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {number} Belief value
|
||||
*/
|
||||
static dempsterShaferBelief(proposition, capacity) {
|
||||
const set = proposition instanceof Set ? proposition : new Set(proposition);
|
||||
return capacity.getCapacity(set);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dempster-Shafer plausibility function
|
||||
* @param {Array|Set} proposition - Proposition to evaluate
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {number} Plausibility value
|
||||
*/
|
||||
static dempsterShaferPlausibility(proposition, capacity) {
|
||||
const set = proposition instanceof Set ? proposition : new Set(proposition);
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
const complement = new Set([...stateSpace].filter(x => !set.has(x)));
|
||||
|
||||
// Pl(A) = 1 - Bel(Aᶜ)
|
||||
// But we need to handle the case where the complement might be empty
|
||||
if (complement.size === 0) {
|
||||
return 1.0; // If complement is empty, plausibility is 1
|
||||
}
|
||||
|
||||
return 1 - capacity.getCapacity(complement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dempster-Shafer uncertainty function
|
||||
* @param {Array|Set} proposition - Proposition to evaluate
|
||||
* @param {Object} capacity - Capacity function
|
||||
* @returns {number} Uncertainty value
|
||||
*/
|
||||
static dempsterShaferUncertainty(proposition, capacity) {
|
||||
const belief = this.dempsterShaferBelief(proposition, capacity);
|
||||
const plausibility = this.dempsterShaferPlausibility(proposition, capacity);
|
||||
|
||||
// U(A) = Pl(A) - Bel(A)
|
||||
return plausibility - belief;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subjective Logic opinion from epistemic pair
|
||||
* @param {Object} epistemic - Epistemic pair {belief: number, disbelief: number}
|
||||
* @returns {Object} Subjective Logic opinion {b: belief, d: disbelief, u: uncertainty}
|
||||
*/
|
||||
static subjectiveLogicOpinion(epistemic) {
|
||||
const { belief, disbelief } = epistemic;
|
||||
const uncertainty = Math.max(0, 1 - belief - disbelief);
|
||||
|
||||
return {
|
||||
b: belief, // Belief
|
||||
d: disbelief, // Disbelief
|
||||
u: uncertainty // Uncertainty
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subjective Logic expectation value
|
||||
* @param {Object} opinion - Subjective Logic opinion
|
||||
* @returns {number} Expectation value
|
||||
*/
|
||||
static subjectiveLogicExpectation(opinion) {
|
||||
const { b, u } = opinion;
|
||||
// E = b + u/2 (assuming uniform distribution of uncertainty)
|
||||
return b + u / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get epistemic pair for a proposition
|
||||
* @private
|
||||
*/
|
||||
static _getEpistemicPair(proposition, capacity) {
|
||||
const stateSpace = new Set(capacity.stateSpace);
|
||||
const set = proposition instanceof Set ? proposition : new Set(proposition);
|
||||
|
||||
const belief = capacity.getCapacity(set);
|
||||
const complement = new Set([...stateSpace].filter(x => !set.has(x)));
|
||||
const disbelief = capacity.getCapacity(complement);
|
||||
|
||||
return { belief, disbelief };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* OWA Qualitative Fusion - Bag Algebras for Qualitative Scales
|
||||
*
|
||||
* Implements Ordered Weighted Averaging (OWA) operations for qualitative scales,
|
||||
* similar to the existing OWAFusion but adapted for qualitative capacities and scales.
|
||||
*
|
||||
* This provides sophisticated aggregation methods for qualitative values that go beyond
|
||||
* simple max/min operations, enabling nuanced evidential reasoning in qualitative settings.
|
||||
*/
|
||||
|
||||
import { QualitativeScale, DEFAULT_QUALITATIVE_SCALE } from './QualitativeScale.js';
|
||||
import { QualitativeCapacity } from './QualitativeCapacity.js';
|
||||
|
||||
export class OWAQualitativeFusion {
|
||||
|
||||
/**
|
||||
* Fuse qualitative values with metadata using Qualitative OWA
|
||||
*
|
||||
* This implements a novel qualitative weighted maximum operator where weights act as
|
||||
* "gates" that must pass a threshold to allow their corresponding values to be considered.
|
||||
* This is distinct from the standard Sugeno integral but provides a practical way to
|
||||
* introduce weight influence in purely ordinal contexts.
|
||||
*
|
||||
* @param {Array<number>} values - Array of qualitative values from the scale
|
||||
* @param {Array} metas - Metadata for each value
|
||||
* @param {Array<number>} weights - OWA weights (optional)
|
||||
* @param {string} mode - Aggregation mode
|
||||
* @param {QualitativeScale} scale - The qualitative scale to use
|
||||
* @param {number} activationThreshold - Threshold for weight activation (default 0.5)
|
||||
* @returns {{value: number, meta: any}} Fused result with metadata
|
||||
*/
|
||||
static fuseWithMeta(values, metas, weights, mode = 'max', scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
if (!values.length) return { value: scale.bottom, meta: null };
|
||||
|
||||
// Validate all values are in the scale
|
||||
for (const value of values) {
|
||||
if (!scale.contains(value)) {
|
||||
throw new Error(`Value ${value} not found in scale ${scale.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
const typeOrder = { strict: 3, defeasible: 2, defeater: 1 };
|
||||
|
||||
// Create pairs of values and metadata for sorting
|
||||
const zipped = values.map((v, i) => ({
|
||||
v,
|
||||
meta: metas[i],
|
||||
idx: i,
|
||||
priority: metas[i]?.rule?.priority ?? 0,
|
||||
ruleType: typeOrder[metas[i]?.ruleType] ?? 0
|
||||
}));
|
||||
|
||||
// Sort by value (descending), then by priority, then by rule type
|
||||
// This is the core of OWA - we always sort first
|
||||
zipped.sort((a, b) => {
|
||||
const valueComparison = scale.compare(b.v, a.v);
|
||||
if (valueComparison !== 0) return valueComparison;
|
||||
if (b.priority !== a.priority) return b.priority - a.priority;
|
||||
return b.ruleType - a.ruleType;
|
||||
});
|
||||
|
||||
const sortedValues = zipped.map(z => z.v);
|
||||
const sortedMetas = zipped.map(z => z.meta);
|
||||
|
||||
// Determine OWA weights based on mode or explicit weights
|
||||
let owaWeights;
|
||||
|
||||
if (weights && weights.length === values.length) {
|
||||
// Use explicit weights as provided
|
||||
owaWeights = weights;
|
||||
} else {
|
||||
// Generate weights based on mode using unified method
|
||||
owaWeights = this.generateOWAWeights(values.length, mode, null, true, scale);
|
||||
}
|
||||
|
||||
// Apply Qualitative OWA: weighted maximum with configurable activation threshold
|
||||
// This is a novel operator where weights act as "gates" that must pass a threshold
|
||||
// to allow their corresponding values to be considered in the final max operation
|
||||
let resultValue = scale.bottom;
|
||||
let selectedIdx = 0;
|
||||
let maxWeightedValue = scale.bottom;
|
||||
|
||||
// Special handling for certain modes
|
||||
if (mode.toLowerCase() === 'sum') {
|
||||
// For sum, we take the maximum value (all evidence contributes to the strongest)
|
||||
resultValue = sortedValues[0]; // Already sorted in descending order
|
||||
selectedIdx = 0;
|
||||
maxWeightedValue = resultValue;
|
||||
} else if (mode.toLowerCase() === 'avg' || mode.toLowerCase() === 'average' || mode.toLowerCase() === 'mean') {
|
||||
// For average, we take the median value (middle of the sorted values)
|
||||
const medianIndex = Math.floor(sortedValues.length / 2);
|
||||
resultValue = sortedValues[medianIndex];
|
||||
selectedIdx = medianIndex;
|
||||
maxWeightedValue = resultValue;
|
||||
} else {
|
||||
// Standard OWA logic for other modes
|
||||
for (let i = 0; i < sortedValues.length; i++) {
|
||||
// Convert qualitative weight to numeric for threshold comparison
|
||||
const numericWeight = this._qualitativeWeightToNumeric(owaWeights[i], scale);
|
||||
|
||||
// If weight passes activation threshold, use the value; otherwise use bottom
|
||||
const effectiveValue = numericWeight > activationThreshold ? sortedValues[i] : scale.bottom;
|
||||
|
||||
if (scale.compare(effectiveValue, resultValue) > 0) {
|
||||
resultValue = effectiveValue;
|
||||
selectedIdx = i;
|
||||
maxWeightedValue = effectiveValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { value: resultValue, meta: sortedMetas[selectedIdx] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate OWA weights for different aggregation strategies
|
||||
* Adapted for qualitative scales where arithmetic operations are limited
|
||||
*
|
||||
* @param {number} length - Number of values to aggregate
|
||||
* @param {string} mode - Aggregation mode
|
||||
* @param {Array<number>} customWeights - Custom weights for specific modes
|
||||
* @param {boolean} normalize - Whether to normalize weights
|
||||
* @param {QualitativeScale} scale - The qualitative scale
|
||||
* @returns {Array<number>} OWA weights
|
||||
*/
|
||||
static generateOWAWeights(length, mode, customWeights = null, normalize = true, scale = DEFAULT_QUALITATIVE_SCALE) {
|
||||
if (!mode || length <= 0) {
|
||||
return [scale.top, ...Array(Math.max(0, length - 1)).fill(scale.bottom)]; // Default: max
|
||||
}
|
||||
|
||||
const weights = new Array(length).fill(scale.bottom);
|
||||
|
||||
switch (mode.toLowerCase()) {
|
||||
case 'max':
|
||||
// MAX: All weight on the highest value
|
||||
weights[0] = scale.top;
|
||||
break;
|
||||
|
||||
case 'min':
|
||||
// MIN: All weight on the lowest value (last in sorted order)
|
||||
weights[length - 1] = scale.top;
|
||||
break;
|
||||
|
||||
case 'sum':
|
||||
// SUM: In qualitative bag algebra, sum means "all values contribute fully"
|
||||
// We use a special approach where all weights are set to top, but the fusion
|
||||
// logic will handle this differently to ensure all values contribute
|
||||
weights.fill(scale.top); // All elements get full weight
|
||||
break;
|
||||
|
||||
case 'avg':
|
||||
case 'average':
|
||||
case 'mean':
|
||||
// AVERAGE: Equal weights
|
||||
const avgWeight = this._getMiddleValue(scale);
|
||||
weights.fill(avgWeight);
|
||||
break;
|
||||
|
||||
case 'majority':
|
||||
// MAJORITY: Weight on the median position(s) or top 60% for larger sets
|
||||
if (length <= 3) {
|
||||
// For small sets, use median logic
|
||||
if (length % 2 === 1) {
|
||||
// Odd length: weight on middle element
|
||||
weights[Math.floor(length / 2)] = scale.top;
|
||||
} else {
|
||||
// Even length: equal weight on two middle elements
|
||||
const mid1 = length / 2 - 1;
|
||||
const mid2 = length / 2;
|
||||
weights[mid1] = this._getMiddleValue(scale);
|
||||
weights[mid2] = this._getMiddleValue(scale);
|
||||
}
|
||||
} else {
|
||||
// For larger sets, weight toward the majority (top 60% of values)
|
||||
const majorityCount = Math.max(1, Math.ceil(length * 0.6));
|
||||
const majorityWeight = this._getMiddleValue(scale);
|
||||
for (let i = 0; i < majorityCount; i++) {
|
||||
weights[i] = majorityWeight;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'median':
|
||||
// MEDIAN: Focus on the middle value(s)
|
||||
if (length === 1) {
|
||||
weights[0] = scale.top;
|
||||
} else if (length === 2) {
|
||||
weights[0] = this._getMiddleValue(scale);
|
||||
weights[1] = this._getMiddleValue(scale);
|
||||
} else if (length % 2 === 1) {
|
||||
// Odd length: single median
|
||||
const medianIndex = Math.floor(length / 2);
|
||||
weights[medianIndex] = scale.top;
|
||||
} else {
|
||||
// Even length: equal weight on two middle values
|
||||
const mid1 = Math.floor(length / 2) - 1;
|
||||
const mid2 = Math.floor(length / 2);
|
||||
weights[mid1] = this._getMiddleValue(scale);
|
||||
weights[mid2] = this._getMiddleValue(scale);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'optimistic':
|
||||
// OPTIMISTIC: More weight on higher values (qualitative decay)
|
||||
for (let i = 0; i < length; i++) {
|
||||
weights[i] = this._getOptimisticWeight(i, scale);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'pessimistic':
|
||||
// PESSIMISTIC: More weight on lower values
|
||||
for (let i = 0; i < length; i++) {
|
||||
weights[length - 1 - i] = this._getOptimisticWeight(i, scale);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'top2':
|
||||
// TOP2: Equal weight on top 2 values
|
||||
if (length >= 2) {
|
||||
weights[0] = this._getMiddleValue(scale);
|
||||
weights[1] = this._getMiddleValue(scale);
|
||||
} else if (length === 1) {
|
||||
weights[0] = scale.top;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'top3':
|
||||
// TOP3: Equal weight on top 3 values
|
||||
const top3Count = Math.min(3, length);
|
||||
const top3Weight = this._getMiddleValue(scale);
|
||||
for (let i = 0; i < top3Count; i++) {
|
||||
weights[i] = top3Weight;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'priority':
|
||||
// PRIORITY: Use normalized rule priorities as weights
|
||||
if (customWeights && Array.isArray(customWeights) && customWeights.length === length) {
|
||||
const maxPriority = Math.max(...customWeights, 1);
|
||||
for (let i = 0; i < length; i++) {
|
||||
const normalizedPriority = (customWeights[i] || 0) / maxPriority;
|
||||
weights[i] = this._priorityToQualitativeWeight(normalizedPriority, scale);
|
||||
}
|
||||
} else {
|
||||
// Fallback to max if no priorities provided
|
||||
weights[0] = scale.top;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'owa':
|
||||
case 'custom':
|
||||
// CUSTOM: Custom OWA weights provided by user
|
||||
if (customWeights && Array.isArray(customWeights)) {
|
||||
if (customWeights.length === length) {
|
||||
return customWeights.map(w => this._numericToQualitativeWeight(w, scale));
|
||||
} else if (customWeights.length > 0) {
|
||||
// Extend or truncate to match length
|
||||
const normalized = [...customWeights];
|
||||
while (normalized.length < length) normalized.push(0);
|
||||
if (normalized.length > length) normalized.splice(length);
|
||||
|
||||
return normalized.map(w => this._numericToQualitativeWeight(w, scale));
|
||||
}
|
||||
}
|
||||
// Fallback to max if no valid custom weights
|
||||
weights[0] = scale.top;
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown aggregator '${mode}', defaulting to 'max'`);
|
||||
weights[0] = scale.top;
|
||||
break;
|
||||
}
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a middle value from the scale (for equal weighting)
|
||||
*/
|
||||
static _getMiddleValue(scale) {
|
||||
const middleIndex = Math.floor(scale.size / 2);
|
||||
return scale.at(middleIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimistic weight based on position (higher positions get higher weights)
|
||||
*/
|
||||
static _getOptimisticWeight(position, scale) {
|
||||
// Map position to scale values with exponential-like decay
|
||||
const totalPositions = scale.size;
|
||||
const positionRatio = position / Math.max(1, totalPositions - 1);
|
||||
|
||||
// Map to scale index with bias toward higher values
|
||||
const scaleIndex = Math.floor(positionRatio * (scale.size - 1));
|
||||
return scale.at(scaleIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert numeric priority to qualitative weight
|
||||
*/
|
||||
static _priorityToQualitativeWeight(priority, scale) {
|
||||
// Map priority (0-1) to scale values
|
||||
const scaleIndex = Math.floor(priority * (scale.size - 1));
|
||||
return scale.at(scaleIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert numeric weight to qualitative weight
|
||||
*/
|
||||
static _numericToQualitativeWeight(weight, scale) {
|
||||
// Clamp weight to [0, 1] and map to scale
|
||||
const clampedWeight = Math.max(0, Math.min(1, weight));
|
||||
const scaleIndex = Math.floor(clampedWeight * (scale.size - 1));
|
||||
return scale.at(scaleIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert qualitative weight to numeric value for threshold comparison
|
||||
* Maps qualitative scale values to [0, 1] range
|
||||
*/
|
||||
static _qualitativeWeightToNumeric(qualitativeWeight, scale) {
|
||||
const index = scale.indexOf(qualitativeWeight);
|
||||
if (index === -1) return 0;
|
||||
return index / (scale.size - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience methods for common OWA operations
|
||||
*/
|
||||
static max(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'max', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static min(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'min', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static majority(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'majority', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static median(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'median', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static optimistic(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'optimistic', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static pessimistic(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'pessimistic', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static top2(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'top2', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static top3(values, metas, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, null, 'top3', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static priority(values, metas, priorities, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, this.generateOWAWeights(values.length, 'priority', priorities, true, scale), 'priority', scale, activationThreshold);
|
||||
}
|
||||
|
||||
static custom(values, metas, customWeights, scale = DEFAULT_QUALITATIVE_SCALE, activationThreshold = 0.5) {
|
||||
return this.fuseWithMeta(values, metas, this.generateOWAWeights(values.length, 'custom', customWeights, true, scale), 'custom', scale, activationThreshold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pointwise OWA Fusion of Qualitative Capacities
|
||||
*
|
||||
* ⚠️ THEORETICAL WARNING: This method performs pointwise OWA fusion on capacity values,
|
||||
* which does NOT guarantee that the result is a valid qualitative capacity. The resulting
|
||||
* set-function may violate the fundamental monotonicity property: A⊆B ⟹ γ(A)≤γ(B).
|
||||
*
|
||||
* This happens because OWA operators are not guaranteed to preserve monotonicity when
|
||||
* applied pointwise. The "winning" value for subset A might come from a different capacity
|
||||
* than the "winning" value for superset B, breaking the monotonicity constraint.
|
||||
*
|
||||
* For theoretically sound capacity fusion, use QualitativeFusion.normalizedConjunctive()
|
||||
* or QualitativeFusion.disjunctive() which work on QMTs directly.
|
||||
*
|
||||
* This method is provided for experimental purposes and other applications where
|
||||
* monotonicity is not required.
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities
|
||||
* @param {string} mode - Aggregation mode
|
||||
* @param {Array<number>} weights - Optional custom weights
|
||||
* @returns {QualitativeCapacity} Pointwise fused capacity (may not be monotonic)
|
||||
*/
|
||||
static pointwiseOWAFusion(capacities, mode = 'max', weights = null) {
|
||||
if (!capacities.length) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Validate all capacities use the same scale and state space
|
||||
for (const capacity of capacities) {
|
||||
if (!capacity.scale.equals(scale)) {
|
||||
throw new Error('All capacities must use the same qualitative scale');
|
||||
}
|
||||
if (capacity.stateSpace.length !== stateSpace.length) {
|
||||
throw new Error('All capacities must use the same state space');
|
||||
}
|
||||
}
|
||||
|
||||
// Generate all subsets
|
||||
const allSubsets = this._generateAllSubsets(stateSpace);
|
||||
const resultQMT = new Map();
|
||||
|
||||
for (const subset of allSubsets) {
|
||||
// Get capacity values for this subset from all capacities
|
||||
const capacityValues = capacities.map(cap => cap.getCapacity(subset));
|
||||
const metas = capacities.map((cap, i) => ({ capacityIndex: i, source: 'capacity' }));
|
||||
|
||||
// Fuse using OWA
|
||||
const fusedResult = this.fuseWithMeta(capacityValues, metas, weights, mode, scale);
|
||||
|
||||
if (fusedResult.value !== scale.bottom) {
|
||||
resultQMT.set(subset, fusedResult.value);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sugeno Integral - Theoretically Sound Qualitative Aggregation
|
||||
*
|
||||
* The Sugeno integral is the qualitative counterpart to the Choquet integral and provides
|
||||
* a theoretically sound way to aggregate qualitative values with respect to a capacity.
|
||||
*
|
||||
* S_γ(f) = max_{i=1}^n min(f_{(i)}, γ(A_{(i)}))
|
||||
*
|
||||
* where f_{(i)} are the sorted values in descending order and A_{(i)} = {w_{(1)}, ..., w_{(i)}}
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity - The capacity γ
|
||||
* @param {Map|Object} decisionFunction - Function f: W → L mapping states to scale values
|
||||
* @returns {number} Sugeno integral value
|
||||
*/
|
||||
static sugenoIntegral(capacity, decisionFunction) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = capacity.stateSpace;
|
||||
|
||||
// Convert decision function to Map if needed
|
||||
const f = decisionFunction instanceof Map ? decisionFunction : new Map(Object.entries(decisionFunction));
|
||||
|
||||
// Create pairs of (state, value) and sort by value in descending order
|
||||
const stateValuePairs = stateSpace
|
||||
.map(state => ({
|
||||
state,
|
||||
value: f.get(state) || scale.bottom
|
||||
}))
|
||||
.sort((a, b) => scale.compare(b.value, a.value)); // Descending order
|
||||
|
||||
let result = scale.bottom;
|
||||
|
||||
// Compute Sugeno integral: max_{i=1}^n min(f_{(i)}, γ(A_{(i)}))
|
||||
for (let i = 0; i < stateValuePairs.length; i++) {
|
||||
// A_{(i)} = {w_{(1)}, ..., w_{(i)}} (first i states in sorted order)
|
||||
const Ai = new Set(stateValuePairs.slice(0, i + 1).map(pair => pair.state));
|
||||
|
||||
// f_{(i)} is the value of the i-th state in sorted order
|
||||
const fi = stateValuePairs[i].value;
|
||||
|
||||
// γ(A_{(i)}) is the capacity value of the subset
|
||||
const gammaAi = capacity.getCapacity(Ai);
|
||||
|
||||
// min(f_{(i)}, γ(A_{(i)}))
|
||||
const minValue = scale.min(fi, gammaAi);
|
||||
|
||||
// max over all i
|
||||
result = scale.max(result, minValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all possible subsets of a state space
|
||||
*/
|
||||
static _generateAllSubsets(stateSpace) {
|
||||
const subsets = [];
|
||||
const n = stateSpace.length;
|
||||
|
||||
// Generate all 2^n subsets
|
||||
for (let i = 0; i < (1 << n); i++) {
|
||||
const subset = new Set();
|
||||
for (let j = 0; j < n; j++) {
|
||||
if (i & (1 << j)) {
|
||||
subset.add(stateSpace[j]);
|
||||
}
|
||||
}
|
||||
subsets.push(subset);
|
||||
}
|
||||
|
||||
return subsets;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert aggregator name to OWA weights for qualitative scales
|
||||
*/
|
||||
export function getOWAQualitativeWeights(aggregator, length, customWeights = null, scale = DEFAULT_QUALITATIVE_SCALE) {
|
||||
return OWAQualitativeFusion.generateOWAWeights(length, aggregator, customWeights, true, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get OWA weights from rule configuration for qualitative scales
|
||||
*/
|
||||
export function getOWAQualitativeWeightsFromRule(rule, length, metas = null, scale = DEFAULT_QUALITATIVE_SCALE) {
|
||||
// If explicit owaWeights provided, use them
|
||||
if (rule.owaWeights && Array.isArray(rule.owaWeights)) {
|
||||
return OWAQualitativeFusion.generateOWAWeights(length, 'custom', rule.owaWeights, true, scale);
|
||||
}
|
||||
|
||||
// If aggregator specified, convert to OWA weights
|
||||
if (rule.aggregator) {
|
||||
// For priority aggregator, extract priorities from metadata
|
||||
if (rule.aggregator === 'priority' && metas && Array.isArray(metas)) {
|
||||
const priorities = metas.map(meta => meta?.rule?.priority || meta?.priority || 0);
|
||||
return OWAQualitativeFusion.generateOWAWeights(length, 'priority', priorities, true, scale);
|
||||
}
|
||||
|
||||
return OWAQualitativeFusion.generateOWAWeights(length, rule.aggregator, rule.owaWeights, true, scale);
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return OWAQualitativeFusion.generateOWAWeights(length, 'max', null, true, scale);
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
/**
|
||||
* PossibilisticConverter - Converts between possibilistic values and qualitative scales
|
||||
*
|
||||
* This module provides bidirectional conversion between:
|
||||
* - Numeric possibility values [0,1] ↔ Qualitative scale values
|
||||
* - Linguistic expressions ↔ Qualitative scale values
|
||||
* - Possibilistic intervals ↔ Qualitative intervals
|
||||
*
|
||||
* The converter supports both "upgrading" (qualitative → possibilistic) and "downgrading"
|
||||
* (possibilistic → qualitative) operations, with downgrading as the default for fusion.
|
||||
*/
|
||||
|
||||
import { QualitativeScale } from './QualitativeScale.js';
|
||||
|
||||
// Linguistic mapping for converting natural language to possibilistic values
|
||||
export const LINGUISTIC_MAPPING = {
|
||||
'Almost Certain': { median: 0.98, q1: 0.95, q3: 0.99 },
|
||||
'Highly Likely': { median: 0.90, q1: 0.85, q3: 0.95 },
|
||||
'Very Good Chance': { median: 0.85, q1: 0.78, q3: 0.92 },
|
||||
'Believable': { median: 0.75, q1: 0.65, q3: 0.85 },
|
||||
'Likely': { median: 0.72, q1: 0.65, q3: 0.80 },
|
||||
'Probable': { median: 0.70, q1: 0.60, q3: 0.80 },
|
||||
'Probably': { median: 0.68, q1: 0.55, q3: 0.80 },
|
||||
'Even': { median: 0.60, q1: 0.52, q3: 0.68 },
|
||||
'About Even': { median: 0.50, q1: 0.48, q3: 0.52 },
|
||||
'Slightly Against': { median: 0.40, q1: 0.32, q3: 0.48 },
|
||||
'Probably Not': { median: 0.32, q1: 0.20, q3: 0.45 },
|
||||
'Doubtful': { median: 0.25, q1: 0.15, q3: 0.35 },
|
||||
'Unlikely': { median: 0.18, q1: 0.10, q3: 0.25 },
|
||||
'Improbable': { median: 0.10, q1: 0.05, q3: 0.20 },
|
||||
'Slight': { median: 0.10, q1: 0.05, q3: 0.18 },
|
||||
'Little Chance': { median: 0.05, q1: 0.02, q3: 0.10 },
|
||||
'Certainly Not': { median: 0.02, q1: 0.01, q3: 0.05 }
|
||||
};
|
||||
|
||||
export class PossibilisticConverter {
|
||||
/**
|
||||
* Convert a possibilistic value to a qualitative scale value
|
||||
* @param {number} possibility - Possibility value in [0,1]
|
||||
* @param {QualitativeScale} scale - Target qualitative scale
|
||||
* @param {string} strategy - Conversion strategy: 'closest', 'floor', 'ceiling', 'interpolate'
|
||||
* @returns {number} Qualitative scale value
|
||||
*/
|
||||
static downgradePossibility(possibility, scale, strategy = 'closest') {
|
||||
if (!scale.contains(possibility)) {
|
||||
// Find the closest value on the scale
|
||||
return this._findClosestValue(possibility, scale, strategy);
|
||||
}
|
||||
return possibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a qualitative scale value to a possibilistic value
|
||||
* @param {number} qualitativeValue - Value from qualitative scale
|
||||
* @param {QualitativeScale} scale - Source qualitative scale
|
||||
* @param {string} strategy - Conversion strategy: 'direct', 'interpolate', 'linguistic'
|
||||
* @returns {number} Possibility value in [0,1]
|
||||
*/
|
||||
static upgradePossibility(qualitativeValue, scale, strategy = 'direct') {
|
||||
if (scale.contains(qualitativeValue)) {
|
||||
return qualitativeValue; // Already a valid possibility value
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case 'interpolate':
|
||||
return this._interpolateValue(qualitativeValue, scale);
|
||||
case 'linguistic':
|
||||
return this._linguisticToPossibility(qualitativeValue);
|
||||
default:
|
||||
return qualitativeValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a linguistic expression to a qualitative scale value
|
||||
* @param {string} linguistic - Linguistic expression (e.g., "Highly Likely")
|
||||
* @param {QualitativeScale} scale - Target qualitative scale
|
||||
* @param {string} strategy - Conversion strategy: 'median', 'q1', 'q3', 'closest'
|
||||
* @returns {number} Qualitative scale value
|
||||
*/
|
||||
static linguisticToQualitative(linguistic, scale, strategy = 'median') {
|
||||
const mapping = LINGUISTIC_MAPPING[linguistic];
|
||||
if (!mapping) {
|
||||
throw new Error(`Unknown linguistic expression: ${linguistic}`);
|
||||
}
|
||||
|
||||
let possibility;
|
||||
switch (strategy) {
|
||||
case 'q1':
|
||||
possibility = mapping.q1;
|
||||
break;
|
||||
case 'q3':
|
||||
possibility = mapping.q3;
|
||||
break;
|
||||
case 'closest':
|
||||
// Find the closest value on the scale to the median
|
||||
possibility = this._findClosestValue(mapping.median, scale, 'closest');
|
||||
break;
|
||||
default:
|
||||
possibility = mapping.median;
|
||||
}
|
||||
|
||||
return this.downgradePossibility(possibility, scale, 'closest');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a qualitative scale value to a linguistic expression
|
||||
* @param {number} qualitativeValue - Value from qualitative scale
|
||||
* @param {QualitativeScale} scale - Source qualitative scale
|
||||
* @returns {string} Linguistic expression
|
||||
*/
|
||||
static qualitativeToLinguistic(qualitativeValue, scale) {
|
||||
const possibility = this.upgradePossibility(qualitativeValue, scale, 'direct');
|
||||
|
||||
// Find the closest linguistic mapping
|
||||
let closestLinguistic = 'About Even';
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const [linguistic, mapping] of Object.entries(LINGUISTIC_MAPPING)) {
|
||||
const distance = Math.abs(possibility - mapping.median);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestLinguistic = linguistic;
|
||||
}
|
||||
}
|
||||
|
||||
return closestLinguistic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a possibilistic interval to a qualitative interval
|
||||
* @param {Object} interval - Possibilistic interval {min: number, max: number}
|
||||
* @param {QualitativeScale} scale - Target qualitative scale
|
||||
* @param {string} strategy - Conversion strategy: 'closest', 'floor', 'ceiling'
|
||||
* @returns {Object} Qualitative interval {lower: number, upper: number}
|
||||
*/
|
||||
static downgradeInterval(interval, scale, strategy = 'closest') {
|
||||
const lower = this.downgradePossibility(interval.min, scale, strategy);
|
||||
const upper = this.downgradePossibility(interval.max, scale, strategy);
|
||||
|
||||
// Ensure lower <= upper on the qualitative scale
|
||||
const orderedLower = scale.min(lower, upper);
|
||||
const orderedUpper = scale.max(lower, upper);
|
||||
|
||||
return { lower: orderedLower, upper: orderedUpper };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a qualitative interval to a possibilistic interval
|
||||
* @param {Object} interval - Qualitative interval {lower: number, upper: number}
|
||||
* @param {QualitativeScale} scale - Source qualitative scale
|
||||
* @param {string} strategy - Conversion strategy: 'direct', 'interpolate'
|
||||
* @returns {Object} Possibilistic interval {min: number, max: number}
|
||||
*/
|
||||
static upgradeInterval(interval, scale, strategy = 'direct') {
|
||||
const min = this.upgradePossibility(interval.lower, scale, strategy);
|
||||
const max = this.upgradePossibility(interval.upper, scale, strategy);
|
||||
|
||||
// Ensure min <= max for possibilistic intervals
|
||||
const orderedMin = Math.min(min, max);
|
||||
const orderedMax = Math.max(min, max);
|
||||
|
||||
return { min: orderedMin, max: orderedMax };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of possibilistic values to qualitative scale values
|
||||
* @param {number[]} possibilities - Array of possibility values
|
||||
* @param {QualitativeScale} scale - Target qualitative scale
|
||||
* @param {string} strategy - Conversion strategy
|
||||
* @returns {number[]} Array of qualitative scale values
|
||||
*/
|
||||
static downgradePossibilities(possibilities, scale, strategy = 'closest') {
|
||||
return possibilities.map(p => this.downgradePossibility(p, scale, strategy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an array of qualitative scale values to possibilistic values
|
||||
* @param {number[]} qualitativeValues - Array of qualitative scale values
|
||||
* @param {QualitativeScale} scale - Source qualitative scale
|
||||
* @param {string} strategy - Conversion strategy
|
||||
* @returns {number[]} Array of possibility values
|
||||
*/
|
||||
static upgradePossibilities(qualitativeValues, scale, strategy = 'direct') {
|
||||
return qualitativeValues.map(v => this.upgradePossibility(v, scale, strategy));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a hybrid fusion result that can work with both possibilistic and qualitative values
|
||||
* @param {Array} values - Array of values (mixed possibilistic and qualitative)
|
||||
* @param {QualitativeScale} targetScale - Target scale for conversion
|
||||
* @param {string} strategy - Conversion strategy
|
||||
* @returns {Object} Fusion result with both possibilistic and qualitative representations
|
||||
*/
|
||||
static createHybridFusion(values, targetScale, strategy = 'downgrade') {
|
||||
const convertedValues = values.map(value => {
|
||||
if (typeof value === 'number') {
|
||||
if (targetScale.contains(value)) {
|
||||
return { possibilistic: value, qualitative: value };
|
||||
} else {
|
||||
if (strategy === 'downgrade') {
|
||||
const qualitative = this.downgradePossibility(value, targetScale, 'closest');
|
||||
return { possibilistic: value, qualitative };
|
||||
} else {
|
||||
const possibilistic = this.upgradePossibility(value, targetScale, 'direct');
|
||||
return { possibilistic, qualitative: value };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { possibilistic: value, qualitative: value };
|
||||
});
|
||||
|
||||
return {
|
||||
possibilisticValues: convertedValues.map(v => v.possibilistic),
|
||||
qualitativeValues: convertedValues.map(v => v.qualitative),
|
||||
scale: targetScale,
|
||||
strategy
|
||||
};
|
||||
}
|
||||
|
||||
// ========== PRIVATE HELPER METHODS ==========
|
||||
|
||||
/**
|
||||
* Find the closest value on a scale to a given possibility
|
||||
* @private
|
||||
*/
|
||||
static _findClosestValue(possibility, scale, strategy) {
|
||||
const values = scale.values;
|
||||
let closest = values[0];
|
||||
let minDistance = Math.abs(possibility - closest);
|
||||
|
||||
for (const value of values) {
|
||||
const distance = Math.abs(possibility - value);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closest = value;
|
||||
}
|
||||
}
|
||||
|
||||
switch (strategy) {
|
||||
case 'floor':
|
||||
// Find the largest value <= possibility
|
||||
return values.filter(v => v <= possibility).pop() || values[0];
|
||||
case 'ceiling':
|
||||
// Find the smallest value >= possibility
|
||||
return values.find(v => v >= possibility) || values[values.length - 1];
|
||||
default:
|
||||
return closest;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate a value based on its position in the scale
|
||||
* @private
|
||||
*/
|
||||
static _interpolateValue(value, scale) {
|
||||
const values = scale.values;
|
||||
const index = scale.indexOf(value);
|
||||
|
||||
if (index >= 0) {
|
||||
return value; // Already on the scale
|
||||
}
|
||||
|
||||
// Find the two closest values for interpolation
|
||||
let lower = values[0];
|
||||
let upper = values[values.length - 1];
|
||||
|
||||
for (let i = 0; i < values.length - 1; i++) {
|
||||
if (value >= values[i] && value <= values[i + 1]) {
|
||||
lower = values[i];
|
||||
upper = values[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Linear interpolation
|
||||
const ratio = (value - lower) / (upper - lower);
|
||||
return lower + ratio * (upper - lower);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a linguistic expression to a possibility value
|
||||
* @private
|
||||
*/
|
||||
static _linguisticToPossibility(linguistic) {
|
||||
const mapping = LINGUISTIC_MAPPING[linguistic];
|
||||
if (!mapping) {
|
||||
throw new Error(`Unknown linguistic expression: ${linguistic}`);
|
||||
}
|
||||
return mapping.median;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available linguistic expressions
|
||||
* @returns {string[]} Array of linguistic expressions
|
||||
*/
|
||||
static getLinguisticExpressions() {
|
||||
return Object.keys(LINGUISTIC_MAPPING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get linguistic mapping for a specific expression
|
||||
* @param {string} linguistic - Linguistic expression
|
||||
* @returns {Object|null} Mapping object or null if not found
|
||||
*/
|
||||
static getLinguisticMapping(linguistic) {
|
||||
return LINGUISTIC_MAPPING[linguistic] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a possibility value is in the valid range
|
||||
* @param {number} possibility - Possibility value to validate
|
||||
* @returns {boolean} True if valid
|
||||
*/
|
||||
static isValidPossibility(possibility) {
|
||||
return typeof possibility === 'number' && possibility >= 0 && possibility <= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a qualitative value is on the given scale
|
||||
* @param {number} value - Qualitative value to validate
|
||||
* @param {QualitativeScale} scale - Scale to validate against
|
||||
* @returns {boolean} True if valid
|
||||
*/
|
||||
static isValidQualitative(value, scale) {
|
||||
return scale.contains(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a qualitative interval from possibilistic bounds
|
||||
* @param {number} lowerBound - Lower possibility bound
|
||||
* @param {number} upperBound - Upper possibility bound
|
||||
* @param {QualitativeScale} scale - Target qualitative scale
|
||||
* @param {string} strategy - Conversion strategy
|
||||
* @returns {Object} Qualitative interval {lower: number, upper: number}
|
||||
*/
|
||||
static createQualitativeInterval(lowerBound, upperBound, scale, strategy = 'closest') {
|
||||
const lower = this.downgradePossibility(lowerBound, scale, strategy);
|
||||
const upper = this.downgradePossibility(upperBound, scale, strategy);
|
||||
|
||||
return { lower, upper };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a possibilistic interval from qualitative bounds
|
||||
* @param {number} lowerBound - Lower qualitative bound
|
||||
* @param {number} upperBound - Upper qualitative bound
|
||||
* @param {QualitativeScale} scale - Source qualitative scale
|
||||
* @param {string} strategy - Conversion strategy
|
||||
* @returns {Object} Possibilistic interval {min: number, max: number}
|
||||
*/
|
||||
static createPossibilisticInterval(lowerBound, upperBound, scale, strategy = 'direct') {
|
||||
const min = this.upgradePossibility(lowerBound, scale, strategy);
|
||||
const max = this.upgradePossibility(upperBound, scale, strategy);
|
||||
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand a qualitative interval by adding uncertainty
|
||||
* @param {Object} interval - Qualitative interval {lower: number, upper: number}
|
||||
* @param {QualitativeScale} scale - Qualitative scale
|
||||
* @param {number} expansionSteps - Number of steps to expand on each side
|
||||
* @returns {Object} Expanded qualitative interval
|
||||
*/
|
||||
static expandQualitativeInterval(interval, scale, expansionSteps = 1) {
|
||||
const { lower, upper } = interval;
|
||||
|
||||
const lowerIndex = scale.indexOf(lower);
|
||||
const upperIndex = scale.indexOf(upper);
|
||||
|
||||
const expandedLowerIndex = Math.max(0, lowerIndex - expansionSteps);
|
||||
const expandedUpperIndex = Math.min(scale.size - 1, upperIndex + expansionSteps);
|
||||
|
||||
return {
|
||||
lower: scale.at(expandedLowerIndex),
|
||||
upper: scale.at(expandedUpperIndex)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Contract a qualitative interval by reducing uncertainty
|
||||
* @param {Object} interval - Qualitative interval {lower: number, upper: number}
|
||||
* @param {QualitativeScale} scale - Qualitative scale
|
||||
* @param {number} contractionSteps - Number of steps to contract on each side
|
||||
* @returns {Object} Contracted qualitative interval
|
||||
*/
|
||||
static contractQualitativeInterval(interval, scale, contractionSteps = 1) {
|
||||
const { lower, upper } = interval;
|
||||
|
||||
const lowerIndex = scale.indexOf(lower);
|
||||
const upperIndex = scale.indexOf(upper);
|
||||
|
||||
const contractedLowerIndex = Math.min(scale.size - 1, lowerIndex + contractionSteps);
|
||||
const contractedUpperIndex = Math.max(0, upperIndex - contractionSteps);
|
||||
|
||||
// Ensure contracted interval is valid (lower <= upper)
|
||||
const finalLowerIndex = Math.min(contractedLowerIndex, contractedUpperIndex);
|
||||
const finalUpperIndex = Math.max(contractedLowerIndex, contractedUpperIndex);
|
||||
|
||||
return {
|
||||
lower: scale.at(finalLowerIndex),
|
||||
upper: scale.at(finalUpperIndex)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the width of a qualitative interval in scale steps
|
||||
* @param {Object} interval - Qualitative interval {lower: number, upper: number}
|
||||
* @param {QualitativeScale} scale - Qualitative scale
|
||||
* @returns {number} Width in scale steps
|
||||
*/
|
||||
static getQualitativeIntervalWidth(interval, scale) {
|
||||
const { lower, upper } = interval;
|
||||
const lowerIndex = scale.indexOf(lower);
|
||||
const upperIndex = scale.indexOf(upper);
|
||||
|
||||
return upperIndex - lowerIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a qualitative interval contains a value
|
||||
* @param {Object} interval - Qualitative interval {lower: number, upper: number}
|
||||
* @param {number} value - Value to check
|
||||
* @param {QualitativeScale} scale - Qualitative scale
|
||||
* @returns {boolean} True if interval contains the value
|
||||
*/
|
||||
static qualitativeIntervalContains(interval, value, scale) {
|
||||
const { lower, upper } = interval;
|
||||
return scale.compare(value, lower) >= 0 && scale.compare(value, upper) <= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the center of a qualitative interval
|
||||
* @param {Object} interval - Qualitative interval {lower: number, upper: number}
|
||||
* @param {QualitativeScale} scale - Qualitative scale
|
||||
* @returns {number} Center value of the interval
|
||||
*/
|
||||
static getQualitativeIntervalCenter(interval, scale) {
|
||||
const { lower, upper } = interval;
|
||||
const lowerIndex = scale.indexOf(lower);
|
||||
const upperIndex = scale.indexOf(upper);
|
||||
const centerIndex = Math.floor((lowerIndex + upperIndex) / 2);
|
||||
|
||||
return scale.at(centerIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* QMT-based OWA Fusion - Theoretically Sound Capacity Combination
|
||||
*
|
||||
* This module implements OWA-like operators that work directly on Qualitative Möbius Transforms (QMTs),
|
||||
* ensuring that the resulting set-function is always a valid qualitative capacity (monotonic).
|
||||
*
|
||||
* This addresses the theoretical issues with pointwise OWA fusion by operating on the canonical
|
||||
* representation of capacities rather than their output values.
|
||||
*/
|
||||
|
||||
import { QualitativeScale, DEFAULT_QUALITATIVE_SCALE } from './QualitativeScale.js';
|
||||
import { QualitativeCapacity } from './QualitativeCapacity.js';
|
||||
|
||||
export class QMTOWAFusion {
|
||||
|
||||
/**
|
||||
* Optimistic QMT Fusion
|
||||
*
|
||||
* Combines QMTs in an optimistic manner by taking the maximum weight for each focal set
|
||||
* across all capacities. This preserves monotonicity since we're working with focal sets
|
||||
* and their weights directly.
|
||||
*
|
||||
* For each focal set E that appears in any capacity:
|
||||
* γ#_optimistic(E) = max_{i=1}^k γ#_i(E)
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities to combine
|
||||
* @returns {QualitativeCapacity} Optimistically fused capacity
|
||||
*/
|
||||
static optimisticFusion(capacities) {
|
||||
if (!capacities.length) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Validate all capacities use the same scale and state space
|
||||
for (const capacity of capacities) {
|
||||
if (!capacity.scale.equals(scale)) {
|
||||
throw new Error('All capacities must use the same qualitative scale');
|
||||
}
|
||||
if (capacity.stateSpace.length !== stateSpace.length) {
|
||||
throw new Error('All capacities must use the same state space');
|
||||
}
|
||||
}
|
||||
|
||||
const resultQMT = new Map();
|
||||
|
||||
// Collect all focal sets from all capacities using string keys for comparison
|
||||
const allFocalSetKeys = new Set();
|
||||
const focalSetMap = new Map(); // Map from string key to actual Set
|
||||
|
||||
for (const capacity of capacities) {
|
||||
for (const focalSet of capacity.getFocalSets()) {
|
||||
const key = Array.from(focalSet).sort().join(',');
|
||||
allFocalSetKeys.add(key);
|
||||
focalSetMap.set(key, focalSet);
|
||||
}
|
||||
}
|
||||
|
||||
// For each focal set, take the maximum weight across all capacities
|
||||
for (const key of allFocalSetKeys) {
|
||||
const focalSet = focalSetMap.get(key);
|
||||
let maxWeight = scale.bottom;
|
||||
|
||||
for (const capacity of capacities) {
|
||||
const weight = capacity.getQMT(focalSet);
|
||||
maxWeight = scale.max(maxWeight, weight);
|
||||
}
|
||||
|
||||
if (maxWeight !== scale.bottom) {
|
||||
resultQMT.set(focalSet, maxWeight);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pessimistic QMT Fusion
|
||||
*
|
||||
* Combines QMTs in a pessimistic manner by taking the minimum weight for each focal set
|
||||
* that appears in ALL capacities. This is more conservative than optimistic fusion.
|
||||
*
|
||||
* For each focal set E that appears in ALL capacities:
|
||||
* γ#_pessimistic(E) = min_{i=1}^k γ#_i(E)
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities to combine
|
||||
* @returns {QualitativeCapacity} Pessimistically fused capacity
|
||||
*/
|
||||
static pessimisticFusion(capacities) {
|
||||
if (!capacities.length) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Validate all capacities use the same scale and state space
|
||||
for (const capacity of capacities) {
|
||||
if (!capacity.scale.equals(scale)) {
|
||||
throw new Error('All capacities must use the same qualitative scale');
|
||||
}
|
||||
if (capacity.stateSpace.length !== stateSpace.length) {
|
||||
throw new Error('All capacities must use the same state space');
|
||||
}
|
||||
}
|
||||
|
||||
const resultQMT = new Map();
|
||||
|
||||
// Find focal sets that appear in ALL capacities
|
||||
const firstCapacityFocalSets = capacities[0].getFocalSets();
|
||||
|
||||
for (const focalSet of firstCapacityFocalSets) {
|
||||
// Check if this focal set appears in all capacities
|
||||
let appearsInAll = true;
|
||||
let minWeight = scale.top;
|
||||
|
||||
for (const capacity of capacities) {
|
||||
// Check if this focal set exists in the capacity by comparing with all focal sets
|
||||
let found = false;
|
||||
for (const capFocalSet of capacity.getFocalSets()) {
|
||||
if (this._setsEqual(focalSet, capFocalSet)) {
|
||||
found = true;
|
||||
const weight = capacity.getQMT(capFocalSet);
|
||||
minWeight = scale.min(minWeight, weight);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
appearsInAll = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (appearsInAll && minWeight !== scale.bottom) {
|
||||
resultQMT.set(focalSet, minWeight);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Majority QMT Fusion
|
||||
*
|
||||
* Combines QMTs using a majority rule: for each focal set, take the median weight
|
||||
* across all capacities that contain it.
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities to combine
|
||||
* @returns {QualitativeCapacity} Majority fused capacity
|
||||
*/
|
||||
static majorityFusion(capacities) {
|
||||
if (!capacities.length) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Validate all capacities use the same scale and state space
|
||||
for (const capacity of capacities) {
|
||||
if (!capacity.scale.equals(scale)) {
|
||||
throw new Error('All capacities must use the same qualitative scale');
|
||||
}
|
||||
if (capacity.stateSpace.length !== stateSpace.length) {
|
||||
throw new Error('All capacities must use the same state space');
|
||||
}
|
||||
}
|
||||
|
||||
const resultQMT = new Map();
|
||||
|
||||
// Collect all focal sets from all capacities using string keys for comparison
|
||||
const allFocalSetKeys = new Set();
|
||||
const focalSetMap = new Map(); // Map from string key to actual Set
|
||||
|
||||
for (const capacity of capacities) {
|
||||
for (const focalSet of capacity.getFocalSets()) {
|
||||
const key = Array.from(focalSet).sort().join(',');
|
||||
allFocalSetKeys.add(key);
|
||||
focalSetMap.set(key, focalSet);
|
||||
}
|
||||
}
|
||||
|
||||
// For each focal set, compute median weight
|
||||
for (const key of allFocalSetKeys) {
|
||||
const focalSet = focalSetMap.get(key);
|
||||
const weights = [];
|
||||
|
||||
for (const capacity of capacities) {
|
||||
const weight = capacity.getQMT(focalSet);
|
||||
if (weight !== scale.bottom) {
|
||||
weights.push(weight);
|
||||
}
|
||||
}
|
||||
|
||||
if (weights.length > 0) {
|
||||
// Sort weights and take median
|
||||
weights.sort((a, b) => scale.compare(a, b));
|
||||
const medianIndex = Math.floor(weights.length / 2);
|
||||
const medianWeight = weights[medianIndex];
|
||||
|
||||
resultQMT.set(focalSet, medianWeight);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority-weighted QMT Fusion
|
||||
*
|
||||
* Combines QMTs using priority weights. For each focal set, the result is the
|
||||
* weighted maximum where weights are determined by capacity priorities.
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities to combine
|
||||
* @param {Array<number>} priorities - Priority weights for each capacity
|
||||
* @returns {QualitativeCapacity} Priority-weighted fused capacity
|
||||
*/
|
||||
static priorityFusion(capacities, priorities) {
|
||||
if (!capacities.length) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
if (!priorities || priorities.length !== capacities.length) {
|
||||
throw new Error('Priorities array must have same length as capacities array');
|
||||
}
|
||||
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Validate all capacities use the same scale and state space
|
||||
for (const capacity of capacities) {
|
||||
if (!capacity.scale.equals(scale)) {
|
||||
throw new Error('All capacities must use the same qualitative scale');
|
||||
}
|
||||
if (capacity.stateSpace.length !== stateSpace.length) {
|
||||
throw new Error('All capacities must use the same state space');
|
||||
}
|
||||
}
|
||||
|
||||
const resultQMT = new Map();
|
||||
|
||||
// Collect all focal sets from all capacities using string keys for comparison
|
||||
const allFocalSetKeys = new Set();
|
||||
const focalSetMap = new Map(); // Map from string key to actual Set
|
||||
|
||||
for (const capacity of capacities) {
|
||||
for (const focalSet of capacity.getFocalSets()) {
|
||||
const key = Array.from(focalSet).sort().join(',');
|
||||
allFocalSetKeys.add(key);
|
||||
focalSetMap.set(key, focalSet);
|
||||
}
|
||||
}
|
||||
|
||||
// For each focal set, compute priority-weighted result
|
||||
for (const key of allFocalSetKeys) {
|
||||
const focalSet = focalSetMap.get(key);
|
||||
let bestWeight = scale.bottom;
|
||||
let bestPriority = -1;
|
||||
|
||||
for (let i = 0; i < capacities.length; i++) {
|
||||
const weight = capacities[i].getQMT(focalSet);
|
||||
const priority = priorities[i] || 0;
|
||||
|
||||
if (weight !== scale.bottom && priority > bestPriority) {
|
||||
bestWeight = weight;
|
||||
bestPriority = priority;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestWeight !== scale.bottom) {
|
||||
resultQMT.set(focalSet, bestWeight);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom QMT Fusion with OWA-like Weights
|
||||
*
|
||||
* This is a more sophisticated fusion method that attempts to implement
|
||||
* OWA-like behavior directly on QMTs. It's an experimental approach that
|
||||
* requires further theoretical development.
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities to combine
|
||||
* @param {Array<number>} owaWeights - OWA weights for each capacity
|
||||
* @param {string} mode - Fusion mode ('optimistic', 'pessimistic', 'majority')
|
||||
* @returns {QualitativeCapacity} Custom fused capacity
|
||||
*/
|
||||
static customOWAFusion(capacities, owaWeights, mode = 'optimistic') {
|
||||
if (!capacities.length) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
if (!owaWeights || owaWeights.length !== capacities.length) {
|
||||
throw new Error('OWA weights array must have same length as capacities array');
|
||||
}
|
||||
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Validate all capacities use the same scale and state space
|
||||
for (const capacity of capacities) {
|
||||
if (!capacity.scale.equals(scale)) {
|
||||
throw new Error('All capacities must use the same qualitative scale');
|
||||
}
|
||||
if (capacity.stateSpace.length !== stateSpace.length) {
|
||||
throw new Error('All capacities must use the same state space');
|
||||
}
|
||||
}
|
||||
|
||||
// For now, fall back to optimistic fusion with priority weighting
|
||||
// This is a placeholder for more sophisticated QMT-based OWA implementation
|
||||
const priorities = owaWeights.map(w => w * 100); // Convert to priority scale
|
||||
return this.priorityFusion(capacities, priorities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if two sets are equal
|
||||
* @param {Set} set1 - First set
|
||||
* @param {Set} set2 - Second set
|
||||
* @returns {boolean} True if sets are equal
|
||||
*/
|
||||
static _setsEqual(set1, set2) {
|
||||
if (set1.size !== set2.size) return false;
|
||||
for (const item of set1) {
|
||||
if (!set2.has(item)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Qualitative Capacity Implementation
|
||||
*
|
||||
* Implements qualitative capacities (q-capacities) as described in the research paper.
|
||||
* A qualitative capacity γ: 2^W → L is a monotonic set-function where:
|
||||
* - γ(∅) = 0, γ(W) = 1
|
||||
* - If A ⊆ B, then γ(A) ≤ γ(B)
|
||||
*
|
||||
* The core design principle is to use the Qualitative Möbius Transform (QMT) γ#
|
||||
* as the canonical internal representation for any q-capacity γ.
|
||||
*/
|
||||
|
||||
import { QualitativeScale, DEFAULT_QUALITATIVE_SCALE } from './QualitativeScale.js';
|
||||
import { getSetKey, setFromKey, setsEqual } from './SetUtils.js';
|
||||
|
||||
export class QualitativeCapacity {
|
||||
constructor(stateSpace, scale = DEFAULT_QUALITATIVE_SCALE, qmt = null) {
|
||||
if (!Array.isArray(stateSpace) || stateSpace.length === 0) {
|
||||
throw new Error('State space must be a non-empty array');
|
||||
}
|
||||
|
||||
this.stateSpace = [...new Set(stateSpace)]; // Ensure unique states
|
||||
this.scale = scale;
|
||||
|
||||
// Internal representation: QMT as a Map from canonical string keys to scale values
|
||||
// Only store non-zero entries
|
||||
this.qmt = new Map();
|
||||
|
||||
if (qmt) {
|
||||
this._initializeFromQMT(qmt);
|
||||
} else {
|
||||
// Initialize as vacuous capacity (ignorance)
|
||||
this.qmt.set(getSetKey(new Set(this.stateSpace)), scale.top);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize from a QMT representation
|
||||
* @param {Map|Object} qmt - QMT as Map or object with subset keys
|
||||
*/
|
||||
_initializeFromQMT(qmt) {
|
||||
this.qmt.clear();
|
||||
|
||||
if (qmt instanceof Map) {
|
||||
for (const [subset, value] of qmt) {
|
||||
if (value !== this.scale.bottom) {
|
||||
// If subset is already a Set, use it directly. Canonical string
|
||||
// keys ('a,b,c') come from getSetKey and must be split back into
|
||||
// elements — new Set('abc') would iterate characters.
|
||||
const subsetSet = subset instanceof Set
|
||||
? subset
|
||||
: (typeof subset === 'string' ? setFromKey(subset) : new Set(subset));
|
||||
this.qmt.set(getSetKey(subsetSet), value);
|
||||
}
|
||||
}
|
||||
} else if (typeof qmt === 'object') {
|
||||
for (const [key, value] of Object.entries(qmt)) {
|
||||
if (value !== this.scale.bottom) {
|
||||
const subset = new Set(JSON.parse(key));
|
||||
this.qmt.set(getSetKey(subset), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the capacity value for a subset A ⊆ W
|
||||
* γ(A) = max_{B ⊆ A, B ∈ dom(γ#)} γ#(B)
|
||||
*/
|
||||
getCapacity(subset) {
|
||||
const A = new Set(subset);
|
||||
|
||||
// Find all focal sets (keys in QMT) that are subsets of A
|
||||
let maxValue = this.scale.bottom;
|
||||
|
||||
for (const [key, value] of this.qmt) {
|
||||
const focalSet = setFromKey(key, this.stateSpace);
|
||||
if (this._isSubset(focalSet, A)) {
|
||||
maxValue = this.scale.max(maxValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the capacity value for a subset by updating the QMT
|
||||
* This is a complex operation that may require recomputing the entire QMT
|
||||
*/
|
||||
setCapacity(subset, value) {
|
||||
const A = new Set(subset);
|
||||
const key = getSetKey(A);
|
||||
|
||||
// For now, we'll implement a simple approach:
|
||||
// Add this subset as a focal set with the given value
|
||||
// In a full implementation, we'd need to recompute the QMT properly
|
||||
if (value !== this.scale.bottom) {
|
||||
this.qmt.set(key, value);
|
||||
} else {
|
||||
this.qmt.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Qualitative Möbius Transform value for a subset
|
||||
*/
|
||||
getQMT(subset) {
|
||||
const A = new Set(subset);
|
||||
const key = getSetKey(A);
|
||||
|
||||
// Direct lookup using canonical key
|
||||
return this.qmt.get(key) || this.scale.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all focal sets (subsets with non-zero QMT values)
|
||||
*/
|
||||
getFocalSets() {
|
||||
return Array.from(this.qmt.keys()).map(key => setFromKey(key, this.stateSpace));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the QMT as a Map
|
||||
*/
|
||||
getQMTMap() {
|
||||
const result = new Map();
|
||||
for (const [key, value] of this.qmt) {
|
||||
result.set(setFromKey(key, this.stateSpace), value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a possibility measure
|
||||
* A capacity is a possibility measure if all focal sets are singletons
|
||||
*/
|
||||
isPossibilityMeasure() {
|
||||
for (const key of this.qmt.keys()) {
|
||||
const focalSet = setFromKey(key, this.stateSpace);
|
||||
if (focalSet.size !== 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a necessity measure
|
||||
* A capacity is a necessity measure if all focal sets form a nested chain
|
||||
*/
|
||||
isNecessityMeasure() {
|
||||
const focalSets = this.getFocalSets();
|
||||
if (focalSets.length === 0) return true;
|
||||
|
||||
// Check if all focal sets are nested (form a chain)
|
||||
for (let i = 0; i < focalSets.length; i++) {
|
||||
for (let j = i + 1; j < focalSets.length; j++) {
|
||||
const A = focalSets[i];
|
||||
const B = focalSets[j];
|
||||
if (!this._isSubset(A, B) && !this._isSubset(B, A)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the contour function π_γ
|
||||
* π_γ(w) = max_{B: w ∈ B} γ#(B)
|
||||
*/
|
||||
getContourFunction() {
|
||||
const contour = new Map();
|
||||
|
||||
for (const state of this.stateSpace) {
|
||||
let maxValue = this.scale.bottom;
|
||||
|
||||
for (const [key, value] of this.qmt) {
|
||||
const focalSet = setFromKey(key, this.stateSpace);
|
||||
if (focalSet.has(state)) {
|
||||
maxValue = this.scale.max(maxValue, value);
|
||||
}
|
||||
}
|
||||
|
||||
contour.set(state, maxValue);
|
||||
}
|
||||
|
||||
return contour;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the upper capacity (possibility measure) Pl_γ
|
||||
* Pl_γ(A) = max_{w ∈ A} π_γ(w)
|
||||
*/
|
||||
getUpperCapacity() {
|
||||
const contour = this.getContourFunction();
|
||||
|
||||
// Create a new capacity that is a possibility measure
|
||||
const upperQMT = new Map();
|
||||
|
||||
for (const [state, value] of contour) {
|
||||
if (value !== this.scale.bottom) {
|
||||
upperQMT.set(new Set([state]), value);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(this.stateSpace, this.scale, upperQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the conjugate capacity γ^c
|
||||
* γ^c(A) = ν(γ(A^c))
|
||||
*/
|
||||
getConjugate() {
|
||||
const conjugateQMT = new Map();
|
||||
|
||||
// For each subset A, compute γ^c(A) = ν(γ(A^c))
|
||||
const allSubsets = this._generateAllSubsets();
|
||||
|
||||
for (const A of allSubsets) {
|
||||
const complement = new Set(this.stateSpace.filter(s => !A.has(s)));
|
||||
const complementValue = this.getCapacity(complement);
|
||||
const conjugateValue = this.scale.negate(complementValue);
|
||||
|
||||
if (conjugateValue !== this.scale.bottom) {
|
||||
conjugateQMT.set(A, conjugateValue);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(this.stateSpace, this.scale, conjugateQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if A is a subset of B
|
||||
*/
|
||||
_isSubset(A, B) {
|
||||
for (const element of A) {
|
||||
if (!B.has(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all possible subsets of the state space
|
||||
*/
|
||||
_generateAllSubsets() {
|
||||
const subsets = [];
|
||||
const n = this.stateSpace.length;
|
||||
|
||||
// Generate all 2^n subsets
|
||||
for (let i = 0; i < (1 << n); i++) {
|
||||
const subset = new Set();
|
||||
for (let j = 0; j < n; j++) {
|
||||
if (i & (1 << j)) {
|
||||
subset.add(this.stateSpace[j]);
|
||||
}
|
||||
}
|
||||
subsets.push(subset);
|
||||
}
|
||||
|
||||
return subsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Simple Support Capacity (SSC)
|
||||
* A necessity measure with focal sets A and W
|
||||
*/
|
||||
static createSimpleSupport(stateSpace, supportSet, supportValue, scale = DEFAULT_QUALITATIVE_SCALE) {
|
||||
const A = new Set(supportSet);
|
||||
const W = new Set(stateSpace);
|
||||
|
||||
const qmt = new Map();
|
||||
qmt.set(A, supportValue);
|
||||
qmt.set(W, scale.top);
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a possibility measure from a possibility distribution
|
||||
*/
|
||||
static createPossibilityMeasure(stateSpace, possibilityDistribution, scale = DEFAULT_QUALITATIVE_SCALE) {
|
||||
const qmt = new Map();
|
||||
|
||||
for (const [state, value] of Object.entries(possibilityDistribution)) {
|
||||
if (value !== scale.bottom) {
|
||||
qmt.set(new Set([state]), value);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a necessity measure from a possibility distribution
|
||||
*/
|
||||
static createNecessityMeasure(stateSpace, possibilityDistribution, scale = DEFAULT_QUALITATIVE_SCALE) {
|
||||
const capacity = this.createPossibilityMeasure(stateSpace, possibilityDistribution, scale);
|
||||
return capacity.getConjugate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string representation of the capacity
|
||||
*/
|
||||
toString() {
|
||||
const focalSets = Array.from(this.qmt.entries())
|
||||
.map(([key, value]) => `{${key || '∅'}}:${value}`)
|
||||
.join(', ');
|
||||
|
||||
return `QualitativeCapacity(${this.stateSpace.length} states, ${this.qmt.size} focal sets): {${focalSets}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this capacity equals another
|
||||
*/
|
||||
equals(other) {
|
||||
if (!(other instanceof QualitativeCapacity)) return false;
|
||||
if (!this.scale.equals(other.scale)) return false;
|
||||
if (this.stateSpace.length !== other.stateSpace.length) return false;
|
||||
|
||||
// Check if all focal sets and values match
|
||||
if (this.qmt.size !== other.qmt.size) return false;
|
||||
|
||||
for (const [key, value] of this.qmt) {
|
||||
if (other.qmt.get(key) !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Qualitative Capacity Fusion Rules
|
||||
*
|
||||
* Implements the fusion operations for qualitative capacities as described in the research paper.
|
||||
* The core operations are:
|
||||
* 1. Dempster-like Maxmin Conjunctive Rule (⊗)
|
||||
* 2. Normalized Conjunctive Rule (⊗̂)
|
||||
* 3. Disjunctive Rule (⊕)
|
||||
*
|
||||
* All operations work on the Qualitative Möbius Transform (QMT) representation.
|
||||
*/
|
||||
|
||||
import { QualitativeCapacity } from './QualitativeCapacity.js';
|
||||
|
||||
export class QualitativeFusion {
|
||||
|
||||
/**
|
||||
* Dempster-like Maxmin Conjunctive Rule (⊗)
|
||||
*
|
||||
* Given two QMTs ρ₁ and ρ₂, compute the unnormalized combination:
|
||||
* ρ_raw(A) = max_{E₁ ∩ E₂ = A} min(ρ₁(E₁), ρ₂(E₂))
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity1 - First capacity
|
||||
* @param {QualitativeCapacity} capacity2 - Second capacity
|
||||
* @returns {Map} Raw QMT result (unnormalized)
|
||||
*/
|
||||
static dempsterLikeConjunctive(capacity1, capacity2) {
|
||||
if (!capacity1.scale.equals(capacity2.scale)) {
|
||||
throw new Error('Capacities must use the same qualitative scale');
|
||||
}
|
||||
|
||||
const scale = capacity1.scale;
|
||||
const stateSpace = capacity1.stateSpace;
|
||||
const qmt1 = capacity1.getQMTMap();
|
||||
const qmt2 = capacity2.getQMTMap();
|
||||
|
||||
const resultQMT = new Map();
|
||||
|
||||
// For each pair of focal sets from both capacities
|
||||
for (const [E1, v1] of qmt1) {
|
||||
for (const [E2, v2] of qmt2) {
|
||||
// Compute intersection
|
||||
const intersection = new Set([...E1].filter(x => E2.has(x)));
|
||||
|
||||
// Compute min value
|
||||
const minValue = scale.min(v1, v2);
|
||||
|
||||
// Update result: max of existing value and new min value
|
||||
const existingValue = resultQMT.get(intersection) || scale.bottom;
|
||||
const newValue = scale.max(existingValue, minValue);
|
||||
|
||||
if (newValue !== scale.bottom) {
|
||||
resultQMT.set(intersection, newValue);
|
||||
} else {
|
||||
resultQMT.delete(intersection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resultQMT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalized Conjunctive Rule (⊗̂)
|
||||
*
|
||||
* Applies the Dempster-like rule followed by normalization:
|
||||
* 1. Compute raw combination using ⊗
|
||||
* 2. Bottom normalization: remove empty set entry
|
||||
* 3. Top normalization: ensure at least one focal set has weight 1
|
||||
*
|
||||
* @param {Array<QualitativeCapacity>} capacities - Array of capacities to combine
|
||||
* @returns {QualitativeCapacity} Normalized combined capacity
|
||||
*/
|
||||
static normalizedConjunctive(capacities) {
|
||||
if (!Array.isArray(capacities) || capacities.length === 0) {
|
||||
throw new Error('At least one capacity is required');
|
||||
}
|
||||
|
||||
if (capacities.length === 1) {
|
||||
return capacities[0];
|
||||
}
|
||||
|
||||
// Start with the first capacity
|
||||
let resultQMT = capacities[0].getQMTMap();
|
||||
const scale = capacities[0].scale;
|
||||
const stateSpace = capacities[0].stateSpace;
|
||||
|
||||
// Apply conjunctive rule iteratively
|
||||
for (let i = 1; i < capacities.length; i++) {
|
||||
const tempCapacity = new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
resultQMT = this.dempsterLikeConjunctive(tempCapacity, capacities[i]);
|
||||
}
|
||||
|
||||
// Bottom normalization: remove empty set entry
|
||||
resultQMT.delete(new Set());
|
||||
|
||||
// Top normalization: ensure at least one focal set has weight 1
|
||||
const maxValue = scale.maxAll(Array.from(resultQMT.values()));
|
||||
if (maxValue < scale.top) {
|
||||
resultQMT.set(new Set(stateSpace), scale.top);
|
||||
}
|
||||
|
||||
// Create new capacity from normalized QMT
|
||||
const resultCapacity = new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
|
||||
// Convert to canonical QMT representation
|
||||
return this._convertToCanonicalQMT(resultCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disjunctive Rule (⊕)
|
||||
*
|
||||
* For two capacities γ₁ and γ₂, the disjunctive combination is:
|
||||
* γ_⊕(A) = min(γ₁(A), γ₂(A))
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity1 - First capacity
|
||||
* @param {QualitativeCapacity} capacity2 - Second capacity
|
||||
* @returns {QualitativeCapacity} Disjunctive combination
|
||||
*/
|
||||
static disjunctive(capacity1, capacity2) {
|
||||
if (!capacity1.scale.equals(capacity2.scale)) {
|
||||
throw new Error('Capacities must use the same qualitative scale');
|
||||
}
|
||||
|
||||
const scale = capacity1.scale;
|
||||
const stateSpace = capacity1.stateSpace;
|
||||
|
||||
// Generate all possible subsets
|
||||
const allSubsets = this._generateAllSubsets(stateSpace);
|
||||
const resultQMT = new Map();
|
||||
|
||||
for (const subset of allSubsets) {
|
||||
const value1 = capacity1.getCapacity(subset);
|
||||
const value2 = capacity2.getCapacity(subset);
|
||||
const minValue = scale.min(value1, value2);
|
||||
|
||||
if (minValue !== scale.bottom) {
|
||||
resultQMT.set(subset, minValue);
|
||||
}
|
||||
}
|
||||
|
||||
const resultCapacity = new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
|
||||
// Convert to canonical QMT representation for consistency
|
||||
return this._convertToCanonicalQMT(resultCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a capacity to its canonical QMT representation
|
||||
*
|
||||
* The canonical QMT γ#(E) = γ(E) if γ(E) > max_{B ⊂ E} γ(B), 0 otherwise
|
||||
*
|
||||
* OPTIMIZED: Uses the fact that γ#(A) > 0 ⟺ γ(A) > max_{w∈A} γ(A∖{w})
|
||||
* This is much more efficient than checking all proper subsets.
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity - Capacity to convert
|
||||
* @returns {QualitativeCapacity} Capacity with canonical QMT
|
||||
*/
|
||||
static _convertToCanonicalQMT(capacity) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = capacity.stateSpace;
|
||||
const canonicalQMT = new Map();
|
||||
|
||||
// Generate all subsets in order of increasing size
|
||||
const allSubsets = this._generateAllSubsets(stateSpace);
|
||||
allSubsets.sort((a, b) => a.size - b.size);
|
||||
|
||||
for (const subset of allSubsets) {
|
||||
const capacityValue = capacity.getCapacity(subset);
|
||||
|
||||
// OPTIMIZATION: Only check immediate proper subsets (size |A| - 1)
|
||||
// Due to monotonicity, if γ(A) > max_{w∈A} γ(A∖{w}), then γ(A) > max_{B⊂A} γ(B)
|
||||
let maxImmediateSubsetValue = scale.bottom;
|
||||
for (const element of subset) {
|
||||
const immediateSubset = new Set(subset);
|
||||
immediateSubset.delete(element);
|
||||
const subsetValue = capacity.getCapacity(immediateSubset);
|
||||
maxImmediateSubsetValue = scale.max(maxImmediateSubsetValue, subsetValue);
|
||||
}
|
||||
|
||||
// If capacity value is greater than max of immediate proper subsets, it's a focal set
|
||||
if (scale.compare(capacityValue, maxImmediateSubsetValue) > 0) {
|
||||
canonicalQMT.set(subset, capacityValue);
|
||||
}
|
||||
}
|
||||
|
||||
return new QualitativeCapacity(stateSpace, scale, canonicalQMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all possible subsets of a state space
|
||||
* @param {Array} stateSpace - Array of states
|
||||
* @returns {Array<Set>} Array of all possible subsets
|
||||
*/
|
||||
static _generateAllSubsets(stateSpace) {
|
||||
const subsets = [];
|
||||
const n = stateSpace.length;
|
||||
|
||||
// Generate all 2^n subsets
|
||||
for (let i = 0; i < (1 << n); i++) {
|
||||
const subset = new Set();
|
||||
for (let j = 0; j < n; j++) {
|
||||
if (i & (1 << j)) {
|
||||
subset.add(stateSpace[j]);
|
||||
}
|
||||
}
|
||||
subsets.push(subset);
|
||||
}
|
||||
|
||||
return subsets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if A is a subset of B
|
||||
* @param {Set} A - First set
|
||||
* @param {Set} B - Second set
|
||||
* @returns {boolean} True if A ⊆ B
|
||||
*/
|
||||
static _isSubset(A, B) {
|
||||
for (const element of A) {
|
||||
if (!B.has(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the Sugeno Integral
|
||||
*
|
||||
* S_γ(f) = max_{A ⊆ W} min(γ(A), min_{w ∈ A} f(w))
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity - The capacity
|
||||
* @param {Map|Object} decisionFunction - Function f: W → L
|
||||
* @returns {number} Sugeno integral value
|
||||
*/
|
||||
static sugenoIntegral(capacity, decisionFunction) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = capacity.stateSpace;
|
||||
|
||||
// Convert decision function to Map if needed
|
||||
const f = decisionFunction instanceof Map ? decisionFunction : new Map(Object.entries(decisionFunction));
|
||||
|
||||
// Generate all subsets
|
||||
const allSubsets = this._generateAllSubsets(stateSpace);
|
||||
let maxValue = scale.bottom;
|
||||
|
||||
for (const subset of allSubsets) {
|
||||
if (subset.size === 0) continue;
|
||||
|
||||
// Compute min_{w ∈ A} f(w)
|
||||
let minFValue = scale.top;
|
||||
for (const state of subset) {
|
||||
const fValue = f.get(state) || scale.bottom;
|
||||
minFValue = scale.min(minFValue, fValue);
|
||||
}
|
||||
|
||||
// Compute min(γ(A), min_{w ∈ A} f(w))
|
||||
const capacityValue = capacity.getCapacity(subset);
|
||||
const minValue = scale.min(capacityValue, minFValue);
|
||||
|
||||
// Take maximum over all subsets
|
||||
maxValue = scale.max(maxValue, minValue);
|
||||
}
|
||||
|
||||
return maxValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pessimistic counterpart γ⁻
|
||||
* γ⁻(A) = min(γ(A), ν(γ(A^c)))
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity - The capacity
|
||||
* @returns {QualitativeCapacity} Pessimistic counterpart
|
||||
*/
|
||||
static pessimisticCounterpart(capacity) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = capacity.stateSpace;
|
||||
const allSubsets = this._generateAllSubsets(stateSpace);
|
||||
const resultQMT = new Map();
|
||||
|
||||
for (const subset of allSubsets) {
|
||||
const capacityValue = capacity.getCapacity(subset);
|
||||
const complement = new Set(stateSpace.filter(s => !subset.has(s)));
|
||||
const complementValue = capacity.getCapacity(complement);
|
||||
const negatedComplementValue = scale.negate(complementValue);
|
||||
|
||||
const pessimisticValue = scale.min(capacityValue, negatedComplementValue);
|
||||
|
||||
if (pessimisticValue !== scale.bottom) {
|
||||
resultQMT.set(subset, pessimisticValue);
|
||||
}
|
||||
}
|
||||
|
||||
const resultCapacity = new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
|
||||
// Convert to canonical QMT representation for consistency
|
||||
return this._convertToCanonicalQMT(resultCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute optimistic counterpart γ⁺
|
||||
* γ⁺(A) = max(γ(A), ν(γ(A^c)))
|
||||
*
|
||||
* @param {QualitativeCapacity} capacity - The capacity
|
||||
* @returns {QualitativeCapacity} Optimistic counterpart
|
||||
*/
|
||||
static optimisticCounterpart(capacity) {
|
||||
const scale = capacity.scale;
|
||||
const stateSpace = capacity.stateSpace;
|
||||
const allSubsets = this._generateAllSubsets(stateSpace);
|
||||
const resultQMT = new Map();
|
||||
|
||||
for (const subset of allSubsets) {
|
||||
const capacityValue = capacity.getCapacity(subset);
|
||||
const complement = new Set(stateSpace.filter(s => !subset.has(s)));
|
||||
const complementValue = capacity.getCapacity(complement);
|
||||
const negatedComplementValue = scale.negate(complementValue);
|
||||
|
||||
const optimisticValue = scale.max(capacityValue, negatedComplementValue);
|
||||
|
||||
if (optimisticValue !== scale.bottom) {
|
||||
resultQMT.set(subset, optimisticValue);
|
||||
}
|
||||
}
|
||||
|
||||
const resultCapacity = new QualitativeCapacity(stateSpace, scale, resultQMT);
|
||||
|
||||
// Convert to canonical QMT representation for consistency
|
||||
return this._convertToCanonicalQMT(resultCapacity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Qualitative Scale System for Qualitative Capacities
|
||||
*
|
||||
* Implements finite totally ordered scales with order-reversing negation
|
||||
* as described in the research paper on qualitative capacities.
|
||||
*
|
||||
* A qualitative scale L is a finite, totally ordered set of values where:
|
||||
* - Only min, max, and comparison operators are used (no arithmetic)
|
||||
* - 0 and 1 are the bottom and top elements
|
||||
* - An order-reversing negation map ν: L → L exists where ν(ν(λ)) = λ
|
||||
*/
|
||||
|
||||
export class QualitativeScale {
|
||||
constructor(values, name = 'default') {
|
||||
if (!Array.isArray(values) || values.length === 0) {
|
||||
throw new Error('QualitativeScale requires a non-empty array of values');
|
||||
}
|
||||
|
||||
// Ensure values are sorted and unique
|
||||
this.values = [...new Set(values)].sort((a, b) => a - b);
|
||||
this.name = name;
|
||||
this.bottom = this.values[0];
|
||||
this.top = this.values[this.values.length - 1];
|
||||
|
||||
// Validate that bottom is 0 and top is 1 (or equivalent)
|
||||
if (this.bottom !== 0 && this.bottom !== 0.0) {
|
||||
console.warn(`QualitativeScale: bottom value should be 0, got ${this.bottom}`);
|
||||
}
|
||||
if (this.top !== 1 && this.top !== 1.0) {
|
||||
console.warn(`QualitativeScale: top value should be 1, got ${this.top}`);
|
||||
}
|
||||
|
||||
// Create order-reversing negation map
|
||||
this._createNegationMap();
|
||||
|
||||
// Create Set for O(1) contains() lookups
|
||||
this.valueSet = new Set(this.values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the order-reversing negation map ν: L → L
|
||||
* For a scale [0, a, b, ..., 1], the negation maps:
|
||||
* 0 → 1, a → ..., b → ..., 1 → 0
|
||||
*/
|
||||
_createNegationMap() {
|
||||
this.negationMap = new Map();
|
||||
const n = this.values.length;
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const value = this.values[i];
|
||||
const negatedValue = this.values[n - 1 - i];
|
||||
this.negationMap.set(value, negatedValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the negation of a value: ν(λ)
|
||||
*/
|
||||
negate(value) {
|
||||
if (!this.negationMap.has(value)) {
|
||||
throw new Error(`Value ${value} not found in scale ${this.name}`);
|
||||
}
|
||||
return this.negationMap.get(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a value is in the scale
|
||||
* OPTIMIZED: Uses Set for O(1) average time complexity
|
||||
*/
|
||||
contains(value) {
|
||||
return this.valueSet.has(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum of two values
|
||||
*/
|
||||
min(a, b) {
|
||||
if (!this.contains(a) || !this.contains(b)) {
|
||||
throw new Error(`Values must be in scale ${this.name}`);
|
||||
}
|
||||
return a <= b ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum of two values
|
||||
*/
|
||||
max(a, b) {
|
||||
if (!this.contains(a) || !this.contains(b)) {
|
||||
throw new Error(`Values must be in scale ${this.name}`);
|
||||
}
|
||||
return a >= b ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minimum of multiple values
|
||||
*/
|
||||
minAll(values) {
|
||||
if (!values.length) return this.bottom;
|
||||
return values.reduce((acc, val) => this.min(acc, val), this.top);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum of multiple values
|
||||
*/
|
||||
maxAll(values) {
|
||||
if (!values.length) return this.bottom;
|
||||
return values.reduce((acc, val) => this.max(acc, val), this.bottom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two values: returns -1, 0, or 1
|
||||
*/
|
||||
compare(a, b) {
|
||||
if (!this.contains(a) || !this.contains(b)) {
|
||||
throw new Error(`Values must be in scale ${this.name}`);
|
||||
}
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index of a value in the scale
|
||||
* OPTIMIZED: Uses binary search for O(log n) time complexity
|
||||
*/
|
||||
indexOf(value) {
|
||||
if (!this.contains(value)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Binary search since values are sorted
|
||||
let left = 0;
|
||||
let right = this.values.length - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
if (this.values[mid] === value) {
|
||||
return mid;
|
||||
} else if (this.values[mid] < value) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1; // Should not reach here if contains() is correct
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at a given index
|
||||
*/
|
||||
at(index) {
|
||||
return this.values[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the scale
|
||||
*/
|
||||
get size() {
|
||||
return this.values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this scale is equivalent to another
|
||||
*/
|
||||
equals(other) {
|
||||
if (!(other instanceof QualitativeScale)) return false;
|
||||
if (this.values.length !== other.values.length) return false;
|
||||
return this.values.every((val, i) => val === other.values[i]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a string representation
|
||||
*/
|
||||
toString() {
|
||||
return `QualitativeScale(${this.name}): [${this.values.join(', ')}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create common qualitative scales
|
||||
*/
|
||||
static binary() {
|
||||
return new QualitativeScale([0, 1], 'binary');
|
||||
}
|
||||
|
||||
static ternary() {
|
||||
return new QualitativeScale([0, 0.5, 1], 'ternary');
|
||||
}
|
||||
|
||||
static fivePoint() {
|
||||
return new QualitativeScale([0, 0.25, 0.5, 0.75, 1], 'five-point');
|
||||
}
|
||||
|
||||
static tenPoint() {
|
||||
return new QualitativeScale([0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1], 'ten-point');
|
||||
}
|
||||
|
||||
static custom(values, name = 'custom') {
|
||||
return new QualitativeScale(values, name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default qualitative scale for the system
|
||||
*/
|
||||
export const DEFAULT_QUALITATIVE_SCALE = QualitativeScale.fivePoint();
|
||||
@@ -0,0 +1,256 @@
|
||||
# Qualitative Capacity System
|
||||
|
||||
This module provides a complete implementation of qualitative capacities (q-capacities) as described in the research paper on qualitative capacities and their applications to evidential reasoning, decision making, and imprecise possibility.
|
||||
|
||||
## Overview
|
||||
|
||||
A qualitative capacity γ: 2^W → L is a monotonic set-function where:
|
||||
- γ(∅) = 0, γ(W) = 1
|
||||
- If A ⊆ B, then γ(A) ≤ γ(B)
|
||||
- L is a finite totally ordered scale with order-reversing negation
|
||||
|
||||
The core design principle is to use the Qualitative Möbius Transform (QMT) γ# as the canonical internal representation for any q-capacity γ.
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. SetUtils
|
||||
|
||||
Utility functions for working with Sets as Map keys, providing canonical string representations for consistent and efficient Map operations.
|
||||
|
||||
```javascript
|
||||
import { getSetKey, setFromKey, setsEqual } from './src/qualitative/index.js';
|
||||
|
||||
const set = new Set(['a', 'b', 'c']);
|
||||
const key = getSetKey(set); // "a,b,c"
|
||||
const reconstructed = setFromKey(key); // Set(['a', 'b', 'c'])
|
||||
const areEqual = setsEqual(set, reconstructed); // true
|
||||
```
|
||||
|
||||
### 2. QualitativeScale
|
||||
|
||||
Finite totally ordered scales with order-reversing negation.
|
||||
|
||||
```javascript
|
||||
import { QualitativeScale } from './src/qualitative/index.js';
|
||||
|
||||
// Create a 5-point scale
|
||||
const scale = QualitativeScale.fivePoint(); // [0, 0.25, 0.5, 0.75, 1]
|
||||
|
||||
// Test operations
|
||||
console.log(scale.min(0.25, 0.75)); // 0.25
|
||||
console.log(scale.max(0.25, 0.75)); // 0.75
|
||||
console.log(scale.negate(0.25)); // 0.75 (order-reversing)
|
||||
```
|
||||
|
||||
### 3. QualitativeCapacity
|
||||
|
||||
Q-capacities with QMT internal representation.
|
||||
|
||||
```javascript
|
||||
import { QualitativeCapacity } from './src/qualitative/index.js';
|
||||
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
const scale = QualitativeScale.ternary();
|
||||
|
||||
// Create a simple support capacity
|
||||
const ssc = QualitativeCapacity.createSimpleSupport(
|
||||
stateSpace,
|
||||
['s1'],
|
||||
0.5,
|
||||
scale
|
||||
);
|
||||
|
||||
// Get capacity values
|
||||
console.log(ssc.getCapacity(['s1'])); // 0.5
|
||||
console.log(ssc.getCapacity(['s1', 's2'])); // 1
|
||||
|
||||
// Check if it's a necessity measure
|
||||
console.log(ssc.isNecessityMeasure()); // true
|
||||
```
|
||||
|
||||
### 4. QualitativeFusion
|
||||
|
||||
Theoretically sound fusion rules for capacity combination.
|
||||
|
||||
```javascript
|
||||
import { QualitativeFusion } from './src/qualitative/index.js';
|
||||
|
||||
// Create multiple capacities
|
||||
const cap1 = QualitativeCapacity.createSimpleSupport(stateSpace, ['s1'], 0.5, scale);
|
||||
const cap2 = QualitativeCapacity.createSimpleSupport(stateSpace, ['s2'], 0.5, scale);
|
||||
|
||||
// Normalized conjunctive fusion (theoretically sound)
|
||||
const fused = QualitativeFusion.normalizedConjunctive([cap1, cap2]);
|
||||
|
||||
// Disjunctive fusion
|
||||
const disjunctive = QualitativeFusion.disjunctive(cap1, cap2);
|
||||
|
||||
// Sugeno integral for decision making
|
||||
const decisionFunction = { 's1': 0.5, 's2': 1, 's3': 0.5 };
|
||||
const sugenoValue = QualitativeFusion.sugenoIntegral(fused, decisionFunction);
|
||||
```
|
||||
|
||||
### 5. OWAQualitativeFusion
|
||||
|
||||
Bag algebras for sophisticated qualitative aggregation.
|
||||
|
||||
```javascript
|
||||
import { OWAQualitativeFusion } from './src/qualitative/index.js';
|
||||
|
||||
const values = [0.25, 0.5, 0.75];
|
||||
const metas = [{ source: 'rule1' }, { source: 'rule2' }, { source: 'rule3' }];
|
||||
|
||||
// Different aggregation modes
|
||||
const maxResult = OWAQualitativeFusion.max(values, metas, scale);
|
||||
const majorityResult = OWAQualitativeFusion.majority(values, metas, scale);
|
||||
const optimisticResult = OWAQualitativeFusion.optimistic(values, metas, scale);
|
||||
|
||||
// Configurable activation threshold
|
||||
const selectiveResult = OWAQualitativeFusion.max(values, metas, scale, 0.8);
|
||||
|
||||
// Proper Sugeno integral
|
||||
const sugenoResult = OWAQualitativeFusion.sugenoIntegral(capacity, decisionFunction);
|
||||
```
|
||||
|
||||
### 6. QMTOWAFusion
|
||||
|
||||
Theoretically sound OWA-like operators that work directly on QMTs.
|
||||
|
||||
```javascript
|
||||
import { QMTOWAFusion } from './src/qualitative/index.js';
|
||||
|
||||
// These methods preserve monotonicity by working on QMTs directly
|
||||
const optimistic = QMTOWAFusion.optimisticFusion([cap1, cap2]);
|
||||
const pessimistic = QMTOWAFusion.pessimisticFusion([cap1, cap2]);
|
||||
const majority = QMTOWAFusion.majorityFusion([cap1, cap2]);
|
||||
const priority = QMTOWAFusion.priorityFusion([cap1, cap2], [10, 5]);
|
||||
```
|
||||
|
||||
## Theoretical Considerations
|
||||
|
||||
### Pointwise OWA Fusion Warning
|
||||
|
||||
The `pointwiseOWAFusion` method (formerly `fuseCapacities`) performs pointwise OWA fusion on capacity values, which **does NOT guarantee** that the result is a valid qualitative capacity. The resulting set-function may violate the fundamental monotonicity property: A⊆B ⟹ γ(A)≤γ(B).
|
||||
|
||||
**Use this method only for experimental purposes or when monotonicity is not required.**
|
||||
|
||||
For theoretically sound capacity fusion, use:
|
||||
- `QualitativeFusion.normalizedConjunctive()`
|
||||
- `QualitativeFusion.disjunctive()`
|
||||
- `QMTOWAFusion` methods
|
||||
|
||||
### Qualitative OWA Operator
|
||||
|
||||
The qualitative OWA operator implements a novel weighted maximum where weights act as "gates" that must pass a threshold to allow their corresponding values to be considered. This is distinct from the standard Sugeno integral but provides a practical way to introduce weight influence in purely ordinal contexts.
|
||||
|
||||
The activation threshold is configurable (default 0.5) to allow for more or less "selective" aggregations.
|
||||
|
||||
### Sugeno Integral
|
||||
|
||||
The Sugeno integral is the qualitative counterpart to the Choquet integral and provides a theoretically sound way to aggregate qualitative values with respect to a capacity:
|
||||
|
||||
S_γ(f) = max_{i=1}^n min(f_{(i)}, γ(A_{(i)}))
|
||||
|
||||
where f_{(i)} are the sorted values in descending order and A_{(i)} = {w_{(1)}, ..., w_{(i)}}.
|
||||
|
||||
## Applications
|
||||
|
||||
### 1. Evidential Reasoning
|
||||
|
||||
Combine testimonies from different sources using Simple Support Capacities and normalized conjunctive fusion.
|
||||
|
||||
```javascript
|
||||
// Create testimonies as Simple Support Capacities
|
||||
const testimony1 = QualitativeCapacity.createSimpleSupport(
|
||||
stateSpace,
|
||||
['s1'],
|
||||
0.8,
|
||||
scale
|
||||
);
|
||||
|
||||
const testimony2 = QualitativeCapacity.createSimpleSupport(
|
||||
stateSpace,
|
||||
['s2'],
|
||||
0.6,
|
||||
scale
|
||||
);
|
||||
|
||||
// Fuse testimonies
|
||||
const combinedEvidence = QualitativeFusion.normalizedConjunctive([
|
||||
testimony1,
|
||||
testimony2
|
||||
]);
|
||||
```
|
||||
|
||||
### 2. Qualitative Decision Making
|
||||
|
||||
Use Sugeno integrals to evaluate decisions based on qualitative utility functions and uncertainty represented by q-capacities.
|
||||
|
||||
```javascript
|
||||
// Define decision function (utility for each state)
|
||||
const utility = {
|
||||
's1': 0.8, // High utility
|
||||
's2': 0.4, // Medium utility
|
||||
's3': 0.2 // Low utility
|
||||
};
|
||||
|
||||
// Evaluate decision using Sugeno integral
|
||||
const decisionValue = QualitativeFusion.sugenoIntegral(capacity, utility);
|
||||
```
|
||||
|
||||
### 3. Imprecise Possibility
|
||||
|
||||
Represent ill-known possibility measures bounded by lower (q-capacity) and upper (possibility) measures.
|
||||
|
||||
```javascript
|
||||
// Get upper capacity (possibility measure)
|
||||
const upperCapacity = capacity.getUpperCapacity();
|
||||
|
||||
// Get contour function
|
||||
const contour = capacity.getContourFunction();
|
||||
|
||||
// Get conjugate capacity
|
||||
const conjugate = capacity.getConjugate();
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
The current implementation has O(2^|W|) complexity for operations that generate all subsets. This is suitable for small state spaces (|W| < 20) but may not scale to larger ones.
|
||||
|
||||
### Optimizations Implemented
|
||||
|
||||
1. **QualitativeScale Optimizations**:
|
||||
- `contains()`: O(1) average time using Set-based lookup
|
||||
- `indexOf()`: O(log n) time using binary search
|
||||
- These optimizations significantly improve performance for scale operations
|
||||
|
||||
2. **Canonical QMT Optimization**:
|
||||
- `_convertToCanonicalQMT()`: Only checks immediate proper subsets instead of all smaller subsets
|
||||
- Uses the mathematical property: γ#(A) > 0 ⟺ γ(A) > max_{w∈A} γ(A∖{w})
|
||||
- Provides substantial performance improvement for canonicalization
|
||||
|
||||
3. **String Key Robustness**:
|
||||
- All Set objects are converted to canonical string keys for Map operations
|
||||
- Eliminates JavaScript Set reference comparison issues
|
||||
- Ensures consistent and efficient Map key operations
|
||||
|
||||
4. **Canonicalization Consistency**:
|
||||
- All fusion methods return canonical QMTs by default
|
||||
- Ensures minimal representation and consistent behavior
|
||||
- Simplifies subsequent operations and saves memory
|
||||
|
||||
For large state spaces, consider:
|
||||
1. Working with QMTs directly (already implemented)
|
||||
2. Using sparse representations
|
||||
3. Implementing approximation algorithms
|
||||
|
||||
## Future Research Directions
|
||||
|
||||
1. **QMT-based OWA**: Develop more sophisticated OWA-like operators that work directly on QMTs
|
||||
2. **Complexity Optimization**: Implement efficient algorithms for large state spaces
|
||||
3. **Approximation Methods**: Develop approximation algorithms for intractable operations
|
||||
4. **Integration with DSL**: Extend the Evidence DSL to support qualitative capacities
|
||||
|
||||
## References
|
||||
|
||||
This implementation is based on the research paper "Qualitative capacities: basic notions and potential applications" and related work on qualitative uncertainty theory, possibility theory, and evidential reasoning.
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Utility functions for working with Sets as Map keys
|
||||
*
|
||||
* Provides canonical string representations for Set objects to ensure
|
||||
* consistent and efficient Map key operations.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a canonical string key for a Set
|
||||
* @param {Set} set - The set to create a key for
|
||||
* @returns {string} Canonical string representation
|
||||
*/
|
||||
export function getSetKey(set) {
|
||||
if (set.size === 0) return ''; // Consistent key for empty set
|
||||
return Array.from(set).sort().join(',');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Set from a canonical string key
|
||||
* @param {string} key - The canonical string key
|
||||
* @param {Array} stateSpace - The state space to validate against
|
||||
* @returns {Set} The reconstructed set
|
||||
*/
|
||||
export function setFromKey(key, stateSpace = null) {
|
||||
if (key === '') return new Set();
|
||||
|
||||
const elements = key.split(',');
|
||||
const set = new Set(elements);
|
||||
|
||||
// Validate against state space if provided
|
||||
if (stateSpace) {
|
||||
for (const element of set) {
|
||||
if (!stateSpace.includes(element)) {
|
||||
throw new Error(`Element ${element} not found in state space`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two sets are equal by comparing their canonical keys
|
||||
* @param {Set} set1 - First set
|
||||
* @param {Set} set2 - Second set
|
||||
* @returns {boolean} True if sets are equal
|
||||
*/
|
||||
export function setsEqual(set1, set2) {
|
||||
return getSetKey(set1) === getSetKey(set2);
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Unified Evidence Fusion System
|
||||
*
|
||||
* This module provides a comprehensive evidence fusion system that separates
|
||||
* aggregation logic (OWA) from reconciliation logic (bilattice/Dempster-Shafer),
|
||||
* supporting both qualitative and quantitative modes with the same lexicon.
|
||||
*
|
||||
* Architecture:
|
||||
* 1. Aggregation: Uses OWA operators for combining evidence values
|
||||
* 2. Reconciliation: Uses theoretical frameworks for handling epistemic uncertainty
|
||||
* 3. Mode Support: Both qualitative and quantitative with consistent lexicon
|
||||
*
|
||||
* This separation allows for sophisticated evidence fusion that can handle
|
||||
* both simple aggregation and complex epistemic reasoning scenarios.
|
||||
*/
|
||||
|
||||
import { EvidenceAggregation } from './EvidenceAggregation.js';
|
||||
import { EvidenceReconciliation } from './EvidenceReconciliation.js';
|
||||
import { QualitativeScale } from './QualitativeScale.js';
|
||||
|
||||
export class UnifiedEvidenceFusion {
|
||||
/**
|
||||
* Fuse evidence using both aggregation and reconciliation
|
||||
* @param {Array} collectedValues - Array of collected evidence values
|
||||
* @param {Object} options - Fusion options
|
||||
* @returns {Object} Fusion result
|
||||
*/
|
||||
static fuse(collectedValues, options = {}) {
|
||||
const {
|
||||
mode = 'quantitative', // 'qualitative' or 'quantitative'
|
||||
aggregationMethod = 'max', // OWA aggregation method
|
||||
reconciliationMethod = 'none', // 'none', 'bilattice', 'dempster_shafer', 'subjective_logic'
|
||||
epistemicMode = 'hybrid', // 'information', 'truth', 'hybrid'
|
||||
capacityType = 'simple_support', // 'simple_support', 'possibility', 'necessity'
|
||||
scale = null, // QualitativeScale for qualitative mode
|
||||
useReconciliation = false, // Whether to use reconciliation
|
||||
reliabilityWeighting = false, // Whether to weight by reliability
|
||||
customWeights = null, // Custom OWA weights
|
||||
weights = null // Legacy weights parameter
|
||||
} = options;
|
||||
|
||||
if (!collectedValues || collectedValues.length === 0) {
|
||||
return {
|
||||
value: 0,
|
||||
possibility: 0,
|
||||
hasValue: false,
|
||||
fusionMethod: 'none',
|
||||
aggregationMethod: 'none',
|
||||
reconciliationMethod: 'none',
|
||||
mode
|
||||
};
|
||||
}
|
||||
|
||||
// Extract values and metadata for aggregation
|
||||
const values = collectedValues.map(cv => cv.value || cv.possibility);
|
||||
const metas = collectedValues.map(cv => ({
|
||||
...cv.metadata,
|
||||
reliability: cv.metadata?.reliability || 1.0,
|
||||
timestamp: cv.metadata?.timestamp || Date.now()
|
||||
}));
|
||||
|
||||
let result;
|
||||
|
||||
if (useReconciliation && reconciliationMethod !== 'none') {
|
||||
// Use reconciliation-based fusion
|
||||
result = this._fuseWithReconciliation(collectedValues, {
|
||||
mode,
|
||||
aggregationMethod,
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
scale: scale || (mode === 'qualitative' ? QualitativeScale.fivePoint() : null),
|
||||
reliabilityWeighting,
|
||||
customWeights: customWeights || weights
|
||||
});
|
||||
} else {
|
||||
// Use pure aggregation-based fusion
|
||||
result = this._fuseWithAggregation(values, metas, {
|
||||
mode,
|
||||
aggregationMethod,
|
||||
scale: scale || (mode === 'qualitative' ? QualitativeScale.fivePoint() : null),
|
||||
reliabilityWeighting,
|
||||
customWeights: customWeights || weights
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
fusionMethod: useReconciliation && reconciliationMethod !== 'none' ? 'reconciliation' : 'aggregation',
|
||||
mode
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse evidence using reconciliation-based approach
|
||||
* @private
|
||||
*/
|
||||
static _fuseWithReconciliation(collectedValues, options) {
|
||||
const {
|
||||
mode,
|
||||
aggregationMethod,
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
scale,
|
||||
reliabilityWeighting,
|
||||
customWeights
|
||||
} = options;
|
||||
|
||||
// First, perform reconciliation to select the best evidence
|
||||
const reconciliationResult = EvidenceReconciliation.reconcile(collectedValues, {
|
||||
mode,
|
||||
reconciliationMethod,
|
||||
epistemicMode,
|
||||
capacityType,
|
||||
scale
|
||||
});
|
||||
|
||||
if (!reconciliationResult.hasValue) {
|
||||
return {
|
||||
value: mode === 'qualitative' ? scale.bottom : 0,
|
||||
possibility: mode === 'qualitative' ? scale.bottom : 0,
|
||||
hasValue: false,
|
||||
aggregationMethod: 'none',
|
||||
reconciliationMethod,
|
||||
epistemicAnalysis: reconciliationResult.epistemicAnalysis
|
||||
};
|
||||
}
|
||||
|
||||
// If reconciliation selected a single value, use it directly
|
||||
if (reconciliationResult.reconciliationMethod !== 'none') {
|
||||
return {
|
||||
value: reconciliationResult.value,
|
||||
possibility: reconciliationResult.possibility,
|
||||
hasValue: true,
|
||||
aggregationMethod: 'reconciliation_selected',
|
||||
reconciliationMethod,
|
||||
epistemicAnalysis: reconciliationResult.epistemicAnalysis
|
||||
};
|
||||
}
|
||||
|
||||
// Otherwise, fall back to aggregation
|
||||
const values = collectedValues.map(cv => cv.value || cv.possibility);
|
||||
const metas = collectedValues.map(cv => ({
|
||||
...cv.metadata,
|
||||
reliability: cv.metadata?.reliability || 1.0
|
||||
}));
|
||||
|
||||
const aggregationResult = EvidenceAggregation.aggregate(values, metas, {
|
||||
mode,
|
||||
aggregator: aggregationMethod,
|
||||
reliabilityWeighting,
|
||||
scale,
|
||||
customWeights
|
||||
});
|
||||
|
||||
return {
|
||||
value: aggregationResult.value,
|
||||
possibility: aggregationResult.possibility,
|
||||
hasValue: true,
|
||||
aggregationMethod,
|
||||
reconciliationMethod,
|
||||
epistemicAnalysis: reconciliationResult.epistemicAnalysis
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fuse evidence using pure aggregation approach
|
||||
* @private
|
||||
*/
|
||||
static _fuseWithAggregation(values, metas, options) {
|
||||
const {
|
||||
mode,
|
||||
aggregationMethod,
|
||||
scale,
|
||||
reliabilityWeighting,
|
||||
customWeights
|
||||
} = options;
|
||||
|
||||
const aggregationResult = EvidenceAggregation.aggregate(values, metas, {
|
||||
mode,
|
||||
aggregator: aggregationMethod,
|
||||
reliabilityWeighting,
|
||||
scale,
|
||||
customWeights
|
||||
});
|
||||
|
||||
return {
|
||||
value: aggregationResult.value,
|
||||
possibility: aggregationResult.possibility,
|
||||
hasValue: true,
|
||||
aggregationMethod,
|
||||
reconciliationMethod: 'none',
|
||||
epistemicAnalysis: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare fusion results between different methods
|
||||
* @param {Array} collectedValues - Array of collected evidence values
|
||||
* @param {Object} options - Comparison options
|
||||
* @returns {Object} Comparison result
|
||||
*/
|
||||
static compareMethods(collectedValues, options = {}) {
|
||||
const {
|
||||
mode = 'quantitative',
|
||||
aggregationMethods = ['max', 'min', 'average', 'majority'],
|
||||
reconciliationMethods = ['none', 'bilattice', 'dempster_shafer'],
|
||||
epistemicModes = ['hybrid', 'information', 'truth'],
|
||||
scale = null
|
||||
} = options;
|
||||
|
||||
const results = {};
|
||||
|
||||
// Test aggregation methods
|
||||
for (const aggMethod of aggregationMethods) {
|
||||
const result = this.fuse(collectedValues, {
|
||||
...options,
|
||||
mode,
|
||||
aggregationMethod: aggMethod,
|
||||
useReconciliation: false,
|
||||
scale
|
||||
});
|
||||
results[`aggregation_${aggMethod}`] = result;
|
||||
}
|
||||
|
||||
// Test reconciliation methods
|
||||
for (const recMethod of reconciliationMethods) {
|
||||
if (recMethod === 'none') continue;
|
||||
|
||||
for (const epMode of epistemicModes) {
|
||||
const result = this.fuse(collectedValues, {
|
||||
...options,
|
||||
mode,
|
||||
reconciliationMethod: recMethod,
|
||||
epistemicMode: epMode,
|
||||
useReconciliation: true,
|
||||
scale
|
||||
});
|
||||
results[`reconciliation_${recMethod}_${epMode}`] = result;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
results,
|
||||
summary: this._generateComparisonSummary(results)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate comparison summary
|
||||
* @private
|
||||
*/
|
||||
static _generateComparisonSummary(results) {
|
||||
const values = Object.values(results).map(r => r.value);
|
||||
const maxValue = Math.max(...values);
|
||||
const minValue = Math.min(...values);
|
||||
const avgValue = values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
|
||||
const bestMethods = Object.entries(results)
|
||||
.filter(([_, result]) => result.value === maxValue)
|
||||
.map(([method, _]) => method);
|
||||
|
||||
const worstMethods = Object.entries(results)
|
||||
.filter(([_, result]) => result.value === minValue)
|
||||
.map(([method, _]) => method);
|
||||
|
||||
return {
|
||||
valueRange: { min: minValue, max: maxValue, average: avgValue },
|
||||
bestMethods,
|
||||
worstMethods,
|
||||
methodCount: Object.keys(results).length,
|
||||
valueVariance: this._calculateVariance(values)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate variance of values
|
||||
* @private
|
||||
*/
|
||||
static _calculateVariance(values) {
|
||||
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
const squaredDiffs = values.map(val => Math.pow(val - mean, 2));
|
||||
return squaredDiffs.reduce((sum, diff) => sum + diff, 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available fusion methods
|
||||
* @returns {Object} Available methods by category
|
||||
*/
|
||||
static getAvailableMethods() {
|
||||
return {
|
||||
aggregation: EvidenceAggregation.getAvailableMethods(),
|
||||
reconciliation: ['none', 'bilattice', 'dempster_shafer', 'subjective_logic'],
|
||||
epistemicModes: ['information', 'truth', 'hybrid'],
|
||||
capacityTypes: ['simple_support', 'possibility', 'necessity']
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate fusion options
|
||||
* @param {Object} options - Options to validate
|
||||
* @returns {Object} Validation result
|
||||
*/
|
||||
static validateOptions(options) {
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
|
||||
const availableMethods = this.getAvailableMethods();
|
||||
|
||||
// Validate aggregation method
|
||||
if (options.aggregationMethod && !availableMethods.aggregation.includes(options.aggregationMethod)) {
|
||||
errors.push(`Invalid aggregation method: ${options.aggregationMethod}`);
|
||||
}
|
||||
|
||||
// Validate reconciliation method
|
||||
if (options.reconciliationMethod && !availableMethods.reconciliation.includes(options.reconciliationMethod)) {
|
||||
errors.push(`Invalid reconciliation method: ${options.reconciliationMethod}`);
|
||||
}
|
||||
|
||||
// Validate epistemic mode
|
||||
if (options.epistemicMode && !availableMethods.epistemicModes.includes(options.epistemicMode)) {
|
||||
errors.push(`Invalid epistemic mode: ${options.epistemicMode}`);
|
||||
}
|
||||
|
||||
// Validate capacity type
|
||||
if (options.capacityType && !availableMethods.capacityTypes.includes(options.capacityType)) {
|
||||
errors.push(`Invalid capacity type: ${options.capacityType}`);
|
||||
}
|
||||
|
||||
// Validate mode
|
||||
if (options.mode && !['qualitative', 'quantitative'].includes(options.mode)) {
|
||||
errors.push(`Invalid mode: ${options.mode}`);
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if (options.useReconciliation && options.reconciliationMethod === 'none') {
|
||||
warnings.push('useReconciliation is true but reconciliationMethod is none');
|
||||
}
|
||||
|
||||
if (options.mode === 'qualitative' && !options.scale) {
|
||||
warnings.push('Qualitative mode recommended with explicit scale');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get method descriptions
|
||||
* @returns {Object} Method descriptions
|
||||
*/
|
||||
static getMethodDescriptions() {
|
||||
return {
|
||||
aggregation: EvidenceAggregation.getAvailableMethods().reduce((desc, method) => {
|
||||
desc[method] = EvidenceAggregation.getMethodDescription(method);
|
||||
return desc;
|
||||
}, {}),
|
||||
reconciliation: {
|
||||
'none': 'No reconciliation (pure aggregation)',
|
||||
'bilattice': 'Bilattice orderings for epistemic reasoning',
|
||||
'dempster_shafer': 'Dempster-Shafer theory for belief functions',
|
||||
'subjective_logic': 'Subjective Logic for opinion-based reasoning'
|
||||
},
|
||||
epistemicModes: {
|
||||
'information': 'Focus on information content (belief + disbelief)',
|
||||
'truth': 'Focus on truth content (capacity ordering)',
|
||||
'hybrid': 'Combine information and truth orderings'
|
||||
},
|
||||
capacityTypes: {
|
||||
'simple_support': 'Simple support functions (basic evidence)',
|
||||
'possibility': 'Possibility measures (singleton focal sets)',
|
||||
'necessity': 'Necessity measures (nested focal sets)'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Qualitative Capacity System - Main Export
|
||||
*
|
||||
* This module provides a complete implementation of qualitative capacities
|
||||
* as described in the research paper on qualitative capacities and their
|
||||
* applications to evidential reasoning, decision making, and imprecise possibility.
|
||||
*
|
||||
* Core Components:
|
||||
* - QualitativeScale: Finite totally ordered scales with negation
|
||||
* - QualitativeCapacity: Q-capacities with QMT internal representation
|
||||
* - QualitativeFusion: Dempster-like fusion rules for capacity combination
|
||||
* - OWAQualitativeFusion: Bag algebras for sophisticated qualitative aggregation
|
||||
*/
|
||||
|
||||
export { QualitativeScale, DEFAULT_QUALITATIVE_SCALE } from './QualitativeScale.js';
|
||||
export { QualitativeCapacity } from './QualitativeCapacity.js';
|
||||
export { QualitativeFusion } from './QualitativeFusion.js';
|
||||
export {
|
||||
OWAQualitativeFusion,
|
||||
getOWAQualitativeWeights,
|
||||
getOWAQualitativeWeightsFromRule
|
||||
} from './OWAQualitativeFusion.js';
|
||||
export { QMTOWAFusion } from './QMTOWAFusion.js';
|
||||
export { getSetKey, setFromKey, setsEqual } from './SetUtils.js';
|
||||
export { PossibilisticConverter, LINGUISTIC_MAPPING } from './PossibilisticConverter.js';
|
||||
export { HybridFusion } from './HybridFusion.js';
|
||||
export { BilatticeOrderings } from './BilatticeOrderings.js';
|
||||
export { NumericBilatticeOrderings } from './NumericBilatticeOrderings.js';
|
||||
export { EvidenceReconciliation } from './EvidenceReconciliation.js';
|
||||
export { EvidenceAggregation } from './EvidenceAggregation.js';
|
||||
export { UnifiedEvidenceFusion } from './UnifiedEvidenceFusion.js';
|
||||
|
||||
Reference in New Issue
Block a user