wolfhece 2.1.124__py3-none-any.whl → 2.1.125__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.
- wolfhece/PyDraw.py +9 -2
- wolfhece/acceptability/acceptability_gui.py +243 -243
- wolfhece/apps/version.py +1 -1
- wolfhece/sigmoid/__init__.py +0 -0
- wolfhece/sigmoid/circle_jax.py +118 -0
- wolfhece/sigmoid/circle_jax_copilot.py +169 -0
- wolfhece/sigmoid/sigmoid.py +776 -0
- wolfhece/wolfresults_2D.py +16 -6
- {wolfhece-2.1.124.dist-info → wolfhece-2.1.125.dist-info}/METADATA +1 -1
- {wolfhece-2.1.124.dist-info → wolfhece-2.1.125.dist-info}/RECORD +13 -9
- {wolfhece-2.1.124.dist-info → wolfhece-2.1.125.dist-info}/WHEEL +1 -1
- {wolfhece-2.1.124.dist-info → wolfhece-2.1.125.dist-info}/entry_points.txt +0 -0
- {wolfhece-2.1.124.dist-info → wolfhece-2.1.125.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,776 @@
|
|
1
|
+
|
2
|
+
import numpy as np
|
3
|
+
from numba import njit, jit
|
4
|
+
from scipy.optimize import minimize
|
5
|
+
import logging
|
6
|
+
|
7
|
+
""" Using JIT to speed up the functions """
|
8
|
+
@njit
|
9
|
+
def sigmoid(x:np.float64, loc:np.float64, scale:np.float64) -> float:
|
10
|
+
""" Sigmoid function """
|
11
|
+
return 1. / (1. + np.exp(-scale * (x - loc)))
|
12
|
+
|
13
|
+
@njit
|
14
|
+
def sigmoid_derivative(x:np.float64, loc:np.float64, scale:np.float64) -> float:
|
15
|
+
""" Derivative of the sigmoid function """
|
16
|
+
s = sigmoid(x, loc, scale)
|
17
|
+
return scale * s * (1. - s)
|
18
|
+
|
19
|
+
@njit
|
20
|
+
def sigmoid_second_derivative(x:np.float64, loc:np.float64, scale:np.float64) -> float:
|
21
|
+
""" Second derivative of the sigmoid function """
|
22
|
+
s = sigmoid(x, loc, scale)
|
23
|
+
return scale**2. * s * (1. - s) * (1. - 2. * s)
|
24
|
+
|
25
|
+
@njit
|
26
|
+
def one_minus_sigmoid(x:np.float64, loc:np.float64, scale:np.float64) -> float:
|
27
|
+
""" 1 - Sigmoid function """
|
28
|
+
return 1. - sigmoid(x, loc, scale)
|
29
|
+
|
30
|
+
@njit
|
31
|
+
def extract_xy_binary_search(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray, nb_elemt:np.int64) -> tuple[np.ndarray, np.ndarray]:
|
32
|
+
""" Binary search to find the interval of x values for each x_candidate
|
33
|
+
|
34
|
+
:param x_candidate: np.ndarray values to extract the x and y values
|
35
|
+
:param x: np.ndarray, list of floats, x values of the points
|
36
|
+
:param y: np.ndarray, list of floats, y values of the points
|
37
|
+
:param nb_elemt: int, number of elements to consider around the x_candidate value (must be odd)
|
38
|
+
"""
|
39
|
+
|
40
|
+
n = len(x) - 1
|
41
|
+
|
42
|
+
if np.mod(nb_elemt, 2) == 0:
|
43
|
+
nb_elemt += 1
|
44
|
+
|
45
|
+
nb_elemt = min(n, nb_elemt)
|
46
|
+
|
47
|
+
results = np.zeros_like(x_candidate, dtype=np.int64)
|
48
|
+
|
49
|
+
for idx, cur_x in enumerate(x_candidate):
|
50
|
+
if cur_x < x[0]:
|
51
|
+
results[idx] = 0
|
52
|
+
elif cur_x > x[-1]:
|
53
|
+
results[idx] = n - 1
|
54
|
+
else:
|
55
|
+
left = 0
|
56
|
+
right = n - 1
|
57
|
+
|
58
|
+
while right - left > 1:
|
59
|
+
mid = (left + right) // 2
|
60
|
+
if cur_x < x[mid]:
|
61
|
+
right = mid
|
62
|
+
else:
|
63
|
+
left = mid
|
64
|
+
|
65
|
+
results[idx] = left
|
66
|
+
|
67
|
+
new_x = np.zeros((len(x_candidate), nb_elemt), dtype=np.float64)
|
68
|
+
new_y = np.zeros((len(x_candidate), nb_elemt), dtype=np.float64)
|
69
|
+
|
70
|
+
for idx, cur_x in enumerate(x_candidate):
|
71
|
+
i = results[idx]
|
72
|
+
if i < nb_elemt//2:
|
73
|
+
new_x[idx,:] = x[:nb_elemt]
|
74
|
+
new_y[idx,:] = y[:nb_elemt]
|
75
|
+
elif i > n - nb_elemt//2:
|
76
|
+
new_x[idx,:] = x[-nb_elemt:]
|
77
|
+
new_y[idx,:] = y[-nb_elemt:]
|
78
|
+
else:
|
79
|
+
new_x[idx,:] = x[i-nb_elemt//2 : i+nb_elemt//2+1]
|
80
|
+
new_y[idx,:] = y[i-nb_elemt//2 : i+nb_elemt//2+1]
|
81
|
+
|
82
|
+
return new_x, new_y
|
83
|
+
|
84
|
+
@njit
|
85
|
+
def _piecewise_linear(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray,
|
86
|
+
scale:np.float64, slope_left:np.float64, slope_right:np.float64) -> np.ndarray:
|
87
|
+
""" Piecewise linear function with continuous transition by sigmoids.
|
88
|
+
|
89
|
+
In extrapolation mode, the function is y[0] for x < x[0] and linearly extrapoletd based on the last slope for x > x[-1].
|
90
|
+
|
91
|
+
:param x_candidate: np.ndarray, list or float, x values to evaluate the function
|
92
|
+
:param x: np.ndarray, list of floats, x values of the points
|
93
|
+
:param y: np.ndarray, list of floats, y values of the points
|
94
|
+
:param scale: float, scale of the sigmoid functions
|
95
|
+
:param slope_left: float, slope before the first segment - extrapolation mode
|
96
|
+
:param slope_right: float, slope after the last segment - extrapolation mode
|
97
|
+
"""
|
98
|
+
|
99
|
+
# FIXME : Numba.JIT does not like np.concatenate, np.hstack... so we pre-allocate the arrays
|
100
|
+
|
101
|
+
# Extend the x and y values to allow extrapolation based on slope_left and slope_right
|
102
|
+
xx = np.zeros(x.shape[0]+1) # add the extrapolation points
|
103
|
+
yy = np.zeros(y.shape[0]+1) # add the extrapolation points
|
104
|
+
slopes = np.zeros(x.shape[0]+1)
|
105
|
+
|
106
|
+
xx[0] = x[0]
|
107
|
+
yy[0] = y[0]
|
108
|
+
xx[1:] = x
|
109
|
+
yy[1:] = y
|
110
|
+
|
111
|
+
x = xx
|
112
|
+
y = yy
|
113
|
+
|
114
|
+
n = len(x) # number of intervals/segments taking into account the extrapolation
|
115
|
+
results = np.zeros_like(x_candidate) # pre-allocate the results, force numpy type and not a list
|
116
|
+
|
117
|
+
functions = np.zeros(n) # values of the linear functions for each segment
|
118
|
+
|
119
|
+
sigmoids = np.ones(n)
|
120
|
+
one_minus_sigmoids = np.ones(n)
|
121
|
+
|
122
|
+
# local slopes of the segments -- must be a **function** (no vertical slope, x increasing...)
|
123
|
+
slopes[0] = slope_left
|
124
|
+
slopes[1:-1] = (y[2:] - y[1:-1]) / (x[2:] - x[1:-1])
|
125
|
+
slopes[-1] = slope_right
|
126
|
+
|
127
|
+
for idx, cur_x in enumerate(x_candidate):
|
128
|
+
# Copy the x value for each segment
|
129
|
+
xvals = np.full(n, cur_x)
|
130
|
+
# Compute the value of the linear function for each segment
|
131
|
+
functions[:] = slopes[:] * (xvals[:] - x[:]) + y[:]
|
132
|
+
# Compute the value of the sigmoid function for each segment (based on the start of the segment)
|
133
|
+
sigmoids[1:] = sigmoid(xvals[1:], x[1:], scale)
|
134
|
+
# Compute the value of 1 - sigmoid for each segment (based on the end of the segment)
|
135
|
+
one_minus_sigmoids[:-1] = 1. - sigmoids[1:]
|
136
|
+
|
137
|
+
"""
|
138
|
+
Interpolation mode : x[0] <= x_candidate <= x[-1]
|
139
|
+
------------------
|
140
|
+
|
141
|
+
We will combine the results of each segment.
|
142
|
+
For each segment, we use a door function that is 1 if we are in the segment and 0 otherwise.
|
143
|
+
|
144
|
+
1 ____________
|
145
|
+
| |
|
146
|
+
| |
|
147
|
+
| |
|
148
|
+
0______| |_______
|
149
|
+
x1 x2
|
150
|
+
|
151
|
+
The door function is the product of the sigmoid of the segment and the (1 - sigmoid) of the next segment.
|
152
|
+
|
153
|
+
Door_i = sigmoid_i * (1 - sigmoid_{i+1})
|
154
|
+
|
155
|
+
So, for x, we can compute the value of the function as a sum of the value of the function for each segment multiplied by the door function.
|
156
|
+
|
157
|
+
f(x) = sum_i (functions_i(x) * Door_i)
|
158
|
+
"""
|
159
|
+
|
160
|
+
results[idx] = np.sum(sigmoids * one_minus_sigmoids * functions)
|
161
|
+
|
162
|
+
return results
|
163
|
+
|
164
|
+
@njit
|
165
|
+
def piecewise_linear(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray,
|
166
|
+
scale:np.float64, nb_elemt:np.int64,
|
167
|
+
slope_left:np.float64, slope_right:np.float64) -> np.ndarray:
|
168
|
+
""" Piecewise linear function with continuous transition by sigmoids
|
169
|
+
|
170
|
+
:param x_candidate: np.ndarray, list or float, x values to evaluate the function
|
171
|
+
:param x: np.ndarray, list of floats, x values of the points
|
172
|
+
:param y: np.ndarray, list of floats, y values of the points
|
173
|
+
:param scale: float, scale of the sigmoid functions
|
174
|
+
:param nb_elemt: int, number of elements to consider around the x_candidate value
|
175
|
+
"""
|
176
|
+
|
177
|
+
results = np.zeros_like(x_candidate)
|
178
|
+
|
179
|
+
if nb_elemt == -1:
|
180
|
+
|
181
|
+
results[:] = _piecewise_linear(x_candidate, x, y, scale, slope_left, slope_right)
|
182
|
+
|
183
|
+
else:
|
184
|
+
xx, yy = extract_xy_binary_search(x_candidate, x, y, nb_elemt)
|
185
|
+
|
186
|
+
for idx, cur_x in enumerate(x_candidate):
|
187
|
+
results[idx] = _piecewise_linear(np.array([cur_x]), xx[idx], yy[idx], scale, slope_left, slope_right)[0]
|
188
|
+
|
189
|
+
return results
|
190
|
+
|
191
|
+
@njit
|
192
|
+
def _gradient_piecewise_linear(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray,
|
193
|
+
scale:np.float64, slope_left:np.float64, slope_right:np.float64) -> np.ndarray:
|
194
|
+
""" Gradient of the piecewise linear function """
|
195
|
+
|
196
|
+
xx = np.zeros(x.shape[0]+1) # add the extrapolation points
|
197
|
+
yy = np.zeros(y.shape[0]+1) # add the extrapolation points
|
198
|
+
slopes = np.zeros(x.shape[0]+1)
|
199
|
+
|
200
|
+
xx[0] = x[0]
|
201
|
+
yy[0] = y[0]
|
202
|
+
xx[1:] = x
|
203
|
+
yy[1:] = y
|
204
|
+
|
205
|
+
x = xx
|
206
|
+
y = yy
|
207
|
+
|
208
|
+
n = len(x)
|
209
|
+
results = np.zeros_like(x_candidate)
|
210
|
+
|
211
|
+
functions = np.zeros(n) # values of the linear functions for each segment
|
212
|
+
|
213
|
+
sigmoids = np.ones(n)
|
214
|
+
one_minus_sigmoids = np.ones(n)
|
215
|
+
|
216
|
+
slopes[0] = slope_left
|
217
|
+
slopes[1:-1] = (y[2:] - y[1:-1]) / (x[2:] - x[1:-1])
|
218
|
+
slopes[-1] = slope_right
|
219
|
+
|
220
|
+
for idx, cur_x in enumerate(x_candidate):
|
221
|
+
# Copy the x value for each segment
|
222
|
+
xvals = np.full(n, cur_x)
|
223
|
+
# Compute the value of the linear function for each segment
|
224
|
+
functions[:] = slopes[:] * (xvals[:] - x[:]) + y[:]
|
225
|
+
# Compute the value of the sigmoid function for each segment (based on the start of the segment)
|
226
|
+
sigmoids[1:] = sigmoid(xvals[1:], x[1:], scale)
|
227
|
+
# Compute the value of 1 - sigmoid for each segment (based on the end of the segment)
|
228
|
+
one_minus_sigmoids[:-1] = 1. - sigmoids[1:]
|
229
|
+
|
230
|
+
def derivative_sigmoid(sig, scale):
|
231
|
+
return scale * sig * (1. - sig)
|
232
|
+
|
233
|
+
def derivative_oneminussigmoid(oneminussig, scale):
|
234
|
+
return -derivative_sigmoid(oneminussig, scale)
|
235
|
+
|
236
|
+
result = 0.0
|
237
|
+
for i in range(n):
|
238
|
+
result += sigmoids[i] * one_minus_sigmoids[i] * slopes[i]
|
239
|
+
result += (derivative_sigmoid(sigmoids[i], scale) * one_minus_sigmoids[i] + sigmoids[i] * derivative_oneminussigmoid(one_minus_sigmoids[i], scale)) * functions[i]
|
240
|
+
|
241
|
+
results[idx] = result
|
242
|
+
|
243
|
+
return results
|
244
|
+
|
245
|
+
@njit
|
246
|
+
def gradient_piecewise_linear(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray,
|
247
|
+
scale:np.float64, nb_elemt:np.int64,
|
248
|
+
slope_left:np.float64, slope_right:np.float64) -> np.ndarray:
|
249
|
+
""" Gradient of the piecewise linear function """
|
250
|
+
|
251
|
+
results = np.zeros_like(x_candidate)
|
252
|
+
|
253
|
+
if nb_elemt == -1:
|
254
|
+
|
255
|
+
results[:] = _gradient_piecewise_linear(x_candidate, x, y, scale, slope_left, slope_right)
|
256
|
+
|
257
|
+
else:
|
258
|
+
xx, yy = extract_xy_binary_search(x_candidate, x, y, nb_elemt)
|
259
|
+
|
260
|
+
for idx, cur_x in enumerate(x_candidate):
|
261
|
+
results[idx] = _gradient_piecewise_linear(np.array([cur_x]), xx[idx], yy[idx], scale, slope_left, slope_right)[0]
|
262
|
+
|
263
|
+
return results
|
264
|
+
|
265
|
+
@njit
|
266
|
+
def _gradient_piecewise_linear_approx(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray,
|
267
|
+
scale:np.float64, slope_left:np.float64, slope_right:np.float64):
|
268
|
+
""" Approximative gradient of the piecewise linear function.
|
269
|
+
|
270
|
+
The derivative contribution of the sigmoids is ignored.
|
271
|
+
"""
|
272
|
+
|
273
|
+
x = np.hstack((x[0], x)) # add the extrapolation points
|
274
|
+
y = np.hstack((y[0], y)) # add the extrapolation points
|
275
|
+
|
276
|
+
n = len(x)
|
277
|
+
results = np.zeros_like(x_candidate)
|
278
|
+
|
279
|
+
functions = np.zeros(n) # values of the linear functions for each segment
|
280
|
+
|
281
|
+
sigmoids = np.ones(n)
|
282
|
+
one_minus_sigmoids = np.ones(n)
|
283
|
+
|
284
|
+
slopes = np.concatenate([ [slope_left], (y[2:] - y[1:-1]) / (x[2:] - x[1:-1]), [slope_right]])
|
285
|
+
|
286
|
+
for idx, cur_x in enumerate(x_candidate):
|
287
|
+
# Copy the x value for each segment
|
288
|
+
xvals = np.full(n, cur_x)
|
289
|
+
# Compute the value of the linear function for each segment
|
290
|
+
functions[:] = slopes[:] * (xvals[:] - x[:]) + y[:]
|
291
|
+
# Compute the value of the sigmoid function for each segment (based on the start of the segment)
|
292
|
+
sigmoids[1:] = sigmoid(xvals[1:], x[1:], scale)
|
293
|
+
# Compute the value of 1 - sigmoid for each segment (based on the end of the segment)
|
294
|
+
one_minus_sigmoids[:-1] = 1. - sigmoids[1:]
|
295
|
+
|
296
|
+
results[idx] = np.sum(sigmoids * one_minus_sigmoids * slopes)
|
297
|
+
# result = 0.0
|
298
|
+
# for i in range(n):
|
299
|
+
# result += sigmoids[i] * one_minus_sigmoids[i] * slopes[i]
|
300
|
+
|
301
|
+
# results[idx] = result
|
302
|
+
|
303
|
+
return results
|
304
|
+
|
305
|
+
@njit
|
306
|
+
def gradient_piecewise_linear_approx(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray,
|
307
|
+
scale:np.float64, nb_elemt:np.int64,
|
308
|
+
slope_left:np.float64, slope_right:np.float64):
|
309
|
+
""" Approximative gradient of the piecewise linear function.
|
310
|
+
|
311
|
+
The derivative contribution of the sigmoids is ignored.
|
312
|
+
"""
|
313
|
+
|
314
|
+
results = np.zeros_like(x_candidate)
|
315
|
+
|
316
|
+
if nb_elemt == -1:
|
317
|
+
|
318
|
+
results[:] = _gradient_piecewise_linear_approx(x_candidate, x, y, scale, slope_left, slope_right)
|
319
|
+
|
320
|
+
else:
|
321
|
+
xx, yy = extract_xy_binary_search(x_candidate, x, y, nb_elemt)
|
322
|
+
|
323
|
+
for idx, cur_x in enumerate(x_candidate):
|
324
|
+
results[idx] = _gradient_piecewise_linear_approx(np.array([cur_x]), xx[idx], yy[idx], scale, slope_left, slope_right)[0]
|
325
|
+
|
326
|
+
return results
|
327
|
+
|
328
|
+
class Piecewise_Linear_Sigmoid():
|
329
|
+
""" Piecewise linear function with smooth transitions using sigmoids """
|
330
|
+
|
331
|
+
def __init__(self, x, y, scale:float = 10., slope_left:float = 0., slope_right:float = 99999.):
|
332
|
+
"""
|
333
|
+
:param x: np.ndarray or list of floats, x values of the points
|
334
|
+
:param y: np.ndarray or list of floats, y values of the points
|
335
|
+
:param scale: float, scale of the sigmoid functions
|
336
|
+
"""
|
337
|
+
|
338
|
+
self.x:np.ndarray = np.asarray(x, dtype= np.float64).flatten()
|
339
|
+
self.y:np.ndarray = np.asarray(y, dtype= np.float64).flatten()
|
340
|
+
self.scale:np.float64 = np.float64(scale)
|
341
|
+
self.slope_left:np.float64 = slope_left
|
342
|
+
self.slope_right:np.float64 = slope_right
|
343
|
+
|
344
|
+
self._checked_x = False
|
345
|
+
self._checked_y = False
|
346
|
+
|
347
|
+
@property
|
348
|
+
def slope_left(self):
|
349
|
+
return self._slope_left
|
350
|
+
|
351
|
+
@property
|
352
|
+
def slope_right(self):
|
353
|
+
return self._slope_right
|
354
|
+
|
355
|
+
@slope_left.setter
|
356
|
+
def slope_left(self, value):
|
357
|
+
self._slope_left = np.float64(value)
|
358
|
+
|
359
|
+
if self._slope_left == 99999.:
|
360
|
+
self._slope_left = (self.y[1] - self.y[0]) / (self.x[1] - self.x[0])
|
361
|
+
|
362
|
+
@slope_right.setter
|
363
|
+
def slope_right(self, value):
|
364
|
+
self._slope_right = np.float64(value)
|
365
|
+
|
366
|
+
if self._slope_right == 99999.:
|
367
|
+
self._slope_right = (self.y[-1] - self.y[-2]) / (self.x[-1] - self.x[-2])
|
368
|
+
|
369
|
+
|
370
|
+
def clip_around_x(self, x:np.ndarray, nb_elemt:int = -1) -> tuple[np.ndarray, np.ndarray]:
|
371
|
+
"""
|
372
|
+
Clip the input array x around the existing x values
|
373
|
+
|
374
|
+
:param x: np.ndarray, list or float, x values to clip
|
375
|
+
:param nb_elemt: int, number of elements to consider around each x value
|
376
|
+
:return: tuple of two np.ndarrays, clipped x and y values
|
377
|
+
"""
|
378
|
+
if not self._checked_x:
|
379
|
+
self.check_x()
|
380
|
+
self._checked_x = True
|
381
|
+
|
382
|
+
x_array = np.asarray(x, dtype=np.float64).flatten()
|
383
|
+
return extract_xy_binary_search(x_array, self.x, self.y, np.int64(nb_elemt))
|
384
|
+
|
385
|
+
def clip_around_y(self, y:np.ndarray, nb_elemt:int = -1) -> tuple[np.ndarray, np.ndarray]:
|
386
|
+
"""
|
387
|
+
Clip the input array y around the existing y values
|
388
|
+
|
389
|
+
:param y: np.ndarray, list or float, y values to clip
|
390
|
+
:param nb_elemt: int, number of elements to consider around each y value
|
391
|
+
:return: tuple of two np.ndarrays, clipped x and y values
|
392
|
+
"""
|
393
|
+
if not self._checked_y:
|
394
|
+
self.check_y()
|
395
|
+
self._checked_y = True
|
396
|
+
|
397
|
+
y_array = np.asarray(y, dtype=np.float64).flatten()
|
398
|
+
return extract_xy_binary_search(y_array, self.y, self.x, np.int64(nb_elemt))
|
399
|
+
|
400
|
+
def gradient(self, x, approximative:bool = False, nb_elemt:int = -1) -> np.ndarray:
|
401
|
+
""" Gradient of the piecewise linear function with smooth transitions using sigmoids
|
402
|
+
|
403
|
+
:param x: np.ndarray, list or float, x values to evaluate the gradient
|
404
|
+
:param approximative: bool, if True, use an approximative gradient (ignoring derivative of the sigmoids)
|
405
|
+
:param nb_elemt: int, number of elements to consider around the x_candidate value
|
406
|
+
"""
|
407
|
+
|
408
|
+
if not self._checked_x:
|
409
|
+
self.check_x()
|
410
|
+
self._checked_x = True
|
411
|
+
|
412
|
+
if approximative:
|
413
|
+
if isinstance(x, np.ndarray):
|
414
|
+
return gradient_piecewise_linear_approx(x.astype(np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)
|
415
|
+
elif isinstance(x, list):
|
416
|
+
return gradient_piecewise_linear_approx(np.array(x, dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)
|
417
|
+
elif isinstance(x, float):
|
418
|
+
return gradient_piecewise_linear_approx(np.array([x], dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)[0]
|
419
|
+
elif isinstance(x, int):
|
420
|
+
return gradient_piecewise_linear_approx(np.array([float(x)], dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)[0]
|
421
|
+
else:
|
422
|
+
raise ValueError('x should be np.ndarray, list or float')
|
423
|
+
else:
|
424
|
+
if isinstance(x, np.ndarray):
|
425
|
+
return gradient_piecewise_linear(x.astype(np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)
|
426
|
+
elif isinstance(x, list):
|
427
|
+
return gradient_piecewise_linear(np.array(x, dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)
|
428
|
+
elif isinstance(x, float):
|
429
|
+
return gradient_piecewise_linear(np.array([x], dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)[0]
|
430
|
+
elif isinstance(x, int):
|
431
|
+
return gradient_piecewise_linear(np.array([float(x)], dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self.slope_left, self._slope_right)[0]
|
432
|
+
else:
|
433
|
+
raise ValueError('x should be np.ndarray, list or float')
|
434
|
+
|
435
|
+
def check_x(self):
|
436
|
+
diff = np.diff(self.x)
|
437
|
+
if np.any(diff <= 0):
|
438
|
+
raise ValueError('x should be in increasing order')
|
439
|
+
|
440
|
+
def check_y(self):
|
441
|
+
diff = np.diff(self.y)
|
442
|
+
if np.any(diff <= 0):
|
443
|
+
raise ValueError('y should be in increasing order')
|
444
|
+
|
445
|
+
def check(self):
|
446
|
+
self.check_x()
|
447
|
+
self.check_y()
|
448
|
+
|
449
|
+
return True
|
450
|
+
|
451
|
+
@property
|
452
|
+
def n(self):
|
453
|
+
""" Number of points """
|
454
|
+
return len(self.x)
|
455
|
+
|
456
|
+
@property
|
457
|
+
def size(self):
|
458
|
+
return self.n
|
459
|
+
|
460
|
+
def __call__(self, x, nb_elemt:int = -1) -> np.ndarray:
|
461
|
+
""" Evaluate the piecewise linear function at x
|
462
|
+
|
463
|
+
:param x: np.ndarray, list or float, x values to evaluate the function
|
464
|
+
:param nb_elemt: int, number of elements to consider around the x_candidate value
|
465
|
+
"""
|
466
|
+
|
467
|
+
if not self._checked_x:
|
468
|
+
self.check_x()
|
469
|
+
self._checked_x = True
|
470
|
+
|
471
|
+
if isinstance(x, np.ndarray):
|
472
|
+
return piecewise_linear(x.astype(np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self._slope_left, self._slope_right)
|
473
|
+
elif isinstance(x, list):
|
474
|
+
return piecewise_linear(np.array(x, dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self._slope_left, self._slope_right)
|
475
|
+
elif isinstance(x, float):
|
476
|
+
return piecewise_linear(np.array([x], dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self._slope_left, self._slope_right)[0]
|
477
|
+
elif isinstance(x, int):
|
478
|
+
return piecewise_linear(np.array([float(x)], dtype=np.float64), self.x, self.y, self.scale, np.int64(nb_elemt), self._slope_left, self._slope_right)[0]
|
479
|
+
else:
|
480
|
+
raise ValueError('x should be np.ndarray, list or float')
|
481
|
+
|
482
|
+
def inverse(self, y, nb_elemt:int = -1) -> np.ndarray:
|
483
|
+
""" Evaluate the inverse of the piecewise linear function at y """
|
484
|
+
|
485
|
+
if not self._checked_y:
|
486
|
+
self.check_y()
|
487
|
+
self._checked_y = True
|
488
|
+
|
489
|
+
if isinstance(y, np.ndarray):
|
490
|
+
return piecewise_linear(y.astype(np.float64), self.y, self.x, self.scale, np.int64(nb_elemt), 1./self._slope_left, 1./self._slope_right)
|
491
|
+
elif isinstance(y, list):
|
492
|
+
return piecewise_linear(np.array(y, dtype=np.float64), self.y, self.x, self.scale, np.int64(nb_elemt), 1./self._slope_left, 1./self._slope_right)
|
493
|
+
elif isinstance(y, float):
|
494
|
+
return piecewise_linear(np.array([y], dtype=np.float64), self.y, self.x, self.scale, np.int64(nb_elemt), 1./self._slope_left, 1./self._slope_right)[0]
|
495
|
+
else:
|
496
|
+
raise ValueError('y should be np.ndarray, list or float')
|
497
|
+
|
498
|
+
def get_y(self, x, nb_elemt:int = -1):
|
499
|
+
""" Get the value of the piecewise linear function at x """
|
500
|
+
return self(x, nb_elemt)
|
501
|
+
|
502
|
+
def get_x(self, y, nb_elemt:int = -1):
|
503
|
+
""" Get the inverse of the piecewise linear function at y """
|
504
|
+
return self.inverse(y, nb_elemt)
|
505
|
+
|
506
|
+
@njit
|
507
|
+
def _polynomial(coeffs:np.ndarray, x:np.float64):
|
508
|
+
""" Polynomial function """
|
509
|
+
return np.sum(coeffs * np.power(x, np.arange(len(coeffs))), dtype=np.float64)
|
510
|
+
|
511
|
+
@njit
|
512
|
+
def piecewise_polynomial(x_candidate:np.ndarray, x:np.ndarray, y:np.ndarray, poly_coeff:np.ndarray,
|
513
|
+
scale:np.float64,
|
514
|
+
slope_left:np.float64, slope_right:np.float64) -> np.ndarray:
|
515
|
+
""" Piecewise polynomial function.
|
516
|
+
|
517
|
+
:param x_candidate: np.ndarray, list or float, x values to evaluate the function
|
518
|
+
:param x: np.ndarray, list of floats, x values of the transition points between polyomials functions
|
519
|
+
:param poly_coeff: np.ndarray, list of floats, coefficients of the polynomial functions
|
520
|
+
"""
|
521
|
+
|
522
|
+
c_left = np.zeros(poly_coeff.shape[1], dtype=np.float64)
|
523
|
+
c_right = np.zeros(poly_coeff.shape[1], dtype=np.float64)
|
524
|
+
|
525
|
+
c_left[0] = y[0] - slope_left * x[0]
|
526
|
+
c_left[1] = slope_left
|
527
|
+
|
528
|
+
c_right[0] = y[-1] - slope_right * x[-1]
|
529
|
+
c_right[1] = slope_right
|
530
|
+
|
531
|
+
shape_max = max([len(coeffs) for coeffs in poly_coeff])
|
532
|
+
polys = np.zeros((len(poly_coeff)+2, shape_max), dtype=np.float64)
|
533
|
+
|
534
|
+
polys[0,:] = c_left
|
535
|
+
polys[-1,:] = c_right
|
536
|
+
polys[1:-1,:] = poly_coeff
|
537
|
+
|
538
|
+
# Extend the x and y values to allow extrapolation based on slope_left and slope_right
|
539
|
+
xx = np.zeros(x.shape[0]+1) # add the extrapolation points
|
540
|
+
|
541
|
+
xx[0] = x[0]
|
542
|
+
xx[1:] = x
|
543
|
+
|
544
|
+
x = xx
|
545
|
+
|
546
|
+
n = len(polys)
|
547
|
+
results = np.zeros(len(x_candidate))
|
548
|
+
sigmoids = np.ones(n)
|
549
|
+
one_minus_sigmoids = np.ones(n)
|
550
|
+
|
551
|
+
for idx, cur_x in enumerate(x_candidate):
|
552
|
+
xvals = np.full(n, cur_x)
|
553
|
+
sigmoids[1:] = sigmoid(xvals[1:], x[1:], scale)
|
554
|
+
one_minus_sigmoids[:-1] = 1. - sigmoids[1:]
|
555
|
+
|
556
|
+
for i in range(n):
|
557
|
+
results[idx] += sigmoids[i] * one_minus_sigmoids[i] * _polynomial(polys[i], cur_x)
|
558
|
+
|
559
|
+
return results
|
560
|
+
|
561
|
+
|
562
|
+
class Piecewise_Polynomial_Sigmoid():
|
563
|
+
""" Polynomial function with smooth transitions using sigmoids """
|
564
|
+
|
565
|
+
def __init__(self, x, y, scale:float = 10., degree:int = 3, slope_left:float = 0., slope_right:float = 99999.):
|
566
|
+
|
567
|
+
self.x:np.ndarray = np.asarray(x, dtype= np.float64).flatten()
|
568
|
+
self.y:np.ndarray = np.asarray(y, dtype= np.float64).flatten()
|
569
|
+
self.scale:np.float64 = np.float64(scale)
|
570
|
+
self.degree:int = degree
|
571
|
+
|
572
|
+
self.slope_left:np.float64 = slope_left
|
573
|
+
self.slope_right:np.float64 = slope_right
|
574
|
+
|
575
|
+
self._poly_coeff = None
|
576
|
+
self._parts_x = None
|
577
|
+
self._parts_y = None
|
578
|
+
|
579
|
+
self._checked_x = False
|
580
|
+
self._checked_y = False
|
581
|
+
|
582
|
+
@property
|
583
|
+
def slope_left(self):
|
584
|
+
return self._slope_left
|
585
|
+
|
586
|
+
@property
|
587
|
+
def slope_right(self):
|
588
|
+
return self._slope_right
|
589
|
+
|
590
|
+
@slope_left.setter
|
591
|
+
def slope_left(self, value):
|
592
|
+
self._slope_left = np.float64(value)
|
593
|
+
|
594
|
+
if self._slope_left == 99999.:
|
595
|
+
self._slope_left = (self.y[1] - self.y[0]) / (self.x[1] - self.x[0])
|
596
|
+
|
597
|
+
@slope_right.setter
|
598
|
+
def slope_right(self, value):
|
599
|
+
self._slope_right = np.float64(value)
|
600
|
+
|
601
|
+
if self._slope_right == 99999.:
|
602
|
+
self._slope_right = (self.y[-1] - self.y[-2]) / (self.x[-1] - self.x[-2])
|
603
|
+
|
604
|
+
def fit(self, forced_passage:np.ndarray, method:str = 'Nelder-Mead') -> np.ndarray:
|
605
|
+
""" Convert XY points into nbparts polynomial segments.
|
606
|
+
|
607
|
+
The segments are combined with sigmoids to ensure a smooth transition.
|
608
|
+
|
609
|
+
The routine must find the best transition points to minimize error.
|
610
|
+
"""
|
611
|
+
|
612
|
+
def error(coeffs, x, y, xx, yy, degree):
|
613
|
+
nbparts = len(xx) - 1
|
614
|
+
loc_coeffs = np.reshape(coeffs, (nbparts, degree+1))
|
615
|
+
return np.sum((y - piecewise_polynomial(x, xx, yy, loc_coeffs, self.scale, self.slope_left, self.slope_right))**2)
|
616
|
+
|
617
|
+
# Fit the polynomals coefficients
|
618
|
+
nbparts = forced_passage.shape[0] - 1
|
619
|
+
coeffs = np.zeros((nbparts, self.degree+1), dtype=np.float64)
|
620
|
+
coeffs[:,0] = 0.
|
621
|
+
coeffs[:,1] = 1.
|
622
|
+
|
623
|
+
self._parts_x = forced_passage[:,0]
|
624
|
+
self._parts_y = forced_passage[:,1]
|
625
|
+
|
626
|
+
ret = minimize(error, args=(self.x, self.y,
|
627
|
+
forced_passage[:,0], forced_passage[:,1],
|
628
|
+
self.degree), x0=coeffs.flatten(),
|
629
|
+
method=method, tol= 1.e-14)
|
630
|
+
|
631
|
+
self._poly_coeff = ret.x.reshape((nbparts, self.degree+1))
|
632
|
+
|
633
|
+
return self._poly_coeff
|
634
|
+
|
635
|
+
def fit_null_first_term(self, forced_passage:np.ndarray, method:str = 'Nelder-Mead') -> np.ndarray:
|
636
|
+
""" Convert XY points into nbparts polynomial segments.
|
637
|
+
|
638
|
+
The segments are combined with sigmoids to ensure a smooth transition.
|
639
|
+
|
640
|
+
The routine must find the best transition points to minimize error.
|
641
|
+
"""
|
642
|
+
|
643
|
+
def error(coeffs, x, y, xx, yy, degree):
|
644
|
+
nbparts = len(xx) - 1
|
645
|
+
loc_coeffs = np.zeros((nbparts, degree+1), dtype=np.float64).flatten()
|
646
|
+
loc_coeffs[1:] = coeffs
|
647
|
+
loc_coeffs = np.reshape(loc_coeffs, (nbparts, degree+1))
|
648
|
+
|
649
|
+
return np.sum((y - piecewise_polynomial(x, xx, yy, loc_coeffs, self.scale, self.slope_left, self.slope_right))**2)
|
650
|
+
|
651
|
+
# Fit the polynomals coefficients
|
652
|
+
nbparts = forced_passage.shape[0] - 1
|
653
|
+
coeffs = np.zeros((nbparts, self.degree+1), dtype=np.float64)
|
654
|
+
coeffs[:,1] = (self.y[-1] - self.y[0]) / (self.x[-1] - self.x[0])
|
655
|
+
|
656
|
+
self._parts_x = forced_passage[:,0]
|
657
|
+
self._parts_y = forced_passage[:,1]
|
658
|
+
|
659
|
+
ret = minimize(error, args=(self.x, self.y,
|
660
|
+
forced_passage[:,0], forced_passage[:,1],
|
661
|
+
self.degree), x0=coeffs.flatten()[1:],
|
662
|
+
method=method, tol= 1.e-14,
|
663
|
+
options={'maxiter': 100000})
|
664
|
+
|
665
|
+
self._poly_coeff = np.zeros((nbparts, self.degree+1), dtype=np.float64).flatten()
|
666
|
+
self._poly_coeff[1:] = ret.x
|
667
|
+
self._poly_coeff = self._poly_coeff.reshape((nbparts, self.degree+1))
|
668
|
+
|
669
|
+
return self._poly_coeff
|
670
|
+
|
671
|
+
def check_x(self):
|
672
|
+
diff = np.diff(self.x)
|
673
|
+
if np.any(diff <= 0):
|
674
|
+
raise ValueError('x should be in increasing order')
|
675
|
+
|
676
|
+
def check_y(self):
|
677
|
+
diff = np.diff(self.y)
|
678
|
+
if np.any(diff <= 0):
|
679
|
+
raise ValueError('y should be in increasing order')
|
680
|
+
|
681
|
+
def check(self):
|
682
|
+
self.check_x()
|
683
|
+
self.check_y()
|
684
|
+
|
685
|
+
return True
|
686
|
+
|
687
|
+
@property
|
688
|
+
def n(self):
|
689
|
+
""" Number of points """
|
690
|
+
return len(self.x)
|
691
|
+
|
692
|
+
@property
|
693
|
+
def size(self):
|
694
|
+
return self.n
|
695
|
+
|
696
|
+
def __call__(self, x, nb_elemt:int = -1) -> np.ndarray:
|
697
|
+
""" Evaluate the piecewise linear function at x
|
698
|
+
|
699
|
+
:param x: np.ndarray, list or float, x values to evaluate the function
|
700
|
+
:param nb_elemt: int, number of elements to consider around the x_candidate value
|
701
|
+
"""
|
702
|
+
|
703
|
+
if not self._checked_x:
|
704
|
+
self.check_x()
|
705
|
+
self._checked_x = True
|
706
|
+
|
707
|
+
if isinstance(x, np.ndarray):
|
708
|
+
return piecewise_polynomial(x.astype(np.float64), self._parts_x, self._parts_y, self._poly_coeff, self.scale, self._slope_left, self._slope_right)
|
709
|
+
elif isinstance(x, list):
|
710
|
+
return piecewise_polynomial(np.array(x, dtype=np.float64), self._parts_x, self._parts_y, self._poly_coeff, self.scale, self._slope_left, self._slope_right)
|
711
|
+
elif isinstance(x, float):
|
712
|
+
return piecewise_polynomial(np.array([x], dtype=np.float64), self._parts_x, self._parts_y, self._poly_coeff, self.scale, self._slope_left, self._slope_right)[0]
|
713
|
+
elif isinstance(x, int):
|
714
|
+
return piecewise_polynomial(np.array([float(x)], dtype=np.float64), self._parts_x, self._parts_y, self._poly_coeff, self.scale, self._slope_left, self._slope_right)[0]
|
715
|
+
else:
|
716
|
+
raise ValueError('x should be np.ndarray, list or float')
|
717
|
+
|
718
|
+
def inverse(self, y, nb_elemt:int = -1) -> np.ndarray:
|
719
|
+
""" Evaluate the inverse of the piecewise linear function at y """
|
720
|
+
|
721
|
+
logging.warning('Inverse function not implemented yet')
|
722
|
+
pass
|
723
|
+
|
724
|
+
def get_y(self, x, nb_elemt:int = -1):
|
725
|
+
""" Get the value of the piecewise linear function at x """
|
726
|
+
return self(x, nb_elemt)
|
727
|
+
|
728
|
+
def get_y_symmetry(self, x:np.ndarray, nb_elemt:int = -1):
|
729
|
+
|
730
|
+
idx_sup = np.where(x > self.x[-1])[0]
|
731
|
+
|
732
|
+
xloc = np.where(x <= self.x[-1], x, 2. * self.x[-1] - x)
|
733
|
+
yloc = self(xloc, nb_elemt)
|
734
|
+
|
735
|
+
if len(idx_sup) > 0:
|
736
|
+
yloc[idx_sup] = 2. * self.y[-1] - yloc[idx_sup]
|
737
|
+
|
738
|
+
return yloc
|
739
|
+
|
740
|
+
def get_x(self, y, nb_elemt:int = -1):
|
741
|
+
""" Get the inverse of the piecewise linear function at y """
|
742
|
+
return self.inverse(y, nb_elemt)
|
743
|
+
|
744
|
+
if __name__ == '__main__':
|
745
|
+
|
746
|
+
import matplotlib.pyplot as plt
|
747
|
+
|
748
|
+
test_sigmoid = sigmoid(0., 0, 10)
|
749
|
+
|
750
|
+
x = np.asarray([-2., 0, 1., 2., 4., 5.], dtype=np.float64)
|
751
|
+
y = np.asarray([0., 0., 2., 8., -2., 5.], dtype=np.float64)
|
752
|
+
x_test = np.linspace(-2, 6, 1000)
|
753
|
+
|
754
|
+
x = np.arange(1000)
|
755
|
+
y = np.sort(np.random.randn(1000))
|
756
|
+
|
757
|
+
newx, newy = extract_xy_binary_search(x_test, x, y, np.int64(100))
|
758
|
+
|
759
|
+
x_test = np.linspace(5, 1000, 100)
|
760
|
+
|
761
|
+
fig, ax = plt.subplots()
|
762
|
+
ax.plot(x, y, 'o')
|
763
|
+
|
764
|
+
for scale in [0.01,.1,10.,100.]:
|
765
|
+
# for scale in [100.]:
|
766
|
+
xy = Piecewise_Linear_Sigmoid(x, y, scale)
|
767
|
+
ax.plot(x_test, xy(x_test, nb_elemt= 5), label=f'scale={scale}')
|
768
|
+
# ax.plot(x_test, xy.gradient(x_test), label=f'gradient={scale}')
|
769
|
+
ax.plot(x_test, xy.gradient(x_test, True, nb_elemt= 5), label=f'gradient_approx={scale}')
|
770
|
+
|
771
|
+
ax.legend()
|
772
|
+
fig.show()
|
773
|
+
|
774
|
+
plt.show()
|
775
|
+
|
776
|
+
pass
|