melafit 0.1.1__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.
- melafit/__init__.py +3 -0
- melafit/fitting.py +497 -0
- melafit/markers.py +136 -0
- melafit/utils.py +322 -0
- melafit-0.1.1.dist-info/METADATA +14 -0
- melafit-0.1.1.dist-info/RECORD +9 -0
- melafit-0.1.1.dist-info/WHEEL +5 -0
- melafit-0.1.1.dist-info/licenses/LICENSE +21 -0
- melafit-0.1.1.dist-info/top_level.txt +1 -0
melafit/__init__.py
ADDED
melafit/fitting.py
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import scipy.optimize as opt
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
# Parameter names for melatonin wave approximation functions
|
|
5
|
+
BCF_PARAM_NAMES = ["phi", "b", "H", "c"]
|
|
6
|
+
SBCF_PARAM_NAMES = ["phi", "b", "H", "c", "v"]
|
|
7
|
+
BBCF_PARAM_NAMES = ["phi", "b", "H", "c", "m"]
|
|
8
|
+
BSBCF_PARAM_NAMES = ["phi", "b", "H", "c", "v", "m"]
|
|
9
|
+
|
|
10
|
+
def _resolve_params(p: np.ndarray | dict) -> np.ndarray:
|
|
11
|
+
"""
|
|
12
|
+
Convert parameter dict to array if needed, pass array through unchanged.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
if isinstance(p, dict):
|
|
16
|
+
return np.array(list(p.values()))
|
|
17
|
+
return p
|
|
18
|
+
|
|
19
|
+
def bcf(t: np.ndarray,
|
|
20
|
+
p: dict | np.ndarray) -> np.ndarray:
|
|
21
|
+
"""
|
|
22
|
+
Baseline cosine function
|
|
23
|
+
[Ruf '92](https://doi.org/10.1076/brhm.27.2.153.12942)
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
t : Numpy array of floats
|
|
28
|
+
Time values for the BCF waveform
|
|
29
|
+
p : Dictionary or Numpy array of floats
|
|
30
|
+
BCF parameters phi, b, H, c
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
bcf_val : Numpy array of floats
|
|
35
|
+
Values of the BCF function for the respective time points
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
p = _resolve_params(p)
|
|
39
|
+
|
|
40
|
+
phi = p[0]
|
|
41
|
+
b = p[1]
|
|
42
|
+
H = p[2]
|
|
43
|
+
c = p[3]
|
|
44
|
+
|
|
45
|
+
phi = 2 * np.pi * phi
|
|
46
|
+
t = 2 * np.pi * t
|
|
47
|
+
|
|
48
|
+
bcf_val = b + H / (2 * (1 - c)) * (
|
|
49
|
+
np.cos(t - phi) - c + abs(np.cos(t - phi) - c))
|
|
50
|
+
|
|
51
|
+
return bcf_val
|
|
52
|
+
|
|
53
|
+
def sbcf(t: np.ndarray,
|
|
54
|
+
p: dict | np.ndarray) -> np.ndarray:
|
|
55
|
+
"""
|
|
56
|
+
Skewed baseline cosine function
|
|
57
|
+
[Van Someren & Nagtegaal '07](https://doi.org/10.1016/j.sleep.2007.03.012)
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
t : Numpy array of floats
|
|
62
|
+
Time values for the SBCF waveform
|
|
63
|
+
p : Dictionary or Numpy array of floats
|
|
64
|
+
SBCF parameters phi, b, H, c, v
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
sbcf_val : Numpy array of floats
|
|
69
|
+
Values of the SBCF function for the respective time points
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
p = _resolve_params(p)
|
|
73
|
+
|
|
74
|
+
phi = p[0]
|
|
75
|
+
b = p[1]
|
|
76
|
+
H = p[2]
|
|
77
|
+
c = p[3]
|
|
78
|
+
v = p[4]
|
|
79
|
+
|
|
80
|
+
phi = 2 * np.pi * phi
|
|
81
|
+
t = 2 * np.pi * t
|
|
82
|
+
|
|
83
|
+
sbcf_val = b + H / (2 * (1 - c)) * (
|
|
84
|
+
np.cos(t - phi + v * np.cos(t - phi)) - c +
|
|
85
|
+
abs(np.cos(t - phi + v * np.cos(t - phi)) - c))
|
|
86
|
+
|
|
87
|
+
return sbcf_val
|
|
88
|
+
|
|
89
|
+
def bbcf(t: np.ndarray,
|
|
90
|
+
p: dict | np.ndarray) -> np.ndarray:
|
|
91
|
+
"""
|
|
92
|
+
Bimodal baseline cosine function
|
|
93
|
+
[Van Someren & Nagtegaal '07](https://doi.org/10.1016/j.sleep.2007.03.012)
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
t : Numpy array of floats
|
|
98
|
+
Time values for the BBCF waveform
|
|
99
|
+
p : Dictionary or Numpy array of floats
|
|
100
|
+
BBCF parameters phi, b, H, c, m
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
bbcf_val : Numpy array of floats
|
|
105
|
+
Values of the BBCF function for the respective time points
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
p = _resolve_params(p)
|
|
109
|
+
|
|
110
|
+
phi = p[0]
|
|
111
|
+
b = p[1]
|
|
112
|
+
H = p[2]
|
|
113
|
+
c = p[3]
|
|
114
|
+
m = p[4]
|
|
115
|
+
|
|
116
|
+
phi = 2 * np.pi * phi
|
|
117
|
+
t = 2 * np.pi * t
|
|
118
|
+
|
|
119
|
+
bbcf_val = b + H / (2 * (1 - c)) * (
|
|
120
|
+
np.cos(t - phi) + m * np.cos(2 * t - 2 * phi - np.pi) - c +
|
|
121
|
+
abs(np.cos(t - phi) + m * np.cos(2 * t - 2 * phi - np.pi) - c))
|
|
122
|
+
|
|
123
|
+
return bbcf_val
|
|
124
|
+
|
|
125
|
+
def bsbcf(t: np.ndarray,
|
|
126
|
+
p: np.ndarray) -> np.ndarray:
|
|
127
|
+
"""
|
|
128
|
+
Bimodal skewed baseline cosine function
|
|
129
|
+
[Van Someren & Nagtegaal '07](https://doi.org/10.1016/j.sleep.2007.03.012)
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
t : Numpy array of floats
|
|
134
|
+
Time values for the bsbcf waveform
|
|
135
|
+
p : Numpy array of floats
|
|
136
|
+
BSBCF parameters phi, b, H, c, v, m
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
bsbcf_val : Dictionary or Numpy array of floats
|
|
141
|
+
Values of the BSBCF function for the respective time points
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
p = _resolve_params(p)
|
|
145
|
+
|
|
146
|
+
phi = p[0]
|
|
147
|
+
b = p[1]
|
|
148
|
+
H = p[2]
|
|
149
|
+
c = p[3]
|
|
150
|
+
v = p[4]
|
|
151
|
+
m = p[5]
|
|
152
|
+
|
|
153
|
+
phi = 2 * np.pi * phi
|
|
154
|
+
t = 2 * np.pi * t
|
|
155
|
+
|
|
156
|
+
bsbcf_val = b + H / (2 * (1 - c)) * (
|
|
157
|
+
np.cos(t - phi + v * np.cos(t - phi)) +
|
|
158
|
+
m * np.cos(2 * t - 2 * phi - np.pi) - c +
|
|
159
|
+
abs(np.cos(t - phi + v * np.cos(t - phi)) +
|
|
160
|
+
m * np.cos(2 * t - 2 * phi - np.pi) - c))
|
|
161
|
+
|
|
162
|
+
return bsbcf_val
|
|
163
|
+
|
|
164
|
+
# Mapping of functions to parameter names for conversion between dict
|
|
165
|
+
# and array representations
|
|
166
|
+
PARAM_NAMES = {
|
|
167
|
+
bcf: BCF_PARAM_NAMES,
|
|
168
|
+
sbcf: SBCF_PARAM_NAMES,
|
|
169
|
+
bbcf: BBCF_PARAM_NAMES,
|
|
170
|
+
bsbcf: BSBCF_PARAM_NAMES,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
def params_to_array(params: dict) -> np.ndarray:
|
|
174
|
+
"""
|
|
175
|
+
Convert parameter dictionary to numpy array for scipy.optimize.
|
|
176
|
+
|
|
177
|
+
Parameters
|
|
178
|
+
----------
|
|
179
|
+
params : dict
|
|
180
|
+
Dictionary of parameter names and values
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
p : Numpy array of floats
|
|
184
|
+
Parameter vector for scipy.optimize
|
|
185
|
+
"""
|
|
186
|
+
return np.array(list(params.values()))
|
|
187
|
+
|
|
188
|
+
def array_to_params(x: np.ndarray, f: callable) -> dict:
|
|
189
|
+
"""
|
|
190
|
+
Convert scipy.optimize result array to named parameter dictionary.
|
|
191
|
+
|
|
192
|
+
Parameters
|
|
193
|
+
----------
|
|
194
|
+
x : Numpy array of floats
|
|
195
|
+
Parameter vector from scipy.optimize
|
|
196
|
+
f : callable
|
|
197
|
+
Melatonin wave approximation function for which the parameters were fitted
|
|
198
|
+
Returns
|
|
199
|
+
-------
|
|
200
|
+
params : dict
|
|
201
|
+
Dictionary of parameter names and values for the respective function
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
param_names = PARAM_NAMES.get(f)
|
|
205
|
+
if param_names is None:
|
|
206
|
+
raise ValueError(f"Function {f.__name__} not recognized for parameter " +
|
|
207
|
+
"conversion.")
|
|
208
|
+
return dict(zip(param_names, x))
|
|
209
|
+
|
|
210
|
+
def cost(p: np.ndarray,
|
|
211
|
+
t: np.ndarray,
|
|
212
|
+
y: np.ndarray,
|
|
213
|
+
f: callable,
|
|
214
|
+
cost_p : dict | None = None) -> np.float64:
|
|
215
|
+
"""
|
|
216
|
+
Cost function for melatonin fitting, penalizes the trivial solution when
|
|
217
|
+
all model values = 0
|
|
218
|
+
[Gabel et al. '17](https://doi.org/10.1038/s41598-017-07060-8)
|
|
219
|
+
NOTE: the order of parameters is pre-defined by the SciPy optimization
|
|
220
|
+
routine
|
|
221
|
+
|
|
222
|
+
Parameters
|
|
223
|
+
----------
|
|
224
|
+
p : Numpy array of floats
|
|
225
|
+
Function parameter vector
|
|
226
|
+
t : Numpy array of floats
|
|
227
|
+
X-values for curve fitting (time)
|
|
228
|
+
y: Numpy array of floats
|
|
229
|
+
Y-values for curve fitting (melatonin levels)
|
|
230
|
+
f : callable
|
|
231
|
+
Melatonin wave approximation function
|
|
232
|
+
cost_p : dict | None
|
|
233
|
+
Cost function parameters (defaults to None) in which case
|
|
234
|
+
{"eps": 1e-8} is used
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
val : float
|
|
239
|
+
Value of the cost function
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
if cost_p is None:
|
|
243
|
+
cost_p = {}
|
|
244
|
+
eps = cost_p.get("eps", 1e-8)
|
|
245
|
+
|
|
246
|
+
y_ = f(t, p)
|
|
247
|
+
|
|
248
|
+
return np.nanmean(np.square(y - y_)) / (np.var(y_) + eps)
|
|
249
|
+
|
|
250
|
+
def rsquared(Y: np.ndarray,
|
|
251
|
+
y: np.ndarray) -> np.float64:
|
|
252
|
+
"""
|
|
253
|
+
R2 goodness of fit
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
Y : Numpy array of floats
|
|
258
|
+
Reference values
|
|
259
|
+
y : Numpy array of floats
|
|
260
|
+
Fitted values
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
r2 : float
|
|
265
|
+
R2 value
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
err = Y - y
|
|
269
|
+
Y_ = Y - np.nanmean(Y)
|
|
270
|
+
r2 = 1 - np.nansum(np.square(err)) / np.nansum(np.square(Y_))
|
|
271
|
+
|
|
272
|
+
return r2
|
|
273
|
+
|
|
274
|
+
def func_defaults(data_fit: np.ndarray,
|
|
275
|
+
f: callable) -> tuple[dict, dict, dict]:
|
|
276
|
+
"""
|
|
277
|
+
Default initial conditions and constraints for melatonin wave approximation
|
|
278
|
+
functions
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
data_fit : Numpy array of floats
|
|
283
|
+
Y-values for curve fitting (melatonin levels)
|
|
284
|
+
f : callable
|
|
285
|
+
Melatonin wave approximation function
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
p0 : Dictionary
|
|
290
|
+
Initial guess for the function parameters
|
|
291
|
+
lb : Dictionary
|
|
292
|
+
Lower bounds for the function parameters
|
|
293
|
+
ub : Dictionary
|
|
294
|
+
Upper bounds for the function parameters
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
minx = np.min(data_fit)
|
|
298
|
+
maxx = np.max(data_fit)
|
|
299
|
+
|
|
300
|
+
data_range = (maxx - minx)
|
|
301
|
+
|
|
302
|
+
if f==bcf:
|
|
303
|
+
# Initial guess for BCF parameters
|
|
304
|
+
p0 = [
|
|
305
|
+
0, # phi
|
|
306
|
+
minx, # b
|
|
307
|
+
(maxx-minx), # H
|
|
308
|
+
0 # c
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
# Lower bounds for BCF parameters
|
|
312
|
+
lb = [
|
|
313
|
+
-0.5, # phi
|
|
314
|
+
minx, # b
|
|
315
|
+
0.5 * data_range, # H
|
|
316
|
+
-1 # c
|
|
317
|
+
]
|
|
318
|
+
|
|
319
|
+
# Upper bounds for BCF parameters
|
|
320
|
+
ub = [
|
|
321
|
+
0.5, # phi
|
|
322
|
+
maxx, # b
|
|
323
|
+
2 * data_range, # H
|
|
324
|
+
1 - 1e-6 # c
|
|
325
|
+
]
|
|
326
|
+
elif f==sbcf:
|
|
327
|
+
# Initial guess for SBCF parameters
|
|
328
|
+
p0 = [
|
|
329
|
+
0, # phi
|
|
330
|
+
minx, # b
|
|
331
|
+
(maxx-minx), # H
|
|
332
|
+
0, # c
|
|
333
|
+
0 # v
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
# Lower bounds for SBCF parameters
|
|
337
|
+
lb = [
|
|
338
|
+
-0.5, # phi
|
|
339
|
+
minx, # b
|
|
340
|
+
0.5 * data_range, # H
|
|
341
|
+
-1, # c
|
|
342
|
+
-1 # v
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
# Upper bounds for SBCF parameters
|
|
346
|
+
ub = [
|
|
347
|
+
0.5, # phi
|
|
348
|
+
maxx, # b
|
|
349
|
+
2 * data_range, # H
|
|
350
|
+
1 - 1e-6, # c
|
|
351
|
+
1 # v
|
|
352
|
+
]
|
|
353
|
+
elif f==bbcf:
|
|
354
|
+
# Initial guess for BBCF parameters
|
|
355
|
+
p0 = [
|
|
356
|
+
0, # phi
|
|
357
|
+
minx, # b
|
|
358
|
+
(maxx-minx), # H
|
|
359
|
+
0, # c
|
|
360
|
+
0 # m
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
# Lower bounds for BBCF parameters
|
|
364
|
+
lb = [
|
|
365
|
+
-0.5, # phi
|
|
366
|
+
minx, # b
|
|
367
|
+
0.5 * data_range, # H
|
|
368
|
+
-1, # c
|
|
369
|
+
0 # m
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
# Upper bounds for BBCF parameters
|
|
373
|
+
ub = [
|
|
374
|
+
0.5, # phi
|
|
375
|
+
maxx, # b
|
|
376
|
+
2 * data_range, # H
|
|
377
|
+
1 - 1e-6, # c
|
|
378
|
+
1 - 1e-6 # m
|
|
379
|
+
]
|
|
380
|
+
elif f==bsbcf:
|
|
381
|
+
# Initial guess for BSBCF parameters
|
|
382
|
+
p0 = [
|
|
383
|
+
0, # phi
|
|
384
|
+
minx, # b
|
|
385
|
+
(maxx-minx), # H
|
|
386
|
+
0, # c
|
|
387
|
+
0, # v
|
|
388
|
+
0 # m
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
# Lower bounds for BSBCF parameters
|
|
392
|
+
lb = [
|
|
393
|
+
-0.5, # phi
|
|
394
|
+
minx, # b
|
|
395
|
+
0.5 * data_range, # H
|
|
396
|
+
-1, # c
|
|
397
|
+
-1, # v
|
|
398
|
+
0 # m
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
# Upper bounds for BSBCF parameters
|
|
402
|
+
ub = [
|
|
403
|
+
0.5, # phi
|
|
404
|
+
maxx, # b
|
|
405
|
+
2 * data_range, # H
|
|
406
|
+
1 - 1e-6, # c
|
|
407
|
+
1, # v
|
|
408
|
+
1 - 1e-6 # m
|
|
409
|
+
]
|
|
410
|
+
else:
|
|
411
|
+
raise NotImplementedError("Constraints and initial conditions for " +
|
|
412
|
+
f"function '{f.__name__}' are not defined!")
|
|
413
|
+
|
|
414
|
+
return (array_to_params(p0, f),
|
|
415
|
+
array_to_params(lb, f),
|
|
416
|
+
array_to_params(ub, f))
|
|
417
|
+
|
|
418
|
+
def fit(time_fit: np.ndarray,
|
|
419
|
+
data_fit: np.ndarray,
|
|
420
|
+
f: callable=bsbcf,
|
|
421
|
+
p0: np.ndarray | None = None,
|
|
422
|
+
lb: np.ndarray | None = None,
|
|
423
|
+
ub: np.ndarray | None = None,
|
|
424
|
+
cost_f: callable=cost,
|
|
425
|
+
cost_p: dict | None = None) -> opt.OptimizeResult:
|
|
426
|
+
"""
|
|
427
|
+
Melatonin data fitting routine
|
|
428
|
+
|
|
429
|
+
Parameters
|
|
430
|
+
----------
|
|
431
|
+
time_fit : Numpy array of floats
|
|
432
|
+
X-values for curve fitting (time)
|
|
433
|
+
data_fit : Numpy array of floats
|
|
434
|
+
Y-values for curve fitting (melatonin levels)
|
|
435
|
+
f : callable
|
|
436
|
+
Melatonin wave approximation function (defaults to `bsbcf`)
|
|
437
|
+
p0 : Numpy array of floats or None
|
|
438
|
+
Non-standard initial values for wave approximation function
|
|
439
|
+
(defaults to 'None')
|
|
440
|
+
lb : Numpy array of floats or None
|
|
441
|
+
Non-standard lower bounds for wave approximation function
|
|
442
|
+
parameters (defaults to 'None')
|
|
443
|
+
ub : Numpy array of floats or None
|
|
444
|
+
Non-standard upper bounds for wave approximation function
|
|
445
|
+
parameters (defaults to 'None')
|
|
446
|
+
cost_f : callable
|
|
447
|
+
Cost function for curve fitting (defaults to `cost`)
|
|
448
|
+
cost_p : dict | None
|
|
449
|
+
Cost function parameters as dictionary or None (defaults to None)
|
|
450
|
+
|
|
451
|
+
Returns
|
|
452
|
+
-------
|
|
453
|
+
res : OptimizeResult
|
|
454
|
+
Optimization result including parameters of the fitted function
|
|
455
|
+
in the field `x`
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
# Only try to fetch defaults if we recognize the function
|
|
459
|
+
if f in PARAM_NAMES.keys():
|
|
460
|
+
_p0, _lb, _ub = func_defaults(data_fit, f)
|
|
461
|
+
|
|
462
|
+
if p0 is not None:
|
|
463
|
+
_p0 = p0
|
|
464
|
+
|
|
465
|
+
if lb is not None:
|
|
466
|
+
_lb = lb
|
|
467
|
+
|
|
468
|
+
if ub is not None:
|
|
469
|
+
_ub = ub
|
|
470
|
+
else:
|
|
471
|
+
# For custom functions, require the user to have provided p0/lb/ub
|
|
472
|
+
if p0 is None or lb is None or ub is None:
|
|
473
|
+
raise ValueError(f"Function '{f.__name__}' is not a built-in model. " +
|
|
474
|
+
"You must provide p0, lb, and ub manually.")
|
|
475
|
+
_p0, _lb, _ub = p0, lb, ub
|
|
476
|
+
|
|
477
|
+
bounds = opt.Bounds(_resolve_params(_lb), _resolve_params(_ub))
|
|
478
|
+
res = opt.minimize(fun=cost_f,
|
|
479
|
+
args=(time_fit, data_fit, f, cost_p),
|
|
480
|
+
x0=_resolve_params(_p0),
|
|
481
|
+
bounds=bounds)
|
|
482
|
+
|
|
483
|
+
if f in PARAM_NAMES:
|
|
484
|
+
res.p = array_to_params(res.x, f)
|
|
485
|
+
else:
|
|
486
|
+
if isinstance(_p0, dict):
|
|
487
|
+
param_names = list(_p0.keys())
|
|
488
|
+
elif isinstance(_lb, dict):
|
|
489
|
+
param_names = list(_lb.keys())
|
|
490
|
+
elif isinstance(_ub, dict):
|
|
491
|
+
param_names = list(_ub.keys())
|
|
492
|
+
else:
|
|
493
|
+
param_names = None
|
|
494
|
+
|
|
495
|
+
res.p = dict(zip(param_names, res.x)) if param_names is not None else None
|
|
496
|
+
|
|
497
|
+
return res
|
melafit/markers.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from melafit.utils import day_profile, abs_threshold, time_to_phase
|
|
4
|
+
|
|
5
|
+
def amplitude(values: np.ndarray) -> np.float64:
|
|
6
|
+
"""
|
|
7
|
+
Peak-to-baseline amplitude of fitted waveform
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
values : Numpy array of floats
|
|
12
|
+
Waveform values
|
|
13
|
+
|
|
14
|
+
Returns
|
|
15
|
+
-------
|
|
16
|
+
ampl : float
|
|
17
|
+
Peak-to-baseline amplitude
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
return np.max(values) - np.min(values)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def midpoint(times: pd.DatetimeIndex,
|
|
24
|
+
values: np.ndarray,
|
|
25
|
+
threshold: np.float64,
|
|
26
|
+
thresh_abs: bool = False
|
|
27
|
+
) -> tuple[np.float64, np.float64, np.float64, np.float64]:
|
|
28
|
+
"""
|
|
29
|
+
Compute melatonin midpoint, DLMOn and DLMOff times. NOTE: This function
|
|
30
|
+
assumes that there is at least 24h of data. If this is not the case, the
|
|
31
|
+
results may be inaccurate. When working with waveforms, make sure to
|
|
32
|
+
generate a full 24h curve which is usually possible even with shorter
|
|
33
|
+
raw data the curve was fitted to.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
times : pandas DatetimeIndex
|
|
38
|
+
Datetime values
|
|
39
|
+
values : Numpy array of floats
|
|
40
|
+
Melatonin waveform values
|
|
41
|
+
threshold: float
|
|
42
|
+
Relative threshold, fraction of range peak-to-baseline (0 to 1)
|
|
43
|
+
thresh_abs: bool
|
|
44
|
+
If True, the given threshold is absolute. Otherwise, the absolute
|
|
45
|
+
threshold is computed from the given relative threshold and the
|
|
46
|
+
range of values (defaults to False)
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
result : tuple[float, float, float, float]
|
|
51
|
+
Melatonin midpoint, DLMOn and DLMOff times as phase (from 0.0 to
|
|
52
|
+
1.0, 1.0 = 24h), and absolute threshold
|
|
53
|
+
|
|
54
|
+
See also
|
|
55
|
+
--------
|
|
56
|
+
melafit.utils.compute_wave: Compute waveform resampled to given time
|
|
57
|
+
resolution
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
data_series = pd.Series(index=times, data=values)
|
|
61
|
+
d_profile = day_profile(data_series, binsize=1)[0]
|
|
62
|
+
|
|
63
|
+
if not thresh_abs:
|
|
64
|
+
threshold = abs_threshold(values, threshold)
|
|
65
|
+
|
|
66
|
+
idx_on = np.argwhere((d_profile.values[:-1] < threshold) &
|
|
67
|
+
(d_profile.values[1:] >= threshold))[0]
|
|
68
|
+
idx_off = np.argwhere((d_profile.values[:-1] >= threshold) &
|
|
69
|
+
(d_profile.values[1:] < threshold))[0]
|
|
70
|
+
|
|
71
|
+
time_on = d_profile.index.values[idx_on][0] / 24.0
|
|
72
|
+
time_off = d_profile.index.values[idx_off][0] / 24.0
|
|
73
|
+
|
|
74
|
+
if time_on > time_off:
|
|
75
|
+
time_off += 1.0
|
|
76
|
+
|
|
77
|
+
time_midpoint = 0.5 * (time_on + time_off)
|
|
78
|
+
|
|
79
|
+
time_midpoint = time_to_phase(time_midpoint)
|
|
80
|
+
time_on = time_to_phase(time_on)
|
|
81
|
+
time_off = time_to_phase(time_off)
|
|
82
|
+
|
|
83
|
+
return time_midpoint, time_on, time_off, threshold
|
|
84
|
+
|
|
85
|
+
def area_cog(times: pd.DatetimeIndex,
|
|
86
|
+
values: np.ndarray,
|
|
87
|
+
baseline: np.float64 | None = None
|
|
88
|
+
) -> tuple[np.float64, np.float64]:
|
|
89
|
+
"""
|
|
90
|
+
Center of gravity of area under the curve
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
times : pandas DatetimeIndex
|
|
95
|
+
Datetime values
|
|
96
|
+
values : Numpy array of floats
|
|
97
|
+
Waveform values
|
|
98
|
+
baseline : float or None
|
|
99
|
+
Baseline for area computation. Equals to minimum of values if
|
|
100
|
+
None is given (default)
|
|
101
|
+
|
|
102
|
+
Returns
|
|
103
|
+
-------
|
|
104
|
+
area : float
|
|
105
|
+
Area under the curve
|
|
106
|
+
cog : float
|
|
107
|
+
Center of gravity of area under the curve as phase (from 0.0 to
|
|
108
|
+
1.0, 1.0 = 24h)
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if baseline is None:
|
|
112
|
+
baseline = np.min(values)
|
|
113
|
+
|
|
114
|
+
bin_minutes = 1
|
|
115
|
+
|
|
116
|
+
data_series = pd.Series(index=times, data=values)
|
|
117
|
+
d_profile = day_profile(data_series, binsize=bin_minutes)[0]
|
|
118
|
+
|
|
119
|
+
times = d_profile.index.values / 24.0
|
|
120
|
+
values = d_profile.values
|
|
121
|
+
|
|
122
|
+
idx_on = np.argwhere((values[:-1] <= baseline) &
|
|
123
|
+
(values[1:] > baseline))[0][0]
|
|
124
|
+
|
|
125
|
+
times = np.concatenate([times[idx_on:], 1.0 + times[:idx_on]])
|
|
126
|
+
values = np.concatenate([values[idx_on:], values[:idx_on]]) - baseline
|
|
127
|
+
|
|
128
|
+
area = np.sum(values)
|
|
129
|
+
cog = np.dot(values, times) / area
|
|
130
|
+
|
|
131
|
+
# Convert COG to phase (from 0.0 to 1.0, 1.0 = 24h)
|
|
132
|
+
cog = time_to_phase(cog)
|
|
133
|
+
|
|
134
|
+
area /= (24.0 * 60.0 / bin_minutes) # Normalize by bin size in minutes
|
|
135
|
+
|
|
136
|
+
return area, cog
|
melafit/utils.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datetime as dt
|
|
4
|
+
|
|
5
|
+
def read_data(data_pathname: str) -> pd.DataFrame:
|
|
6
|
+
"""
|
|
7
|
+
Read data to be analyzed from an Excel spreadsheet.
|
|
8
|
+
|
|
9
|
+
Column must be named as follows:
|
|
10
|
+
* *Participant* for study participant ID
|
|
11
|
+
* *Date* for dates of the respective samples
|
|
12
|
+
* *Time* for sample timestamps
|
|
13
|
+
* *Mel* for melatonin level values
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
data_pathname : str
|
|
18
|
+
Pathname of the Excel spreadsheet file to read data from
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
data : pandas DataFrame
|
|
23
|
+
Data for all participants read from the Excel table
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Read data from Excel spreadsheet
|
|
27
|
+
data = pd.read_excel(data_pathname)
|
|
28
|
+
|
|
29
|
+
# Enforce correct data types
|
|
30
|
+
data.Participant = data.Participant.astype(int, errors="ignore")
|
|
31
|
+
data.Date = pd.to_datetime(data.Date, dayfirst=True, errors="coerce").dt.date
|
|
32
|
+
data.Time = pd.to_datetime(data.Time.astype(str), errors="coerce").dt.time
|
|
33
|
+
data.Mel = data.Mel.astype(float, errors="ignore")
|
|
34
|
+
|
|
35
|
+
# Add combined datetime timestamp
|
|
36
|
+
data["Timestamp"] = data.apply(lambda x: dt.datetime.combine(x.Date, x.Time), axis=1)
|
|
37
|
+
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
def prepare_part_data(data: pd.DataFrame,
|
|
41
|
+
participant: str | int) -> pd.DataFrame:
|
|
42
|
+
"""
|
|
43
|
+
Prepare one participant's data for analysis
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
data : pandas DataFrame
|
|
48
|
+
All participants' data
|
|
49
|
+
participant : string or integer
|
|
50
|
+
Participant's identifier
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
p_data : pandas DataFrame
|
|
55
|
+
Prepared data for one participant
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
# Select participant's data
|
|
59
|
+
p_data = data.loc[data.Participant==participant]
|
|
60
|
+
|
|
61
|
+
# Extract cumulative time in days
|
|
62
|
+
base = p_data.Timestamp.min()
|
|
63
|
+
diff = p_data.Timestamp - base
|
|
64
|
+
p_data["Timedays"] = (diff.dt.total_seconds() / (24*60*60) +
|
|
65
|
+
base.hour / 24 +
|
|
66
|
+
base.minute / (24*60) +
|
|
67
|
+
base.second / (24*60*60))
|
|
68
|
+
|
|
69
|
+
# Check and fix errors in timestamps
|
|
70
|
+
idiff = np.diff(p_data.Timedays) < 0
|
|
71
|
+
|
|
72
|
+
if any(idiff):
|
|
73
|
+
ix = np.where(idiff)
|
|
74
|
+
|
|
75
|
+
for i in ix:
|
|
76
|
+
idx = p_data.index[i[0]+1] # get the actual index label
|
|
77
|
+
p_data.loc[idx, 'Timestamp'] += pd.Timedelta(days=1)
|
|
78
|
+
p_data.loc[idx, 'Timedays'] += 1.0
|
|
79
|
+
print(f"Corrected one timestamp for participant {participant}")
|
|
80
|
+
|
|
81
|
+
return p_data
|
|
82
|
+
|
|
83
|
+
def compute_wave(tmin: np.float64,
|
|
84
|
+
tmax: np.float64,
|
|
85
|
+
dt_minutes: np.float64,
|
|
86
|
+
f: callable,
|
|
87
|
+
p: dict | np.ndarray,
|
|
88
|
+
full_wave: bool = True) -> np.ndarray:
|
|
89
|
+
"""
|
|
90
|
+
Compute waveform resampled to given time resolution
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
tmin : float
|
|
95
|
+
Start time (1.0 = 24 hours)
|
|
96
|
+
tmax : float
|
|
97
|
+
Stop time (inclusive, 1.0 = 24 hours)
|
|
98
|
+
dt_minutes : float
|
|
99
|
+
Time increment in minutes
|
|
100
|
+
f : callable
|
|
101
|
+
Waveform function
|
|
102
|
+
p : Dictionary or Numpy array of floats
|
|
103
|
+
Waveform parameter vector
|
|
104
|
+
full_wave: bool
|
|
105
|
+
If True and (tmax-tmin) < 1.0, tmax = tmin + 1.0 (defaults to
|
|
106
|
+
True)
|
|
107
|
+
|
|
108
|
+
Returns
|
|
109
|
+
-------
|
|
110
|
+
curve_val : Numpy array of floats
|
|
111
|
+
Values of the waveform function for the respective time points
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
if full_wave and ((tmax - tmin) < 1.0):
|
|
115
|
+
tmax = tmin + 1.0
|
|
116
|
+
|
|
117
|
+
step = 1.0 / (dt_minutes * 24 * 60)
|
|
118
|
+
time_curve = np.arange(tmin, tmax + 1.1 * step, step)
|
|
119
|
+
curve_val = f(t=time_curve, p=p)
|
|
120
|
+
|
|
121
|
+
return curve_val
|
|
122
|
+
|
|
123
|
+
def day_profile(data: pd.Series,
|
|
124
|
+
binsize: int = 60,
|
|
125
|
+
double: bool = False,
|
|
126
|
+
stderr: bool = False,
|
|
127
|
+
repfirst: bool = False)->tuple[pd.Series, pd.Series]:
|
|
128
|
+
"""
|
|
129
|
+
Compute averaged day profile of a (quasi-)periodic time series
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
data : pandas Series
|
|
134
|
+
Time series data
|
|
135
|
+
binsize : int
|
|
136
|
+
Bin size in minutes (defaults to 60)
|
|
137
|
+
double: bool
|
|
138
|
+
Prepare data for double plot (defaults to False)
|
|
139
|
+
stderr : bool
|
|
140
|
+
Compute standard errors per bin (defaults to False)
|
|
141
|
+
repfirst : bool
|
|
142
|
+
Add first bin at 00:00 to the end (defaults to False)
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
profile : tuple[pd.Series, pd.Series]
|
|
147
|
+
Bin averages and standard errors with index in hours (0..24)
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
# Bin data, ensure centering of the data points around bin centers
|
|
151
|
+
smpstr=str(binsize)+'min'
|
|
152
|
+
profile = data.shift(0.5, freq=smpstr).resample(smpstr).mean()
|
|
153
|
+
profile = profile.groupby(profile.index.hour + profile.index.minute/60)
|
|
154
|
+
|
|
155
|
+
# Compute average profile and standard deviations for each bin
|
|
156
|
+
profile_mean = profile.mean()
|
|
157
|
+
profile_std = profile.std()
|
|
158
|
+
|
|
159
|
+
# If standard errors requested, compute these from std's and bin counts
|
|
160
|
+
if stderr:
|
|
161
|
+
profile_std = profile_std / np.sqrt(profile.count())
|
|
162
|
+
|
|
163
|
+
# Concatenate results
|
|
164
|
+
profile = pd.DataFrame(data=pd.concat([profile_mean, profile_std],
|
|
165
|
+
axis=1))
|
|
166
|
+
|
|
167
|
+
# Prepare data for double plot if requested
|
|
168
|
+
if double:
|
|
169
|
+
profile = pd.concat([profile, profile])
|
|
170
|
+
|
|
171
|
+
# Add first bin at 00:00 to the end
|
|
172
|
+
if repfirst:
|
|
173
|
+
profile = pd.concat([profile, pd.DataFrame(profile.iloc[0,:]).T])
|
|
174
|
+
|
|
175
|
+
# Split returned results up for maximum flexibility
|
|
176
|
+
return profile.iloc[:,0], profile.iloc[:,1]
|
|
177
|
+
|
|
178
|
+
def time_to_phase(t: np.float64,
|
|
179
|
+
hours: bool = False) -> np.float64:
|
|
180
|
+
"""
|
|
181
|
+
Convert time values to phase representation (0.0 to 1.0, 1.0 = 24h)
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
t : float
|
|
186
|
+
Time value (in days or hours)
|
|
187
|
+
hours: bool
|
|
188
|
+
If True, time value is in hours and will be converted to phase by
|
|
189
|
+
dividing by 24. If False, time value is in days (1.0 = 24h).
|
|
190
|
+
Defaults to False.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
phase : float
|
|
195
|
+
Time as phase (0.0 to 1.0, 1.0 = 24h)
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
if hours:
|
|
199
|
+
t = t / 24.0
|
|
200
|
+
|
|
201
|
+
if t < 0:
|
|
202
|
+
return np.ceil(-t) + t
|
|
203
|
+
else:
|
|
204
|
+
return t - np.floor(t)
|
|
205
|
+
|
|
206
|
+
def phase_to_string(phase: np.float64) -> str:
|
|
207
|
+
"""
|
|
208
|
+
Convert phase representation of time (0.0 to 1.0) to string
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
phase : float
|
|
213
|
+
Time as phase (0.0 to 1.0, 1.0 = 24h)
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
string : str
|
|
218
|
+
String representation of phase
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
if phase < 0:
|
|
222
|
+
sign_str = "-"
|
|
223
|
+
phase = -phase
|
|
224
|
+
else:
|
|
225
|
+
sign_str = ""
|
|
226
|
+
|
|
227
|
+
td = pd.Timedelta(days=phase)
|
|
228
|
+
td_in_seconds = td.total_seconds()
|
|
229
|
+
|
|
230
|
+
hours, remainder = divmod(td_in_seconds, 3600)
|
|
231
|
+
minutes, _ = divmod(remainder, 60)
|
|
232
|
+
hours = int(hours)
|
|
233
|
+
minutes = int(minutes)
|
|
234
|
+
|
|
235
|
+
string = f"{sign_str}{hours:02d}:{minutes:02d}"
|
|
236
|
+
|
|
237
|
+
return string
|
|
238
|
+
|
|
239
|
+
def abs_threshold(values: np.ndarray,
|
|
240
|
+
thresh_rel: np.float64) -> np.float64:
|
|
241
|
+
"""
|
|
242
|
+
Compute absolute threshold from relative threshold
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
values : Numpy array of floats
|
|
247
|
+
Waveform values
|
|
248
|
+
thresh_rel: float
|
|
249
|
+
Relative threshold, fraction of range peak-to-baseline (0 to 1)
|
|
250
|
+
|
|
251
|
+
Returns
|
|
252
|
+
-------
|
|
253
|
+
thresh_abs : float
|
|
254
|
+
Absolute threshold
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
baseline = np.min(values)
|
|
258
|
+
val_range = np.max(values) - baseline
|
|
259
|
+
thresh_abs = baseline + thresh_rel * val_range
|
|
260
|
+
|
|
261
|
+
return thresh_abs
|
|
262
|
+
|
|
263
|
+
def phase_diff(phase1: np.float64,
|
|
264
|
+
phase2: np.float64) -> np.float64:
|
|
265
|
+
"""
|
|
266
|
+
Compute difference between two phases (0.0 to 1.0, 1.0 = 24h)
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
phase1 : float
|
|
271
|
+
First time as phase (0.0 to 1.0, 1.0 = 24h)
|
|
272
|
+
phase2 : float
|
|
273
|
+
Second time as phase (0.0 to 1.0, 1.0 = 24h)
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
dp : float
|
|
278
|
+
Difference between the two phases (0.0 to 1.0, 1.0 = 24h),
|
|
279
|
+
adjusted to be in the range -0.5 to 0.5 (i.e., -12h to 12h)
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
# Make sure we deal with phases in the range 0.0 to 1.0
|
|
283
|
+
phase1 = time_to_phase(phase1, hours=False)
|
|
284
|
+
phase2 = time_to_phase(phase2, hours=False)
|
|
285
|
+
|
|
286
|
+
dp = phase1 - phase2
|
|
287
|
+
|
|
288
|
+
# Adjust difference to be in the range -0.5 to 0.5 (i.e., -12h to 12h)
|
|
289
|
+
if dp < -0.5:
|
|
290
|
+
dp += 1.0
|
|
291
|
+
elif dp > 0.5:
|
|
292
|
+
dp -= 1.0
|
|
293
|
+
|
|
294
|
+
return dp
|
|
295
|
+
|
|
296
|
+
def params_to_string(params: dict | np.ndarray, ndec: int = 3) -> str:
|
|
297
|
+
"""
|
|
298
|
+
Convert curve fitting parameters to string
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
params : dict or Numpy array of floats
|
|
303
|
+
Curve fitting parameters
|
|
304
|
+
ndec : int
|
|
305
|
+
Number of decimal places to display (defaults to 3)
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
string : str
|
|
310
|
+
String representation of curve fitting parameters
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
if isinstance(params, dict):
|
|
314
|
+
param_strs = [f"{key}={value:.{ndec}f}"
|
|
315
|
+
for key, value in params.items()]
|
|
316
|
+
else:
|
|
317
|
+
param_strs = [f"p{i}={value:.{ndec}f}"
|
|
318
|
+
for i, value in enumerate(params)]
|
|
319
|
+
|
|
320
|
+
string = ", ".join(param_strs)
|
|
321
|
+
|
|
322
|
+
return string
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: melafit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: High-precision circadian melatonin profile analysis
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/vitaliy-ch25/melafit
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: scipy
|
|
11
|
+
Requires-Dist: pandas
|
|
12
|
+
Requires-Dist: openpyxl
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
melafit/__init__.py,sha256=J26zmnP1H9BSnzAqLpeQPpwi4sNcaxEhdtfsQXVjxsU,66
|
|
2
|
+
melafit/fitting.py,sha256=nr0ozZAryy-CWrocQz_tICIC355Bb7JMhU1hSIofrQo,13190
|
|
3
|
+
melafit/markers.py,sha256=4mLNp0HcXydMwSO0sK4sV1_35HWsvjVL2XCBx2A5m8o,4224
|
|
4
|
+
melafit/utils.py,sha256=tELEIRzqwWtu7PLtVHS2Vrbrl8M7oYYHDJqkPteqlew,9188
|
|
5
|
+
melafit-0.1.1.dist-info/licenses/LICENSE,sha256=LmGtoza4siaMUr-hbi1mSVLAVrNrTk0roIjs_WIeMeU,1096
|
|
6
|
+
melafit-0.1.1.dist-info/METADATA,sha256=o9jIY8WPZOfe-JHR5JpqQE4vFGjbhhlYg5eKWfZFN20,370
|
|
7
|
+
melafit-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
melafit-0.1.1.dist-info/top_level.txt,sha256=ZSDwz00o4NuPeW4PSUc0udjKmTgpqy20DNJE5M1sSv0,8
|
|
9
|
+
melafit-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vitaliy Kolodyazhniy, Christian Cajochen
|
|
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
|
+
melafit
|