lsapy 0.1.0__py3-none-any.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.
- lsapy/__init__.py +9 -0
- lsapy/core/__init__.py +1 -0
- lsapy/core/functions.py +52 -0
- lsapy/criteria.py +181 -0
- lsapy/data/climate_sample.nc +0 -0
- lsapy/data/soil_sample.nc +0 -0
- lsapy/functions/__init__.py +7 -0
- lsapy/functions/_discrete.py +30 -0
- lsapy/functions/_suitability.py +158 -0
- lsapy/functions/membership.py +372 -0
- lsapy/lsa.py +624 -0
- lsapy/statistics.py +211 -0
- lsapy/utils.py +56 -0
- lsapy-0.1.0.dist-info/METADATA +187 -0
- lsapy-0.1.0.dist-info/RECORD +17 -0
- lsapy-0.1.0.dist-info/WHEEL +4 -0
- lsapy-0.1.0.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"""Membership Function Module."""
|
|
2
|
+
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
from scipy.optimize import curve_fit
|
|
8
|
+
|
|
9
|
+
from lsapy.core.functions import declare_equation, equations, get_function_from_name
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"fit_membership",
|
|
13
|
+
"logistic",
|
|
14
|
+
"sigmoid",
|
|
15
|
+
"vetharaniam2022_eq3",
|
|
16
|
+
"vetharaniam2022_eq5",
|
|
17
|
+
"vetharaniam2024_eq8",
|
|
18
|
+
"vetharaniam2024_eq10",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
EQUATION_TYPES = ["sigmoid", "gaussian"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@declare_equation("sigmoid")
|
|
25
|
+
def logistic(x, a, b):
|
|
26
|
+
r"""
|
|
27
|
+
Logistic function.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
x : any
|
|
32
|
+
Input values to map.
|
|
33
|
+
a : float | int
|
|
34
|
+
Steepness of the function parameter.
|
|
35
|
+
b : float | int
|
|
36
|
+
Value of the function's midpoint.
|
|
37
|
+
|
|
38
|
+
Returns
|
|
39
|
+
-------
|
|
40
|
+
float
|
|
41
|
+
Suitability values.
|
|
42
|
+
|
|
43
|
+
Notes
|
|
44
|
+
-----
|
|
45
|
+
The logistic function is defined as:
|
|
46
|
+
|
|
47
|
+
.. math::
|
|
48
|
+
|
|
49
|
+
f(x) = \frac{1}{1 + e^{-a(x - b)}}
|
|
50
|
+
"""
|
|
51
|
+
return 1 / (1 + np.exp(-a * (x - b)))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@declare_equation("sigmoid")
|
|
55
|
+
def sigmoid(x):
|
|
56
|
+
r"""
|
|
57
|
+
Sigmoid function.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
x : any
|
|
62
|
+
Input values to map.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
float
|
|
67
|
+
Suitability values.
|
|
68
|
+
|
|
69
|
+
Notes
|
|
70
|
+
-----
|
|
71
|
+
The sigmoid function is defined as:
|
|
72
|
+
|
|
73
|
+
.. math::
|
|
74
|
+
|
|
75
|
+
f(x) = \frac{1}{1 + e^{-x}}
|
|
76
|
+
"""
|
|
77
|
+
return logistic(x, 1, 0)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@declare_equation("sigmoid", "VTR22_3")
|
|
81
|
+
def vetharaniam2022_eq3(x, a, b):
|
|
82
|
+
r"""
|
|
83
|
+
Sigmoid like function.
|
|
84
|
+
|
|
85
|
+
# TODO: add a more detailed description.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
x : any
|
|
90
|
+
Input values to map.
|
|
91
|
+
a : float | int
|
|
92
|
+
Steepness of the function parameter.
|
|
93
|
+
b : float | int
|
|
94
|
+
Value of the function's midpoint.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
float
|
|
99
|
+
Suitability values.
|
|
100
|
+
|
|
101
|
+
Notes
|
|
102
|
+
-----
|
|
103
|
+
The sigmoid like function is defined as:
|
|
104
|
+
|
|
105
|
+
.. math::
|
|
106
|
+
|
|
107
|
+
f(x) = \frac{e^{a(x - b)}}{1 + e^{a(x - b)}}
|
|
108
|
+
|
|
109
|
+
References
|
|
110
|
+
----------
|
|
111
|
+
:cite:cts:`vetharaniam_lsa_2022`
|
|
112
|
+
"""
|
|
113
|
+
return np.exp(a * (x - b)) / (1 + np.exp(a * (x - b)))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@declare_equation("sigmoid", "VTR22_5")
|
|
117
|
+
def vetharaniam2022_eq5(x, a, b):
|
|
118
|
+
r"""
|
|
119
|
+
Sigmoid like function.
|
|
120
|
+
|
|
121
|
+
# TODO: add a more detailed description.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
x : any
|
|
126
|
+
Input values to map.
|
|
127
|
+
a : float | int
|
|
128
|
+
Steepness of the function parameter.
|
|
129
|
+
b : float | int
|
|
130
|
+
Value of the function's midpoint.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
float
|
|
135
|
+
Suitability values.
|
|
136
|
+
|
|
137
|
+
Notes
|
|
138
|
+
-----
|
|
139
|
+
The sigmoid like function is defined as:
|
|
140
|
+
|
|
141
|
+
.. math::
|
|
142
|
+
|
|
143
|
+
f(x) = \frac{1}{1 + e^{a(\sqrt{x} - \sqrt{b})}}
|
|
144
|
+
|
|
145
|
+
References
|
|
146
|
+
----------
|
|
147
|
+
:cite:cts:`vetharaniam_lsa_2022`
|
|
148
|
+
"""
|
|
149
|
+
return 1 / (1 + np.exp(a * (np.sqrt(x) - np.sqrt(b))))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@declare_equation("gaussian", "VTR24_8")
|
|
153
|
+
def vetharaniam2024_eq8(x, a, b, c):
|
|
154
|
+
r"""
|
|
155
|
+
Gaussian like function.
|
|
156
|
+
|
|
157
|
+
# TODO: add a more detailed description.
|
|
158
|
+
|
|
159
|
+
Parameters
|
|
160
|
+
----------
|
|
161
|
+
x : any
|
|
162
|
+
Input values to map.
|
|
163
|
+
a : float | int
|
|
164
|
+
Steepness of the function parameter.
|
|
165
|
+
b : float | int
|
|
166
|
+
Value of the function's midpoint.
|
|
167
|
+
c : float | int
|
|
168
|
+
Scaling parameter.
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
float
|
|
173
|
+
Suitability values.
|
|
174
|
+
|
|
175
|
+
Notes
|
|
176
|
+
-----
|
|
177
|
+
The Gaussian like function is defined as:
|
|
178
|
+
|
|
179
|
+
.. math::
|
|
180
|
+
|
|
181
|
+
f(x) = e^{-a(x - b)^c}
|
|
182
|
+
|
|
183
|
+
References
|
|
184
|
+
----------
|
|
185
|
+
:cite:cts:`vetharaniam_lsa_2024`
|
|
186
|
+
"""
|
|
187
|
+
return np.exp(-a * np.power(x - b, c))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@declare_equation("gaussian", "VTR24_10")
|
|
191
|
+
def vetharaniam2024_eq10(x, a, b, c):
|
|
192
|
+
r"""
|
|
193
|
+
Gaussian like function.
|
|
194
|
+
|
|
195
|
+
# TODO: add a more detailed description.
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
x : any
|
|
200
|
+
Input values to map.
|
|
201
|
+
a : float | int
|
|
202
|
+
Steepness of the function parameter.
|
|
203
|
+
b : float | int
|
|
204
|
+
Value of the function's midpoint.
|
|
205
|
+
c : float | int
|
|
206
|
+
Scaling parameter.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
float
|
|
211
|
+
Suitability values.
|
|
212
|
+
|
|
213
|
+
Notes
|
|
214
|
+
-----
|
|
215
|
+
The Gaussian like function is defined as:
|
|
216
|
+
|
|
217
|
+
.. math::
|
|
218
|
+
|
|
219
|
+
f(x) = e^{-a(x^c - b^c)}
|
|
220
|
+
|
|
221
|
+
References
|
|
222
|
+
----------
|
|
223
|
+
:cite:cts:`vetharaniam_lsa_2024`
|
|
224
|
+
"""
|
|
225
|
+
return 2 / (1 + np.exp(a * np.power(np.power(x, c) - np.power(b, c), 2)))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def fit_membership(x, y=None, fit_on: str | list[str] = "all", plot: bool = False, verbose: bool = False):
|
|
229
|
+
"""
|
|
230
|
+
Fit membership function to data.
|
|
231
|
+
|
|
232
|
+
This function fits membership functions to the provided data. It helps to determine the best membership function
|
|
233
|
+
to use on the data.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
x : any
|
|
238
|
+
Input values to fit the functions on.
|
|
239
|
+
y : any, optional
|
|
240
|
+
Target suitability values to fit the functions. Should be the same length as `x`. If not provided,
|
|
241
|
+
the default values are used (0, 0.25, 0.5, 0.75, 1).
|
|
242
|
+
fit_on : str | list[str], optional
|
|
243
|
+
List of equation or equation types to fit. `all, `sigmoid_like` and `gaussian_like` can also be used.
|
|
244
|
+
If 'all', all available equations are fitted. If '{TYPES}_like', all equations corresponding to the
|
|
245
|
+
type are fitted. Default is 'all'.
|
|
246
|
+
plot : bool, optional
|
|
247
|
+
Whether to plot the fitted functions. Default is False.
|
|
248
|
+
verbose : bool, optional
|
|
249
|
+
Whether to print the fitting results. Default is False.
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
tuple
|
|
254
|
+
A tuple containing the best fitting function and its parameters.
|
|
255
|
+
|
|
256
|
+
Examples
|
|
257
|
+
--------
|
|
258
|
+
>>> from lsapy.functions.membership import fit_membership
|
|
259
|
+
>>> fit_membership([1, 3, 5, 7, 10]) # doctest: +ELLIPSIS
|
|
260
|
+
(<function vetharaniam2022_eq5 at 0x...>, array([-2.78959209, 4.86485647]))
|
|
261
|
+
|
|
262
|
+
By default, the function will fit all available membership equations. If you want to fit only specific equations,
|
|
263
|
+
you can specify it using the `fit_on` parameter: "all", "sigmoid_like", "gaussian_like", or a list of equations.
|
|
264
|
+
The default `y` values can also be changed.
|
|
265
|
+
|
|
266
|
+
>>> fit_membership(x=[1, 3, 5, 5, 7, 9], y=[0, 0.5, 1, 1, 0.5, 0], fit_on="gaussian_like") # doctest: +ELLIPSIS
|
|
267
|
+
(<function vetharaniam2024_eq10 at 0x...>, array([0.38213219, 4.97273138, 0.93922461]))
|
|
268
|
+
"""
|
|
269
|
+
if y is None:
|
|
270
|
+
y = [0, 0.25, 0.5, 0.75, 1]
|
|
271
|
+
y = np.array(y)
|
|
272
|
+
functions, skipped = _check_fitting(fit_on)
|
|
273
|
+
|
|
274
|
+
x_ = np.linspace(min(x), max(x), 100)
|
|
275
|
+
rms_errors = []
|
|
276
|
+
f_params = []
|
|
277
|
+
for func in functions:
|
|
278
|
+
try:
|
|
279
|
+
f = get_function_from_name(func)
|
|
280
|
+
p0 = _get_function_p0(func, x)
|
|
281
|
+
popt, _ = curve_fit(f, x, y, p0=p0, maxfev=15000)
|
|
282
|
+
y_ = f(x_, *popt)
|
|
283
|
+
f_params.append(popt)
|
|
284
|
+
rmse = _rmse(y, f(x, *popt))
|
|
285
|
+
rms_errors.append(rmse)
|
|
286
|
+
if plot:
|
|
287
|
+
plt.plot(x_, y_, label=func + f" (RMSE={rmse:.2f})")
|
|
288
|
+
except Exception:
|
|
289
|
+
skipped.append(func)
|
|
290
|
+
warnings.warn(f"Failed to fit `{func}`. Skipped.", stacklevel=2)
|
|
291
|
+
|
|
292
|
+
if all([f in skipped for f in functions]):
|
|
293
|
+
warnings.warn(f"No methods to fit. fitting for the following methods: {', '.join(skipped)}.", stacklevel=2)
|
|
294
|
+
return None, None
|
|
295
|
+
|
|
296
|
+
if plot:
|
|
297
|
+
plt.scatter(x, y, c="r")
|
|
298
|
+
plt.legend()
|
|
299
|
+
plt.show()
|
|
300
|
+
|
|
301
|
+
f_best, p_best = _get_best_fit([m for m in functions if m not in skipped], rms_errors, f_params, verbose=verbose)
|
|
302
|
+
return get_function_from_name(f_best), p_best
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _check_fitting(fit_on: str | list[str] = "all"):
|
|
306
|
+
_types = [t + "_like" for t in EQUATION_TYPES]
|
|
307
|
+
_skipped = []
|
|
308
|
+
|
|
309
|
+
if not isinstance(fit_on, str) and not isinstance(fit_on, list):
|
|
310
|
+
raise ValueError(f"`fit_on` should be a str or a list of string. Got {type(fit_on)}")
|
|
311
|
+
|
|
312
|
+
functions = []
|
|
313
|
+
if isinstance(fit_on, str):
|
|
314
|
+
if fit_on == "all":
|
|
315
|
+
for t in EQUATION_TYPES:
|
|
316
|
+
functions.extend(equations[t].keys())
|
|
317
|
+
fit_on = None
|
|
318
|
+
else:
|
|
319
|
+
fit_on = [fit_on]
|
|
320
|
+
if fit_on is not None:
|
|
321
|
+
for func in fit_on:
|
|
322
|
+
if not isinstance(func, str):
|
|
323
|
+
continue
|
|
324
|
+
if func in _types:
|
|
325
|
+
for f in equations[func.replace("_like", "")].keys():
|
|
326
|
+
if f not in functions:
|
|
327
|
+
functions.append(f)
|
|
328
|
+
else:
|
|
329
|
+
try:
|
|
330
|
+
get_function_from_name(func)
|
|
331
|
+
if func not in functions:
|
|
332
|
+
functions.append(func)
|
|
333
|
+
except Exception:
|
|
334
|
+
_skipped.append(func)
|
|
335
|
+
warnings.warn(f"`{func}` not found in implemented equations. Skipped.", stacklevel=3)
|
|
336
|
+
|
|
337
|
+
for f in ["sigmoid", "vetharaniam2024_eq8"]:
|
|
338
|
+
if f in functions:
|
|
339
|
+
functions.remove(f)
|
|
340
|
+
_skipped.append(f)
|
|
341
|
+
if f == "sigmoid":
|
|
342
|
+
warnings.warn("No parameters to determine for `sigmoid`. Skipped.", stacklevel=3)
|
|
343
|
+
if f == "vetharaniam2024_eq8":
|
|
344
|
+
warnings.warn("Fitting does not support `vetharaniam2024_eq8`. Skipped.", stacklevel=3)
|
|
345
|
+
|
|
346
|
+
if len(functions) == 0:
|
|
347
|
+
raise ValueError("No functions to fit. Try to modify `fit_on` parameter.")
|
|
348
|
+
return functions, _skipped
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _get_function_p0(func: str, x: np.ndarray) -> list[float]:
|
|
352
|
+
if func in equations["sigmoid"]:
|
|
353
|
+
return [1, np.median(x)]
|
|
354
|
+
if func in equations["gaussian"]:
|
|
355
|
+
return [1, np.median(x), 1]
|
|
356
|
+
return []
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _rmse(y_true, y_pred):
|
|
360
|
+
diff = abs(y_true - y_pred)
|
|
361
|
+
return np.sqrt(np.mean(diff**2))
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _get_best_fit(functions, rmse, params, verbose=True):
|
|
365
|
+
best_fit = np.nanargmin(rmse)
|
|
366
|
+
if verbose:
|
|
367
|
+
print(f"""
|
|
368
|
+
Best fit: {functions[best_fit]}
|
|
369
|
+
RMSE: {rmse[best_fit]:.5f}
|
|
370
|
+
Params: a={params[best_fit][0]}, b={params[best_fit][1]}
|
|
371
|
+
""")
|
|
372
|
+
return functions[best_fit], params[best_fit]
|