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:
John Dvorak
2026-07-31 13:44:06 -07:00
commit 717ae1031e
373 changed files with 654131 additions and 0 deletions
+382
View File
@@ -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('; ');
}
}
+359
View File
@@ -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
}
};
}
}
+718
View File
@@ -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);
}
}
}
+614
View File
@@ -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 };
}
}
+534
View File
@@ -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);
}
+447
View File
@@ -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);
}
}
+333
View File
@@ -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;
}
}
+335
View File
@@ -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;
}
}
+332
View File
@@ -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);
}
}
+206
View File
@@ -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();
+256
View File
@@ -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.
+50
View File
@@ -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);
}
+381
View File
@@ -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)'
}
};
}
}
+32
View File
@@ -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';