melafit 0.1.1__tar.gz

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-0.1.1/LICENSE ADDED
@@ -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.
melafit-0.1.1/PKG-INFO ADDED
@@ -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,137 @@
1
+ # melafit
2
+
3
+ Python package for **high-precision circadian melatonin profile analysis.** Features a variety of baseline cosine functions for curve fitting (Van Someren & Nagtegaal, 2007) and a robust cost function for superior convergence, even with sparse data ([Gabel et al. (2017)](https://doi.org/10.1038/s41598-017-07060-8)).
4
+
5
+ ## Overview
6
+
7
+ [melafit](https://github.com/vitaliy-ch25/melafit) is a Python package designed for high-precision modeling of 24-hour melatonin secretion. While standard cosinor or harmonic analyses fail to capture the physiological nuances of the melatonin "wave," [melafit](https://github.com/vitaliy-ch25/melafit) implements several **baseline cosine functions** including bimodal, skewed and bimodal-skewed modifications. This approach accounts for the characteristic baseline, asymmetry and dual peaks often seen in high-resolution circadian melatonin data.
8
+
9
+ Furthermore, the library utilizes a **specialized cost function** developed to overcome common optimization hurdles (trivial all-zero solutions), ensuring stable convergence even when working with sparse or incomplete time series.
10
+
11
+ ## Installation
12
+
13
+ The workflow described below is based on [Miniconda](https://docs.conda.io/projects/miniconda/en/latest/) as the package and environment manager. Create a dedicated directory `<YOUR-DIRECTORY>` for the [melafit](https://github.com/vitaliy-ch25/melafit) repository, navigate to it, and clone the repository:
14
+
15
+ ```bash
16
+ cd <YOUR-DIRECTORY>
17
+ git clone https://github.com/vitaliy-ch25/melafit.git
18
+ cd melafit
19
+ ```
20
+
21
+ Then create and activate the conda environment, which ensures all dependencies (Python 3.12, NumPy, SciPy, Pandas, etc.) are correctly configured. The environment configuration file [./melafit.yml](https://github.com/vitaliy-ch25/melafit/blob/main/melafit.yml) explicitly uses `conda-forge` as the sole package channel, ensuring reproducibility and avoiding potential conflicts between packages from different channels:
22
+
23
+ ```bash
24
+ conda env create -f melafit.yml
25
+ conda activate melafit
26
+ ```
27
+
28
+ This will create a fully functional analysis environment, including a number of supporting data manipulation and analysis packages (`numpy`, `scipy`, `pandas`, `openpyxl` and `matplotlib`).
29
+
30
+ ## Updating
31
+
32
+ Navigate to the cloned repository directory and pull the latest version:
33
+
34
+ ```bash
35
+ cd <YOUR-DIRECTORY>/melafit
36
+ git pull
37
+ ```
38
+
39
+ Then update the conda environment to match any updated dependencies:
40
+
41
+ ```bash
42
+ conda env update -f melafit.yml --prune
43
+ ```
44
+
45
+ This updates both the dependencies and the `melafit` package itself to the latest version.
46
+
47
+ ## Getting Started
48
+
49
+ Code example and some dummy data demonstrating melatonin profile curve fitting with this package are included in [./examples/](https://github.com/vitaliy-ch25/melafit/blob/main/examples/) and [./data/](https://github.com/vitaliy-ch25/melafit/blob/main/data/). Copy sample scripts and datasets to your working directory and start from there. If you have performed the steps above as described, your script will 'see' all the required packages from any location. Simply make sure to use the virtual environment `melafit` you created.
50
+
51
+ ## Data preparation
52
+ Follow the Excel table format and column naming conventions as in [./data/](https://github.com/vitaliy-ch25/melafit/blob/main/data/):
53
+ * *Participant* for study participant ID
54
+ * *Date* for dates of the respective samples
55
+ * *Time* for sample timestamps
56
+ * *Mel* for melatonin level values
57
+
58
+ ## Key Features
59
+
60
+ * **Bimodal Waveform Fitting:** Implementation of the [Nagtegaal & Van Someren (2007)](https://doi.org/10.1016/j.sleep.2007.03.012) model for superior physiological accuracy.
61
+ * **Optimized Convergence:** Leverages the robust cost function described in [Gabel et al. (2017)](https://doi.org/10.1038/s41598-017-07060-8) to ensure reliable fits across diverse datasets.
62
+ * **Sparse Data Support:** Capable of reconstructing full profiles and estimating circadian phase from limited data points, as well as determining dim light melatonin onset (DLMO) with partial data.
63
+ * **Research-Ready:** Direct derivation of phase markers from continuous, fitted waveforms.
64
+
65
+ ## Scientific Foundations
66
+
67
+ If you use [melafit](https://github.com/vitaliy-ch25/melafit) in your research, please cite the following foundational publications:
68
+
69
+ ### Human-Readable
70
+ 1. [Van Someren, E. J., & Nagtegaal, E. (2007). Improving melatonin circadian phase estimates. Sleep Medicine, 8(6), 590-601.](https://doi.org/10.1016/j.sleep.2007.03.012)
71
+ 2. [Gabel, V., et al. (2017). Differential impact in young and older individuals of blue-enriched white light on circadian physiology and alertness during sustained wakefulness. Scientific Reports, 7, 7620.](https://doi.org/10.1038/s41598-017-07060-8)
72
+
73
+ ### BibTeX
74
+ ```bibtex
75
+ @article{vansomeren2007,
76
+ title={Improving melatonin circadian phase estimates},
77
+ author={Van Someren, Eus JW and Nagtegaal, Elsbeth},
78
+ journal={Sleep Medicine},
79
+ volume={8},
80
+ number={6},
81
+ pages={590--601},
82
+ year={2007},
83
+ publisher={Elsevier}
84
+ }
85
+
86
+ @article{gabel2017,
87
+ title={Differential impact in young and older individuals of blue-enriched white light on circadian physiology and alertness during sustained wakefulness},
88
+ author={Gabel, Virginie and Reichert, Carolin F and Maire, Micheline and Schmidt, Christina and Schlangen, Luc JM and Kolodyazhniy, Vitaliy and Garbazza, Corrado and Cajochen, Christian and Viola, Antoine U},
89
+ journal={Scientific Reports},
90
+ volume={7},
91
+ pages={7620},
92
+ year={2017},
93
+ publisher={Nature Publishing Group}
94
+ }
95
+ ```
96
+
97
+ ## Authors
98
+ * Vitaliy Kolodyazhniy – Lead Developer
99
+ * Christian Cajochen – Scientific Lead
100
+
101
+ ## Revision History
102
+
103
+ ### [v0.1.1](https://github.com/vitaliy-ch25/melafit/releases/tag/v0.1.1) - First PyPI release
104
+ - Enhanced function `fit()` to support custom waveform functions with user-defined initial parameters and bounds
105
+ - Changed named parameter order in `fit()`: `cost_f` and `cost_p` are now the last two parameters
106
+ - Fixed returned type hints in `func_defaults()`
107
+ - Additional unit tests for new functionality
108
+ - Improved README
109
+ - Package registered in Python Package Index PyPI
110
+
111
+ ### [v0.1.0](https://github.com/vitaliy-ch25/melafit/releases/tag/v0.1.0) — First public release
112
+ - Dictionary support for waveform function parameters throughout the package: all functions accept both `dict` and `np.ndarray` for parameter input
113
+ - Named parameter constants: `BCF_PARAM_NAMES`, `SBCF_PARAM_NAMES`, `BBCF_PARAM_NAMES`, `BSBCF_PARAM_NAMES` and `PARAM_NAMES` lookup
114
+ - New utility functions `params_to_array()` and `array_to_params()` for conversion between array and named dictionary representations
115
+ - `fit()` now returns named parameter dictionary as `res.p` in addition to the standard scipy `res.x` array
116
+ - `fit()` now accepts `cost_p` dictionary for passing parameters to the cost function (e.g. `{"eps": 1e-6}`)
117
+ - New utility function `params_to_string()` for human-readable parameter output
118
+ - Fixed `area_cog()`: baseline subtraction and bin size normalization
119
+ - Unit tests for all public functions in `fitting`, `markers` and `utils`
120
+
121
+ ### v0.0.9
122
+ - New function `func_defaults()` in `fitting.py` for standalone access to default initial conditions and constraints for all waveform functions
123
+ - Improved cost function: `eps` parameter for more robust fitting
124
+ - Optional `thresh_abs` parameter in `markers.midpoint()` for absolute threshold support
125
+ - New example script `example_dlmo.py` and dataset for DLMO detection from partial data
126
+ - Previous example renamed to `example_full_profile.py`
127
+ - Improved type hints, docstrings and README
128
+
129
+ ### Initial revisions (v0.0.1 – v0.0.8)
130
+ - Full implementation of melatonin profile analysis as described in [Gabel et al. (2017)](https://doi.org/10.1038/s41598-017-07060-8)
131
+ - Waveform functions: `bcf`, `sbcf`, `bbcf`, `bsbcf`
132
+ - Markers: `amplitude`, `midpoint`, `DLMOn`, `DLMOff`, `area`, `cog`
133
+ - Utilities: `read_data`, `prepare_part_data`, `compute_wave`, `day_profile`, `abs_threshold`, `time_to_phase`, `phase_to_string`, `phase_diff`
134
+ - MIT license, packaging metadata and README
135
+
136
+ ## License
137
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
@@ -0,0 +1,3 @@
1
+ from .fitting import *
2
+ from .markers import *
3
+ from .utils import *
@@ -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