Orthopair numbers: IFN, PFN, FFN, QROFN¶
An orthopair number records an opinion as two degrees: how strongly you support something (membership \(\mu\)) and how strongly you oppose it (non-membership \(\nu\)). Unlike a classical fuzzy number, the two need not add up to 1. What is left over is hesitancy: "I am not sure". Use them for ratings from people, such as expert judgements, where a single degree hides doubt.
The constraint that keeps the pair meaningful depends on the rung \(q\):
| Class | \(q\) | Name | Constraint |
|---|---|---|---|
IFN |
1 | intuitionistic (Atanassov 1986) | \(\mu + \nu \le 1\) |
PFN |
2 | Pythagorean (Yager 2013) | \(\mu^2 + \nu^2 \le 1\) |
FFN |
3 | Fermatean (Senapati and Yager 2020) | \(\mu^3 + \nu^3 \le 1\) |
QROFN |
any integer \(\ge 1\) | q-rung orthopair (Yager 2017) | \(\mu^q + \nu^q \le 1\) |
A higher rung admits more pairs: \((0.8, 0.5)\) is invalid for IFN but fine for
PFN. The hesitancy is \(\pi = (1 - \mu^q - \nu^q)^{1/q}\).
IFN, PFN and FFN are QROFN with the rung fixed, so everything below
holds for all four. You build IFN(mu, nu), PFN(mu, nu), FFN(mu, nu) and
QROFN(mu, nu, q).
from bulanik import FFN, IFN, PFN, QROFN
x = IFN(0.6, 0.3)
print(x, x.q, round(x.hesitancy, 4))
print(PFN(0.8, 0.5).q, round(PFN(0.8, 0.5).hesitancy, 4))
print(FFN(0.8, 0.6), QROFN(0.9, 0.5, 3))
print(x.complement()) # membership and non-membership swapped
print(isinstance(x, QROFN)) # the fixed-q classes are QROFN subclasses
IFN(mu=0.6, nu=0.3) 1 0.1
2 0.3317
FFN(mu=0.8, nu=0.6) QROFN(mu=0.9, nu=0.5, q=3)
IFN(mu=0.3, nu=0.6)
True
One implementation, two operator families¶
Every operation is the intuitionistic one applied to the \(q\)-th powers
\(a = \mu^q\) and \(b = \nu^q\), followed by the \(q\)-th root of each result part.
Two operator families exist, selected by the name ops="algebraic" (the default,
and what + * ** use) or ops="einstein". For two numbers with powers
\((a_1, b_1)\) and \((a_2, b_2)\) the code computes:
| Operation | "algebraic" |
"einstein" |
|---|---|---|
add |
\((a_1 + a_2 - a_1 a_2,\; b_1 b_2)\) | \(\left(\dfrac{a_1 + a_2}{1 + a_1 a_2},\; \dfrac{b_1 b_2}{1 + (1 - b_1)(1 - b_2)}\right)\) |
mul |
\((a_1 a_2,\; b_1 + b_2 - b_1 b_2)\) | \(\left(\dfrac{a_1 a_2}{1 + (1 - a_1)(1 - a_2)},\; \dfrac{b_1 + b_2}{1 + b_1 b_2}\right)\) |
scale by \(k\) |
\((1 - (1 - a)^k,\; b^k)\) | \(\left(\dfrac{(1+a)^k - (1-a)^k}{(1+a)^k + (1-a)^k},\; \dfrac{2 b^k}{(2-b)^k + b^k}\right)\) |
power by \(k\) |
\((a^k,\; 1 - (1 - b)^k)\) | \(\left(\dfrac{2 a^k}{(2-a)^k + a^k},\; \dfrac{(1+b)^k - (1-b)^k}{(1+b)^k + (1-b)^k}\right)\) |
Sum and product are not the same as arithmetic on crisp numbers: adding two opinions raises membership and lowers non-membership, while multiplying them does the opposite: membership falls and non-membership rises. Scale and power need \(k \ge 0\). The Einstein family gives similar but not identical results.
from bulanik import IFN, PFN
a, b = IFN(0.6, 0.3), IFN(0.5, 0.4)
def pair(n):
return f"({n.mu:.4f}, {n.nu:.4f})"
print("a + b ", pair(a + b))
print("a.add(b, einstein)", pair(a.add(b, ops="einstein")))
print("a * b ", pair(a * b))
print("a.mul(b, einstein)", pair(a.mul(b, ops="einstein")))
print("2 * a ", pair(2 * a))
print("a ** 2 ", pair(a ** 2))
print("PFN a ** 2 ", pair(PFN(0.6, 0.5) ** 2))
print(IFN.OPS, PFN.OPS)
a + b (0.8000, 0.1200)
a.add(b, einstein) (0.8462, 0.0845)
a * b (0.3000, 0.5800)
a.mul(b, einstein) (0.2500, 0.6250)
2 * a (0.8400, 0.0900)
a ** 2 (0.3600, 0.5100)
PFN a ** 2 (0.3600, 0.6614)
('algebraic', 'einstein') ('algebraic', 'einstein')
Scores, accuracy and distance¶
A score ranks numbers; an accuracy breaks ties. With \(a = \mu^q\), \(b = \nu^q\):
| Quantity | Names (default first) | Formula |
|---|---|---|
| score | "difference" |
\(\mu^q - \nu^q\) |
"normalized" |
\((1 + \mu^q - \nu^q)/2\), a score in \([0, 1]\) | |
| accuracy | "sum" |
\(\mu^q + \nu^q\) (the certainty: one minus the hesitancy power) |
| distance | "hamming" |
\((\lvert \Delta a \rvert + \lvert \Delta b \rvert + \lvert \Delta p \rvert)/2\) |
"euclidean" |
\(\sqrt{(\Delta a^2 + \Delta b^2 + \Delta p^2)/2}\) |
where \(\Delta\) is the difference between the two numbers and \(p = 1 - a - b\) is the hesitancy power \(\pi^q\). Two numbers can have the same
score and still differ in accuracy: IFN(0.6, 0.3) and IFN(0.5, 0.2) both
score 0.3, but the first is more certain.
from bulanik import IFN
x, y = IFN(0.6, 0.3), IFN(0.5, 0.2)
print(x.score(), y.score()) # a tie on the score ...
print(x.accuracy(), y.accuracy()) # ... broken by the accuracy
print(x.score("normalized"))
print(round(x.distance(y), 4), round(x.distance(y, "euclidean"), 4))
print(IFN.SCORES, IFN.ACCURACIES, IFN.DISTANCES)
0.3 0.3
0.8999999999999999 0.7
0.65
0.2 0.1732
('difference', 'normalized') ('sum',) ('hamming', 'euclidean')
Averaging opinions¶
The weighted average wa of IFNs is the classic IFWA operator
\((1 - \prod (1 - \mu_i)^{w_i},\; \prod \nu_i^{w_i})\), and the weighted geometric
mean wg is IFWG. See Aggregation.
Rounding and the boundary¶
Floating-point rounding can put a pair a hair outside the constraint. Inputs up to \(10^{-12}\) beyond it are accepted and projected back onto \(\mu^q + \nu^q = 1\); anything beyond raises. The exact rules, with a worked example, are in Conventions.
Pitfalls¶
The constraint depends on the rung. IFN(0.8, 0.5) fails while PFN(0.8, 0.5)
works. Pick the lowest rung that holds your data, or construct the rung you need
with QROFN.
Rungs do not mix. IFN + PFN raises TypeError even though both are
QROFN; so does a different q in QROFN. Convert deliberately by choosing the
degrees again.
q must be an integer \(\ge 1\), and fixed-rung classes reject a conflicting q.
Scale and power need \(k \ge 0\). There is no negative scale and no reciprocal,
and results that leave the constraint beyond rounding raise ValueError.
Degrees must be plain numbers in \([0, 1]\).
from bulanik import FFN, IFN, PFN, QROFN, TFN
def show(make):
try:
print(make())
except Exception as error:
print(f"{type(error).__name__}: {error}")
show(lambda: IFN(0.8, 0.5))
show(lambda: PFN(0.8, 0.5)) # fine
show(lambda: FFN(0.9, 0.8)) # needs mu^3 + nu^3 <= 1
show(lambda: IFN(0.5, 0.4) + PFN(0.5, 0.4))
show(lambda: QROFN(0.5, 0.4, 2) + QROFN(0.5, 0.4, 3))
show(lambda: IFN(0.5, 0.4) + TFN(1, 2, 3))
show(lambda: QROFN(0.5, 0.4, 0))
show(lambda: QROFN(0.5, 0.4, 2.5))
show(lambda: IFN(0.5, 0.4, q=2))
show(lambda: IFN(1.2, 0.0))
show(lambda: IFN(True, 0.0))
show(lambda: IFN(0.5, 0.4).scale(-1))
show(lambda: IFN(0.5, 0.4).score("median"))
show(lambda: IFN(0.5, 0.4).add(IFN(0.1, 0.1), ops="hamacher"))
ValueError: need mu^1 + nu^1 <= 1, got mu=0.8, nu=0.5 (sum 1.3)
PFN(mu=0.8, nu=0.5)
ValueError: need mu^3 + nu^3 <= 1, got mu=0.9, nu=0.8 (sum 1.241)
TypeError: unsupported operand type(s) for +: 'IFN' and 'PFN'
TypeError: unsupported operand type(s) for +: 'QROFN' and 'QROFN'
TypeError: unsupported operand type(s) for +: 'IFN' and 'TFN'
ValueError: q must be an integer >= 1, got 0
ValueError: q must be an integer >= 1, got 2.5
ValueError: IFN has q = 1, got q=2
ValueError: mu must be in [0, 1], got 1.2
TypeError: mu must be a number, got bool True
ValueError: scale needs k >= 0, got -1.0
ValueError: unknown score 'median' for IFN; valid: difference, normalized
ValueError: unknown ops 'hamacher' for IFN; valid: algebraic, einstein
See also¶
- Conventions for validation and the named-operator rule.
- Choosing a method for IFN versus PFN versus FFN.
- Three-part numbers when you also need a neutral degree.
- Aggregation, and Sets and scales for
linguistic scales of
IFNterms. - API:
QROFN,IFN,PFN,FFN. - Example: 05 intuitionistic pipeline.