py3toolset 1.2.18__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.
py3toolset/nmath.py ADDED
@@ -0,0 +1,419 @@
1
+ """
2
+ Math utility functions.
3
+ """
4
+
5
+ import re
6
+ import numpy as np
7
+
8
+
9
+
10
+ def str_isint(s, abs=True):
11
+ """
12
+ Tests if a ``str`` can be converted to a ``int``.
13
+
14
+ Args:
15
+ s: ``str``
16
+ character string to test.
17
+ abs: ``bool``
18
+ ``True`` (default) to match only positive/absolute value,
19
+ ``False`` otherwise (to match also a negative value).
20
+
21
+ Return:
22
+ ``True`` if ``str`` ``s`` matches a ``int`` ``False`` otherwise.
23
+
24
+ Examples:
25
+ >>> str_isint('10')
26
+ True
27
+ >>> str_isint('-5')
28
+ False
29
+ >>> str_isint('-5', abs=True)
30
+ False
31
+ >>> str_isint('-5', abs=False)
32
+ True
33
+ >>> str_isint('25', abs=True)
34
+ True
35
+ >>> str_isint('not_an_int')
36
+ False
37
+
38
+ .. seealso::
39
+ str_isfloat
40
+ """
41
+ if abs:
42
+ int_re = r"^[0-9]+$"
43
+ else:
44
+ int_re = r"^-?[0-9]+$"
45
+ return re.fullmatch(int_re, s) is not None
46
+
47
+
48
+ def str_isfloat(s, abs=True):
49
+ """
50
+ Tests if a ``str`` can be converted to a ``float``.
51
+
52
+ Args:
53
+ s: ``str``
54
+ character string to test.
55
+ abs: ``bool``
56
+ ``True`` (default) to match only positive/absolute value,
57
+ ``False`` otherwise (to match also a negative value).
58
+
59
+ Return:
60
+ ``True`` if ``str`` ``s`` matches a ``float`` ``False`` otherwise.
61
+ If ``str_isint(s) == True`` then ``str_isfloat(s) == True``.
62
+
63
+ Examples:
64
+ >>> str_isfloat('10.2')
65
+ True
66
+ >>> str_isfloat('10')
67
+ True
68
+ >>> str_isfloat('-5')
69
+ False
70
+ >>> str_isfloat('-5', abs=True)
71
+ False
72
+ >>> str_isfloat('-5', abs=False)
73
+ True
74
+ >>> str_isfloat('25', abs=True)
75
+ True
76
+ >>> str_isfloat('not_a_float')
77
+ False
78
+
79
+
80
+ .. seealso::
81
+ str_isint
82
+ """
83
+ # exclude first space case (because we don't use fullmatch below)
84
+ if re.match(r'.*\s.*', s):
85
+ return None
86
+ # the reason fullmatch is not used is negative numbers
87
+ if abs:
88
+ float_re = r"^([0-9]+)|([0-9]*\.[0-9]+)$"
89
+ else:
90
+ float_re = r"^-?([0-9]+)|([0-9]*\.[0-9]+)$"
91
+ return re.match(float_re, s) is not None
92
+
93
+
94
+ def get_locmaxs(xy_tuples):
95
+ """
96
+ Finds local maximums in ``xy_tuples``.
97
+
98
+ The first and last points cannot be local maximum.
99
+
100
+ Args:
101
+ xy_tuples: ``Sequence[tuple]``, numpy 2d array
102
+ Sequence of pairs ``(x, y)`` (the points).
103
+ If a numpy array, it must have two columns (``x``, ``y``), one row
104
+ per point.
105
+ The max points are searched on ``y`` dimension along ``x``
106
+ dimension.
107
+
108
+ Return:
109
+ The list of local max found.
110
+ If no max found, returns an empty list.
111
+
112
+ Examples:
113
+ >>> x1 = np.arange(10)
114
+ >>> pts = list(zip(x1, np.cos(x1)))
115
+ >>> get_locmaxs(pts)
116
+ [(np.int64(6), np.float64(0.960170286650366))]
117
+ >>> x2 = np.arange(100)
118
+ >>> more_pts = list(zip(x2, np.cos(x2)))
119
+ >>> np.array(get_locmaxs(more_pts))
120
+ array([[ 6. , 0.96017029],
121
+ [13. , 0.90744678],
122
+ [19. , 0.98870462],
123
+ [25. , 0.99120281],
124
+ [31. , 0.91474236],
125
+ [38. , 0.95507364],
126
+ [44. , 0.99984331],
127
+ [50. , 0.96496603],
128
+ [57. , 0.89986683],
129
+ [63. , 0.98589658],
130
+ [69. , 0.99339038],
131
+ [75. , 0.92175127],
132
+ [82. , 0.9496777 ],
133
+ [88. , 0.99937328],
134
+ [94. , 0.96945937]])
135
+ >>> # passing more points as a numpy array works the same
136
+ >>> np.array(get_locmaxs(np.array(more_pts)))
137
+ array([[ 6. , 0.96017029],
138
+ [13. , 0.90744678],
139
+ [19. , 0.98870462],
140
+ [25. , 0.99120281],
141
+ [31. , 0.91474236],
142
+ [38. , 0.95507364],
143
+ [44. , 0.99984331],
144
+ [50. , 0.96496603],
145
+ [57. , 0.89986683],
146
+ [63. , 0.98589658],
147
+ [69. , 0.99339038],
148
+ [75. , 0.92175127],
149
+ [82. , 0.9496777 ],
150
+ [88. , 0.99937328],
151
+ [94. , 0.96945937]])
152
+ >>> # to render as a figure
153
+ >>> import matplotlib.pyplot as plt
154
+ >>> more_pts = np.array(more_pts)
155
+ >>> max_pts = np.array(get_locmaxs(more_pts))
156
+ >>> plt.scatter(max_pts[:, 0], max_pts[:, 1]) # doctest: +ELLIPSIS
157
+ <matplotlib.collections.PathCollection object at ...>
158
+ >>> plt.plot(more_pts[:, 0], more_pts[:, 1]) # doctest: +ELLIPSIS
159
+ [<matplotlib.lines.Line2D object at ...]
160
+ >>> # plt.show() # uncomment to display
161
+
162
+ .. seealso::
163
+ :func:`.get_globmax`
164
+ """
165
+ locmaxs = []
166
+ # ignore first point (as a local max)
167
+ prev_y = xy_tuples[0][1] + 1
168
+ # the last point is also ignored
169
+ prev_x = -1
170
+ ascending = False
171
+ for x, y in xy_tuples:
172
+ if y > prev_y:
173
+ ascending = True
174
+ elif ascending:
175
+ locmaxs.append((prev_x, prev_y))
176
+ ascending = False
177
+ prev_x, prev_y = x, y
178
+ return locmaxs
179
+
180
+
181
+ _glob_max_meths = ['greatest_local', 'max']
182
+
183
+
184
+ def get_globmax(xy_tuples, meth='greatest_local'):
185
+ """
186
+ Finds the global maximum in ``xy_tuples``.
187
+
188
+ Args:
189
+
190
+ xy_tuples: ``Sequence[tuple]``, numpy 2d array
191
+ Sequence of pairs $(x, y)$ (the points).
192
+ If a numpy array, it must have two columns $(x, y)$, one row
193
+ per point.
194
+ The max is searched on $y$ dimension along $x$ dimension.
195
+
196
+ meth: ``str``
197
+
198
+ - ``'greatest_local'``: (default) the global maximum is the
199
+ greatest of local maximums (see :func:`.get_locmaxs`).
200
+ Hence, if no local maximum is found, it is considered that no
201
+ global maximum exists (the function returns None).
202
+
203
+ - ``'max'``: the function simply returns the max of
204
+ ``xy_tuples.``.
205
+
206
+ Return:
207
+ The global maximum or ``None`` if not found.
208
+
209
+ Example:
210
+ >>> x = np.arange(100)
211
+ >>> pts = list(zip(x, np.cos(x)))
212
+ >>> get_globmax(pts)
213
+ (np.int64(44), np.float64(0.9998433086476912))
214
+ >>> # this is the greatest local maximum
215
+ >>> loc_maxs = np.array(get_locmaxs(pts))
216
+ >>> loc_maxs[np.argmax(loc_maxs[:, 1])]
217
+ array([44. , 0.99984331])
218
+ >>> # situation with no local max (see get_locmaxs for a better
219
+ >>> # understanding)
220
+ >>> pts2 = list(zip(x.tolist(), np.linspace(1, 50, 100).tolist()))
221
+ >>> get_globmax(pts2) is None
222
+ True
223
+ >>> # but a max point exists necessarily
224
+ >>> get_globmax(pts2, meth='max')
225
+ (99, 50.0)
226
+
227
+
228
+ .. seealso::
229
+ :func:`.get_locmaxs`
230
+
231
+ """
232
+ if meth not in _glob_max_meths:
233
+ raise ValueError(str(meth)+' is not known. Valid meth values:' +
234
+ _glob_max_meths)
235
+ # method 'greatest_local' (not optimal but for historical needs)
236
+ loc_maxs = get_locmaxs(xy_tuples)
237
+ loc_maxs.sort(key=lambda t: t[1], reverse=True)
238
+ if len(loc_maxs) > 0:
239
+ return loc_maxs[0]
240
+ if meth == 'max':
241
+ # return max(xy_tuples, key=lambda t: t[1])
242
+ return xy_tuples[np.argmax(np.array(xy_tuples)[:, 1])]
243
+ # tmp = list(xy_tuples)
244
+ # tmp.sort(key=lambda t:t[1], reverse=True)
245
+ # return tmp[0]
246
+ return None
247
+
248
+
249
+ _interpoline_meths = ['slope', 'barycenter']
250
+
251
+
252
+ def interpoline(x1, y1, x2, y2, x, meth='slope'):
253
+ """
254
+ Interpolates linearly (``x1``, ``y1``), (``x2``, ``y2``) at value ``x``.
255
+
256
+ Return:
257
+ $f(x) = ax + b$ such that $f(x_1) = y_1$, $f(x_2) = y_2$.
258
+
259
+ Example:
260
+ >>> x1 = 10
261
+ >>> x2 = 20
262
+ >>> y1 = 18
263
+ >>> y2 = 5
264
+ >>> x = 15
265
+ >>> y = interpoline(x1, y1, x2, y2, x, meth='slope')
266
+ >>> y
267
+ 11.5
268
+ >>> interpoline(x1, y1, x2, y2, x, meth='barycenter')
269
+ 11.5
270
+ >>> # plot the line and point (x, f(x))
271
+ >>> import matplotlib.pyplot as plt
272
+ >>> plt.plot([x1, x2], [y1, y2], marker='+') # doctest: +ELLIPSIS
273
+ [...
274
+ >>> plt.scatter([x], [y]) # doctest: +ELLIPSIS
275
+ <...
276
+ >>> # plt.show() # uncomment to display
277
+
278
+ """
279
+ if meth == 'slope':
280
+ a = (y2 - y1) / (x2 - x1)
281
+ b = y1 - a * x1
282
+ return a * x + b
283
+ elif meth == 'barycenter':
284
+ t = (x - x1) / abs(x2 - x1)
285
+ return (1 - t) * y1 + t * y2
286
+ else:
287
+ raise ValueError(str(meth) + ' is not a valid method: ' +
288
+ _interpoline_meths)
289
+
290
+
291
+ _mv_avg_meths = ['basic', 'cumsum', 'convolve']
292
+
293
+
294
+ def calc_moving_average_list(xy_tuples, window_sz, meth='basic', x_start=None):
295
+ """
296
+ Computes the moving average of ``xy_tuples``.
297
+
298
+ The average is computed for the y-dimension, 2nd column.
299
+
300
+ Args:
301
+ xy_tuples: ``Sequence[tuple]``, numpy 2d array
302
+ Sequence of pairs ``(x, y)`` (the points).
303
+ If a numpy array, it must have two columns (``x``, ``y``), one row
304
+ per point.
305
+ window_sz: ``int``
306
+ The window size for average.
307
+ meth: ``str``
308
+ - 'basic': manual default method.
309
+ - 'cumsum': use numpy.cumsum.
310
+ - 'convolve': use the convolution trick.
311
+ x_start: ``int``
312
+ xy_tuples[x_start][0] is the first element of the x-dimension
313
+ average/output.
314
+ Default is ``None`` for ``x_start = int(window_sz) // 2``.
315
+ In other words, the mean of ``xy_tuples[:window_sz]`` is the first
316
+ element of the moving average and its x coordinate is
317
+ ``xy_tuples[x_start][0]``. The next x-coordinates of the moving
318
+ average are:
319
+ ``xy_tuples[x_start+1][0], xy_tuples[x_start+2][0],`` ...
320
+
321
+ Return:
322
+ Moving average as a list of tuples (x, y).
323
+
324
+ Examples:
325
+ >>> import numpy as np
326
+ >>> xy = list(zip(np.arange(8).tolist(), np.arange(10, 17).tolist()))
327
+ >>> xy = np.round(xy, decimals=3).tolist()
328
+ >>> xy
329
+ [[0, 10], [1, 11], [2, 12], [3, 13], [4, 14], [5, 15], [6, 16]]
330
+ >>> calc_moving_average_list(xy, 5)
331
+ [(2, np.float64(12.0)), (3, np.float64(13.0)), (4, np.float64(14.0))]
332
+ >>> calc_moving_average_list(xy, 5, meth='cumsum')
333
+ [[2.0, 12.0], [3.0, 13.0], [4.0, 14.0]]
334
+ >>> calc_moving_average_list(xy, 5, x_start=3)
335
+ [(3, np.float64(12.0)), (4, np.float64(13.0)), (5, np.float64(14.0))]
336
+ >>> calc_moving_average_list(xy, 5, meth='convolve')
337
+ [[2.0, 12.0], [3.0, 13.0], [4.0, 14.0]]
338
+ """
339
+ if x_start is None:
340
+ x_start = int(window_sz) // 2
341
+ x_offset = x_start
342
+ if meth == 'basic':
343
+ i = 0
344
+ window_amps = np.zeros((window_sz))
345
+ mvavg = []
346
+ avg_amp = 0
347
+ for j, (t, amp) in enumerate(xy_tuples):
348
+ window_amps[i] = amp
349
+ i += 1
350
+ if j >= window_sz - 1:
351
+ avg_amp = np.mean(window_amps)
352
+ mvavg.append((xy_tuples[x_offset][0], avg_amp))
353
+ x_offset += 1
354
+ i %= window_sz
355
+ return mvavg
356
+ # below are two variants of the code found here:
357
+ # https://stackoverflow.com/questions/14313510/how-to-calculate-rolling-
358
+ # moving-average-using-python-numpy-scipy#14314054
359
+ elif meth == 'cumsum':
360
+ sig = np.array([xy[1] for xy in xy_tuples])
361
+ x = np.array([xy[0] for xy in xy_tuples])
362
+ n = window_sz
363
+ avg = np.cumsum(sig)
364
+ avg[n:] = avg[n:] - avg[:-n]
365
+ return np.hstack((
366
+ x.reshape(-1, 1)[x_offset:len(avg) - n + x_offset + 1],
367
+ (avg[n-1:] / n).reshape(-1, 1)
368
+ )).tolist()
369
+ elif meth == 'convolve':
370
+ sig = np.array([xy[1] for xy in xy_tuples])
371
+ x = np.array([xy[0] for xy in xy_tuples])
372
+ avg = np.convolve(sig, np.ones(window_sz), 'valid') / window_sz
373
+ return np.hstack((
374
+ x.reshape(-1, 1)[x_offset:len(avg) + x_offset],
375
+ avg.reshape(-1, 1)
376
+ )).tolist()
377
+ else:
378
+ raise ValueError(str(meth) + ' is not a valid method: ' +
379
+ _mv_avg_meths)
380
+
381
+
382
+ def zeropad(ref_num, nums):
383
+ """
384
+ Pads/Prefixes nums with zeros to match the number of digits of ref_num.
385
+
386
+ Args:
387
+ ref_num: an integer.
388
+ nums: integer or str list.
389
+
390
+ Return:
391
+ - if nums is a sequence returns a list of zero-padded int-s (as str).
392
+ - if nums is a int returns a zero-padded int (as str).
393
+
394
+ Example:
395
+ >>> zeropad(96, 0)
396
+ '00'
397
+ >>> zeropad(96, [0, 1, 2, 3])
398
+ ['00', '01', '02', '03']
399
+ >>> zeropad(96, list(range(4)))
400
+ ['00', '01', '02', '03']
401
+
402
+ """
403
+ zpadded_nums = []
404
+ nums_is_seq = True
405
+ if not hasattr(nums, '__len__'):
406
+ nums = [nums]
407
+ nums_is_seq = False
408
+ for n in nums:
409
+ sn = str(n)
410
+ if not str_isint(sn):
411
+ raise ValueError('nums must be int or sequence of int')
412
+ dl = len(str(ref_num)) - len(sn)
413
+ if dl < 0:
414
+ raise ValueError('The reference number is smaller than the'
415
+ ' integers to pad with zeros')
416
+ zpadded_nums += [('0'*dl)+sn] # equiv. to sn.zfill(len(ref_num))
417
+ if len(zpadded_nums) == 1 and not nums_is_seq:
418
+ return zpadded_nums[0]
419
+ return zpadded_nums
py3toolset/setup.py ADDED
@@ -0,0 +1,32 @@
1
+ from setuptools import setup
2
+ from os import environ
3
+
4
+ version = '1.0.0'
5
+ if 'VERSION' in environ.keys():
6
+ version = environ['VERSION']
7
+
8
+ doc_url = 'https://neemgrp.univ-nantes.io/py3toolset/html'
9
+ api_doc_url = doc_url + '/api.html'
10
+ mods = ['bash_autocomp', 'cmd_interact', 'dep', 'file_backup',
11
+ 'fs', 'nmath', 'tuple_file', 'txt_color']
12
+ nmods = len(mods)
13
+ mod_links = [f'[{mod}]({api_doc_url}#module-{imod})' for imod, mod in enumerate(mods)]
14
+
15
+ setup(
16
+ name='py3toolset',
17
+ version=version,
18
+ packages=['py3toolset'],
19
+ url='',
20
+ description='Python utility modules.',
21
+ classifiers=['License :: OSI Approved :: BSD License',
22
+ 'Programming Language :: Python :: 3',
23
+ 'Topic :: Software Development'],
24
+ install_requires=['numpy>=2'],
25
+ package_data={'py3toolset': ['LICENSE.md']},
26
+ license="3-clause BSD 2.0",
27
+ long_description_content_type='text/markdown',
28
+ long_description= f"""Python collection of utility modules: {", ".join(mod_links)}...
29
+
30
+ This package software engineering has been funded by the [Laboratoire de Planétologie et Géosciences](https://lpg-umr6112.fr/), Nantes (France).
31
+ """
32
+ )
@@ -0,0 +1,104 @@
1
+ """
2
+ Module for conversion of list of tuples <-> text file.
3
+
4
+ This module is mostly to use to avoid:
5
+
6
+ - use of ``numpy.savetxt``/``loadtxt``,
7
+ - and NumPy array to list-of-tuples conversion.
8
+ """
9
+ from os.path import exists
10
+
11
+
12
+ def tuples2file(tuple_list, tfile, format_str='', overwrite=False):
13
+ """
14
+ Converts ``tuple_list`` into a text file ``tfile``.
15
+
16
+ Each tuple occupies one line.
17
+ One column is added in file for each tuple element.
18
+
19
+ For example, the tuple list [(x1,y1,z1,w1), (x2,y2,z2,w2)]
20
+ is converted to the following file lines:
21
+ x1 y1 z1 w1
22
+ x2 y2 z2 w2
23
+
24
+ Args:
25
+ tuple_list: ``list[tuple]``
26
+ The list of tuples to write as in file.
27
+ tfile: ``str``
28
+ Output filepath.
29
+ format_str: ``str``
30
+ if specified is used as a line format string to the data in tuple.
31
+ See example below.
32
+
33
+ .. warning::
34
+ The function doesn't check the format string validity.
35
+ overwrite: ``bool``
36
+ - ``True``: a preexisting file is overwritten.
37
+ - ``False`` (default): if file preexists an exception is raised.
38
+
39
+ Example:
40
+ >>> import os
41
+ >>> from random import randint
42
+ >>> rand_suffix = str(randint(1, 2**10))
43
+ >>> output = "t2f_out-" + rand_suffix
44
+ >>> xy_tuples = [(1, 456.), (2, 789), (3, 111213)]
45
+ >>> tuples2file(xy_tuples, output, format_str="{0:1d}={1:3.1f}")
46
+ >>> # output is then:
47
+ >>> f = open(output)
48
+ >>> for line in f.readlines(): print(line, end='')
49
+ 1=456.0
50
+ 2=789.0
51
+ 3=111213.0
52
+ >>> f.close()
53
+ >>> # delete file
54
+ >>> os.unlink(output)
55
+
56
+ """
57
+ if exists(tfile) and not overwrite:
58
+ raise Exception("Error tuples2file(): file "+tfile+" already exists.")
59
+ f = open(tfile, "w")
60
+ for t in tuple_list:
61
+ if format_str:
62
+ if not format_str.endswith("\n"):
63
+ format_str += "\n"
64
+ f.write(str(format_str).format(*t))
65
+ else:
66
+ f.write(" ".join([str(e) for e in t])+"\n")
67
+ f.close()
68
+
69
+
70
+ def file2tuples(tfile, tuple_list=None):
71
+ """
72
+ Converts a file ``tfile`` organized in columns to a list of tuples.
73
+
74
+ Returns a list of tuples (one tuple per line of file).
75
+ The elements in file as taken as float numbers.
76
+
77
+ Args:
78
+ tfile: ``str``
79
+ input filepath.
80
+ tuple_list: ``list[tuple]``
81
+ The output list of tuples. Defaultly ``None``.
82
+
83
+ Return:
84
+ tuple list corresponding to ``tfile`` input.
85
+
86
+ Example:
87
+ >>> import os
88
+ >>> from random import randint
89
+ >>> rand_suffix = str(randint(1, 2**10))
90
+ >>> output = "t2f_out-" + rand_suffix
91
+ >>> in_tuples = [(1, 456.), (2, 789), (3, 111213)]
92
+ >>> tuples2file(in_tuples, output)
93
+ >>> # output is then:
94
+ >>> out_tuples = file2tuples(output)
95
+ >>> print(out_tuples)
96
+ [(1.0, 456.0), (2.0, 789.0), (3.0, 111213.0)]
97
+ """
98
+ if tuple_list is None:
99
+ tuple_list = []
100
+ f = open(tfile)
101
+ for line in f:
102
+ tuple_list.append(tuple([float(n) for n in line.strip().split()]))
103
+ f.close()
104
+ return tuple_list