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__.py
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
"""Публичный Python API для OpenFLL.
|
|
2
|
+
|
|
3
|
+
`openfll` — это пакет, предназначенный для непосредственного использования.
|
|
4
|
+
Во время выполнения он делегирует задачи внутреннему пакету-бэкенду `PyFLL`, который содержит модуль расширения на машинном коде `PyFLL._core`.
|
|
5
|
+
|
|
6
|
+
Ожидаемая цепочка вызовов во время выполнения:
|
|
7
|
+
|
|
8
|
+
openfll -> PyFLL -> PyFLL._core -> OFLL::Linear::SugenoEngine
|
|
9
|
+
|
|
10
|
+
Приведенный ниже класс сохраняется для целей статического анализа и генерации документации.
|
|
11
|
+
При наличии встроенного бэкенда `SugenoEngine` заменяется на `PyFLL.SugenoEngine`.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import importlib
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
19
|
+
from weakref import WeakKeyDictionary
|
|
20
|
+
|
|
21
|
+
from ._helpers.plotting import (
|
|
22
|
+
MembershipPlot,
|
|
23
|
+
_register_membership_function,
|
|
24
|
+
_register_variable,
|
|
25
|
+
plot_mf,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import numpy as _np
|
|
30
|
+
except ImportError:
|
|
31
|
+
_np = None # type: ignore[assignment]
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"SugenoEngine",
|
|
35
|
+
"MfType",
|
|
36
|
+
"TNorms",
|
|
37
|
+
"SNorms",
|
|
38
|
+
"constant",
|
|
39
|
+
"triangular",
|
|
40
|
+
"trapezoidal",
|
|
41
|
+
"gaussian",
|
|
42
|
+
"MembershipPlot",
|
|
43
|
+
"plot_mf",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
_BACKEND_IMPORT_ERROR: BaseException | None = None
|
|
47
|
+
_ENGINE_INPUT_NAMES: WeakKeyDictionary[object, list[str]] = WeakKeyDictionary()
|
|
48
|
+
_ENGINE_OUTPUT_NAMES: WeakKeyDictionary[object, list[str]] = WeakKeyDictionary()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _backend_error_message() -> str:
|
|
52
|
+
reason = ""
|
|
53
|
+
if _BACKEND_IMPORT_ERROR is not None:
|
|
54
|
+
reason = (
|
|
55
|
+
f"\nИсходная причина: "
|
|
56
|
+
f"{type(_BACKEND_IMPORT_ERROR).__name__}: {_BACKEND_IMPORT_ERROR}"
|
|
57
|
+
)
|
|
58
|
+
return (
|
|
59
|
+
"openfll не смог загрузить внутренний backend `PyFLL` или его "
|
|
60
|
+
"native module `PyFLL._core`.\n"
|
|
61
|
+
"Проверьте, что пакет установлен из wheel, собранного вместе с "
|
|
62
|
+
"native backend: `pip install openfll_python`."
|
|
63
|
+
f"{reason}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# Literal типы — экспортируются для IDE/mkdocstrings
|
|
69
|
+
# ============================================================================
|
|
70
|
+
MfType = Literal[
|
|
71
|
+
"constant", "triangular", "trapezoidal", "gaussian"
|
|
72
|
+
]
|
|
73
|
+
TNorms = Literal[
|
|
74
|
+
"min", "prod_and", "bounded_diff", "drastic_prod", "einstein_prod", "hamacher_prod"
|
|
75
|
+
]
|
|
76
|
+
SNorms = Literal[
|
|
77
|
+
"max", "algebraic_sum", "bounded_sum", "drastic_sum", "einstein_sum", "hamacher_sum"
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def constant(value: float) -> list[float]:
|
|
82
|
+
"""Вернуть параметры для constant membership function."""
|
|
83
|
+
return [value]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def triangular(a: float, b: float, c: float) -> list[float]:
|
|
87
|
+
"""Вернуть параметры для triangular membership function в порядке a, b, c."""
|
|
88
|
+
return [a, b, c]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def trapezoidal(a: float, b: float, c: float, d: float) -> list[float]:
|
|
92
|
+
"""Вернуть параметры для trapezoidal membership function в порядке a, b, c, d."""
|
|
93
|
+
return [a, b, c, d]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def gaussian(*, center: float, sigma: float) -> list[float]:
|
|
97
|
+
"""Вернуть параметры для gaussian membership function в порядке center, sigma."""
|
|
98
|
+
return [center, sigma]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _install_scalar_predict(engine_cls: type) -> None:
|
|
102
|
+
"""Добавить Python convenience predict к настоящему native engine class."""
|
|
103
|
+
required_methods = (
|
|
104
|
+
"add_input_var",
|
|
105
|
+
"add_output_var",
|
|
106
|
+
"add_membership_func",
|
|
107
|
+
"set_input",
|
|
108
|
+
"calculate",
|
|
109
|
+
"get_output",
|
|
110
|
+
)
|
|
111
|
+
if getattr(engine_cls, "_openfll_predict_installed", False):
|
|
112
|
+
return
|
|
113
|
+
if not all(hasattr(engine_cls, name) for name in required_methods):
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
native_add_input_var = engine_cls.add_input_var
|
|
117
|
+
native_add_output_var = engine_cls.add_output_var
|
|
118
|
+
native_add_membership_func = engine_cls.add_membership_func
|
|
119
|
+
|
|
120
|
+
def add_input_var(self, name: str) -> None:
|
|
121
|
+
native_add_input_var(self, name)
|
|
122
|
+
_ENGINE_INPUT_NAMES.setdefault(self, []).append(name)
|
|
123
|
+
_register_variable(self, name, "input")
|
|
124
|
+
|
|
125
|
+
def add_output_var(self, name: str) -> None:
|
|
126
|
+
native_add_output_var(self, name)
|
|
127
|
+
_ENGINE_OUTPUT_NAMES.setdefault(self, []).append(name)
|
|
128
|
+
_register_variable(self, name, "output")
|
|
129
|
+
|
|
130
|
+
def add_membership_func(
|
|
131
|
+
self,
|
|
132
|
+
var_name: str,
|
|
133
|
+
mf_name: str,
|
|
134
|
+
type: str,
|
|
135
|
+
params: Sequence[float],
|
|
136
|
+
) -> None:
|
|
137
|
+
native_add_membership_func(self, var_name, mf_name, type, params)
|
|
138
|
+
_register_membership_function(self, var_name, mf_name, type, params)
|
|
139
|
+
|
|
140
|
+
def _is_bool(val: Any) -> bool:
|
|
141
|
+
if isinstance(val, bool):
|
|
142
|
+
return True
|
|
143
|
+
if _np is not None:
|
|
144
|
+
if isinstance(val, (_np.bool_,)):
|
|
145
|
+
return True
|
|
146
|
+
if isinstance(val, _np.ndarray):
|
|
147
|
+
if val.dtype == bool or _np.issubdtype(val.dtype, _np.bool_):
|
|
148
|
+
return True
|
|
149
|
+
if val.ndim == 0 and isinstance(val.item(), bool):
|
|
150
|
+
return True
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
def _contains_bool(seq: Any) -> bool:
|
|
154
|
+
if isinstance(seq, (list, tuple)):
|
|
155
|
+
for item in seq:
|
|
156
|
+
if _is_bool(item) or _contains_bool(item):
|
|
157
|
+
return True
|
|
158
|
+
elif _np is not None and isinstance(seq, _np.ndarray):
|
|
159
|
+
if seq.dtype == bool or _np.issubdtype(seq.dtype, _np.bool_):
|
|
160
|
+
return True
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def _validate_output_name(self, output_name: str | None) -> str:
|
|
164
|
+
output_names = _ENGINE_OUTPUT_NAMES.get(self, [])
|
|
165
|
+
if output_name is not None:
|
|
166
|
+
if output_names and output_name not in output_names:
|
|
167
|
+
raise KeyError(
|
|
168
|
+
f"Output variable '{output_name}' is not registered in engine. "
|
|
169
|
+
f"Registered outputs: {', '.join(output_names)}"
|
|
170
|
+
)
|
|
171
|
+
return output_name
|
|
172
|
+
if len(output_names) == 1:
|
|
173
|
+
return output_names[0]
|
|
174
|
+
if len(output_names) == 0:
|
|
175
|
+
raise ValueError("No output variables registered through add_output_var")
|
|
176
|
+
raise ValueError(
|
|
177
|
+
"predict requires output_name unless exactly one output "
|
|
178
|
+
"variable was registered through add_output_var"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _run_scalar(
|
|
182
|
+
self,
|
|
183
|
+
scalar_inputs: Mapping[str, float],
|
|
184
|
+
output_name: str | None = None,
|
|
185
|
+
) -> float:
|
|
186
|
+
input_names = _ENGINE_INPUT_NAMES.get(self, [])
|
|
187
|
+
if input_names:
|
|
188
|
+
missing = [name for name in input_names if name not in scalar_inputs]
|
|
189
|
+
if missing:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"predict inputs missing registered input variables: "
|
|
192
|
+
+ ", ".join(missing)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
resolved_output_name = _validate_output_name(self, output_name)
|
|
196
|
+
|
|
197
|
+
for name, value in scalar_inputs.items():
|
|
198
|
+
if _is_bool(value):
|
|
199
|
+
raise TypeError(
|
|
200
|
+
f"Value for input variable '{name}' cannot be boolean, got {value!r}"
|
|
201
|
+
)
|
|
202
|
+
self.set_input(name, float(value))
|
|
203
|
+
self.calculate()
|
|
204
|
+
return self.get_output(resolved_output_name)
|
|
205
|
+
|
|
206
|
+
def predict(
|
|
207
|
+
self,
|
|
208
|
+
inputs: Any,
|
|
209
|
+
output_name: str | None = None,
|
|
210
|
+
*,
|
|
211
|
+
input_names: Sequence[str] | None = None,
|
|
212
|
+
) -> Any:
|
|
213
|
+
"""Выполнить inference для скаляра, словаря, последовательности или NumPy-батча.
|
|
214
|
+
|
|
215
|
+
Предупреждение:
|
|
216
|
+
Инстанс не является потокобезопасным для инференса. Для многопоточной
|
|
217
|
+
обработки необходимо клонировать модель или создать независимые экземпляры.
|
|
218
|
+
"""
|
|
219
|
+
if _is_bool(inputs):
|
|
220
|
+
raise TypeError("inputs cannot be a boolean value")
|
|
221
|
+
|
|
222
|
+
if isinstance(inputs, Mapping):
|
|
223
|
+
return _run_scalar(self, inputs, output_name)
|
|
224
|
+
|
|
225
|
+
if _np is not None and isinstance(inputs, _np.ndarray):
|
|
226
|
+
if inputs.dtype == bool or _np.issubdtype(inputs.dtype, _np.bool_):
|
|
227
|
+
raise TypeError("boolean numpy arrays are not supported")
|
|
228
|
+
|
|
229
|
+
if inputs.ndim > 2:
|
|
230
|
+
raise ValueError(
|
|
231
|
+
f"X must be a 1D or 2D array, got {inputs.ndim}D array with shape {inputs.shape}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if inputs.ndim == 0:
|
|
235
|
+
val = inputs.item()
|
|
236
|
+
if _is_bool(val):
|
|
237
|
+
raise TypeError("inputs cannot be a boolean value")
|
|
238
|
+
inputs = float(val)
|
|
239
|
+
# Переходит к ветке is_scalar ниже
|
|
240
|
+
|
|
241
|
+
elif inputs.ndim == 2:
|
|
242
|
+
_validate_output_name(self, output_name)
|
|
243
|
+
resolved_input_names = input_names
|
|
244
|
+
if resolved_input_names is None:
|
|
245
|
+
registered = _ENGINE_INPUT_NAMES.get(self, [])
|
|
246
|
+
if not registered:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"predict with 2D array requires input_names or previously registered input variables"
|
|
249
|
+
)
|
|
250
|
+
resolved_input_names = registered
|
|
251
|
+
elif len(resolved_input_names) != inputs.shape[1]:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"Expected {inputs.shape[1]} input_names, got {len(resolved_input_names)}"
|
|
254
|
+
)
|
|
255
|
+
return self.predict_batch(inputs, input_names=resolved_input_names)
|
|
256
|
+
|
|
257
|
+
elif inputs.ndim == 1:
|
|
258
|
+
registered = _ENGINE_INPUT_NAMES.get(self, [])
|
|
259
|
+
if len(registered) == 1:
|
|
260
|
+
_validate_output_name(self, output_name)
|
|
261
|
+
if input_names is not None:
|
|
262
|
+
if len(input_names) != 1 or input_names[0] != registered[0]:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Expected input_names={registered}, got {list(input_names)}"
|
|
265
|
+
)
|
|
266
|
+
resolved_input_names = input_names
|
|
267
|
+
else:
|
|
268
|
+
resolved_input_names = registered
|
|
269
|
+
return self.predict_batch(inputs.reshape(-1, 1), input_names=resolved_input_names)
|
|
270
|
+
|
|
271
|
+
if len(registered) > 1 and inputs.shape[0] == len(registered):
|
|
272
|
+
if input_names is not None:
|
|
273
|
+
if len(input_names) != len(registered):
|
|
274
|
+
raise ValueError(
|
|
275
|
+
f"Expected {len(registered)} input_names, got {len(input_names)}"
|
|
276
|
+
)
|
|
277
|
+
missing = [name for name in input_names if name not in registered]
|
|
278
|
+
if missing:
|
|
279
|
+
raise ValueError(f"Unknown input_names: {', '.join(missing)}")
|
|
280
|
+
names_to_use = list(input_names)
|
|
281
|
+
else:
|
|
282
|
+
names_to_use = registered
|
|
283
|
+
sample_dict = {name: float(val) for name, val in zip(names_to_use, inputs)}
|
|
284
|
+
return _run_scalar(self, sample_dict, output_name)
|
|
285
|
+
|
|
286
|
+
raise ValueError(
|
|
287
|
+
f"1D array of shape {inputs.shape} does not match {len(registered)} registered input variables ({', '.join(registered)})"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
is_scalar = (
|
|
291
|
+
isinstance(inputs, (int, float))
|
|
292
|
+
or (_np is not None and isinstance(inputs, _np.number))
|
|
293
|
+
) and not _is_bool(inputs)
|
|
294
|
+
|
|
295
|
+
if is_scalar:
|
|
296
|
+
registered = _ENGINE_INPUT_NAMES.get(self, [])
|
|
297
|
+
if len(registered) == 1:
|
|
298
|
+
return _run_scalar(self, {registered[0]: float(inputs)}, output_name)
|
|
299
|
+
if len(registered) == 0:
|
|
300
|
+
raise ValueError("No input variables registered to map scalar input")
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"Model requires {len(registered)} inputs ({', '.join(registered)}), "
|
|
303
|
+
f"but a single scalar was provided. Provide a dict, sequence, or 2D array."
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if isinstance(inputs, (list, tuple)):
|
|
307
|
+
if len(inputs) == 0:
|
|
308
|
+
raise ValueError("inputs sequence cannot be empty")
|
|
309
|
+
if _contains_bool(inputs):
|
|
310
|
+
raise TypeError("inputs sequence cannot contain boolean values")
|
|
311
|
+
|
|
312
|
+
# Проверка на 2D последовательность (батч сэмплов)
|
|
313
|
+
if isinstance(inputs[0], (list, tuple)) or (_np is not None and isinstance(inputs[0], _np.ndarray)):
|
|
314
|
+
if _np is None:
|
|
315
|
+
raise RuntimeError("NumPy is required for batch inference")
|
|
316
|
+
_validate_output_name(self, output_name)
|
|
317
|
+
arr = _np.asarray(inputs)
|
|
318
|
+
if arr.ndim != 2:
|
|
319
|
+
raise ValueError(f"X must be a 1D or 2D sequence, got {arr.ndim}D sequence with shape {arr.shape}")
|
|
320
|
+
resolved_input_names = input_names
|
|
321
|
+
if resolved_input_names is None:
|
|
322
|
+
registered = _ENGINE_INPUT_NAMES.get(self, [])
|
|
323
|
+
if not registered:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
"predict with batch sequence requires input_names or previously registered input variables"
|
|
326
|
+
)
|
|
327
|
+
resolved_input_names = registered
|
|
328
|
+
elif len(resolved_input_names) != arr.shape[1]:
|
|
329
|
+
raise ValueError(
|
|
330
|
+
f"Expected {arr.shape[1]} input_names, got {len(resolved_input_names)}"
|
|
331
|
+
)
|
|
332
|
+
return self.predict_batch(arr, input_names=resolved_input_names)
|
|
333
|
+
|
|
334
|
+
# 1D последовательность
|
|
335
|
+
registered = _ENGINE_INPUT_NAMES.get(self, [])
|
|
336
|
+
if len(registered) == 1:
|
|
337
|
+
if input_names is not None:
|
|
338
|
+
if len(input_names) != 1 or input_names[0] != registered[0]:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
f"Expected input_names={registered}, got {list(input_names)}"
|
|
341
|
+
)
|
|
342
|
+
resolved_input_names = input_names
|
|
343
|
+
else:
|
|
344
|
+
resolved_input_names = registered
|
|
345
|
+
|
|
346
|
+
if len(inputs) > 1:
|
|
347
|
+
if _np is None:
|
|
348
|
+
raise RuntimeError("NumPy is required for batch inference")
|
|
349
|
+
_validate_output_name(self, output_name)
|
|
350
|
+
arr = _np.asarray(inputs, dtype=_np.float64).reshape(-1, 1)
|
|
351
|
+
return self.predict_batch(arr, input_names=resolved_input_names)
|
|
352
|
+
|
|
353
|
+
# Единичный сэмпл из 1 значения
|
|
354
|
+
sample_dict = {resolved_input_names[0]: float(inputs[0])}
|
|
355
|
+
return _run_scalar(self, sample_dict, output_name)
|
|
356
|
+
|
|
357
|
+
# len(registered) > 1 (многовходовая модель)
|
|
358
|
+
if len(inputs) != len(registered):
|
|
359
|
+
raise ValueError(
|
|
360
|
+
f"Expected {len(registered)} inputs ({', '.join(registered)}), but got {len(inputs)} values in sequence"
|
|
361
|
+
)
|
|
362
|
+
if input_names is not None:
|
|
363
|
+
if len(input_names) != len(registered):
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"Expected {len(registered)} input_names, got {len(input_names)}"
|
|
366
|
+
)
|
|
367
|
+
missing = [name for name in input_names if name not in registered]
|
|
368
|
+
if missing:
|
|
369
|
+
raise ValueError(f"Unknown input_names: {', '.join(missing)}")
|
|
370
|
+
names_to_use = list(input_names)
|
|
371
|
+
else:
|
|
372
|
+
names_to_use = registered
|
|
373
|
+
sample_dict = {name: float(val) for name, val in zip(names_to_use, inputs)}
|
|
374
|
+
return _run_scalar(self, sample_dict, output_name)
|
|
375
|
+
|
|
376
|
+
raise TypeError(
|
|
377
|
+
f"inputs must be a mapping, scalar number, sequence, or numpy ndarray, got {type(inputs).__name__}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def plot_membership_functions(
|
|
381
|
+
self,
|
|
382
|
+
variables: str | Sequence[str] | None = None,
|
|
383
|
+
*,
|
|
384
|
+
variable_type: Literal["all", "input", "output"] = "all",
|
|
385
|
+
num_points: int = 181,
|
|
386
|
+
ranges: Mapping[str, tuple[float, float]] | None = None,
|
|
387
|
+
show: bool = True,
|
|
388
|
+
save_dir: str | Path | None = None,
|
|
389
|
+
) -> dict[str, MembershipPlot]:
|
|
390
|
+
"""Построить функции принадлежности, зарегистрированные в движке."""
|
|
391
|
+
return plot_mf(
|
|
392
|
+
self,
|
|
393
|
+
variables,
|
|
394
|
+
variable_type=variable_type,
|
|
395
|
+
num_points=num_points,
|
|
396
|
+
ranges=ranges,
|
|
397
|
+
show=show,
|
|
398
|
+
save_dir=save_dir,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
engine_cls.add_input_var = add_input_var
|
|
402
|
+
engine_cls.add_output_var = add_output_var
|
|
403
|
+
engine_cls.add_membership_func = add_membership_func
|
|
404
|
+
engine_cls.predict = predict
|
|
405
|
+
engine_cls.plot_mf = plot_membership_functions
|
|
406
|
+
engine_cls._openfll_predict_installed = True
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
class SugenoEngine:
|
|
410
|
+
"""Placeholder, который заменяется реальным PyFLL.SugenoEngine."""
|
|
411
|
+
|
|
412
|
+
def __init__(self) -> None:
|
|
413
|
+
"""Создать engine или явно сообщить, что native backend не загружен."""
|
|
414
|
+
raise ImportError(_backend_error_message()) from _BACKEND_IMPORT_ERROR
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ============================================================================
|
|
418
|
+
# Время выполнения: загрузка существующего пакета бэкенда. openfll намеренно не
|
|
419
|
+
# импортирует и не включает в поставку собственное расширение _core.
|
|
420
|
+
# ============================================================================
|
|
421
|
+
try:
|
|
422
|
+
_PyFLL = importlib.import_module("PyFLL")
|
|
423
|
+
_RealSugenoEngine = _PyFLL.SugenoEngine
|
|
424
|
+
if getattr(_RealSugenoEngine, "__module__", "") != "PyFLL._core":
|
|
425
|
+
raise ImportError(
|
|
426
|
+
"PyFLL импортирован, но PyFLL.SugenoEngine не загружен из "
|
|
427
|
+
f"PyFLL._core (получен модуль "
|
|
428
|
+
f"{getattr(_RealSugenoEngine, '__module__', '<unknown>')!r})"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
SugenoEngine = _RealSugenoEngine
|
|
432
|
+
_install_scalar_predict(SugenoEngine)
|
|
433
|
+
del _PyFLL
|
|
434
|
+
del _RealSugenoEngine
|
|
435
|
+
except ImportError as exc:
|
|
436
|
+
# Для документации и type checking стаб остаётся доступным, но в обычном
|
|
437
|
+
# runtime его нельзя использовать как рабочий engine.
|
|
438
|
+
_BACKEND_IMPORT_ERROR = exc
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ============================================================================
|
|
443
|
+
# Python-only helpers (`openfll._helpers.*`).
|
|
444
|
+
#
|
|
445
|
+
# Эти модули используют ТОЛЬКО публичный Python-API (SugenoEngine) и
|
|
446
|
+
# развиваются независимо от C++ ядра. В минорных релизах wheel они могут
|
|
447
|
+
# расти свободно, не требуя пересборки .pyd.
|
|
448
|
+
#
|
|
449
|
+
# Импортируем здесь, чтобы коллеги могли:
|
|
450
|
+
# from openfll import monte_carlo
|
|
451
|
+
# вместо
|
|
452
|
+
# from openfll._helpers.sampling import monte_carlo
|
|
453
|
+
# ============================================================================
|
|
454
|
+
if TYPE_CHECKING:
|
|
455
|
+
# Для IDE / mypy / mkdocstrings — type hints подтягиваются без runtime-импорта.
|
|
456
|
+
from ._helpers.sampling import check_output_finite, grid_2d, monte_carlo
|
|
457
|
+
else:
|
|
458
|
+
try:
|
|
459
|
+
from ._helpers.sampling import check_output_finite, grid_2d, monte_carlo
|
|
460
|
+
__all__ = [*__all__, "monte_carlo", "grid_2d", "check_output_finite"]
|
|
461
|
+
except ImportError:
|
|
462
|
+
# Минимальная установка (только wheel, без _helpers) — fallback.
|
|
463
|
+
pass
|