decision_making_core 0.1.0
decision_making_core: ^0.1.0 copied to clipboard
Pure Dart multi-criteria decision-making (MCDM) core library implementing AHP, SAW, and TOPSIS algorithms.
decision_making_core #
A pure Dart multi-criteria decision-making (MCDM) library implementing SAW, TOPSIS, and AHP algorithms.
- Zero Flutter dependencies: Can be used in CLI tools, background workers, backend microservices, or frontend applications.
- Synchronous & fast: Pure computational engine without Futures or event loop overhead.
- Strictly tested: Validated with golden datasets under a numerical tolerance of 1e-4.
Installation #
Add decision_making_core to your pubspec.yaml:
dependencies:
decision_making_core: ^0.1.0
Usage Examples #
1. Simple Additive Weighting (SAW) #
import 'package:decision_making_core/decision_making_core.dart';
void main() {
final alternatives = [
WeightedDecisionAlternative(id: 'a1', name: 'Alternative 1'),
WeightedDecisionAlternative(id: 'a2', name: 'Alternative 2'),
];
final criteria = [
WeightedDecisionCriteria(
id: 'c1',
name: 'Cost',
weightPercent: 40,
isBenefit: false, // Cost criteria
maxValue: 100,
),
WeightedDecisionCriteria(
id: 'c2',
name: 'Quality',
weightPercent: 60,
isBenefit: true, // Benefit criteria
maxValue: 100,
),
];
final matrix = [
WeightedDecisionMatrix(
id: 'm1',
alternative: alternatives[0],
ratings: [
WeightedDecisionRating(id: 'r1_1', criteria: criteria[0], value: 40),
WeightedDecisionRating(id: 'r1_2', criteria: criteria[1], value: 80),
],
),
WeightedDecisionMatrix(
id: 'm2',
alternative: alternatives[1],
ratings: [
WeightedDecisionRating(id: 'r2_1', criteria: criteria[0], value: 60),
WeightedDecisionRating(id: 'r2_2', criteria: criteria[1], value: 90),
],
),
];
final results = SawCalculator.calculate(matrix);
for (final res in results) {
print('Rank ${res.rank}: ${res.alternative.name} (Score: ${res.score.toStringAsFixed(4)})');
}
// To inspect all intermediate matrices and normalized weights:
// final detailed = SawCalculator.calculateDetailed(matrix);
// print(detailed.normalizedWeights);
// print(detailed.toJson());
}
2. TOPSIS (Similarity to Ideal Solution) #
import 'package:decision_making_core/decision_making_core.dart';
void main() {
final criteria = [
WeightedDecisionCriteria(
id: 'c1',
name: 'Price',
weightPercent: 50,
isBenefit: false,
maxValue: 100,
),
WeightedDecisionCriteria(
id: 'c2',
name: 'Quality',
weightPercent: 50,
isBenefit: true,
maxValue: 100,
),
];
final alternatives = [
WeightedDecisionAlternative(id: 'a1', name: 'Vendor A'),
WeightedDecisionAlternative(id: 'a2', name: 'Vendor B'),
];
final matrix = [
WeightedDecisionMatrix(
id: 'm1',
alternative: alternatives[0],
ratings: [
WeightedDecisionRating(id: 'r1', criteria: criteria[0], value: 30),
WeightedDecisionRating(id: 'r2', criteria: criteria[1], value: 80),
],
),
WeightedDecisionMatrix(
id: 'm2',
alternative: alternatives[1],
ratings: [
WeightedDecisionRating(id: 'r3', criteria: criteria[0], value: 50),
WeightedDecisionRating(id: 'r4', criteria: criteria[1], value: 60),
],
),
];
final rawMatrix = TopsisRawMatrix(criterias: criteria, matrixs: matrix);
final results = TopsisCalculator.calculate(rawMatrix);
for (final res in results) {
print('Rank ${res.rank}: ${res.alternative.name} (Score: ${res.score.toStringAsFixed(4)})');
}
// To inspect all intermediate matrices, ideal solutions, and separation measures:
// final detailed = TopsisCalculator.calculateDetailed(rawMatrix);
// print(detailed.positiveIdeal);
// print(detailed.separationPlus);
// print(detailed.toJson());
}
3. Analytic Hierarchy Process (AHP) #
import 'package:decision_making_core/decision_making_core.dart';
void main() {
final criteria = [
AhpItem(id: 'c1', name: 'Cost'),
AhpItem(id: 'c2', name: 'Quality'),
];
final alternatives = [
AhpItem(id: 'a1', name: 'Option A'),
AhpItem(id: 'a2', name: 'Option B'),
];
final hierarchy = AhpCalculator.generateHierarchy(criteria, alternatives);
// Criteria pairwise comparison (Cost is 2x more important than Quality)
final criteriaInputs = [
PairwiseComparisonInput(
id: 'p1',
left: criteria[0],
right: criteria[1],
preferenceValue: 2,
isLeftMoreImportant: true,
),
];
// Alternatives pairwise comparisons under each criterion
final alternativeInputs = [
PairwiseAlternativeInput(
criteria: criteria[0],
alternative: [
PairwiseComparisonInput(
id: 'alt_cost',
left: alternatives[0],
right: alternatives[1],
preferenceValue: 3,
isLeftMoreImportant: true,
),
],
),
PairwiseAlternativeInput(
criteria: criteria[1],
alternative: [
PairwiseComparisonInput(
id: 'alt_quality',
left: alternatives[0],
right: alternatives[1],
preferenceValue: 2,
isLeftMoreImportant: false,
),
],
),
];
final result = AhpCalculator.calculate(
hierarchy: hierarchy,
inputsCriteria: criteriaInputs,
inputsAlternative: alternativeInputs,
);
print('Criteria Consistent: ${result.isConsistentCriteria} (CR: ${result.consistencyCriteriaRatio})');
for (final detail in result.results!) {
print('${detail.name}: ${detail.value.toStringAsFixed(4)}');
}
// To inspect all intermediate comparison matrices, weights, and consistency metrics:
// final detailed = AhpCalculator.calculateDetailed(
// hierarchy: hierarchy,
// inputsCriteria: criteriaInputs,
// inputsAlternative: alternativeInputs,
// );
// print(detailed.criteriaWeights);
// print(detailed.toJson());
}
Sensitivity Analysis & Rank Reversal Detection #
The library provides automated weight sensitivity analysis and rank reversal detection for SAW (SawCalculator.analyzeSensitivity) and TOPSIS (TopsisCalculator.analyzeSensitivity).
Note: This feature detects rank reversal phenomena; it does not prevent them. Rank reversal is an inherent mathematical characteristic of multi-criteria decision making methods where relative rankings naturally shift due to changing normalization boundaries or ideal reference points when alternatives are added or removed.
Definitions #
-
Weight Sensitivity:
- Evaluates systematic +/- delta (default 10% relative,
delta: 0.10) perturbations across each criterion in deterministic order (input criteria order, then+, then-). - Formula:
w_k' = w_k * (1 +/- delta), while all other criteria weights are rescaled by(1 - w_k') / (1 - w_k)so the total weight strictly equals 1.0. - If
w_k' >= 1.0,w_k' <= 0.0, or only 1 criterion exists, the case is safely skipped and its reason recorded inskipReason.
- Evaluates systematic +/- delta (default 10% relative,
-
Pairwise Ranking Changes:
- Order change is evaluated strictly pairwise between every alternative pair
(i, j): order is considered "changed" if the sign of(score_i - score_j)flips and the absolute difference on both sides is> 1e-9. - Score differences
<= 1e-9are treated as ties and do not trigger order change.
- Order change is evaluated strictly pairwise between every alternative pair
-
Rank Reversal Detection:
- For datasets with
n >= 3alternatives, each alternativeris removed one by one, scores are recalculated, and the relative ordering among remaining alternatives is compared to their original baseline order. - For datasets with
n < 3, rank reversal testing is skipped and documented inskipReason.
- For datasets with
-
Stability Summary (
isStable):- Returns
trueif and only if no ranking changes occur across all sensitivity perturbation cases and no rank reversals occur upon any alternative removal.
- Returns
Caution & Caveats: Sensitivity and rank reversal behaviors depend directly on the chosen
deltaparameter and the mathematical normalization technique (e.g. min/max boundary normalization in SAW vs Euclidean vector normalization in TOPSIS). Rank reversals can naturally occur in multi-criteria methods when an ideal point or normalization denominator shifts upon alternative removal.
Example: Sensitivity Analysis (SAW & TOPSIS) #
import 'package:decision_making_core/decision_making_core.dart';
void main() {
// SAW Sensitivity Analysis
final sawResult = SawCalculator.analyzeSensitivity(matrix, delta: 0.1);
print('SAW isStable: ${sawResult.isStable}');
for (final sCase in sawResult.sensitivityCases) {
if (sCase.hasOrderChanged) {
print('Criterion ${sCase.criterionId} ${sCase.direction.symbol}: order changed!');
for (final pair in sCase.swappedPairs) {
print(' Swapped pair: ${pair.firstId} <-> ${pair.secondId}');
}
}
}
// TOPSIS Sensitivity Analysis
final topsisResult = TopsisCalculator.analyzeSensitivity(rawMatrix, delta: 0.1);
print('TOPSIS isStable: ${topsisResult.isStable}');
for (final rCase in topsisResult.rankReversalCases) {
if (rCase.hasReversal) {
print('Rank reversal when removing ${rCase.removedAlternativeId}!');
for (final pair in rCase.reversedPairs) {
print(' Inverted relative order: ${pair.firstId} <-> ${pair.secondId}');
}
}
}
}
Existing Behavior & Edge Cases #
The core calculation engines maintain consistent mathematical specifications for edge cases:
- SAW cost criteria with value 0: If a cost criterion receives a rating value of
0.0, an explicitException("Rating value cannot be 0 for cost criteria")is thrown to prevent division by zero (min / 0.0) or infinite scores. - TOPSIS all-zero column: When all alternatives have a value of
0.0for a criterion (resulting in Euclidean norm = 0.0), each normalized cell safely evaluates to0.0instead ofNaNor divide-by-zero. - TOPSIS equidistant alternatives (D+ = D- = 0): When all alternatives have identical distance to ideal solutions (such as single-alternative datasets or identical rows where
D+ = 0.0andD- = 0.0), the relative closeness formulaD- / (D+ + D-)safely resolves to a neutral score of0.5instead ofNaN.
License #
MIT License. See LICENSE for details.