openfll-python 0.2.0__cp314-cp314-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyFLL/PyFLL.pyi +306 -0
- PyFLL/__init__.py +97 -0
- PyFLL/_core.cp314-win_amd64.pyd +0 -0
- PyFLL/libgcc_s_seh-1.dll +0 -0
- PyFLL/libstdc++-6.dll +0 -0
- PyFLL/libwinpthread-1.dll +0 -0
- openfll/PyFLL.pyi +284 -0
- openfll/__init__.py +463 -0
- openfll/__init__.pyi +270 -0
- openfll/_helpers/__init__.py +6 -0
- openfll/_helpers/plotting.py +305 -0
- openfll/_helpers/sampling.py +143 -0
- openfll/sklearn.py +546 -0
- openfll_python-0.2.0.dist-info/METADATA +246 -0
- openfll_python-0.2.0.dist-info/RECORD +18 -0
- openfll_python-0.2.0.dist-info/WHEEL +5 -0
- openfll_python-0.2.0.dist-info/licenses/LICENSE +21 -0
- openfll_python-0.2.0.dist-info/top_level.txt +2 -0
openfll/__init__.pyi
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Type stubs для публичного API openfll.
|
|
2
|
+
|
|
3
|
+
openfll — пользовательский пакет. Во время выполнения он делегирует работу
|
|
4
|
+
внутреннему backend-пакету PyFLL, который загружает PyFLL._core.
|
|
5
|
+
"""
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, List, Literal, Mapping, Sequence, Tuple, Union, overload
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
MfType = Literal[
|
|
11
|
+
"constant",
|
|
12
|
+
"triangular",
|
|
13
|
+
"trapezoidal",
|
|
14
|
+
"gaussian",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
TNorms = Literal[
|
|
18
|
+
"min",
|
|
19
|
+
"prod_and",
|
|
20
|
+
"bounded_diff",
|
|
21
|
+
"drastic_prod",
|
|
22
|
+
"einstein_prod",
|
|
23
|
+
"hamacher_prod",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
SNorms = Literal[
|
|
27
|
+
"max",
|
|
28
|
+
"algebraic_sum",
|
|
29
|
+
"bounded_sum",
|
|
30
|
+
"drastic_sum",
|
|
31
|
+
"einstein_sum",
|
|
32
|
+
"hamacher_sum",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def constant(value: float) -> List[float]:
|
|
37
|
+
"""Вернуть params для constant membership function: [value]."""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def triangular(a: float, b: float, c: float) -> List[float]:
|
|
42
|
+
"""Вернуть params для triangular membership function: [a, b, c]."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def trapezoidal(a: float, b: float, c: float, d: float) -> List[float]:
|
|
47
|
+
"""Вернуть params для trapezoidal membership function: [a, b, c, d]."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def gaussian(*, center: float, sigma: float) -> List[float]:
|
|
52
|
+
"""Вернуть params для gaussian membership function: [center, sigma]."""
|
|
53
|
+
...
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MembershipPlot:
|
|
57
|
+
"""Рисунок и оси, созданные для одной нечёткой переменной."""
|
|
58
|
+
|
|
59
|
+
variable_name: str
|
|
60
|
+
variable_type: Literal["input", "output"]
|
|
61
|
+
figure: Any
|
|
62
|
+
axes: Any
|
|
63
|
+
saved_path: Path | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SugenoEngine:
|
|
67
|
+
"""Плоский native-backed facade для модели нечеткой логики Сугено.
|
|
68
|
+
|
|
69
|
+
Жизненный цикл:
|
|
70
|
+
До build() настраиваются переменные, membership-функции, правила и
|
|
71
|
+
default-нормы. После build() используются set_input(), calculate() и
|
|
72
|
+
get_output(). Нарушения lifecycle backend сообщает как RuntimeError.
|
|
73
|
+
|
|
74
|
+
Примечание:
|
|
75
|
+
Неизвестные native-имена сейчас приходят из std::out_of_range и в
|
|
76
|
+
текущем backend отображаются pybind11 как IndexError.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
"""Создать пустой engine."""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def add_input_var(self, name: str) -> None:
|
|
84
|
+
"""Зарегистрировать input-переменную до build()."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
def add_output_var(self, name: str) -> None:
|
|
88
|
+
"""Зарегистрировать output-переменную до build()."""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def add_membership_func(
|
|
92
|
+
self,
|
|
93
|
+
var_name: str,
|
|
94
|
+
mf_name: str,
|
|
95
|
+
type: MfType,
|
|
96
|
+
params: Union[List[float], Tuple[float, ...]],
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Добавить поддержанную membership-функцию к зарегистрированной переменной.
|
|
99
|
+
|
|
100
|
+
Поддержанные значения type:
|
|
101
|
+
constant: params = [value]
|
|
102
|
+
triangular: params = [a, b, c]
|
|
103
|
+
trapezoidal: params = [a, b, c, d]
|
|
104
|
+
gaussian: params = [center, sigma]
|
|
105
|
+
|
|
106
|
+
polynomial не входит в публичный API openfll v1.
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
@overload
|
|
111
|
+
def add_rule(self, rule: str) -> None:
|
|
112
|
+
"""Добавить текстовое правило.
|
|
113
|
+
|
|
114
|
+
Текстовые правила поддерживают AND/OR и явные имена норм, например
|
|
115
|
+
AND[prod_and] or OR[algebraic_sum].
|
|
116
|
+
"""
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def add_rule(
|
|
121
|
+
self,
|
|
122
|
+
antecedent: List[Tuple[str, str]],
|
|
123
|
+
consequent: List[Tuple[str, str]],
|
|
124
|
+
t_norm: Union[TNorms, str, None] = None,
|
|
125
|
+
s_norm: Union[SNorms, str, None] = None,
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Добавить кортежное/структурное правило.
|
|
128
|
+
|
|
129
|
+
Tuple rules задают AND-цепочку по всем antecedent-термам. Если нужен
|
|
130
|
+
OR, используйте текстовую форму правила.
|
|
131
|
+
"""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
def set_default_t_norm(self, name: TNorms) -> None:
|
|
135
|
+
"""Задать default t-норму для AND в правилах без явной нормы."""
|
|
136
|
+
...
|
|
137
|
+
|
|
138
|
+
def set_default_s_norm(self, name: SNorms) -> None:
|
|
139
|
+
"""Задать default s-норму для OR в правилах без явной нормы."""
|
|
140
|
+
...
|
|
141
|
+
|
|
142
|
+
def build(self) -> None:
|
|
143
|
+
"""Скомпилировать настроенную модель и перейти к execution lifecycle."""
|
|
144
|
+
...
|
|
145
|
+
|
|
146
|
+
def set_input(self, name: str, value: float) -> None:
|
|
147
|
+
"""Задать значение input-переменной после build()."""
|
|
148
|
+
...
|
|
149
|
+
|
|
150
|
+
def calculate(self) -> None:
|
|
151
|
+
"""Запустить fuzzy inference на текущих input-значениях."""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
def get_output(self, name: str) -> float:
|
|
155
|
+
"""Прочитать output-значение после calculate()."""
|
|
156
|
+
...
|
|
157
|
+
|
|
158
|
+
@overload
|
|
159
|
+
def predict(
|
|
160
|
+
self,
|
|
161
|
+
inputs: Mapping[str, float] | float | int,
|
|
162
|
+
output_name: str | None = None,
|
|
163
|
+
) -> float: ...
|
|
164
|
+
|
|
165
|
+
@overload
|
|
166
|
+
def predict(
|
|
167
|
+
self,
|
|
168
|
+
inputs: Sequence[float] | Sequence[Sequence[float]] | np.ndarray,
|
|
169
|
+
output_name: str | None = None,
|
|
170
|
+
*,
|
|
171
|
+
input_names: Sequence[str] | None = None,
|
|
172
|
+
) -> float | np.ndarray: ...
|
|
173
|
+
|
|
174
|
+
def predict(
|
|
175
|
+
self,
|
|
176
|
+
inputs: Any,
|
|
177
|
+
output_name: str | None = None,
|
|
178
|
+
*,
|
|
179
|
+
input_names: Sequence[str] | None = None,
|
|
180
|
+
) -> float | np.ndarray:
|
|
181
|
+
"""Выполнить универсальный inference для скаляра, словаря, последовательности или NumPy-батча.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
inputs: входные данные:
|
|
185
|
+
- Mapping[str, float]: словарь входов вида {"x1": 1.0, "x2": 2.0} -> float;
|
|
186
|
+
- float | int: скалярное значение для модели с ровно одним входом -> float;
|
|
187
|
+
- Sequence[float]: список значений для одного сэмпла (многовходовая модель) или батч сэмплов (одновходовая модель) -> float | ndarray;
|
|
188
|
+
- 1D ndarray / 2D ndarray | Sequence[Sequence]: батч сэмплов -> numpy ndarray.
|
|
189
|
+
output_name: имя целевой выходной переменной (если не передано, используется
|
|
190
|
+
единственный зарегистрированный output).
|
|
191
|
+
input_names: соответствие колонок именам входных переменных для батч-массива.
|
|
192
|
+
Если не указано, берутся имена в порядке регистрации в модели.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
float для единичного предсказания или ndarray для батча.
|
|
196
|
+
"""
|
|
197
|
+
...
|
|
198
|
+
|
|
199
|
+
def predict_batch(self, X: object, *, input_names: Sequence[str]) -> Any:
|
|
200
|
+
"""Выполнить native batch inference.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
X: 2D NumPy-compatible numeric array shape (n_samples, n_features).
|
|
204
|
+
Non-contiguous arrays accepted by the native backend.
|
|
205
|
+
input_names: обязательное соответствие колонок input-переменным.
|
|
206
|
+
Колонка i соответствует input_names[i], все input-переменные
|
|
207
|
+
должны присутствовать ровно один раз.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
NumPy ndarray dtype float64, shape (n_samples,) для v1 single-output.
|
|
211
|
+
"""
|
|
212
|
+
...
|
|
213
|
+
|
|
214
|
+
def plot_mf(
|
|
215
|
+
self,
|
|
216
|
+
variables: str | Sequence[str] | None = None,
|
|
217
|
+
*,
|
|
218
|
+
variable_type: Literal["all", "input", "output"] = "all",
|
|
219
|
+
num_points: int = 181,
|
|
220
|
+
ranges: Mapping[str, tuple[float, float]] | None = None,
|
|
221
|
+
show: bool = True,
|
|
222
|
+
save_dir: str | Path | None = None,
|
|
223
|
+
) -> dict[str, MembershipPlot]:
|
|
224
|
+
"""Построить графики MF для зарегистрированных переменных."""
|
|
225
|
+
...
|
|
226
|
+
|
|
227
|
+
def is_built(self) -> bool:
|
|
228
|
+
"""Вернуть True после успешного build()."""
|
|
229
|
+
...
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ============================================================================
|
|
233
|
+
# Python-only helpers (submodule `openfll._helpers`).
|
|
234
|
+
# Сигнатуры ниже — ручные type hints; runtime-импорт через `openfll.__init__`.
|
|
235
|
+
# ============================================================================
|
|
236
|
+
def plot_mf(
|
|
237
|
+
engine: SugenoEngine,
|
|
238
|
+
variables: str | Sequence[str] | None = None,
|
|
239
|
+
*,
|
|
240
|
+
variable_type: Literal["all", "input", "output"] = "all",
|
|
241
|
+
num_points: int = 181,
|
|
242
|
+
ranges: Mapping[str, tuple[float, float]] | None = None,
|
|
243
|
+
show: bool = True,
|
|
244
|
+
save_dir: str | Path | None = None,
|
|
245
|
+
) -> dict[str, MembershipPlot]: ...
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def monte_carlo(
|
|
249
|
+
engine: SugenoEngine,
|
|
250
|
+
var_inputs: list[tuple[str, tuple[float, float]]],
|
|
251
|
+
var_outputs: list[str],
|
|
252
|
+
n_samples: int = 1000,
|
|
253
|
+
seed: int | None = None,
|
|
254
|
+
) -> list[dict[str, float]]: ...
|
|
255
|
+
def grid_2d(
|
|
256
|
+
engine: SugenoEngine,
|
|
257
|
+
x_var: str,
|
|
258
|
+
y_var: str,
|
|
259
|
+
x_range: tuple[float, float],
|
|
260
|
+
y_range: tuple[float, float],
|
|
261
|
+
out_var: str,
|
|
262
|
+
n: int = 20,
|
|
263
|
+
) -> tuple[list[float], list[float], list[list[float]]]: ...
|
|
264
|
+
def check_output_finite(
|
|
265
|
+
engine: SugenoEngine,
|
|
266
|
+
var_inputs: list[tuple[str, tuple[float, float]]],
|
|
267
|
+
var_outputs: list[str],
|
|
268
|
+
n_samples: int = 1000,
|
|
269
|
+
seed: int | None = None,
|
|
270
|
+
) -> dict[str, int]: ...
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Визуализация функций принадлежности, зарегистрированных через API OpenFLL."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Literal, Mapping, Sequence
|
|
10
|
+
from weakref import WeakKeyDictionary
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
__all__ = ["MembershipPlot", "plot_mf"]
|
|
15
|
+
|
|
16
|
+
VariableType = Literal["all", "input", "output"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class MembershipPlot:
|
|
21
|
+
"""Рисунок и оси, созданные для одной нечёткой переменной."""
|
|
22
|
+
|
|
23
|
+
variable_name: str
|
|
24
|
+
variable_type: Literal["input", "output"]
|
|
25
|
+
figure: Any
|
|
26
|
+
axes: Any
|
|
27
|
+
saved_path: Path | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class _MembershipFunction:
|
|
32
|
+
name: str
|
|
33
|
+
mf_type: str
|
|
34
|
+
params: tuple[float, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_VARIABLE_ROLES: WeakKeyDictionary[object, dict[str, Literal["input", "output"]]] = (
|
|
38
|
+
WeakKeyDictionary()
|
|
39
|
+
)
|
|
40
|
+
_MEMBERSHIP_FUNCTIONS: WeakKeyDictionary[
|
|
41
|
+
object, dict[str, list[_MembershipFunction]]
|
|
42
|
+
] = WeakKeyDictionary()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _register_variable(
|
|
46
|
+
engine: object,
|
|
47
|
+
name: str,
|
|
48
|
+
variable_type: Literal["input", "output"],
|
|
49
|
+
) -> None:
|
|
50
|
+
_VARIABLE_ROLES.setdefault(engine, {})[name] = variable_type
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _register_membership_function(
|
|
54
|
+
engine: object,
|
|
55
|
+
variable_name: str,
|
|
56
|
+
mf_name: str,
|
|
57
|
+
mf_type: str,
|
|
58
|
+
params: Sequence[float],
|
|
59
|
+
) -> None:
|
|
60
|
+
definitions = _MEMBERSHIP_FUNCTIONS.setdefault(engine, {})
|
|
61
|
+
definitions.setdefault(variable_name, []).append(
|
|
62
|
+
_MembershipFunction(
|
|
63
|
+
name=mf_name,
|
|
64
|
+
mf_type=mf_type,
|
|
65
|
+
params=tuple(float(value) for value in params),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _automatic_range(
|
|
71
|
+
definitions: Sequence[_MembershipFunction],
|
|
72
|
+
variable_type: Literal["input", "output"],
|
|
73
|
+
) -> tuple[float, float]:
|
|
74
|
+
lower_bounds: list[float] = []
|
|
75
|
+
upper_bounds: list[float] = []
|
|
76
|
+
for definition in definitions:
|
|
77
|
+
params = definition.params
|
|
78
|
+
if definition.mf_type == "triangular":
|
|
79
|
+
lower_bounds.append(params[0])
|
|
80
|
+
upper_bounds.append(params[2])
|
|
81
|
+
elif definition.mf_type == "trapezoidal":
|
|
82
|
+
lower_bounds.append(params[0])
|
|
83
|
+
upper_bounds.append(params[3])
|
|
84
|
+
elif definition.mf_type == "gaussian":
|
|
85
|
+
center, sigma = params
|
|
86
|
+
lower_bounds.append(center - 4.0 * sigma)
|
|
87
|
+
upper_bounds.append(center + 4.0 * sigma)
|
|
88
|
+
elif definition.mf_type == "constant" and variable_type == "output":
|
|
89
|
+
lower_bounds.append(params[0])
|
|
90
|
+
upper_bounds.append(params[0])
|
|
91
|
+
|
|
92
|
+
if not lower_bounds:
|
|
93
|
+
return -1.0, 1.0
|
|
94
|
+
lower = min(lower_bounds)
|
|
95
|
+
upper = max(upper_bounds)
|
|
96
|
+
span = upper - lower
|
|
97
|
+
padding = max(span * 0.05, max(abs(lower), abs(upper), 1.0) * 0.05)
|
|
98
|
+
if math.isclose(lower, upper):
|
|
99
|
+
padding = max(abs(lower) * 0.1, 1.0)
|
|
100
|
+
return lower - padding, upper + padding
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _triangular(x: np.ndarray, params: tuple[float, ...]) -> np.ndarray:
|
|
104
|
+
a, b, c = params
|
|
105
|
+
values = np.zeros_like(x)
|
|
106
|
+
if a < b:
|
|
107
|
+
rising = (x >= a) & (x <= b)
|
|
108
|
+
values[rising] = (x[rising] - a) / (b - a)
|
|
109
|
+
else:
|
|
110
|
+
values[x == b] = 1.0
|
|
111
|
+
if b < c:
|
|
112
|
+
falling = (x >= b) & (x <= c)
|
|
113
|
+
values[falling] = np.maximum(values[falling], (c - x[falling]) / (c - b))
|
|
114
|
+
else:
|
|
115
|
+
values[x == b] = 1.0
|
|
116
|
+
return np.clip(values, 0.0, 1.0)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _trapezoidal(x: np.ndarray, params: tuple[float, ...]) -> np.ndarray:
|
|
120
|
+
a, b, c, d = params
|
|
121
|
+
values = np.zeros_like(x)
|
|
122
|
+
if a < b:
|
|
123
|
+
rising = (x >= a) & (x < b)
|
|
124
|
+
values[rising] = (x[rising] - a) / (b - a)
|
|
125
|
+
plateau = (x >= b) & (x <= c)
|
|
126
|
+
values[plateau] = 1.0
|
|
127
|
+
if c < d:
|
|
128
|
+
falling = (x > c) & (x <= d)
|
|
129
|
+
values[falling] = (d - x[falling]) / (d - c)
|
|
130
|
+
return np.clip(values, 0.0, 1.0)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _evaluate_membership_function(
|
|
134
|
+
definition: _MembershipFunction,
|
|
135
|
+
x: np.ndarray,
|
|
136
|
+
) -> np.ndarray:
|
|
137
|
+
if definition.mf_type == "triangular":
|
|
138
|
+
return _triangular(x, definition.params)
|
|
139
|
+
if definition.mf_type == "trapezoidal":
|
|
140
|
+
return _trapezoidal(x, definition.params)
|
|
141
|
+
if definition.mf_type == "gaussian":
|
|
142
|
+
center, sigma = definition.params
|
|
143
|
+
return np.exp(-0.5 * ((x - center) / sigma) ** 2)
|
|
144
|
+
if definition.mf_type == "constant":
|
|
145
|
+
return np.full_like(x, definition.params[0], dtype=np.float64)
|
|
146
|
+
raise ValueError(f"Unsupported membership function type: {definition.mf_type!r}")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _safe_filename(variable_name: str, position: int) -> str:
|
|
150
|
+
safe_name = re.sub(r"[^A-Za-z0-9._-]+", "_", variable_name).strip("._")
|
|
151
|
+
return f"membership_{position:02d}_{safe_name or 'variable'}.png"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def plot_mf(
|
|
155
|
+
engine: object,
|
|
156
|
+
variables: str | Sequence[str] | None = None,
|
|
157
|
+
*,
|
|
158
|
+
variable_type: VariableType = "all",
|
|
159
|
+
num_points: int = 181,
|
|
160
|
+
ranges: Mapping[str, tuple[float, float]] | None = None,
|
|
161
|
+
show: bool = True,
|
|
162
|
+
save_dir: str | Path | None = None,
|
|
163
|
+
) -> dict[str, MembershipPlot]:
|
|
164
|
+
"""Построить функции принадлежности входных и выходных переменных.
|
|
165
|
+
|
|
166
|
+
По умолчанию для каждой переменной создаётся отдельный рисунок. Функции
|
|
167
|
+
входных переменных отображаются как кривые степени принадлежности.
|
|
168
|
+
Константные консеквенты Сугено отображаются singleton-маркерами, поскольку
|
|
169
|
+
являются чёткими выходными значениями, а не нечёткими выходными множествами.
|
|
170
|
+
|
|
171
|
+
Параметры:
|
|
172
|
+
engine: Экземпляр ``openfll.SugenoEngine``, настроенный через публичный API.
|
|
173
|
+
variables: Имя одной переменной, последовательность имён или ``None``
|
|
174
|
+
для построения всех переменных.
|
|
175
|
+
variable_type: Фильтр переменных: ``"input"``, ``"output"`` или
|
|
176
|
+
``"all"``.
|
|
177
|
+
num_points: Количество точек по оси X для каждой непрерывной кривой.
|
|
178
|
+
ranges: Необязательные границы оси X для отдельных переменных.
|
|
179
|
+
show: Вызвать ``matplotlib.pyplot.show()`` после создания рисунков.
|
|
180
|
+
save_dir: Необязательный каталог для сохранения PNG каждой переменной.
|
|
181
|
+
|
|
182
|
+
Возвращает:
|
|
183
|
+
Словарь от имени переменной к :class:`MembershipPlot`.
|
|
184
|
+
|
|
185
|
+
Исключения:
|
|
186
|
+
RuntimeError: Библиотека matplotlib не установлена.
|
|
187
|
+
ValueError: Аргументы некорректны либо движок не был настроен через
|
|
188
|
+
публичные методы регистрации этого пакета.
|
|
189
|
+
"""
|
|
190
|
+
if variable_type not in {"all", "input", "output"}:
|
|
191
|
+
raise ValueError("variable_type must be 'all', 'input', or 'output'")
|
|
192
|
+
if num_points < 2:
|
|
193
|
+
raise ValueError("num_points must be at least 2")
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
import matplotlib.pyplot as plt
|
|
197
|
+
except ImportError as error:
|
|
198
|
+
raise RuntimeError(
|
|
199
|
+
"plot_mf requires matplotlib; install it with `pip install openfll_python[plot]`"
|
|
200
|
+
) from error
|
|
201
|
+
|
|
202
|
+
roles = _VARIABLE_ROLES.get(engine, {})
|
|
203
|
+
definitions_by_variable = _MEMBERSHIP_FUNCTIONS.get(engine, {})
|
|
204
|
+
if not roles:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
"No variables were registered through openfll.SugenoEngine; "
|
|
207
|
+
"configure the engine before calling plot_mf"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if variables is None:
|
|
211
|
+
selected = list(roles)
|
|
212
|
+
elif isinstance(variables, str):
|
|
213
|
+
selected = [variables]
|
|
214
|
+
else:
|
|
215
|
+
selected = list(variables)
|
|
216
|
+
if not selected:
|
|
217
|
+
raise ValueError("variables cannot be empty")
|
|
218
|
+
|
|
219
|
+
unknown = [name for name in selected if name not in roles]
|
|
220
|
+
if unknown:
|
|
221
|
+
raise KeyError(
|
|
222
|
+
f"Unknown variables: {', '.join(unknown)}. Registered variables: "
|
|
223
|
+
+ ", ".join(roles)
|
|
224
|
+
)
|
|
225
|
+
if variable_type != "all":
|
|
226
|
+
selected = [name for name in selected if roles[name] == variable_type]
|
|
227
|
+
if not selected:
|
|
228
|
+
raise ValueError(f"No {variable_type} variables selected")
|
|
229
|
+
|
|
230
|
+
requested_ranges = dict(ranges or {})
|
|
231
|
+
unknown_ranges = [name for name in requested_ranges if name not in roles]
|
|
232
|
+
if unknown_ranges:
|
|
233
|
+
raise KeyError(f"Ranges provided for unknown variables: {', '.join(unknown_ranges)}")
|
|
234
|
+
for name, bounds in requested_ranges.items():
|
|
235
|
+
if len(bounds) != 2:
|
|
236
|
+
raise ValueError(f"Range for {name!r} must contain exactly two values")
|
|
237
|
+
lower, upper = (float(bounds[0]), float(bounds[1]))
|
|
238
|
+
if not math.isfinite(lower) or not math.isfinite(upper) or lower >= upper:
|
|
239
|
+
raise ValueError(f"Range for {name!r} must be finite and increasing")
|
|
240
|
+
|
|
241
|
+
output_dir = Path(save_dir) if save_dir is not None else None
|
|
242
|
+
if output_dir is not None:
|
|
243
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
|
|
245
|
+
results: dict[str, MembershipPlot] = {}
|
|
246
|
+
for position, variable_name in enumerate(selected, start=1):
|
|
247
|
+
definitions = definitions_by_variable.get(variable_name, [])
|
|
248
|
+
if not definitions:
|
|
249
|
+
raise ValueError(f"Variable {variable_name!r} has no membership functions")
|
|
250
|
+
role = roles[variable_name]
|
|
251
|
+
lower, upper = requested_ranges.get(
|
|
252
|
+
variable_name, _automatic_range(definitions, role)
|
|
253
|
+
)
|
|
254
|
+
x = np.linspace(float(lower), float(upper), num_points, dtype=np.float64)
|
|
255
|
+
figure, axes = plt.subplots(figsize=(8.0, 4.8))
|
|
256
|
+
|
|
257
|
+
singleton_output = role == "output" and all(
|
|
258
|
+
definition.mf_type == "constant" for definition in definitions
|
|
259
|
+
)
|
|
260
|
+
if singleton_output:
|
|
261
|
+
for definition in definitions:
|
|
262
|
+
value = definition.params[0]
|
|
263
|
+
axes.vlines(value, 0.0, 1.0, linewidth=2.0)
|
|
264
|
+
axes.plot(
|
|
265
|
+
[value],
|
|
266
|
+
[1.0],
|
|
267
|
+
marker="o",
|
|
268
|
+
linestyle="none",
|
|
269
|
+
label=f"{definition.name} = {value:g}",
|
|
270
|
+
)
|
|
271
|
+
axes.set_title(f"Sugeno singleton consequents: {variable_name}")
|
|
272
|
+
axes.set_ylabel("Singleton marker")
|
|
273
|
+
else:
|
|
274
|
+
for definition in definitions:
|
|
275
|
+
axes.plot(
|
|
276
|
+
x,
|
|
277
|
+
_evaluate_membership_function(definition, x),
|
|
278
|
+
linewidth=2.0,
|
|
279
|
+
label=definition.name,
|
|
280
|
+
)
|
|
281
|
+
axes.set_title(f"Membership functions: {variable_name} ({role})")
|
|
282
|
+
axes.set_ylabel("Degree of membership")
|
|
283
|
+
|
|
284
|
+
axes.set_xlabel(variable_name)
|
|
285
|
+
axes.set_xlim(float(lower), float(upper))
|
|
286
|
+
axes.set_ylim(-0.05, 1.05)
|
|
287
|
+
axes.grid(alpha=0.25)
|
|
288
|
+
axes.legend()
|
|
289
|
+
figure.tight_layout()
|
|
290
|
+
|
|
291
|
+
saved_path: Path | None = None
|
|
292
|
+
if output_dir is not None:
|
|
293
|
+
saved_path = output_dir / _safe_filename(variable_name, position)
|
|
294
|
+
figure.savefig(saved_path, dpi=160)
|
|
295
|
+
results[variable_name] = MembershipPlot(
|
|
296
|
+
variable_name=variable_name,
|
|
297
|
+
variable_type=role,
|
|
298
|
+
figure=figure,
|
|
299
|
+
axes=axes,
|
|
300
|
+
saved_path=saved_path,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if show:
|
|
304
|
+
plt.show()
|
|
305
|
+
return results
|