SignalForge 0.0.2__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.
SignalForge/utils.py ADDED
@@ -0,0 +1,526 @@
1
+ import numpy as np
2
+ import scipy as sp
3
+ from tqdm import tqdm
4
+ import pyNNST
5
+ from statsmodels.tsa.stattools import kpss, adfuller
6
+
7
+
8
+ def method_existance_check(method:str, methods:dict):
9
+ """
10
+ Check if a method is in a dictionary of available options.
11
+
12
+ Parameters:
13
+ method (str): The method name to check.
14
+ methods (dict): A dictionary of available methods.
15
+
16
+ Raises:
17
+ KeyError: If method is not in the dictionary keys.
18
+ """
19
+ if method not in methods.keys():
20
+ options = ', '.join(f'"{key}"' for key in methods.keys())
21
+ raise KeyError(f'Index {method} not supported. Try with: {options}')
22
+ # raise KeyError(f'Index {method} not supported. Try with: {[f'"{key}"' for key in methods.keys()]}')
23
+
24
+ def step_interp(x, xp, yp):
25
+ """
26
+ Step interpolation of `yp` over `xp`, evaluated at `x`.
27
+
28
+ Parameters:
29
+ x (array-like): Points at which to evaluate the interpolated values.
30
+ xp (array-like): Known x-coordinates of data points.
31
+ yp (array-like): Known y-values at each xp.
32
+
33
+ Returns:
34
+ np.ndarray: Interpolated values at `x`.
35
+ """
36
+ xp = np.asarray(xp)
37
+ yp = np.asarray(yp)
38
+ x_mid = (xp[:-1] + xp[1:]) / 2
39
+ x_extended = np.concatenate([[xp[0]], x_mid, [xp[-1]]])
40
+ y_extended = np.concatenate([[yp[0]], yp[1:], [yp[-1]]])
41
+ idx = np.searchsorted(x_extended, x, side='right') - 1
42
+ y = y_extended
43
+ idx = np.clip(idx, 0, len(yp)-1)
44
+ return y[idx]
45
+
46
+ def lin_interp_psd(fpsd:np.ndarray, psd:np.ndarray, n_points:int, fs = None):
47
+ """
48
+ Linear interpolation of a PSD to a uniformly spaced frequency grid.
49
+
50
+ Parameters:
51
+ fpsd (array-like): Original frequency vector.
52
+ psd (array-like): Original PSD values.
53
+ n_points (int): Number of points in the interpolated frequency vector.
54
+
55
+ Returns:
56
+ tuple: (interpolated frequency vector, interpolated PSD)
57
+ """
58
+ if fs is None:
59
+ fs = fpsd[-1] * 2
60
+ new_fpsd = np.linspace(0,1,round(n_points))*fs/2
61
+ new_psd = np.interp(new_fpsd, fpsd, psd, left=0, right=0)
62
+ new_psd[0] = 0
63
+ return new_fpsd, new_psd
64
+
65
+ def log_interp_psd(fpsd, psd, n_points, fs = None):
66
+ """
67
+ Log-log interpolation of a PSD to a uniformly spaced frequency grid.
68
+
69
+ Parameters:
70
+ fpsd (array-like): Original frequency vector.
71
+ psd (array-like): Original PSD values.
72
+ n_points (int): Number of points in the interpolated frequency vector.
73
+
74
+ Returns:
75
+ tuple: (interpolated frequency vector, interpolated PSD)
76
+ """
77
+ if fs is None:
78
+ fs = fpsd[-1] * 2
79
+ psd = psd.astype('float')
80
+ psd[psd<=0]= 1e-30
81
+ new_fpsd = np.linspace(0,1,round(n_points))*fs/2
82
+ new_psd = np.interp(np.log10(new_fpsd), np.log10(fpsd), np.log10(psd), left=-30, right=-30)
83
+ new_psd = 10**new_psd
84
+ new_psd[new_psd<=1e-30]= 0
85
+ new_psd[0] = 0
86
+ return new_fpsd, new_psd
87
+
88
+ def get_stat_mom(var, probability, order):
89
+ """
90
+ Compute statistical moment of a given order from a PDF.
91
+
92
+ Parameters:
93
+ var (np.ndarray): Grid of variable values.
94
+ probability (np.ndarray): PDF values corresponding to `var`.
95
+ order (int): Moment order.
96
+
97
+ Returns:
98
+ float: Statistical moment of the given order.
99
+ """
100
+ return np.trapezoid(probability*var**order,var)
101
+
102
+ def alpha_spec_index(fpsd, psd, order, deriv_order):
103
+ """
104
+ Compute the spectral index (alpha) of a process.
105
+
106
+ Parameters:
107
+ fpsd (np.ndarray): Frequency vector.
108
+ psd (np.ndarray): Power spectral density.
109
+ order (int): Order of the moment.
110
+ deriv_order (int): Derivative order.
111
+
112
+ Returns:
113
+ float: Spectral index.
114
+ """
115
+ m2n_x = get_stat_mom(fpsd, psd, 2*deriv_order+order)
116
+ m2n = get_stat_mom(fpsd, psd, 2*deriv_order)
117
+ m2n_2x = get_stat_mom(fpsd, psd, 2*(deriv_order+order))
118
+ return m2n_x/np.sqrt(m2n*m2n_2x)
119
+
120
+ def get_psd_impulse_response(fpsd,psd, N, fs = None):
121
+ """
122
+ Compute an impulse response from a target PSD.
123
+
124
+ Parameters:
125
+ fpsd (np.ndarray): Frequency vector.
126
+ psd (np.ndarray): PSD values.
127
+ N (int): Number of samples in the impulse response.
128
+
129
+ Returns:
130
+ np.ndarray: Time-domain impulse response.
131
+ """
132
+ if fs is None:
133
+ fs = fpsd[-1] * 2
134
+ N_os = (N // 2 + 1)
135
+ _, psd_interp = lin_interp_psd(fpsd, psd,fs, N_os)
136
+ T = N/(fs)
137
+ freq_filter = psd_interp*T*fpsd[1]
138
+ h_t = np.fft.irfft((np.sqrt(freq_filter)), n = N)*fs
139
+ h_t = np.roll(h_t, N//2) # circular shift to make the impulse centred
140
+ return h_t
141
+
142
+ def get_stat_history(signal:np.ndarray, winsize:int, olap:int = 0, idx_type = 'rms'):
143
+ """
144
+ Compute a statistical index over a moving window.
145
+
146
+ Parameters:
147
+ signal : np.ndarray
148
+ Time history signal.
149
+ winsize : int:
150
+ Window size.
151
+ olap : int, optional
152
+ The number of points to overlap on the moving window. If not specified, no overlap is considered
153
+ idx_type : str
154
+ Index type: 'mean', 'rms', 'var', or 'kurtosis'.
155
+
156
+ Returns:
157
+ stat_history: np.ndarray
158
+ Evolution of the statistical index over time.
159
+
160
+ """
161
+ idx_types = {'mean':np.mean,
162
+ 'rms':np.std,
163
+ 'var':np.var,
164
+ 'kurtosis':sp.stats.kurtosis}
165
+
166
+ method_existance_check(idx_type, idx_types)
167
+
168
+ winsize = int(np.round(winsize))
169
+ olap = int(np.round(olap))
170
+ step = winsize-olap
171
+
172
+ idx_history = np.zeros((len(signal) - winsize + 1)//(step)+1)
173
+
174
+ print(f'Calculating {idx_type} evolution')
175
+ k = 0
176
+ for i in tqdm(range(0, len(signal) - winsize + 1, step)):
177
+ index = idx_types[idx_type](signal[i : i + winsize])
178
+ idx_history[k] = index
179
+ k += 1
180
+ if 'kurtosis' in idx_type:
181
+ idx_history = idx_history+3
182
+ return idx_history
183
+
184
+ def get_nnst_index(signal, nperseg = 100, noverlap = 0, *args, **kwargs):
185
+ """
186
+ Non-stationarity index identification using modified run-test.
187
+ Standard deviation of entire signal is compared to standard deviation of segmented signal,
188
+ and the number of variations (i.e., runs) is compared to expected value of variations to obtain
189
+ the non-stationarity index.
190
+
191
+ For the complete documentation please refer to: https://github.com/LolloCappo/pyNNST
192
+
193
+ Parameters
194
+ ----------
195
+ signal: array-like
196
+ One dimensional time history
197
+ nperseg : int
198
+ Length of each segment
199
+
200
+ noverlap: int, optional
201
+ Number of points to overlap between segments. If None,
202
+ "noverlap = 0". Defaults to None.
203
+
204
+ confidence: int, optional
205
+ Confidence interval [90-95-98-99] %. If None,
206
+ "confidence = 95". Defaults to None.
207
+
208
+ *args, **kwargs: optional
209
+ Additional arguments for the method
210
+
211
+ Returns
212
+ -------
213
+ test_results: dict
214
+ Results of the non-stationary test
215
+
216
+ Source
217
+ ------
218
+ L. Capponi, M. Česnik, J. Slavič, F. Cianetti, e M. Boltežar, «Non-stationarity index in vibration fatigue: Theoretical and experimental research», Int. J. Fatigue, vol. 104, pp. 221–230, nov. 2017, doi: 10.1016/j.ijfatigue.2017.07.020.
219
+ """
220
+ test = pyNNST.nnst(signal, nperseg = nperseg, noverlap=noverlap, *args, **kwargs)
221
+ test.idns() # perform index assessment
222
+ test_results = {
223
+ 'outcome': test.get_outcome(),
224
+ 'test': test.get_index(),
225
+ 'winsize': test.nperseg,
226
+ 'olap': test.noverlap,
227
+ 'confidence': test.confidence,
228
+ }
229
+ return test_results
230
+
231
+ def get_adf_index(signal, maxlag=None, regression='c', autolag='AIC', *args, **kwargs):
232
+ """
233
+ Augmented Dickey-Fuller unit root test from the statsmodels package.
234
+
235
+ The Augmented Dickey-Fuller test can be used to test for a unit root in a univariate process in the presence of serial correlation.
236
+
237
+ For the complete documentation please refer to: https://www.statsmodels.org/stable/generated/statsmodels.tsa.stattools.adfuller.html#statsmodels.tsa.stattools.adfuller
238
+
239
+ Parameters
240
+ ----------
241
+ signal : array_like, 1d
242
+ The data series to test.
243
+ maxlag : {None, int}
244
+ Maximum lag which is included in test, default value of
245
+ 12*(nobs/100)^{1/4} is used when ``None``.
246
+ regression : {"c","ct","ctt","n"}
247
+ Constant and trend order to include in regression.
248
+
249
+ * "c" : constant only (default).
250
+ * "ct" : constant and trend.
251
+ * "ctt" : constant, and linear and quadratic trend.
252
+ * "n" : no constant, no trend.
253
+
254
+ autolag : {"AIC", "BIC", "t-stat", None}
255
+ Method to use when automatically determining the lag length among the
256
+ values 0, 1, ..., maxlag.
257
+
258
+ * If "AIC" (default) or "BIC", then the number of lags is chosen to minimize the corresponding information criterion.
259
+ * "t-stat" based choice of maxlag. Starts with maxlag and drops a lag until the t-statistic on the last lag length is significant using a 5%-sized test.
260
+ * If None, then the number of included lags is set to maxlag.
261
+
262
+ *args, **kwargs : optional
263
+ Additional arguments for the method
264
+
265
+ Returns
266
+ -------
267
+ test_results : dict
268
+ Dictionary containing the non-stationarity test results:
269
+ 'outcome', 'test', 'p-value', 'lag', 'crit_values'
270
+
271
+ Source
272
+ ------
273
+ Dickey, D. A., and W. A. Fuller. "Distribution of the Estimators for Autoregressive Time Series with a Unit Root." Journal of the American Statistical Association. Vol. 74, 1979, pp. 427–431.
274
+ """
275
+ test = adfuller(signal, maxlag, regression, autolag, *args, **kwargs)
276
+ if test[1] < 0.05: # p-value
277
+ outcome = 'Stationary'
278
+ else:
279
+ outcome = 'Non-stationary'
280
+ test_results = {
281
+ 'outcome': outcome,
282
+ 'test': test[0],
283
+ 'p-value': test[1],
284
+ 'lag': test[2],
285
+ 'crit_values': test[4]
286
+ }
287
+ return test_results
288
+
289
+ def get_kpss_index(signal, regression = 'c', nlags = 'auto', *args, **kwargs):
290
+ """
291
+ Kwiatkowski-Phillips-Schmidt-Shin test for stationarity.
292
+
293
+ Computes the Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test for the null
294
+ hypothesis that x is level or trend stationary.
295
+
296
+ For the complete documentation please refer to: https://www.statsmodels.org/stable/generated/statsmodels.tsa.stattools.kpss.html#statsmodels.tsa.stattools.kpss
297
+
298
+ Parameters
299
+ ----------
300
+ signal : array_like, 1d
301
+ The data series to test.
302
+ regression : str{"c", "ct"}
303
+ The null hypothesis for the KPSS test.
304
+
305
+ * "c" : The data is stationary around a constant (default).
306
+ * "ct" : The data is stationary around a trend.
307
+
308
+ nlags : {str, int}, optional
309
+ Indicates the number of lags to be used. If "auto" (default), lags
310
+ is calculated using the data-dependent method of Hobijn et al. (1998).
311
+ See also Andrews (1991), Newey & West (1994), and Schwert (1989). If
312
+ set to "legacy", uses int(12 * (n / 100)**(1 / 4)) , as outlined in
313
+ Schwert (1989).
314
+ *args, **kwargs : optional
315
+ Additional arguments for the method
316
+
317
+ Returns
318
+ -------
319
+ test_results : dict
320
+ Dictionary containing the non-stationarity test results:
321
+ 'outcome', 'test', 'p-value', 'lag', 'crit_values'
322
+
323
+ Source
324
+ ------
325
+ Kwiatkowski, D., Phillips, P. C. B., Schmidt, P., Shin, Y. (1992). "Testing the null hypothesis of stationarity against the alternative of a unit root". Journal of Econometrics. 54 (1–3): 159–178. doi:10.1016/0304-4076(92)90104-Y.
326
+ """
327
+ test = kpss(signal, regression=regression, nlags=nlags, *args, **kwargs)
328
+ if test[1] < 0.05: # p-value
329
+ outcome = 'Non-stationary'
330
+ else:
331
+ outcome = 'Stationary'
332
+
333
+ test_results = {
334
+ 'outcome': outcome,
335
+ 'test': test[0],
336
+ 'p-value': test[1],
337
+ 'lag': test[2],
338
+ 'crit_values': test[3]
339
+ }
340
+ return test_results
341
+
342
+ def print_nonstat_results(ns_test, indent=0):
343
+ for key, value in ns_test.items():
344
+ print(' ' * indent + str(key) + ':\t', end=' ')
345
+ if isinstance(value, dict):
346
+ print()
347
+ print_nonstat_results(value, indent + 4)
348
+ else:
349
+ print(value)
350
+ pass
351
+
352
+ def get_banded_spectral_kurtosis(
353
+ signal: np.ndarray,
354
+ dt:float,
355
+ n_bins:int = 10,
356
+ fl : int = 0,
357
+ fu : int = 0
358
+ ):# * GCurti's theory
359
+ """
360
+ Compute spectral kurtosis in frequency bands.
361
+
362
+ Parameters:
363
+ signal (np.ndarray): Input time history.
364
+ dt (float): Sampling interval.
365
+ n_bins (int): Number of frequency bands.
366
+ fl (float): Lower frequency limit.
367
+ fu (float): Upper frequency limit (default is Nyquist).
368
+
369
+ Returns:
370
+ tuple: (center frequencies, spectral kurtosis values)
371
+ """
372
+ f_ny = 1/2/dt
373
+ if fu <= fl: fu = f_ny
374
+ if not isinstance(n_bins, int):
375
+ n_bins = round(n_bins)
376
+ Nos = np.ceil(len(signal)/2)
377
+ Xts_full_woShift = dt*np.fft.fft(signal)
378
+ Xos_full = Xts_full_woShift[0:int(Nos)]
379
+ fper = np.linspace(0, f_ny, len(Xos_full))
380
+ N = len(Xts_full_woShift)
381
+ binsize = (fu-fl) / n_bins
382
+ fvec = np.arange(n_bins) * binsize + fl + binsize/ 2
383
+ SK = np.zeros(n_bins)
384
+ print('Evaluating kurtosis on frequency bands:')
385
+ for i in tqdm(np.arange(n_bins)):
386
+ Xos = np.zeros(len(Xos_full), dtype=np.complex128)
387
+ Xos[(fper>=(fl+binsize*i)) & (fper<=(fl+binsize*(i+1)))] = Xos_full[(fper>=(fl+binsize*i)) & (fper<=(fl+binsize*(i+1)))]
388
+ x = np.fft.irfft(Xos,n = N)/dt
389
+ SK[i] = sp.stats.kurtosis(x)+3
390
+ return fvec, SK
391
+
392
+ def get_welch_spectral_kurtosis(x:np.ndarray, Nfft:int, noverlap:int = None):
393
+ """
394
+ Compute Welch-based spectral kurtosis.
395
+
396
+ Parameters:
397
+ x (np.ndarray): Input signal.
398
+ Nfft (int): FFT size.
399
+ noverlap (int): Number of overlapping samples.
400
+
401
+ Returns:
402
+ tuple: (normalized frequency vector, spectral kurtosis)
403
+
404
+ Source:
405
+ J. Antoni, «The spectral kurtosis: a useful tool for characterising non-stationary signals», Mech. Syst. Signal Process., vol. 20, fasc. 2, pp. 282–307, feb. 2006, doi: 10.1016/j.ymssp.2004.09.001.
406
+ """
407
+ if noverlap is None:
408
+ noverlap = Nfft//2
409
+
410
+ # Convert scalar window input to actual window
411
+ Window = sp.signal.windows.hann(Nfft//2)
412
+
413
+ Window = np.asarray(Window).flatten()
414
+ Window = Window / np.linalg.norm(Window) # normalize window
415
+
416
+ x = np.asarray(x)
417
+ n = len(x)
418
+ nwind = len(Window)
419
+
420
+ if nwind <= noverlap:
421
+ raise ValueError('Window length must be > noverlap')
422
+ if Nfft < nwind:
423
+ raise ValueError('Window length must be <= Nfft')
424
+
425
+ step = nwind - noverlap
426
+ k = (n - noverlap) // step # number of windows
427
+
428
+ M4 = np.zeros(Nfft)
429
+ M2 = np.zeros(Nfft)
430
+
431
+ for i in range(k):
432
+ start = i * step
433
+ xw = Window * x[start:start + nwind]
434
+ Xw = np.fft.fft(xw, Nfft)
435
+ absX = np.abs(Xw)
436
+ M4 += absX**4
437
+ M2 += absX**2
438
+
439
+ M4 /= k
440
+ M2 /= k
441
+ SK = M4 / (M2**2) - 2
442
+
443
+ # Bias correction
444
+ W = np.abs(np.fft.fft(Window**2, Nfft))**2
445
+ Wb = np.array([W[(2*i) % Nfft] / W[0] for i in range(Nfft)])
446
+ SK -= Wb
447
+
448
+ f_norm = np.arange(Nfft) / Nfft
449
+
450
+ return f_norm[:Nfft//2], SK[:Nfft//2]
451
+
452
+ def get_stationary_gaussian(fpsd:np.ndarray, psd:np.ndarray, T:float, fs:float = None, seed = None, interp:str = 'lin'):
453
+ """
454
+ Generate a stationary Gaussian signal with a target PSD.
455
+
456
+ Parameters:
457
+ fpsd (np.ndarray): Frequency vector.
458
+ psd (np.ndarray): Power spectral density values.
459
+ T (float): Duration [s].
460
+ seed (int, optional): Random seed for reproducibility.
461
+
462
+ Returns:
463
+ tuple: (time vector, generated signal)
464
+
465
+ Source:
466
+ D. E. Newland, An introduction to random vibrations, spectral & wavelet analysis, 3. ed., Unabridged republ. Mineola, NY: Dover Publ, 2005
467
+ """
468
+ fpsd = np.array(fpsd)
469
+ psd = np.array(psd)
470
+
471
+ psd[np.isnan(psd)] = 0
472
+
473
+ if fs is None:
474
+ fs = fpsd[-1] * 2
475
+
476
+ N = int(fs * T)
477
+
478
+ N_os = N // 2 + 1
479
+
480
+ # Ensure zero-frequency component exists
481
+ # if fpsd[0] != 0:
482
+ # fpsd = np.concatenate(([0], fpsd))
483
+ # fpsd = np.concatenate(([0], psd))
484
+
485
+ # fvec = np.linspace(0, fs // 2, N_os)
486
+ mue2 = np.trapezoid(psd, fpsd)
487
+
488
+ # Interpolate PSD onto frequency vector
489
+ interpolators ={
490
+ 'lin':lin_interp_psd,
491
+ 'log':log_interp_psd
492
+ }
493
+ method_existance_check(interp, interpolators)
494
+ # per = np.interp(fvec, fpsd, psd)
495
+ fper, per = interpolators[interp](fpsd, psd, N_os, fs)
496
+ per = mue2 / (np.sum(per / T)) * per # Equivalent to dfpsd * inp['T'] * per
497
+
498
+ # Generate random phase and magnitude
499
+ Xabs = np.sqrt(per * T / 2)
500
+ if seed is not None:
501
+ np.random.seed(seed)
502
+ Xos = Xabs * np.exp(1j * np.random.uniform(-np.pi, np.pi, len(Xabs)))
503
+ np.random.seed(None) # Reset random seed
504
+
505
+ # Compute time-domain signal
506
+ signal = np.fft.irfft(Xos * fs, n=N)
507
+ t = np.arange(0, N) / fs
508
+
509
+ return t, signal # Return time-domain signal
510
+
511
+ def gaussian_bell(grid, var, mean = 0):
512
+ """
513
+ Evaluate a Gaussian distribution over a grid.
514
+
515
+ Parameters:
516
+ grid (np.ndarray): Input grid.
517
+ var (float): Variance.
518
+ mean (float, optional): Mean. Default is 0.
519
+
520
+ Returns:
521
+ np.ndarray: Evaluated Gaussian PDF values.
522
+
523
+ Source:
524
+ Squires, G. L. (2001-08-30). Practical Physics (4 ed.). Cambridge University Press. doi:10.1017/cbo9781139164498. ISBN 978-0-521-77940-1.
525
+ """
526
+ return 1/ np.sqrt(var*2*np.pi) * np.exp(-(grid-mean)**2 / (2 * var))
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: SignalForge
3
+ Version: 0.0.2
4
+ Summary: Tools for analyzing and generating non-Gaussian, non-stationary signals in engineering applications.
5
+ Home-page: https://github.com/GiulioCurti/SignalForge
6
+ Author: Giulio Curti
7
+ Author-email: giulio.curti@dottorandi.unipg.it
8
+ License: MIT license
9
+ License-File: LICENSE
10
+ Requires-Dist: numpy>=2.0
11
+ Requires-Dist: scipy
12
+ Requires-Dist: matplotlib>=3.10
13
+ Requires-Dist: tqdm
14
+ Requires-Dist: PyWavelets
15
+ Requires-Dist: pyNNST
16
+ Requires-Dist: statsmodels
17
+ Dynamic: author
18
+ Dynamic: author-email
19
+ Dynamic: home-page
20
+ Dynamic: license
21
+ Dynamic: license-file
22
+ Dynamic: requires-dist
23
+ Dynamic: summary
@@ -0,0 +1,11 @@
1
+ SignalForge/__init__.py,sha256=IWhcCQyaikp2PvGPVQyYdIIU_xD7SA1mS1sNmZqBqcw,197
2
+ SignalForge/nonstationary_nongaussian.py,sha256=6-W0sQb8xOh2loVOTr0cDmWo7WRz_Q4r4SRHMfya4Vk,21005
3
+ SignalForge/single_chan_signal.py,sha256=UShwhbd95pIrsBmCEbIAbZ-La4p1cQzQDPBgqPlQ8t8,40798
4
+ SignalForge/stationary_gaussian.py,sha256=GBwuizh0b7A1rqSCWoyR1uXLl3TQvrJge9y05oX5I4I,3158
5
+ SignalForge/stationary_nongaussian.py,sha256=ilaCwogWNYocSDjsWNUXi_8ZfVj4X1UEAIxkgRXIrtY,37532
6
+ SignalForge/utils.py,sha256=A_QYziRFEp6GM1M0Hus9oUR-q6qNo-yTd1mI25NpHAA,18160
7
+ signalforge-0.0.2.dist-info/licenses/LICENSE,sha256=M96bTSvnJ4ErjAzj_sH3CiRiH4UwyGmKgJxzlUcxGd0,1089
8
+ signalforge-0.0.2.dist-info/METADATA,sha256=JdxiTDuOtiBSsnId5kvktaCm9E8KFxFGLgvLP2ugrM8,663
9
+ signalforge-0.0.2.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
10
+ signalforge-0.0.2.dist-info/top_level.txt,sha256=OQ8Plijrs3HMds2-re0GC14gfI6dRTlZfRgyegGje4o,12
11
+ signalforge-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (79.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 GiulioCurti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ SignalForge