Skip to content

bulanik.lumix

lumix

LumiX bridge: linear BWM, Zimmermann fuzzy goals and scores as LumiX data.

Needs the lumix extra (pip install "bulanik[lumix]", which installs lumix-opt; a solver such as OR-Tools is used by default, so install it too if your environment does not already have it).

The two consistency_ratio fields differ

The consistency_ratio of the BWMResult returned by bwm_linear is the optimum \(\xi^L\) of the linear model itself (the same number as xi). The nonlinear and fuzzy models in bulanik.sezgi store the ratio \(\xi / CI(a_{BW})\) under the same field name. \(\xi^L\) is on the scale of the comparisons (0 to a few units) and has no fixed upper bound, so compare it only with other linear-model results; operators["consistency"] is "xi_L" here and "rezaei2015_ci" / "guo_zhao2017_ci" in the sezgi bridge.

Examples:

>>> from bulanik.lumix import bwm_linear
>>> r = bwm_linear({"price": 1, "quality": 2, "speed": 8},
...                {"price": 8, "quality": 4, "speed": 1})
>>> {k: round(v, 3) for k, v in r.weights.items()}
{'price': 0.615, 'quality': 0.308, 'speed': 0.077}

FuzzyGoal dataclass

FuzzyGoal(name: str, expression: Any, aspiration: float, tolerance: float, sense: str)

A fuzzy goal on a linear expression \(z\) (Zimmermann 1978).

With aspiration \(g\) and tolerance \(t > 0\) the membership of the goal is

  • "ge" ("at least about \(g\)"): rises linearly from 0 at \(g - t\) to 1 at \(g\);
  • "le" ("at most about \(g\)"): falls linearly from 1 at \(g\) to 0 at \(g + t\);
  • "eq" ("about \(g\)"): \(\mu = 1 - |z - g| / t\) (triangular, 0 beyond \(g \pm t\)).

Attributes:

Name Type Description
name str

Goal name, used in the added variable and row names.

expression Any

An LXLinearExpression in the model's variables.

aspiration float

The aspiration level \(g\) (finite).

tolerance float

The tolerance \(t\) (finite, positive).

sense str

"ge", "le" or "eq".

Raises:

Type Description
TypeError

If expression is not an LXLinearExpression.

ValueError

If sense is unknown, aspiration or tolerance is not finite, or tolerance is not positive.

Examples:

>>> from lumix.core import LXLinearExpression
>>> from bulanik.lumix import FuzzyGoal
>>> goal = FuzzyGoal("profit", LXLinearExpression(), aspiration=20,
...                  tolerance=8, sense="ge")
>>> goal.membership(12), goal.membership(16), goal.membership(25)
(0.0, 0.5, 1.0)

membership

membership(z: float) -> float

Membership degree of the value z in the goal.

Parameters:

Name Type Description Default
z float

A value of the goal expression.

required

Returns:

Type Description
float

The degree in [0, 1], using the membership functions of

float

ZimmermannResult dataclass

ZimmermannResult(lambda_: float, goal_values: dict[str, float], memberships: dict[str, float], solution: Any)

Outcome of solve_zimmermann.

Attributes:

Name Type Description
lambda_ float

The optimal \(\lambda\), the smallest goal membership (clipped to at most 1).

goal_values dict[str, float]

The value \(z\) of each goal expression, by goal name.

memberships dict[str, float]

The membership \(\mu(z)\) of each goal at that value.

solution Any

The LumiX LXSolution, for the decision variables.

ScoreRecord dataclass

ScoreRecord(key: str, value: float, rank: int)

One scored item.

Attributes:

Name Type Description
key str

The criterion or alternative name.

value float

Its weight or score.

rank int

Its 1-based rank (1 is best).

bwm_linear

bwm_linear(best_to_others: Mapping[str, float], others_to_worst: Mapping[str, float], criteria: Sequence[str] | None = None, *, best: str | None = None, worst: str | None = None, solver: str = 'ortools') -> BWMResult

Linear best-worst method (Rezaei 2016, Omega 64:126-130).

With best criterion \(B\), worst criterion \(W\) and comparison vectors \(a_{Bj}\) (best_to_others) and \(a_{jW}\) (others_to_worst), solve the linear program

\[\min \xi \quad \text{s.t.} \quad |w_B - a_{Bj} w_j| \le \xi\ (j \ne B),\; |w_j - a_{jW} w_W| \le \xi\ (j \ne W),\; \textstyle\sum_j w_j = 1,\; w \ge 0.\]

The optimal \(\xi^L\) is the consistency indicator: 0 for perfectly consistent comparisons, larger when they conflict. Each absolute value is modelled by two rows. The LumiX model and solution are kept in extras["model"] and extras["solution"].

consistency_ratio here is \(\xi^L\) itself

The returned consistency_ratio equals xi, the optimum \(\xi^L\) of the linear model. It is not the ratio \(\xi / CI(a_{BW})\) that BWMProblem and FuzzyBWMProblem in bulanik.sezgi report under the same field name (there None when \(CI = 0\) and the comparisons are inconsistent). Compare \(\xi^L\) only with other linear-model results; operators["consistency"] is "xi_L".

Parameters:

Name Type Description Default
best_to_others Mapping[str, float]

Comparison of the best criterion over each criterion (finite, at least 1; the best criterion has 1 unless best is given).

required
others_to_worst Mapping[str, float]

Comparison of each criterion over the worst criterion.

required
criteria Sequence[str] | None

Criterion names, fixing the order. Defaults to the order of best_to_others.

None
best str | None

The best criterion. Inferred as the unique one with comparison 1 when None.

None
worst str | None

The worst criterion, inferred likewise from others_to_worst.

None
solver str

LumiX solver name, "ortools" by default.

'ortools'

Returns:

Type Description
BWMResult

A BWMResult with

BWMResult

operators == {"model": "linear", "consistency": "xi_L"}.

Raises:

Type Description
TypeError

If a comparison value is a bool or str instead of a number.

ValueError

For invalid comparison vectors (see resolve_best_worst).

RuntimeError

If the solver finds no optimum or returns a clearly negative weight.

Examples:

>>> from bulanik.lumix import bwm_linear
>>> r = bwm_linear({"a": 1, "b": 2, "c": 8}, {"a": 8, "b": 3, "c": 1})
>>> r.best, r.worst, r.ranking
('a', 'c', ('a', 'b', 'c'))
>>> round(r.xi, 4), r.consistency_ratio == r.xi
(0.0417, True)
References

Rezaei (2016), Omega 64, 126-130.

zimmermann

zimmermann(model: Any, goals: Sequence[FuzzyGoal], *, lambda_name: str = 'fuzzy_lambda') -> Any

Add Zimmermann's max-min fuzzy goal rows to model and return it.

Zimmermann's (1978) approach maximises the smallest goal membership \(\lambda\). Each goal gets a free variable goal_{name} with the definition row goal_{name}_def (expression \(- z = 0\)) and the membership rows

\[z - t\lambda \ge g - t \quad (\texttt{ge}), \qquad z + t\lambda \le g + t \quad (\texttt{le}),\]

in rows goal_{name}_ge and goal_{name}_le; "eq" adds both. The variable \(0 \le \lambda \le 1\) is added and the objective becomes \(\max \lambda\) (replacing any previous objective). Your variables and constraints stay, so the model can still be extended before solving. The goal expressions are not mutated.

Parameters:

Name Type Description Default
model Any

A lumix.LXModel holding the hard constraints.

required
goals Sequence[FuzzyGoal]

At least one FuzzyGoal with unique names.

required
lambda_name str

Name of the added \(\lambda\) variable.

'fuzzy_lambda'

Returns:

Type Description
Any

The same model, modified in place.

Raises:

Type Description
ValueError

If there are no goals, goal names repeat, or an added variable name collides with a model variable.

TypeError

If a goal is not a FuzzyGoal.

Examples:

>>> from lumix import LXConstraint, LXModel, LXVariable
>>> from lumix.core import LXLinearExpression
>>> from bulanik.lumix import FuzzyGoal, zimmermann
>>> x = LXVariable[None, float]("x").continuous().bounds(lower=0, upper=10)
>>> model = LXModel("demo").add_variable(x)
>>> goal = FuzzyGoal("big", LXLinearExpression().add_term(x, coeff=1.0),
...                  aspiration=8, tolerance=4, sense="ge")
>>> model = zimmermann(model, [goal])
>>> sorted(v.name for v in model.variables)
['fuzzy_lambda', 'goal_big', 'x']
References

Zimmermann (1978), Fuzzy Sets and Systems 1(1), 45-55.

solve_zimmermann

solve_zimmermann(model: Any, goals: Sequence[FuzzyGoal], *, solver: str = 'ortools', lambda_name: str = 'fuzzy_lambda') -> ZimmermannResult

Build the Zimmermann model on model and solve it.

Calls zimmermann (so model is modified in place) and optimises it, then reads the goal values and memberships.

Parameters:

Name Type Description Default
model Any

A lumix.LXModel holding the hard constraints.

required
goals Sequence[FuzzyGoal]

The FuzzyGoal list.

required
solver str

LumiX solver name, "ortools" by default.

'ortools'
lambda_name str

Name of the added \(\lambda\) variable.

'fuzzy_lambda'

Returns:

Type Description
ZimmermannResult

Raises:

Type Description
RuntimeError

With the solver status when no optimum is found, e.g. when a goal cannot even reach membership 0 under the hard constraints.

ValueError

As for zimmermann.

TypeError

As for zimmermann.

Examples:

Maximise the worst of two goals: profit at least about 20 (tolerance 8) and total use at most about 4 (tolerance 4), with at most 8 units.

>>> from lumix import LXConstraint, LXModel, LXVariable
>>> from lumix.core import LXLinearExpression
>>> from bulanik.lumix import FuzzyGoal, solve_zimmermann
>>> x = LXVariable[None, float]("x").continuous().bounds(lower=0, upper=10)
>>> y = LXVariable[None, float]("y").continuous().bounds(lower=0, upper=10)
>>> use = LXLinearExpression().add_term(x, coeff=1).add_term(y, coeff=1)
>>> model = (LXModel("plan").add_variable(x).add_variable(y)
...          .add_constraint(LXConstraint("cap").expression(use).le().rhs(8)))
>>> profit = LXLinearExpression().add_term(x, coeff=3).add_term(y, coeff=2)
>>> goals = [FuzzyGoal("profit", profit, aspiration=20, tolerance=8, sense="ge"),
...          FuzzyGoal("use", use, aspiration=4, tolerance=4, sense="le")]
>>> res = solve_zimmermann(model, goals)
>>> round(res.lambda_, 3)
0.6
>>> {k: round(v, 2) for k, v in res.goal_values.items()}
{'profit': 16.8, 'use': 5.6}
>>> {k: round(v, 2) for k, v in res.memberships.items()}
{'profit': 0.6, 'use': 0.6}
References

Zimmermann (1978), Fuzzy Sets and Systems 1(1), 45-55.

scores_as_data

scores_as_data(result: WeightingResult | RankingResult | BWMResult | Mapping[str, float]) -> list[ScoreRecord]

MCDM scores as records for LXVariable(...).from_data(records).

Records keep the order of the result's scores/weights; ranks are 1-based from result.ranking. A plain mapping is ranked by descending value (ties keep input order). value is the method's raw score, so for VIKOR, where lower is better, use rank or mind the direction.

Parameters:

Name Type Description Default
result WeightingResult | RankingResult | BWMResult | Mapping[str, float]

A WeightingResult, RankingResult, BWMResult, or a mapping from name to a finite score.

required

Returns:

Type Description
list[ScoreRecord]

A list of ScoreRecord.

Raises:

Type Description
TypeError

If result is none of the accepted kinds, or a score is a bool or str instead of a number.

ValueError

If a score is not finite.

Examples:

>>> from bulanik.mcdm import ahp
>>> from bulanik.lumix import scores_as_data
>>> w = ahp([[1, 3], [1/3, 1]], ["cost", "quality"])
>>> for rec in scores_as_data(w):
...     print(rec.key, round(rec.value, 2), rec.rank)
cost 0.75 1
quality 0.25 2