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).
bwm_linear- Rezaei's (2016) linear best-worst method as a linear program.zimmermann/solve_zimmermann- Zimmermann's (1978) max-min fuzzy linear programming on top of your ownlumix.LXModel, with the goals described byFuzzyGoaland the outcome in aZimmermannResult.scores_as_data- MCDM results as plain records (ScoreRecord), ready forLXVariable(...).from_data(records).
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 |
aspiration |
float
|
The aspiration level \(g\) (finite). |
tolerance |
float
|
The tolerance \(t\) (finite, positive). |
sense |
str
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
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 |
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
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 |
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
|
None
|
best
|
str | None
|
The best criterion. Inferred as the unique one with comparison 1
when |
None
|
worst
|
str | None
|
The worst criterion, inferred likewise from |
None
|
solver
|
str
|
LumiX solver name, |
'ortools'
|
Returns:
| Type | Description |
|---|---|
BWMResult
|
A |
BWMResult
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a comparison value is a |
ValueError
|
For invalid comparison vectors (see
|
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
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 |
required |
goals
|
Sequence[FuzzyGoal]
|
At least one |
required |
lambda_name
|
str
|
Name of the added \(\lambda\) variable. |
'fuzzy_lambda'
|
Returns:
| Type | Description |
|---|---|
Any
|
The same |
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 |
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 |
required |
goals
|
Sequence[FuzzyGoal]
|
The |
required |
solver
|
str
|
LumiX solver name, |
'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 |
TypeError
|
As for |
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 |
required |
Returns:
| Type | Description |
|---|---|
list[ScoreRecord]
|
A list of |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
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