Skip to content

Triangular and trapezoidal numbers

A triangular fuzzy number (TFN) is a vague quantity such as "about 4, certainly between 2 and 7". It is three numbers \((l, m, u)\): the lower foot, the peak and the upper foot. A trapezoidal fuzzy number (TrFN) has four, \((a, b, c, d)\): a plateau \([b, c]\) of fully plausible values instead of a single peak. Use them when your input is a measurement with a spread, an interval estimate, or a linguistic rating such as "good" that you have mapped to a range.

The idea

The membership degree \(\mu(x) \in [0, 1]\) says how well the crisp value \(x\) fits the number. For a triangle it rises linearly from 0 at \(l\) to 1 at \(m\) and falls back to 0 at \(u\). For a trapezoid it stays 1 on the whole plateau.

Membership functions of TFN(2, 4, 7) and TrFN(1, 3, 5, 8), with support, core and the α-cut at 0.5 marked Membership functions of TFN(2, 4, 7) and TrFN(1, 3, 5, 8), with support, core and the α-cut at 0.5 marked
The support is where \(\mu > 0\), the core is where \(\mu = 1\), and the α-cut at level α is the interval where \(\mu \ge \alpha\).

Two rules govern the points, and the constructor checks both: every point is a finite real number (a bool or a string is a TypeError), and the points are ordered, \(l \le m \le u\) or \(a \le b \le c \le d\). Equal points are allowed, so TFN(5, 5, 5) is the crisp number 5 and TFN(0, 0, 5) has a vertical left edge.

from bulanik import TFN, TrFN

a = TFN(1, 2, 4)
t = TrFN(1, 2, 3, 5)
print(a, t, sep="\n")                  # points are stored as floats
print(a.l, a.m, a.u)
print(a.mu(1.5), a.mu(3), a.mu(9))     # membership: rising edge, falling edge, outside
print(t.mu(2.5))                       # on the plateau
print(a.to_trfn())                     # a TFN is a trapezoid with a one-point core
print(TFN(5, 5, 5).score())            # a crisp number is a degenerate triangle
TFN(l=1.0, m=2.0, u=4.0) TrFN(a=1.0, b=2.0, c=3.0, d=5.0)
1.0 2.0 4.0
0.5 0.5 0.0
1.0
TrFN(a=1.0, b=2.0, c=2.0, d=4.0)
5.0

α-cuts

The α-cut \(\{x : \mu(x) \ge \alpha\}\) of a triangle or trapezoid is always an interval. With support \([p_0, p_n]\) and core \([c_\text{lo}, c_\text{hi}]\) the code computes

\[ \bigl[p_0 + \alpha\,(c_\text{lo} - p_0),\;\; p_n - \alpha\,(p_n - c_\text{hi})\bigr]. \]

At \(\alpha = 0\) this is the support and at \(\alpha = 1\) the core. α-cuts are the bridge to interval arithmetic: fuzzy arithmetic is interval arithmetic carried out at every level.

from bulanik import TFN, TrFN

a = TFN(1, 2, 4)
for alpha in (0, 0.5, 1):
    print(alpha, a.alpha_cut(alpha))
print(TrFN(1, 2, 3, 5).alpha_cut(1))   # the plateau
0 (1.0, 4.0)
0.5 (1.5, 3.0)
1 (2.0, 2.0)
(2.0, 3.0)

Arithmetic

The operators + - * /, ** and unary - use the only operator family, "algebraic"; the methods add, sub, mul, scale, power and reciprocal do the same and take an ops= argument that only accepts that name. Every operation returns a new number of the same type and combines TFN with TFN and TrFN with TrFN. The code is the standard approximate arithmetic on the defining points:

Operation Result, for a triangle \((l_1, m_1, u_1)\) and \((l_2, m_2, u_2)\)
A + B \((l_1 + l_2,\; m_1 + m_2,\; u_1 + u_2)\)
A - B \((l_1 - u_2,\; m_1 - m_2,\; u_1 - l_2)\): each point against the opposite point of \(B\)
A * B outer points are the smallest and largest of the four products \(l_1 l_2,\, l_1 u_2,\, u_1 l_2,\, u_1 u_2\); the inner points are the products of the inner points, sorted. For non-negative numbers this is \((l_1 l_2,\; m_1 m_2,\; u_1 u_2)\)
k * A, A * k, A / k every point times \(k\) (or \(1/k\)); for \(k < 0\) the points are also reversed so the order is kept
A ** k every point to the power \(k\); reversed for \(k < 0\); needs the lowest point \(\ge 0\) (and \(> 0\) for \(k < 0\))
A.reciprocal() \((1/u,\; 1/m,\; 1/l)\); the support must not contain 0
A / B A * B.reciprocal()

A trapezoid works the same way on its four points. The sum and the difference are exact in the sense of α-cuts (the cut of \(A + B\) is the sum of the cuts). The product and the quotient are approximations: the exact product of two triangles has curved sides, and A * B returns the triangle through its corner products. The two agree at \(\alpha = 0\) and \(\alpha = 1\) but not in between, and the triangle is not simply wider or narrower: for the example below, at \(\alpha = 0.5\) the exact cut is \([3.75, 12]\) and the triangle's is \([4, 13]\), so it is inside at the lower end and outside at the upper end.

α-cuts of A = TFN(1, 2, 4), B = TFN(2, 3, 5), A + B and A × B, stacked on one x axis α-cuts of A = TFN(1, 2, 4), B = TFN(2, 3, 5), A + B and A × B, stacked on one x axis
Each row shows five α-cuts. The sum is exact. The outline on the last row is the exact product; the returned triangle matches it at \(\alpha = 0\) and \(\alpha = 1\) and differs in between (at \(\alpha = 0.5\): \([4, 13]\) against the exact \([3.75, 12]\)).
from bulanik import TFN

a, b = TFN(1, 2, 4), TFN(2, 3, 5)
print("a + b  =", a + b)
print("a - b  =", a - b)
print("a * b  =", a * b)
print("a / b  =", a / b)
print("2 * a  =", 2 * a, "| -a =", -a)             # a negative factor reverses the points
print("a ** 2 =", a ** 2)
print("1 / a  =", a.reciprocal())
print(a.add(b, ops="algebraic"))                   # the only valid name
a + b  = TFN(l=3.0, m=5.0, u=9.0)
a - b  = TFN(l=-4.0, m=-1.0, u=2.0)
a * b  = TFN(l=2.0, m=6.0, u=20.0)
a / b  = TFN(l=0.2, m=0.6666666666666666, u=2.0)
2 * a  = TFN(l=2.0, m=4.0, u=8.0) | -a = TFN(l=-4.0, m=-2.0, u=-1.0)
a ** 2 = TFN(l=1.0, m=4.0, u=16.0)
1 / a  = TFN(l=0.25, m=0.5, u=1.0)
TFN(l=3.0, m=5.0, u=9.0)

Scores, defuzzification and distance

To rank numbers you need one crisp value per number. score(method) (and its synonym defuzzify(method)) offers three, the default being the centroid:

Method Triangle \((l, m, u)\) Trapezoid \((a, b, c, d)\)
"centroid" (default) \((l + m + u)/3\) \(\dfrac{d^2 + c^2 + cd - a^2 - b^2 - ab}{3\,(d + c - a - b)}\)
"graded_mean" \((l + 4m + u)/6\) \((a + 2b + 2c + d)/6\)
"mean" \((l + m + u)/3\), the same as the centroid \((a + b + c + d)/4\)
TFN(1, 2, 7) with stems at its graded mean 2.667 and its centroid and mean 3.333 TFN(1, 2, 7) with stems at its graded mean 2.667 and its centroid and mean 3.333
For a skewed triangle the methods disagree. The graded mean weights the peak four times, so it stays closer to \(m\).

The only distance is "vertex" (Chen 2000): the root mean square of the point differences,

\[ d(A, B) = \sqrt{\tfrac{1}{n} \sum_{i=1}^{n} (x_i - y_i)^2}, \]

with \(n = 3\) for triangles and \(n = 4\) for trapezoids. Fuzzy TOPSIS (fuzzy_topsis) uses it. Triangular numbers have no accuracy function: ties between equal scores are not broken by a second quantity.

from bulanik import TFN, TrFN

a = TFN(1, 2, 7)
for method in TFN.SCORES:
    print(f"{method:12s} {a.score(method):.4f}")
print(a.score() == a.score("centroid"), a.defuzzify("graded_mean") == a.score("graded_mean"))

t = TrFN(1, 2, 3, 5)
print({m: round(t.score(m), 4) for m in TrFN.SCORES})
print(TFN(1, 2, 4).distance(TFN(2, 3, 5)))           # sqrt(((1-2)^2 + (2-3)^2 + (4-5)^2) / 3)
centroid     3.3333
graded_mean  2.6667
mean         3.3333
True True
{'centroid': 2.8, 'graded_mean': 2.6667, 'mean': 2.75}
1.0

Drawing them

bulanik.plot (the plot extra) draws membership functions, α-cuts and defuzzification markers; the figures above come from it. The gallery shows more.

2026-11-05T21:44:20.548919 image/svg+xml Matplotlib v3.11.1, https://matplotlib.org/
2026-11-05T21:44:20.610292 image/svg+xml Matplotlib v3.11.1, https://matplotlib.org/

Pitfalls

Points must be ordered, finite and real numbers. Swapped points, NaN, a bool or a string are rejected when the number is built, so a bad TFN never exists.

Numbers of different types do not mix. A TFN is not a TrFN, and neither is an IFN. Convert a triangle with to_trfn(). A plain number is not a fuzzy number either: TFN + 1 fails, so write TFN + TFN(1, 1, 1). Only multiplication, division and ** accept a plain number.

Reciprocal, division and powers need a safe support. The support, endpoints included, must not contain 0 for reciprocal() and /, and the lowest point must be non-negative for **.

Multiplication is an approximation. A * B is not the exact product of the fuzzy sets, only the standard point-based one (see the figure above). If you need exact products, work on α-cuts with interval arithmetic yourself.

Score names and accuracy. accuracy() always raises, and an unknown score name lists the valid ones.

from bulanik import IFN, TFN, TrFN

def show(make):
    try:
        print(make())
    except Exception as error:
        print(f"{type(error).__name__}: {error}")

show(lambda: TFN(3, 2, 1))                      # points out of order
show(lambda: TFN(1, 2, float("inf")))           # not finite
show(lambda: TFN(True, 2, 3))                   # a bool is not a number
show(lambda: TFN(1, 2, 3) + TrFN(1, 2, 3, 4))   # different types
show(lambda: TFN(1, 2, 3) + IFN(0.5, 0.4))
show(lambda: TFN(1, 2, 3) + 1)                  # a plain number is not a fuzzy number
show(lambda: TFN(-1, 0, 1).reciprocal())        # the support contains 0
show(lambda: TFN(0, 1, 2).reciprocal())         # a zero endpoint counts too
show(lambda: TFN(1, 2, 3) / TFN(-1, 0, 1))
show(lambda: TFN(-1, 2, 4) ** 2)                # negative lowest point
show(lambda: TFN(1, 2, 3).score("median"))
show(lambda: TFN(1, 2, 3).accuracy())
show(lambda: TFN(1, 2, 3).add(TFN(1, 2, 3), ops="einstein"))
ValueError: TFN needs l <= m <= u, got (3.0, 2.0, 1.0)
ValueError: u must be finite, got inf
TypeError: l must be a number, got bool True
TypeError: unsupported operand type(s) for +: 'TFN' and 'TrFN'
TypeError: unsupported operand type(s) for +: 'TFN' and 'IFN'
TypeError: unsupported operand type(s) for +: 'TFN' and 'int'
ValueError: cannot take the reciprocal of TFN(l=-1.0, m=0.0, u=1.0): its support contains 0
ValueError: cannot take the reciprocal of TFN(l=0.0, m=1.0, u=2.0): its support contains 0
ValueError: cannot take the reciprocal of TFN(l=-1.0, m=0.0, u=1.0): its support contains 0
ValueError: power 2.0 needs the lowest point >= 0, got -1.0
ValueError: unknown score 'median' for TFN; valid: centroid, graded_mean, mean
ValueError: TFN has no accuracy function
ValueError: unknown ops 'einstein' for TFN; valid: algebraic

See also