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 +21 -0
- melafit-0.1.1/PKG-INFO +14 -0
- melafit-0.1.1/README.md +137 -0
- melafit-0.1.1/melafit/__init__.py +3 -0
- melafit-0.1.1/melafit/fitting.py +497 -0
- melafit-0.1.1/melafit/markers.py +136 -0
- melafit-0.1.1/melafit/utils.py +322 -0
- melafit-0.1.1/melafit.egg-info/PKG-INFO +14 -0
- melafit-0.1.1/melafit.egg-info/SOURCES.txt +13 -0
- melafit-0.1.1/melafit.egg-info/dependency_links.txt +1 -0
- melafit-0.1.1/melafit.egg-info/requires.txt +5 -0
- melafit-0.1.1/melafit.egg-info/top_level.txt +1 -0
- melafit-0.1.1/pyproject.toml +23 -0
- melafit-0.1.1/setup.cfg +4 -0
- melafit-0.1.1/tests/test_melafit.py +839 -0
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
|
melafit-0.1.1/README.md
ADDED
|
@@ -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,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
|