evidencelib 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evidencelib/__init__.py +8 -0
- evidencelib/exceptions.py +14 -0
- evidencelib/frame.py +198 -0
- evidencelib/mass.py +305 -0
- evidencelib/parser.py +104 -0
- evidencelib/proposition.py +80 -0
- evidencelib/py.typed +1 -0
- evidencelib-1.0.0.dist-info/METADATA +225 -0
- evidencelib-1.0.0.dist-info/RECORD +11 -0
- evidencelib-1.0.0.dist-info/WHEEL +4 -0
- evidencelib-1.0.0.dist-info/licenses/LICENSE +22 -0
evidencelib/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Package-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EvidenceLibError(Exception):
|
|
5
|
+
"""Base exception for evidencelib."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InvalidMassError(EvidenceLibError, ValueError):
|
|
9
|
+
"""Raised when a mass assignment is invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TotalConflictError(EvidenceLibError, ZeroDivisionError):
|
|
13
|
+
"""Raised when normalized combination is undefined due to total conflict."""
|
|
14
|
+
|
evidencelib/frame.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Frames of discernment for DST and DSmT."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import combinations
|
|
6
|
+
from typing import Iterable, Iterator, Mapping, Sequence
|
|
7
|
+
|
|
8
|
+
from evidencelib.parser import PropositionParser
|
|
9
|
+
from evidencelib.proposition import Proposition
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Frame:
|
|
13
|
+
"""A finite frame of discernment.
|
|
14
|
+
|
|
15
|
+
The internal representation uses Venn regions. This lets the same
|
|
16
|
+
proposition algebra represent Shafer's exclusive DST model, the free DSmT
|
|
17
|
+
model, and constrained hybrid DSm models.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
atoms: Sequence[str],
|
|
23
|
+
*,
|
|
24
|
+
empty: Iterable[str | Proposition] = (),
|
|
25
|
+
exclusive: bool | Iterable[Sequence[str]] = False,
|
|
26
|
+
model: str = "hybrid",
|
|
27
|
+
) -> None:
|
|
28
|
+
if not atoms:
|
|
29
|
+
raise ValueError("A frame needs at least one atom.")
|
|
30
|
+
if len(set(atoms)) != len(atoms):
|
|
31
|
+
raise ValueError("Frame atom names must be unique.")
|
|
32
|
+
|
|
33
|
+
self.atoms = tuple(atoms)
|
|
34
|
+
self.model = model
|
|
35
|
+
self._index = {name: i for i, name in enumerate(self.atoms)}
|
|
36
|
+
self._full_universe = frozenset(range(1, 1 << len(self.atoms)))
|
|
37
|
+
self._impossible_regions: set[int] = set()
|
|
38
|
+
self._universe = self._full_universe
|
|
39
|
+
|
|
40
|
+
constraints: list[str | Proposition] = []
|
|
41
|
+
if exclusive is True:
|
|
42
|
+
constraints.extend("&".join(pair) for pair in combinations(self.atoms, 2))
|
|
43
|
+
elif exclusive:
|
|
44
|
+
constraints.extend("&".join(group) for group in exclusive)
|
|
45
|
+
constraints.extend(empty)
|
|
46
|
+
|
|
47
|
+
for constraint in constraints:
|
|
48
|
+
prop = self._parse_free(constraint)
|
|
49
|
+
self._impossible_regions.update(prop.regions)
|
|
50
|
+
self._universe = frozenset(self._full_universe - self._impossible_regions)
|
|
51
|
+
|
|
52
|
+
self.empty = Proposition(self, frozenset())
|
|
53
|
+
self.total = Proposition(self, self._universe)
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def dst(cls, atoms: Sequence[str]) -> "Frame":
|
|
57
|
+
"""Create Shafer's DST model with mutually exclusive hypotheses."""
|
|
58
|
+
|
|
59
|
+
return cls(atoms, exclusive=True, model="dst")
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def dsmt(cls, atoms: Sequence[str]) -> "Frame":
|
|
63
|
+
"""Create the free DSm model where hypotheses may overlap."""
|
|
64
|
+
|
|
65
|
+
return cls(atoms, model="dsmt")
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def hybrid(
|
|
69
|
+
cls,
|
|
70
|
+
atoms: Sequence[str],
|
|
71
|
+
*,
|
|
72
|
+
empty: Iterable[str | Proposition] = (),
|
|
73
|
+
exclusive: bool | Iterable[Sequence[str]] = False,
|
|
74
|
+
) -> "Frame":
|
|
75
|
+
"""Create a constrained DSm model."""
|
|
76
|
+
|
|
77
|
+
return cls(atoms, empty=empty, exclusive=exclusive, model="hybrid")
|
|
78
|
+
|
|
79
|
+
def symbols(self, names: str | None = None) -> tuple[Proposition, ...]:
|
|
80
|
+
"""Return atom propositions.
|
|
81
|
+
|
|
82
|
+
``names`` may be omitted to return all frame atoms, or supplied as a
|
|
83
|
+
whitespace/comma-separated subset.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if names is None:
|
|
87
|
+
selected = self.atoms
|
|
88
|
+
else:
|
|
89
|
+
selected = tuple(part for part in names.replace(",", " ").split() if part)
|
|
90
|
+
return tuple(self.atom(name) for name in selected)
|
|
91
|
+
|
|
92
|
+
def atom(self, name: str) -> Proposition:
|
|
93
|
+
if name not in self._index:
|
|
94
|
+
raise KeyError(f"Unknown frame atom: {name!r}")
|
|
95
|
+
bit = 1 << self._index[name]
|
|
96
|
+
return Proposition(self, frozenset(r for r in self._universe if r & bit))
|
|
97
|
+
|
|
98
|
+
def proposition(self, value: str | Proposition | Iterable[str]) -> Proposition:
|
|
99
|
+
"""Coerce a string, proposition, or iterable of atoms into a proposition."""
|
|
100
|
+
|
|
101
|
+
if isinstance(value, Proposition):
|
|
102
|
+
if value.frame is not self:
|
|
103
|
+
raise ValueError("Proposition belongs to a different frame.")
|
|
104
|
+
return value
|
|
105
|
+
if isinstance(value, str):
|
|
106
|
+
return self._parse(value)
|
|
107
|
+
prop = self.empty
|
|
108
|
+
for atom in value:
|
|
109
|
+
prop = prop | self.atom(atom)
|
|
110
|
+
return prop
|
|
111
|
+
|
|
112
|
+
def mass(self, values: Mapping[str | Proposition | Iterable[str], float], **kwargs):
|
|
113
|
+
from evidencelib.mass import MassFunction
|
|
114
|
+
|
|
115
|
+
return MassFunction(self, values, **kwargs)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def region_count(self) -> int:
|
|
119
|
+
"""Number of non-empty disjoint Venn regions in the current model."""
|
|
120
|
+
|
|
121
|
+
return len(self._universe)
|
|
122
|
+
|
|
123
|
+
def elements(self, *, max_count: int | None = 100_000) -> tuple[Proposition, ...]:
|
|
124
|
+
"""Generate the model's closure under union and intersection.
|
|
125
|
+
|
|
126
|
+
The result is the power set for DST and the hyper-power set for the free
|
|
127
|
+
DSm model. DSmT cardinality grows very quickly; pass ``max_count=None``
|
|
128
|
+
only when you really want the full closure.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
elements = {self.empty, *self.symbols()}
|
|
132
|
+
changed = True
|
|
133
|
+
while changed:
|
|
134
|
+
changed = False
|
|
135
|
+
current = tuple(elements)
|
|
136
|
+
for left in current:
|
|
137
|
+
for right in current:
|
|
138
|
+
for combined in (left | right, left & right):
|
|
139
|
+
if combined not in elements:
|
|
140
|
+
elements.add(combined)
|
|
141
|
+
changed = True
|
|
142
|
+
if max_count is not None and len(elements) > max_count:
|
|
143
|
+
raise RuntimeError(
|
|
144
|
+
"Element generation exceeded max_count; "
|
|
145
|
+
"DSmT hyper-power sets grow very quickly."
|
|
146
|
+
)
|
|
147
|
+
return tuple(sorted(elements, key=lambda p: (len(p.regions), str(p))))
|
|
148
|
+
|
|
149
|
+
def format(self, prop: Proposition) -> str:
|
|
150
|
+
if not prop.regions:
|
|
151
|
+
return "empty"
|
|
152
|
+
|
|
153
|
+
terms = self._minimal_terms(prop.regions)
|
|
154
|
+
rendered_terms = []
|
|
155
|
+
for term in terms:
|
|
156
|
+
names = [name for i, name in enumerate(self.atoms) if term & (1 << i)]
|
|
157
|
+
rendered_terms.append("&".join(names))
|
|
158
|
+
return "|".join(rendered_terms)
|
|
159
|
+
|
|
160
|
+
def _normalize_regions(self, regions: Iterable[int]) -> frozenset[int]:
|
|
161
|
+
return frozenset(r for r in regions if r in self._universe)
|
|
162
|
+
|
|
163
|
+
def _parse_free(self, value: str | Proposition) -> Proposition:
|
|
164
|
+
old_universe = self._universe
|
|
165
|
+
self._universe = self._full_universe
|
|
166
|
+
try:
|
|
167
|
+
return self._parse(value)
|
|
168
|
+
finally:
|
|
169
|
+
self._universe = old_universe
|
|
170
|
+
|
|
171
|
+
def _parse(self, value: str | Proposition) -> Proposition:
|
|
172
|
+
if isinstance(value, Proposition):
|
|
173
|
+
if value.frame is not self:
|
|
174
|
+
raise ValueError("Proposition belongs to a different frame.")
|
|
175
|
+
return value
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
result = PropositionParser(self).parse(value)
|
|
179
|
+
except Exception as exc: # pragma: no cover - exception message is the value.
|
|
180
|
+
raise ValueError(f"Could not parse proposition {value!r}") from exc
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def _minimal_terms(self, regions: Iterable[int]) -> tuple[int, ...]:
|
|
184
|
+
region_set = set(regions)
|
|
185
|
+
terms: list[int] = []
|
|
186
|
+
for region in sorted(region_set, key=lambda r: (r.bit_count(), r)):
|
|
187
|
+
if any((term & region) == term for term in terms):
|
|
188
|
+
continue
|
|
189
|
+
upward = {r for r in self._universe if (r & region) == region}
|
|
190
|
+
if upward & region_set:
|
|
191
|
+
terms.append(region)
|
|
192
|
+
return tuple(terms)
|
|
193
|
+
|
|
194
|
+
def __iter__(self) -> Iterator[Proposition]:
|
|
195
|
+
return iter(self.symbols())
|
|
196
|
+
|
|
197
|
+
def __repr__(self) -> str:
|
|
198
|
+
return f"Frame({self.atoms!r}, model={self.model!r})"
|
evidencelib/mass.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Mass functions and fusion rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import product
|
|
6
|
+
from math import prod
|
|
7
|
+
from typing import Iterable, Iterator, Mapping
|
|
8
|
+
|
|
9
|
+
from evidencelib.exceptions import InvalidMassError, TotalConflictError
|
|
10
|
+
from evidencelib.proposition import Proposition
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MassFunction:
|
|
14
|
+
"""A basic belief assignment over a frame."""
|
|
15
|
+
|
|
16
|
+
normalization_tolerance = 1e-6
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
frame,
|
|
21
|
+
values: Mapping[str | Proposition | Iterable[str], float],
|
|
22
|
+
*,
|
|
23
|
+
validate: bool = True,
|
|
24
|
+
tolerance: float = 1e-9,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.frame = frame
|
|
27
|
+
self.tolerance = tolerance
|
|
28
|
+
masses: dict[Proposition, float] = {}
|
|
29
|
+
for key, value in values.items():
|
|
30
|
+
prop = frame.proposition(key)
|
|
31
|
+
mass = float(value)
|
|
32
|
+
if mass < -tolerance:
|
|
33
|
+
raise InvalidMassError("Mass values must be non-negative.")
|
|
34
|
+
if abs(mass) <= tolerance:
|
|
35
|
+
continue
|
|
36
|
+
masses[prop] = masses.get(prop, 0.0) + mass
|
|
37
|
+
self._masses = self._clean(masses)
|
|
38
|
+
if validate:
|
|
39
|
+
self._validate_sum()
|
|
40
|
+
|
|
41
|
+
def __getitem__(self, key: str | Proposition | Iterable[str]) -> float:
|
|
42
|
+
return self.mass(key)
|
|
43
|
+
|
|
44
|
+
def __iter__(self) -> Iterator[tuple[Proposition, float]]:
|
|
45
|
+
return iter(self.items())
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
body = ", ".join(f"{prop}: {value:.6g}" for prop, value in self.items())
|
|
49
|
+
return f"MassFunction({{{body}}})"
|
|
50
|
+
|
|
51
|
+
def items(self) -> tuple[tuple[Proposition, float], ...]:
|
|
52
|
+
return tuple(sorted(self._masses.items(), key=lambda item: str(item[0])))
|
|
53
|
+
|
|
54
|
+
def focal(self) -> tuple[Proposition, ...]:
|
|
55
|
+
return tuple(prop for prop, _ in self.items())
|
|
56
|
+
|
|
57
|
+
def to_dict(self, *, string_keys: bool = True) -> dict[str | Proposition, float]:
|
|
58
|
+
"""Return the mass assignment as a plain dictionary."""
|
|
59
|
+
|
|
60
|
+
if string_keys:
|
|
61
|
+
return {str(prop): value for prop, value in self.items()}
|
|
62
|
+
return dict(self.items())
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def total_mass(self) -> float:
|
|
66
|
+
"""Sum of all stored masses."""
|
|
67
|
+
|
|
68
|
+
return sum(self._masses.values())
|
|
69
|
+
|
|
70
|
+
def mass(self, key: str | Proposition | Iterable[str]) -> float:
|
|
71
|
+
return self._masses.get(self.frame.proposition(key), 0.0)
|
|
72
|
+
|
|
73
|
+
def belief(self, key: str | Proposition | Iterable[str]) -> float:
|
|
74
|
+
target = self.frame.proposition(key)
|
|
75
|
+
return sum(value for prop, value in self._masses.items() if prop <= target)
|
|
76
|
+
|
|
77
|
+
def plausibility(self, key: str | Proposition | Iterable[str]) -> float:
|
|
78
|
+
target = self.frame.proposition(key)
|
|
79
|
+
return sum(value for prop, value in self._masses.items() if prop.intersects(target))
|
|
80
|
+
|
|
81
|
+
def commonality(self, key: str | Proposition | Iterable[str]) -> float:
|
|
82
|
+
target = self.frame.proposition(key)
|
|
83
|
+
return sum(value for prop, value in self._masses.items() if target <= prop)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def conflict(self) -> float:
|
|
87
|
+
return self.mass(self.frame.empty)
|
|
88
|
+
|
|
89
|
+
def conjunctive(self, *others: "MassFunction") -> "MassFunction":
|
|
90
|
+
"""Unnormalized conjunctive rule.
|
|
91
|
+
|
|
92
|
+
On a free DSm frame this is the classic DSm rule (DSmC). On Shafer's
|
|
93
|
+
DST model, contradictory intersections are accumulated on ``empty``.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
return self._combine_intersection((self, *others), normalize=False)
|
|
97
|
+
|
|
98
|
+
def dsmc(self, *others: "MassFunction") -> "MassFunction":
|
|
99
|
+
"""Alias for the classic conjunctive DSm rule."""
|
|
100
|
+
|
|
101
|
+
return self.conjunctive(*others)
|
|
102
|
+
|
|
103
|
+
def smets(self, *others: "MassFunction") -> "MassFunction":
|
|
104
|
+
"""Smets/TBM unnormalized rule, keeping conflict on the empty set."""
|
|
105
|
+
|
|
106
|
+
return self.conjunctive(*others)
|
|
107
|
+
|
|
108
|
+
def dempster(self, *others: "MassFunction") -> "MassFunction":
|
|
109
|
+
"""Dempster's normalized rule of combination."""
|
|
110
|
+
|
|
111
|
+
return self._combine_intersection((self, *others), normalize=True)
|
|
112
|
+
|
|
113
|
+
def yager(self, *others: "MassFunction") -> "MassFunction":
|
|
114
|
+
"""Yager's rule: transfer total conflict to total ignorance."""
|
|
115
|
+
|
|
116
|
+
conjunctive = self.conjunctive(*others)
|
|
117
|
+
conflict = conjunctive.conflict
|
|
118
|
+
masses = {prop: value for prop, value in conjunctive.items() if prop}
|
|
119
|
+
if conflict:
|
|
120
|
+
masses[self.frame.total] = masses.get(self.frame.total, 0.0) + conflict
|
|
121
|
+
return MassFunction(self.frame, masses)
|
|
122
|
+
|
|
123
|
+
def dubois_prade(self, *others: "MassFunction") -> "MassFunction":
|
|
124
|
+
"""Dubois-Prade style transfer of conflicts to disjunctions."""
|
|
125
|
+
|
|
126
|
+
return self.dsmh(*others)
|
|
127
|
+
|
|
128
|
+
def dsmh(self, *others: "MassFunction") -> "MassFunction":
|
|
129
|
+
"""Hybrid DSm rule for constrained models.
|
|
130
|
+
|
|
131
|
+
Products whose intersection is non-empty go to that intersection.
|
|
132
|
+
Products whose intersection is empty are transferred to the union of
|
|
133
|
+
the involved propositions. If that union is also empty under the model,
|
|
134
|
+
the mass goes to total ignorance.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
self._check_sources((self, *others))
|
|
138
|
+
masses: dict[Proposition, float] = {}
|
|
139
|
+
for props, values in self._focal_product((self, *others)):
|
|
140
|
+
amount = prod(values)
|
|
141
|
+
intersection = self._intersection_all(props)
|
|
142
|
+
if intersection:
|
|
143
|
+
target = intersection
|
|
144
|
+
else:
|
|
145
|
+
target = self._union_all(props)
|
|
146
|
+
if not target:
|
|
147
|
+
target = self.frame.total
|
|
148
|
+
masses[target] = masses.get(target, 0.0) + amount
|
|
149
|
+
masses.pop(self.frame.empty, None)
|
|
150
|
+
return MassFunction(self.frame, masses)
|
|
151
|
+
|
|
152
|
+
def pcr5(self, other: "MassFunction") -> "MassFunction":
|
|
153
|
+
"""PCR5 for two sources."""
|
|
154
|
+
|
|
155
|
+
return self.pcr6(other)
|
|
156
|
+
|
|
157
|
+
def pcr6(self, *others: "MassFunction") -> "MassFunction":
|
|
158
|
+
"""PCR6 proportional conflict redistribution for two or more sources."""
|
|
159
|
+
|
|
160
|
+
sources = (self, *others)
|
|
161
|
+
self._check_sources(sources)
|
|
162
|
+
masses: dict[Proposition, float] = {}
|
|
163
|
+
for props, values in self._focal_product(sources):
|
|
164
|
+
amount = prod(values)
|
|
165
|
+
intersection = self._intersection_all(props)
|
|
166
|
+
if intersection:
|
|
167
|
+
masses[intersection] = masses.get(intersection, 0.0) + amount
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
denominator = sum(values)
|
|
171
|
+
if denominator <= self.tolerance:
|
|
172
|
+
continue
|
|
173
|
+
for prop, source_mass in zip(props, values, strict=True):
|
|
174
|
+
target = prop if prop else self.frame.total
|
|
175
|
+
if not target:
|
|
176
|
+
continue
|
|
177
|
+
share = amount * source_mass / denominator
|
|
178
|
+
masses[target] = masses.get(target, 0.0) + share
|
|
179
|
+
masses.pop(self.frame.empty, None)
|
|
180
|
+
return MassFunction(self.frame, masses)
|
|
181
|
+
|
|
182
|
+
def normalize(self) -> "MassFunction":
|
|
183
|
+
"""Normalize a conjunctive result by removing empty-set conflict."""
|
|
184
|
+
|
|
185
|
+
conflict = self.conflict
|
|
186
|
+
denominator = 1.0 - conflict
|
|
187
|
+
if denominator <= self.tolerance:
|
|
188
|
+
raise TotalConflictError("Dempster normalization is undefined at total conflict.")
|
|
189
|
+
masses = {
|
|
190
|
+
prop: value / denominator
|
|
191
|
+
for prop, value in self._masses.items()
|
|
192
|
+
if prop and abs(value) > self.tolerance
|
|
193
|
+
}
|
|
194
|
+
return MassFunction(self.frame, masses)
|
|
195
|
+
|
|
196
|
+
def pignistic(self) -> dict[str, float]:
|
|
197
|
+
"""Return pignistic scores for singleton hypotheses.
|
|
198
|
+
|
|
199
|
+
This is the classical pignistic transformation on DST frames. On free
|
|
200
|
+
or hybrid DSmT frames, singleton hypotheses can overlap, so the returned
|
|
201
|
+
event scores are useful for decisions but do not have to sum to one.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
result = {name: 0.0 for name in self.frame.atoms}
|
|
205
|
+
singletons = dict(zip(self.frame.atoms, self.frame.symbols(), strict=True))
|
|
206
|
+
for prop, mass in self._masses.items():
|
|
207
|
+
if not prop:
|
|
208
|
+
continue
|
|
209
|
+
cardinality = prop.cardinality
|
|
210
|
+
if cardinality == 0:
|
|
211
|
+
continue
|
|
212
|
+
for name, atom in singletons.items():
|
|
213
|
+
overlap = (atom & prop).cardinality
|
|
214
|
+
if overlap:
|
|
215
|
+
result[name] += mass * overlap / cardinality
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def pignistic_regions(self) -> dict[str, float]:
|
|
219
|
+
"""Return a probability distribution over model Venn regions."""
|
|
220
|
+
|
|
221
|
+
result = {self._format_region(region): 0.0 for region in self.frame._universe}
|
|
222
|
+
for prop, mass in self._masses.items():
|
|
223
|
+
if not prop:
|
|
224
|
+
continue
|
|
225
|
+
cardinality = prop.cardinality
|
|
226
|
+
if cardinality == 0:
|
|
227
|
+
continue
|
|
228
|
+
share = mass / cardinality
|
|
229
|
+
for region in prop.regions:
|
|
230
|
+
result[self._format_region(region)] += share
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
def decision(self) -> str:
|
|
234
|
+
"""Return the singleton with the largest pignistic probability."""
|
|
235
|
+
|
|
236
|
+
probabilities = self.pignistic()
|
|
237
|
+
return max(probabilities, key=probabilities.__getitem__)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def _from_unchecked(cls, frame, values: Mapping[Proposition, float]) -> "MassFunction":
|
|
241
|
+
return cls(frame, values, validate=False)
|
|
242
|
+
|
|
243
|
+
def _combine_intersection(
|
|
244
|
+
self,
|
|
245
|
+
sources: tuple["MassFunction", ...],
|
|
246
|
+
*,
|
|
247
|
+
normalize: bool,
|
|
248
|
+
) -> "MassFunction":
|
|
249
|
+
self._check_sources(sources)
|
|
250
|
+
masses: dict[Proposition, float] = {}
|
|
251
|
+
for props, values in self._focal_product(sources):
|
|
252
|
+
target = self._intersection_all(props)
|
|
253
|
+
masses[target] = masses.get(target, 0.0) + prod(values)
|
|
254
|
+
result = MassFunction(self.frame, masses)
|
|
255
|
+
return result.normalize() if normalize else result
|
|
256
|
+
|
|
257
|
+
def _focal_product(
|
|
258
|
+
self,
|
|
259
|
+
sources: tuple["MassFunction", ...],
|
|
260
|
+
) -> Iterator[tuple[tuple[Proposition, ...], tuple[float, ...]]]:
|
|
261
|
+
item_groups = [source.items() for source in sources]
|
|
262
|
+
for combo in product(*item_groups):
|
|
263
|
+
props = tuple(prop for prop, _ in combo)
|
|
264
|
+
values = tuple(value for _, value in combo)
|
|
265
|
+
yield props, values
|
|
266
|
+
|
|
267
|
+
def _intersection_all(self, props: Iterable[Proposition]) -> Proposition:
|
|
268
|
+
iterator = iter(props)
|
|
269
|
+
result = next(iterator)
|
|
270
|
+
for prop in iterator:
|
|
271
|
+
result = result & prop
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
def _union_all(self, props: Iterable[Proposition]) -> Proposition:
|
|
275
|
+
result = self.frame.empty
|
|
276
|
+
for prop in props:
|
|
277
|
+
result = result | prop
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
def _check_sources(self, sources: tuple["MassFunction", ...]) -> None:
|
|
281
|
+
if len(sources) < 2:
|
|
282
|
+
raise ValueError("At least two sources are required.")
|
|
283
|
+
if any(source.frame is not self.frame for source in sources):
|
|
284
|
+
raise ValueError("All mass functions must belong to the same frame.")
|
|
285
|
+
|
|
286
|
+
def _validate_sum(self) -> None:
|
|
287
|
+
total = sum(self._masses.values())
|
|
288
|
+
if abs(total - 1.0) <= self.tolerance:
|
|
289
|
+
return
|
|
290
|
+
if abs(total - 1.0) <= self.normalization_tolerance:
|
|
291
|
+
self._masses = {prop: value / total for prop, value in self._masses.items()}
|
|
292
|
+
return
|
|
293
|
+
if abs(total - 1.0) > self.tolerance:
|
|
294
|
+
raise InvalidMassError(f"Mass values must sum to 1.0, got {total}.")
|
|
295
|
+
|
|
296
|
+
def _clean(self, masses: Mapping[Proposition, float]) -> dict[Proposition, float]:
|
|
297
|
+
return {
|
|
298
|
+
prop: value
|
|
299
|
+
for prop, value in masses.items()
|
|
300
|
+
if abs(value) > self.tolerance
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
def _format_region(self, region: int) -> str:
|
|
304
|
+
names = [name for i, name in enumerate(self.frame.atoms) if region & (1 << i)]
|
|
305
|
+
return "&".join(names)
|
evidencelib/parser.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Small parser for proposition expressions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from evidencelib.proposition import Proposition
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class _Token:
|
|
12
|
+
kind: str
|
|
13
|
+
value: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PropositionParser:
|
|
17
|
+
"""Parse proposition expressions without executing Python code."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, frame) -> None:
|
|
20
|
+
self.frame = frame
|
|
21
|
+
|
|
22
|
+
def parse(self, expression: str) -> Proposition:
|
|
23
|
+
self._tokens = self._tokenize(expression)
|
|
24
|
+
self._position = 0
|
|
25
|
+
result = self._parse_union()
|
|
26
|
+
if self._peek().kind != "end":
|
|
27
|
+
raise ValueError(f"Unexpected token {self._peek().value!r}.")
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
def _parse_union(self) -> Proposition:
|
|
31
|
+
result = self._parse_intersection()
|
|
32
|
+
while self._accept("|"):
|
|
33
|
+
result = result | self._parse_intersection()
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
def _parse_intersection(self) -> Proposition:
|
|
37
|
+
result = self._parse_primary()
|
|
38
|
+
while self._accept("&"):
|
|
39
|
+
result = result & self._parse_primary()
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
def _parse_primary(self) -> Proposition:
|
|
43
|
+
token = self._peek()
|
|
44
|
+
if self._accept("("):
|
|
45
|
+
result = self._parse_union()
|
|
46
|
+
self._expect(")")
|
|
47
|
+
return result
|
|
48
|
+
if token.kind == "atom":
|
|
49
|
+
self._position += 1
|
|
50
|
+
if token.value in {"empty", "EMPTY"}:
|
|
51
|
+
return Proposition(self.frame, frozenset())
|
|
52
|
+
return self.frame.atom(token.value)
|
|
53
|
+
raise ValueError(f"Expected proposition, got {token.value!r}.")
|
|
54
|
+
|
|
55
|
+
def _accept(self, kind: str) -> bool:
|
|
56
|
+
if self._peek().kind == kind:
|
|
57
|
+
self._position += 1
|
|
58
|
+
return True
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
def _expect(self, kind: str) -> None:
|
|
62
|
+
if not self._accept(kind):
|
|
63
|
+
raise ValueError(f"Expected {kind!r}, got {self._peek().value!r}.")
|
|
64
|
+
|
|
65
|
+
def _peek(self) -> _Token:
|
|
66
|
+
return self._tokens[self._position]
|
|
67
|
+
|
|
68
|
+
def _tokenize(self, expression: str) -> list[_Token]:
|
|
69
|
+
tokens: list[_Token] = []
|
|
70
|
+
i = 0
|
|
71
|
+
while i < len(expression):
|
|
72
|
+
char = expression[i]
|
|
73
|
+
if char.isspace():
|
|
74
|
+
i += 1
|
|
75
|
+
continue
|
|
76
|
+
if char in "()":
|
|
77
|
+
tokens.append(_Token(char, char))
|
|
78
|
+
i += 1
|
|
79
|
+
continue
|
|
80
|
+
if char in {"&", "∩", "∧"}:
|
|
81
|
+
tokens.append(_Token("&", char))
|
|
82
|
+
i += 1
|
|
83
|
+
continue
|
|
84
|
+
if char in {"|", "∪", "∨"}:
|
|
85
|
+
tokens.append(_Token("|", char))
|
|
86
|
+
i += 1
|
|
87
|
+
continue
|
|
88
|
+
if char == "∅":
|
|
89
|
+
tokens.append(_Token("atom", "empty"))
|
|
90
|
+
i += 1
|
|
91
|
+
continue
|
|
92
|
+
|
|
93
|
+
start = i
|
|
94
|
+
while i < len(expression):
|
|
95
|
+
current = expression[i]
|
|
96
|
+
if current.isspace() or current in "()&|∩∧∪∨":
|
|
97
|
+
break
|
|
98
|
+
i += 1
|
|
99
|
+
value = expression[start:i]
|
|
100
|
+
if value not in self.frame.atoms and value not in {"empty", "EMPTY"}:
|
|
101
|
+
raise ValueError(f"Unknown proposition atom {value!r}.")
|
|
102
|
+
tokens.append(_Token("atom", value))
|
|
103
|
+
tokens.append(_Token("end", "end of expression"))
|
|
104
|
+
return tokens
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Symbolic propositions used by DST and DSmT frames."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, FrozenSet
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from evidencelib.frame import Frame
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class Proposition:
|
|
14
|
+
"""A canonical proposition represented by possible Venn regions.
|
|
15
|
+
|
|
16
|
+
Users normally create propositions through a :class:`evidencelib.Frame` and
|
|
17
|
+
combine them with ``|`` for union and ``&`` for intersection.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
frame: "Frame"
|
|
21
|
+
regions: FrozenSet[int]
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
normalized = self.frame._normalize_regions(self.regions)
|
|
25
|
+
object.__setattr__(self, "regions", frozenset(normalized))
|
|
26
|
+
|
|
27
|
+
def __or__(self, other: "Proposition") -> "Proposition":
|
|
28
|
+
self._check_same_frame(other)
|
|
29
|
+
return Proposition(self.frame, self.regions | other.regions)
|
|
30
|
+
|
|
31
|
+
def __and__(self, other: "Proposition") -> "Proposition":
|
|
32
|
+
self._check_same_frame(other)
|
|
33
|
+
return Proposition(self.frame, self.regions & other.regions)
|
|
34
|
+
|
|
35
|
+
def __le__(self, other: "Proposition") -> bool:
|
|
36
|
+
self._check_same_frame(other)
|
|
37
|
+
return self.regions <= other.regions
|
|
38
|
+
|
|
39
|
+
def __lt__(self, other: "Proposition") -> bool:
|
|
40
|
+
return self <= other and self.regions != other.regions
|
|
41
|
+
|
|
42
|
+
def __bool__(self) -> bool:
|
|
43
|
+
return bool(self.regions)
|
|
44
|
+
|
|
45
|
+
def __str__(self) -> str:
|
|
46
|
+
return self.frame.format(self)
|
|
47
|
+
|
|
48
|
+
def __format__(self, format_spec: str) -> str:
|
|
49
|
+
return format(str(self), format_spec)
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return f"Proposition({str(self)!r})"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def is_empty(self) -> bool:
|
|
56
|
+
return not self.regions
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def cardinality(self) -> int:
|
|
60
|
+
"""DSm cardinality: number of non-empty model regions."""
|
|
61
|
+
|
|
62
|
+
return len(self.regions)
|
|
63
|
+
|
|
64
|
+
def intersects(self, other: "Proposition") -> bool:
|
|
65
|
+
self._check_same_frame(other)
|
|
66
|
+
return bool(self.regions & other.regions)
|
|
67
|
+
|
|
68
|
+
def union_atoms(self) -> "Proposition":
|
|
69
|
+
"""Return the disjunction of singleton hypotheses involved in this proposition."""
|
|
70
|
+
|
|
71
|
+
mask = 0
|
|
72
|
+
for region in self.regions:
|
|
73
|
+
mask |= region
|
|
74
|
+
if mask == 0:
|
|
75
|
+
return self.frame.empty
|
|
76
|
+
return Proposition(self.frame, frozenset(r for r in self.frame._universe if r & mask))
|
|
77
|
+
|
|
78
|
+
def _check_same_frame(self, other: "Proposition") -> None:
|
|
79
|
+
if not isinstance(other, Proposition) or other.frame is not self.frame:
|
|
80
|
+
raise ValueError("Propositions must belong to the same frame.")
|
evidencelib/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: evidencelib
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Computational belief functions for DST and DSmT.
|
|
5
|
+
Project-URL: Documentation, https://evidencelib.readthedocs.io/en/latest/
|
|
6
|
+
Project-URL: Source, https://github.com/itaprac/evidencelib
|
|
7
|
+
Project-URL: Issues, https://github.com/itaprac/evidencelib/issues
|
|
8
|
+
Author: evidencelib contributors
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 evidencelib contributors
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: belief-functions,dempster-shafer,dsmt,dst,evidence-theory
|
|
33
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
34
|
+
Classifier: Intended Audience :: Science/Research
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
44
|
+
Requires-Dist: myst-parser>=4.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: sphinx-rtd-theme>=3.0; extra == 'dev'
|
|
47
|
+
Requires-Dist: sphinx>=8.0; extra == 'dev'
|
|
48
|
+
Provides-Extra: docs
|
|
49
|
+
Requires-Dist: myst-parser>=4.0; extra == 'docs'
|
|
50
|
+
Requires-Dist: sphinx-rtd-theme>=3.0; extra == 'docs'
|
|
51
|
+
Requires-Dist: sphinx>=8.0; extra == 'docs'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# evidencelib
|
|
55
|
+
|
|
56
|
+
[](https://github.com/itaprac/evidencelib/actions/workflows/ci.yml)
|
|
57
|
+
[](https://evidencelib.readthedocs.io/en/latest/?badge=latest)
|
|
58
|
+
[](LICENSE)
|
|
59
|
+
|
|
60
|
+
Python library for belief-function calculations in Dempster-Shafer theory
|
|
61
|
+
(DST) and Dezert-Smarandache theory (DSmT).
|
|
62
|
+
|
|
63
|
+
`evidencelib` provides a compact quantitative core for finite frames: symbolic
|
|
64
|
+
propositions, basic belief assignments, evidence fusion rules, belief measures,
|
|
65
|
+
and pignistic decision support.
|
|
66
|
+
|
|
67
|
+
Documentation is available on
|
|
68
|
+
[Read the Docs](https://evidencelib.readthedocs.io/en/latest/).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
You can install `evidencelib` using pip:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install evidencelib
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For local development:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/itaprac/evidencelib.git
|
|
84
|
+
cd evidencelib
|
|
85
|
+
python3.10 -m venv .venv
|
|
86
|
+
source .venv/bin/activate
|
|
87
|
+
pip install -e ".[dev,docs]"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Run the test suite:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python -m pytest -q
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Build the documentation locally:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m sphinx -W -b html docs docs/_build/html
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Available Functionality
|
|
105
|
+
|
|
106
|
+
The library contains:
|
|
107
|
+
|
|
108
|
+
### Models
|
|
109
|
+
|
|
110
|
+
| Constructor | Description |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `Frame.dst(...)` | Shafer's classical DST model with exhaustive and mutually exclusive hypotheses. |
|
|
113
|
+
| `Frame.dsmt(...)` | Free DSm model where hypotheses may overlap. |
|
|
114
|
+
| `Frame.hybrid(...)` | Constrained DSm model with explicit emptiness or exclusivity constraints. |
|
|
115
|
+
|
|
116
|
+
### Proposition Algebra
|
|
117
|
+
|
|
118
|
+
| Operation | Meaning |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `A \| B` | Union / disjunction, `A ∪ B`. |
|
|
121
|
+
| `A & B` | Intersection / conjunction, `A ∩ B`. |
|
|
122
|
+
| `frame.proposition("A ∩ (B ∪ C)")` | Parse a proposition from text. |
|
|
123
|
+
| `frame.elements()` | Generate the model's power set or hyper-power set. |
|
|
124
|
+
|
|
125
|
+
### Belief Measures
|
|
126
|
+
|
|
127
|
+
| Method | Description |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `mass(A)` | Direct mass assigned to a proposition. |
|
|
130
|
+
| `belief(A)` | Sum of masses contained in `A`. |
|
|
131
|
+
| `plausibility(A)` | Sum of masses intersecting `A`. |
|
|
132
|
+
| `commonality(A)` | Sum of masses containing `A`. |
|
|
133
|
+
| `conflict` | Mass assigned to the empty proposition. |
|
|
134
|
+
|
|
135
|
+
### Fusion Rules
|
|
136
|
+
|
|
137
|
+
| Method | Description |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `conjunctive(...)` | Unnormalized conjunctive rule. |
|
|
140
|
+
| `dsmc(...)` | Classic DSm rule on a free DSm frame. |
|
|
141
|
+
| `smets(...)` | TBM/Smets rule, keeping conflict on the empty proposition. |
|
|
142
|
+
| `dempster(...)` | Normalized Dempster rule. |
|
|
143
|
+
| `yager(...)` | Yager rule, moving conflict to total ignorance. |
|
|
144
|
+
| `dsmh(...)` | Hybrid DSm rule for constrained models. |
|
|
145
|
+
| `dubois_prade(...)` | Static Dubois-Prade-style conflict transfer. |
|
|
146
|
+
| `pcr5(...)` | PCR5 for two sources. |
|
|
147
|
+
| `pcr6(...)` | PCR6 for two or more sources. |
|
|
148
|
+
|
|
149
|
+
### Decision Support
|
|
150
|
+
|
|
151
|
+
| Method | Description |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `pignistic()` | Singleton pignistic scores. |
|
|
154
|
+
| `pignistic_regions()` | Probability distribution over disjoint model regions. |
|
|
155
|
+
| `decision()` | Singleton with the largest pignistic score. |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Usage Example
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from evidencelib import Frame
|
|
163
|
+
|
|
164
|
+
frame = Frame.dst(["A", "B"])
|
|
165
|
+
A, B = frame.symbols()
|
|
166
|
+
|
|
167
|
+
m1 = frame.mass({
|
|
168
|
+
A: 0.6,
|
|
169
|
+
A | B: 0.4,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
m2 = frame.mass({
|
|
173
|
+
B: 0.3,
|
|
174
|
+
A | B: 0.7,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
print(m1.dempster(m2).to_dict())
|
|
178
|
+
print(m1.pcr5(m2).to_dict())
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Output:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
{"A": 0.5121951219512195, "A|B": 0.34146341463414637, "B": 0.14634146341463414}
|
|
185
|
+
{"A": 0.54, "A|B": 0.28, "B": 0.18}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Free DSmT example:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
frame = Frame.dsmt(["A", "B"])
|
|
192
|
+
A, B = frame.symbols()
|
|
193
|
+
|
|
194
|
+
m = frame.mass({
|
|
195
|
+
A: 0.2,
|
|
196
|
+
B: 0.3,
|
|
197
|
+
A & B: 0.4,
|
|
198
|
+
A | B: 0.1,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
print(m.pignistic())
|
|
202
|
+
print(m.pignistic_regions())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
In DSmT, singleton hypotheses can overlap, so `pignistic()` returns decision
|
|
206
|
+
scores that do not necessarily sum to one. Use `pignistic_regions()` for a
|
|
207
|
+
probability distribution over disjoint Venn regions.
|
|
208
|
+
|
|
209
|
+
More examples are available in the [`examples/`](examples/) directory and in
|
|
210
|
+
the documentation.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## References
|
|
215
|
+
|
|
216
|
+
- Shafer, G. (1976). *A Mathematical Theory of Evidence*. Princeton University Press.
|
|
217
|
+
- Smarandache, F., & Dezert, J. (eds.). *Advances and Applications of DSmT for Information Fusion*.
|
|
218
|
+
- Dezert, J., & Smarandache, F. *An Introduction to DSmT*.
|
|
219
|
+
- Zadeh, L. A. (1986). A simple view of the Dempster-Shafer theory of evidence and its implication for the rule of combination. *AI Magazine*, 7(2), 85-90.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
`evidencelib` is released under the MIT License.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
evidencelib/__init__.py,sha256=s9pRq0RXF1j6Kl7IRjxNBhroVla0lPif-cxcBd5rBFQ,233
|
|
2
|
+
evidencelib/exceptions.py,sha256=vh683OUtlryjLX6l5OKZL_jOwqJgus-XKpJSBXFEfjg,369
|
|
3
|
+
evidencelib/frame.py,sha256=62AFTVpVAfPCMzeBoVzUok1IAYhRNT54CGDdgOfVsn0,7436
|
|
4
|
+
evidencelib/mass.py,sha256=Vjl8kmo8tJsseHvY5tZJwKORms6va2oBq7myAc2ti5c,11699
|
|
5
|
+
evidencelib/parser.py,sha256=aMm_Ai2cB2E4DUvUX67059EIyft1zhBmCgtqF8TCh8M,3333
|
|
6
|
+
evidencelib/proposition.py,sha256=zLaUOWskXI8hMbxfQ6X2KQ-l9peyK0s6-fsWLd-ma2w,2584
|
|
7
|
+
evidencelib/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
8
|
+
evidencelib-1.0.0.dist-info/METADATA,sha256=x6D0Qbm7F7H20xFOFPzSyVyJqzKb_dfIrNGzGA2s0mc,7137
|
|
9
|
+
evidencelib-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
10
|
+
evidencelib-1.0.0.dist-info/licenses/LICENSE,sha256=4LvwJSWU4qyrxxGYsh0ABUM28zI-5Q7MOO1NFhqlEQ8,1082
|
|
11
|
+
evidencelib-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 evidencelib contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|