Skip to content

Quickstart

Five short recipes. Every code block runs when the site is built, so the output under each one is real. Recipe 5 (the lumix bridge) and the plot in recipe 1 (bulanik.plot) need their extras; everything else needs nothing but bulanik.

1. Fuzzy numbers

Numbers are immutable, validated values. Operators call the default ("algebraic") operator family; add, mul and friends take an ops= name.

from bulanik import IFN, TFN

a, b = TFN(1, 2, 4), TFN(2, 3, 5)
print(a + b)                    # componentwise sum of the triangles
print(a * b)                    # min/max of the corner products, product of the peaks
print(a.alpha_cut(0.5))         # the interval where membership >= 0.5
print(round(a.score(), 4))      # crisp score for ranking (centroid by default)

x, y = IFN(0.6, 0.3), IFN(0.5, 0.4)   # intuitionistic: membership + non-membership <= 1
print(x + y)                          # algebraic operators (default)
print(x.add(y, ops="einstein"))       # Einstein operators
print(x.score(), x.score("normalized"))

try:
    IFN(0.8, 0.5)                     # 0.8 + 0.5 > 1
except ValueError as error:
    print("ValueError:", error)
TFN(l=3.0, m=5.0, u=9.0)
TFN(l=2.0, m=6.0, u=20.0)
(1.5, 3.0)
2.3333
IFN(mu=0.8, nu=0.12)
IFN(mu=0.8461538461538461, nu=0.08450704225352113)
0.3 0.65
ValueError: need mu^1 + nu^1 <= 1, got mu=0.8, nu=0.5 (sum 1.3)

A membership plot of two triangles, drawn with bulanik.plot (the plot extra) at build time in both themes; the one matching your colour scheme is shown:

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

2. Aggregate expert opinions

Experts write words; a LinguisticScale turns them into fuzzy numbers; aggregate_experts combines the [expert][alternative][criterion] table cell by cell.

from bulanik.aggregate import aggregate_experts, wa, wg
from bulanik.scales import EXAMPLE_IFN_5

words = [
    [["H", "M"], ["L", "VL"]],      # expert 1: two alternatives, two criteria
    [["VH", "M"], ["M", "H"]],      # expert 2
]
ratings = EXAMPLE_IFN_5.convert(words)
combined = aggregate_experts(ratings, [0.6, 0.4], method="wa")
for row in combined:
    print([f"({n.mu:.3f}, {n.nu:.3f})" for n in row])

a, b = ratings[0][0][0], ratings[1][0][0]
for name, mean in [("wa", wa([a, b], [0.5, 0.5])), ("wg", wg([a, b], [0.5, 0.5]))]:
    print(f"{name}: ({mean.mu:.4f}, {mean.nu:.4f})")    # weighted average, weighted geometric mean
['(0.807, 0.115)', '(0.500, 0.400)']
['(0.388, 0.510)', '(0.420, 0.459)']
wa: (0.8268, 0.1000)
wg: (0.7937, 0.1282)

3. A Mamdani fuzzy controller

Rules are plain text. evaluate returns the defuzzified outputs; explain shows why.

from bulanik.inference import MamdaniFIS
from bulanik.sets import LinguisticVariable, triangular

service = LinguisticVariable("service", (0, 10), {
    "poor": triangular(0, 0, 5), "average": triangular(0, 5, 10), "good": triangular(5, 10, 10)})
food = LinguisticVariable("food", (0, 10), {
    "rancid": triangular(0, 0, 5), "delicious": triangular(5, 10, 10)})
tip = LinguisticVariable("tip", (0, 30), {
    "cheap": triangular(0, 5, 10), "average": triangular(10, 15, 20),
    "generous": triangular(20, 25, 30)})

fis = MamdaniFIS([service, food], [tip], [
    "IF service IS poor OR food IS rancid THEN tip IS cheap",
    "IF service IS average THEN tip IS average",
    "IF service IS good OR food IS delicious THEN tip IS generous",
])
inputs = {"service": 3, "food": 8}
print(f"tip = {fis.evaluate(inputs)['tip']:.2f}")
for activation in fis.explain(inputs).activations:
    print(f"  fired {activation.strength:.2f}: {activation.rule}")
tip = 15.86
  fired 0.40: IF service IS poor OR food IS rancid THEN tip IS cheap
  fired 0.60: IF service IS average THEN tip IS average
  fired 0.60: IF service IS good OR food IS delicious THEN tip IS generous

4. Fuzzy TOPSIS

Triangular ratings and triangular weights, ranked with Chen's (2000) fuzzy TOPSIS.

from bulanik import TFN
from bulanik.mcdm import fuzzy_topsis
from bulanik.scales import EXAMPLE_TFN_7 as scale

criteria = ["quality", "service", "price"]
ratings = scale.convert([
    ["G", "VG", "F"],               # alternative A on each criterion
    ["F", "G", "G"],                # B
    ["MP", "F", "VG"],              # C
])
weights = [TFN(0.7, 0.9, 1.0), TFN(0.5, 0.7, 0.9), TFN(0.3, 0.5, 0.7)]
result = fuzzy_topsis(ratings, weights, ["benefit", "benefit", "cost"],
                      alternatives=["A", "B", "C"], criteria=criteria)
print(result.ranking)
for name in result.ranking:
    print(f"  {name}: closeness {result.scores[name]:.4f}")
print(result.operators)             # the names of the methods that were used
('A', 'B', 'C')
  A: closeness 0.5910
  B: closeness 0.4265
  C: closeness 0.2921
{'normalization': 'linear_scale_chen2000', 'distance': 'vertex'}

5. Best-worst weights, then a ranking

The best-worst method (BWM) asks for two short comparison vectors. It needs a solver, so it lives in the bridges: bwm_linear (the lumix extra) solves it as a linear program. The result feeds straight into topsis.

from bulanik.lumix import bwm_linear
from bulanik.mcdm import topsis

# quality is the best criterion, price the worst
best_to_others = {"quality": 1, "service": 2, "price": 4}
others_to_worst = {"quality": 4, "service": 3, "price": 1}
bwm = bwm_linear(best_to_others, others_to_worst)
print({k: round(v, 4) for k, v in bwm.weights.items()}, f"xi = {bwm.xi:.4f}")

data = [[8, 7, 120], [7, 5, 100], [9, 6, 110]]       # rows: A, B, C
result = topsis(data, bwm, ["benefit", "benefit", "cost"],
                alternatives=["A", "B", "C"], criteria=list(bwm.weights))
print(result.ranking)
print({k: round(v, 4) for k, v in result.scores.items()})
{'quality': 0.5625, 'service': 0.3125, 'price': 0.125} xi = 0.0625
('C', 'A', 'B')
{'A': 0.629, 'B': 0.1153, 'C': 0.7389}

Next