jeam 0.0.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.
- jeam-0.0.1/LICENSE +21 -0
- jeam-0.0.1/PKG-INFO +99 -0
- jeam-0.0.1/README.md +65 -0
- jeam-0.0.1/pyproject.toml +64 -0
- jeam-0.0.1/setup.cfg +4 -0
- jeam-0.0.1/src/jeam/Models/Circular.py +280 -0
- jeam-0.0.1/src/jeam/Models/HyperSpherical.py +589 -0
- jeam-0.0.1/src/jeam/Models/Spherical.py +574 -0
- jeam-0.0.1/src/jeam/Models/__init__.py +19 -0
- jeam-0.0.1/src/jeam/__init__.py +16 -0
- jeam-0.0.1/src/jeam/data/Fennell2023_exp1.csv +10943 -0
- jeam-0.0.1/src/jeam/data/Fennell2023_exp4.csv +21173 -0
- jeam-0.0.1/src/jeam/data/Kvam2019.csv +5803 -0
- jeam-0.0.1/src/jeam/utility/Constants.py +14 -0
- jeam-0.0.1/src/jeam/utility/__init__.py +0 -0
- jeam-0.0.1/src/jeam/utility/datasets.py +48 -0
- jeam-0.0.1/src/jeam/utility/fpts.py +251 -0
- jeam-0.0.1/src/jeam/utility/helpers.py +180 -0
- jeam-0.0.1/src/jeam/utility/simulators.py +655 -0
- jeam-0.0.1/src/jeam/version.py +1 -0
- jeam-0.0.1/src/jeam.egg-info/PKG-INFO +99 -0
- jeam-0.0.1/src/jeam.egg-info/SOURCES.txt +25 -0
- jeam-0.0.1/src/jeam.egg-info/dependency_links.txt +1 -0
- jeam-0.0.1/src/jeam.egg-info/requires.txt +13 -0
- jeam-0.0.1/src/jeam.egg-info/top_level.txt +1 -0
- jeam-0.0.1/tests/test_datasets.py +6 -0
- jeam-0.0.1/tests/test_import.py +5 -0
jeam-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Amir Hosein Hadian Rasanan
|
|
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.
|
jeam-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jeam
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Evidence accumulation models for judgment and continuous-response decisions
|
|
5
|
+
Author-email: Amir Hosein Hadian Rasanan <amir.h.hadian@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AmirHoseinHadian/JEAM
|
|
8
|
+
Project-URL: Documentation, https://amirhoseinhadian.github.io/JEAM/
|
|
9
|
+
Project-URL: Repository, https://github.com/AmirHoseinHadian/JEAM
|
|
10
|
+
Project-URL: Issues, https://github.com/AmirHoseinHadian/JEAM/issues
|
|
11
|
+
Keywords: evidence accumulation,diffusion models,decision making,cognitive modeling,continuous response
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.24
|
|
23
|
+
Requires-Dist: scipy>=1.10
|
|
24
|
+
Requires-Dist: numba>=0.58
|
|
25
|
+
Requires-Dist: pandas>=2.0
|
|
26
|
+
Requires-Dist: mpmath>1.4
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
30
|
+
Provides-Extra: docs
|
|
31
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
32
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# JEAM:
|
|
36
|
+
|
|
37
|
+
JEAM is a Python package for evidence accumulation modeling of continuous judgment tasks.
|
|
38
|
+
|
|
39
|
+
The package provides fast and numerically stable likelihood evaluation for multi-dimensional diffusion decision models using the integral equation method proposed by [Hadian Rasanan et al.,(2025)](https://doi.org/10.3758/s13428-025-02810-3). JEAM supports a wide range of continuous response scale, that can be employed in experimental studies including:
|
|
40
|
+
- Bounded one-dimensional scales (e.g., arcs or sliders),
|
|
41
|
+
- Circular scales (e.g., color wheels),
|
|
42
|
+
- Two-dimensional scales (e.g., 2D planes).
|
|
43
|
+
|
|
44
|
+
JEAM is designed for researchers in cognitive science, mathematical psychology, and neuroscience who work with diffusion models of continuous responses.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
### Install via `pip`
|
|
50
|
+
The package can be installed via pip:
|
|
51
|
+
```bash
|
|
52
|
+
pip istall jeam
|
|
53
|
+
```
|
|
54
|
+
### Install from source
|
|
55
|
+
Alternatively, clone or download the source code and install locally:
|
|
56
|
+
```bash
|
|
57
|
+
python -m setup.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Dependencies
|
|
63
|
+
JEAM requires the following Python packages:
|
|
64
|
+
|
|
65
|
+
- `numpy`
|
|
66
|
+
- `scipy`
|
|
67
|
+
- `pandas`
|
|
68
|
+
- `numba`
|
|
69
|
+
|
|
70
|
+
All dependencies are installed automatically when using `pip`.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### Conda environment (suggested)
|
|
75
|
+
If you have Andaconda or miniconda installed and you would like to create a separate environment:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
conda create --n jeam python=3 numpy scipy pandas numba
|
|
79
|
+
conda activate jeam
|
|
80
|
+
pip install jeam
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Documentation
|
|
84
|
+
|
|
85
|
+
The latest documentation can be found here: **[amirhoseinhadian.github.io/JEAM/](https://amirhoseinhadian.github.io/JEAM/)**
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Selected References
|
|
90
|
+
|
|
91
|
+
For background on diffusion models for continuous response tasks and the estimation methods implemented in JEAM, see:
|
|
92
|
+
|
|
93
|
+
- Hadian Rasanan, A. H., Evans, N. J., Amani Rad, J., & Rieskamp, J. (2025). Parameter estimation of hyper-spherical diffusion models with a time-dependent threshold: An integral equation method. Behavior research methods, 57(10), 283. https://doi.org/10.3758/s13428-025-02810-3
|
|
94
|
+
|
|
95
|
+
- Hadian Rasanan, A. H., Olschewski, S., & Rieskamp, J. (2026). The Projected Spherical Diffusion Model: An Evidence Accumulation Theory for Estimation. https://doi.org/10.31234/osf.io/mhj6v_v1
|
|
96
|
+
|
|
97
|
+
- Smith, P. L. (2016). Diffusion theory of decision making in continuous report. Psychological Review, 123 (4), 425–451, https://doi.org/10.1037/rev0000023
|
|
98
|
+
|
|
99
|
+
- Smith, P.L., & Corbett, E.A. (2019). Speeded multielement decision-making as diffusion in a hypersphere: Theory and application to double-target detection. Psychonomic Bulletin & Review, 26, https://doi.org/10.3758/s13423-018-1491-0
|
jeam-0.0.1/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# JEAM:
|
|
2
|
+
|
|
3
|
+
JEAM is a Python package for evidence accumulation modeling of continuous judgment tasks.
|
|
4
|
+
|
|
5
|
+
The package provides fast and numerically stable likelihood evaluation for multi-dimensional diffusion decision models using the integral equation method proposed by [Hadian Rasanan et al.,(2025)](https://doi.org/10.3758/s13428-025-02810-3). JEAM supports a wide range of continuous response scale, that can be employed in experimental studies including:
|
|
6
|
+
- Bounded one-dimensional scales (e.g., arcs or sliders),
|
|
7
|
+
- Circular scales (e.g., color wheels),
|
|
8
|
+
- Two-dimensional scales (e.g., 2D planes).
|
|
9
|
+
|
|
10
|
+
JEAM is designed for researchers in cognitive science, mathematical psychology, and neuroscience who work with diffusion models of continuous responses.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
### Install via `pip`
|
|
16
|
+
The package can be installed via pip:
|
|
17
|
+
```bash
|
|
18
|
+
pip istall jeam
|
|
19
|
+
```
|
|
20
|
+
### Install from source
|
|
21
|
+
Alternatively, clone or download the source code and install locally:
|
|
22
|
+
```bash
|
|
23
|
+
python -m setup.py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Dependencies
|
|
29
|
+
JEAM requires the following Python packages:
|
|
30
|
+
|
|
31
|
+
- `numpy`
|
|
32
|
+
- `scipy`
|
|
33
|
+
- `pandas`
|
|
34
|
+
- `numba`
|
|
35
|
+
|
|
36
|
+
All dependencies are installed automatically when using `pip`.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### Conda environment (suggested)
|
|
41
|
+
If you have Andaconda or miniconda installed and you would like to create a separate environment:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
conda create --n jeam python=3 numpy scipy pandas numba
|
|
45
|
+
conda activate jeam
|
|
46
|
+
pip install jeam
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
The latest documentation can be found here: **[amirhoseinhadian.github.io/JEAM/](https://amirhoseinhadian.github.io/JEAM/)**
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Selected References
|
|
56
|
+
|
|
57
|
+
For background on diffusion models for continuous response tasks and the estimation methods implemented in JEAM, see:
|
|
58
|
+
|
|
59
|
+
- Hadian Rasanan, A. H., Evans, N. J., Amani Rad, J., & Rieskamp, J. (2025). Parameter estimation of hyper-spherical diffusion models with a time-dependent threshold: An integral equation method. Behavior research methods, 57(10), 283. https://doi.org/10.3758/s13428-025-02810-3
|
|
60
|
+
|
|
61
|
+
- Hadian Rasanan, A. H., Olschewski, S., & Rieskamp, J. (2026). The Projected Spherical Diffusion Model: An Evidence Accumulation Theory for Estimation. https://doi.org/10.31234/osf.io/mhj6v_v1
|
|
62
|
+
|
|
63
|
+
- Smith, P. L. (2016). Diffusion theory of decision making in continuous report. Psychological Review, 123 (4), 425–451, https://doi.org/10.1037/rev0000023
|
|
64
|
+
|
|
65
|
+
- Smith, P.L., & Corbett, E.A. (2019). Speeded multielement decision-making as diffusion in a hypersphere: Theory and application to double-target detection. Psychonomic Bulletin & Review, 26, https://doi.org/10.3758/s13423-018-1491-0
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jeam"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Evidence accumulation models for judgment and continuous-response decisions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
|
|
14
|
+
authors = [
|
|
15
|
+
{ name = "Amir Hosein Hadian Rasanan", email = "amir.h.hadian@gmail.com" }
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
keywords = [
|
|
19
|
+
"evidence accumulation",
|
|
20
|
+
"diffusion models",
|
|
21
|
+
"decision making",
|
|
22
|
+
"cognitive modeling",
|
|
23
|
+
"continuous response"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 3 - Alpha",
|
|
28
|
+
"Intended Audience :: Science/Research",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Topic :: Scientific/Engineering"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
dependencies = [
|
|
37
|
+
"numpy>=1.24",
|
|
38
|
+
"scipy>=1.10",
|
|
39
|
+
"numba>=0.58",
|
|
40
|
+
"pandas>=2.0",
|
|
41
|
+
"mpmath>1.4"
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[project.optional-dependencies]
|
|
45
|
+
test = [
|
|
46
|
+
"pytest>=8",
|
|
47
|
+
"pytest-cov"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
docs = [
|
|
51
|
+
"mkdocs",
|
|
52
|
+
"mkdocs-material"
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/AmirHoseinHadian/JEAM"
|
|
57
|
+
Documentation = "https://amirhoseinhadian.github.io/JEAM/"
|
|
58
|
+
Repository = "https://github.com/AmirHoseinHadian/JEAM"
|
|
59
|
+
Issues = "https://github.com/AmirHoseinHadian/JEAM/issues"
|
|
60
|
+
|
|
61
|
+
[tool.setuptools.packages.find]
|
|
62
|
+
where = ["src"]
|
|
63
|
+
[tool.setuptools.package-data]
|
|
64
|
+
jeam = ["data/*.csv"]
|
jeam-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy.special import iv
|
|
4
|
+
|
|
5
|
+
from ..utility.helpers import trapz_1d
|
|
6
|
+
from ..utility.simulators import simulate_CDM_trial, simulate_custom_threshold_CDM_trial
|
|
7
|
+
from ..utility.fpts import cdm_short_t_fpt_z, cdm_long_t_fpt_z, ie_fpt_linear, ie_fpt_exponential, ie_fpt_hyperbolic, ie_fpt_custom
|
|
8
|
+
|
|
9
|
+
class CircularDiffusionModel:
|
|
10
|
+
'''
|
|
11
|
+
Circular Diffusion Model
|
|
12
|
+
'''
|
|
13
|
+
|
|
14
|
+
def __init__(self, threshold_dynamic='fixed'):
|
|
15
|
+
'''
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
threshold_dynamic : str, optional
|
|
19
|
+
The type of threshold collapse ('fixed', 'linear', 'exponential', 'hyperbolic', or 'custom'), default is 'fixed'
|
|
20
|
+
'''
|
|
21
|
+
self.name = 'Circular Diffusion Model'
|
|
22
|
+
|
|
23
|
+
if threshold_dynamic in ['fixed', 'linear', 'exponential', 'hyperbolic', 'custom']:
|
|
24
|
+
self.threshold_dynamic = threshold_dynamic
|
|
25
|
+
else:
|
|
26
|
+
raise ValueError("\'threshold_dynamic\' must be one of \'fixed\', \'linear\', \'exponential\', \'hyperbolic\', or \'custom\'. However, got \'{}\'".format(threshold_dynamic))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def simulate(self, drift_vec, ndt, threshold=1, decay=0, threshold_function=None, s_v=0, s_t=0, sigma=1, dt=0.001, n_sample=1):
|
|
31
|
+
'''
|
|
32
|
+
Simulate data from the Circular Diffusion Model with collapsing boundaries
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
drift_vec : array-like, shape (2,) or (n_samples, 2)
|
|
37
|
+
The drift vector [drift_x, drift_y]
|
|
38
|
+
ndt : float or array-like with the shape of (n_samples,)
|
|
39
|
+
The non-decision time
|
|
40
|
+
threshold : float or array-like with the shape of (n_samples,)
|
|
41
|
+
The initial decision threshold (default is 1)
|
|
42
|
+
decay : float or array-like with the shape of (n_samples,)
|
|
43
|
+
The decay rate of the threshold (default is 0)
|
|
44
|
+
threshold_function : callable, if threshold_dynamic is 'custom'
|
|
45
|
+
A function that takes time t and returns the threshold at time t
|
|
46
|
+
s_v : float, optional
|
|
47
|
+
The standard deviation of drift variability (default is 0)
|
|
48
|
+
s_t : float, optional
|
|
49
|
+
The standard deviation of non-decision time variability (default is 0)
|
|
50
|
+
sigma : float, optional
|
|
51
|
+
The diffusion coefficient (default is 1)
|
|
52
|
+
dt : float, optional
|
|
53
|
+
The time step for simulation (default is 0.001)
|
|
54
|
+
n_sample : int, optional
|
|
55
|
+
The number of samples to simulate (default is 1)
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
pd.DataFrame
|
|
60
|
+
A DataFrame containing simulated response times and choice angles
|
|
61
|
+
'''
|
|
62
|
+
RT = np.empty((n_sample,))
|
|
63
|
+
Choice = np.empty((n_sample,))
|
|
64
|
+
|
|
65
|
+
if drift_vec.ndim == 1:
|
|
66
|
+
drift_vec = drift_vec * np.ones((n_sample, 2))
|
|
67
|
+
elif drift_vec.shape[0] != n_sample:
|
|
68
|
+
raise ValueError("Number of rows in drift_vec must be equal to n_sample")
|
|
69
|
+
|
|
70
|
+
if isinstance(ndt, (float, np.floating)) or isinstance(ndt, (int, np.integer)):
|
|
71
|
+
ndt = np.full((n_sample,), ndt)
|
|
72
|
+
elif len(ndt) != n_sample:
|
|
73
|
+
raise ValueError("Length of ndt must be equal to n_sample")
|
|
74
|
+
|
|
75
|
+
if isinstance(threshold, (float, np.floating)) or isinstance(threshold, (int, np.integer)):
|
|
76
|
+
threshold = np.full((n_sample,), threshold)
|
|
77
|
+
elif len(threshold) != n_sample:
|
|
78
|
+
raise ValueError("Length of threshold must be equal to n_sample")
|
|
79
|
+
|
|
80
|
+
if isinstance(decay, (float, np.floating)) or isinstance(decay, (int, np.integer)):
|
|
81
|
+
decay = np.full((n_sample,), decay)
|
|
82
|
+
elif len(decay) != n_sample:
|
|
83
|
+
raise ValueError("Length of decay must be equal to n_sample")
|
|
84
|
+
|
|
85
|
+
if threshold_function is None and self.threshold_dynamic == 'custom':
|
|
86
|
+
raise ValueError("threshold_function must be provided when threshold_dynamic is 'custom'")
|
|
87
|
+
|
|
88
|
+
if threshold_function is not None and self.threshold_dynamic != 'custom':
|
|
89
|
+
raise ValueError("threshold_function should be None when threshold_dynamic is not 'custom'")
|
|
90
|
+
|
|
91
|
+
if s_v < 0:
|
|
92
|
+
raise ValueError("s_v must be non-negative")
|
|
93
|
+
if s_t < 0:
|
|
94
|
+
raise ValueError("s_t must be non-negative")
|
|
95
|
+
|
|
96
|
+
if self.threshold_dynamic != 'custom':
|
|
97
|
+
for n in range(n_sample):
|
|
98
|
+
RT[n], Choice[n] = simulate_CDM_trial(threshold[n], drift_vec[n, :].astype(np.float64), ndt[n],
|
|
99
|
+
threshold_dynamic=self.threshold_dynamic,
|
|
100
|
+
decay=decay[n], s_v=s_v, s_t=s_t, sigma=sigma, dt=dt)
|
|
101
|
+
else:
|
|
102
|
+
for n in range(n_sample):
|
|
103
|
+
RT[n], Choice[n] = simulate_custom_threshold_CDM_trial(threshold_function,
|
|
104
|
+
drift_vec[n, :].astype(np.float64), ndt[n],
|
|
105
|
+
s_v=s_v, s_t=s_t, sigma=sigma, dt=dt)
|
|
106
|
+
|
|
107
|
+
return pd.DataFrame(np.c_[RT, Choice], columns=['rt', 'response'])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def joint_lpdf(self, rt, theta, drift_vec, ndt, threshold, decay=0, threshold_function=None, dt_threshold_function=None, s_v=0, s_t=0, sigma=1, approximation_step=0.01):
|
|
111
|
+
'''
|
|
112
|
+
Compute the joint log-probability density function of response time and choice angle
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
rt : array-like
|
|
117
|
+
Response times
|
|
118
|
+
theta : array-like
|
|
119
|
+
Choice angles in radians
|
|
120
|
+
drift_vec : array-like, shape (2,) or (n_samples, 2)
|
|
121
|
+
The drift vector [drift_x, drift_y]
|
|
122
|
+
ndt : float or array-like, shape (n_samples,)
|
|
123
|
+
The non-decision time
|
|
124
|
+
threshold : float
|
|
125
|
+
The initial decision threshold
|
|
126
|
+
decay : float
|
|
127
|
+
The decay rate of the threshold (default is 0)
|
|
128
|
+
threshold_function : callable, if threshold_dynamic is 'custom'
|
|
129
|
+
A function that takes time t and returns the threshold at time t
|
|
130
|
+
dt_threshold_function : callable, if threshold_dynamic is 'custom'
|
|
131
|
+
A function that takes time t and returns the derivative of the threshold at time t
|
|
132
|
+
s_v : float, optional
|
|
133
|
+
The standard deviation of drift variability (default is 0)
|
|
134
|
+
s_t : float, optional
|
|
135
|
+
The standard deviation of non-decision time variability (default is 0)
|
|
136
|
+
sigma : float, optional
|
|
137
|
+
The diffusion coefficient (default is 1)
|
|
138
|
+
approximation_step : float, optional
|
|
139
|
+
The time step for numerical estimation of first-passage time densities (default is 0.01)
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
array-like
|
|
144
|
+
The joint log-probability density evaluated at (rt, theta) with same shape as rt and theta
|
|
145
|
+
'''
|
|
146
|
+
|
|
147
|
+
if drift_vec.ndim == 1:
|
|
148
|
+
drift_vec = drift_vec * np.ones((rt.shape[0], 2))
|
|
149
|
+
|
|
150
|
+
if drift_vec.shape[1] != 2 or drift_vec.ndim != 2:
|
|
151
|
+
raise ValueError("drift_vec must have shape (2,) or (n_samples, 2)")
|
|
152
|
+
|
|
153
|
+
tt = np.maximum(rt - ndt, 0)
|
|
154
|
+
|
|
155
|
+
# first-passage time density of zero drift process
|
|
156
|
+
if self.threshold_dynamic == 'fixed':
|
|
157
|
+
a = threshold
|
|
158
|
+
s0 = 0.002
|
|
159
|
+
s1 = 0.02
|
|
160
|
+
if s_t == 0:
|
|
161
|
+
s = tt/threshold**2
|
|
162
|
+
w = np.minimum(np.maximum((s - s0) / (s1 - s0), 0), 1)
|
|
163
|
+
fpt_lt = cdm_long_t_fpt_z(tt, threshold, sigma=sigma)
|
|
164
|
+
fpt_st = sigma**2/threshold**2 * cdm_short_t_fpt_z(sigma**2 * tt/threshold**2, sigma**2 * 0.1**8/threshold**2)
|
|
165
|
+
else:
|
|
166
|
+
T = np.arange(0, tt.max()+0.05, 0.05)
|
|
167
|
+
s = T/threshold**2
|
|
168
|
+
w = np.minimum(np.maximum((s - s0) / (s1 - s0), 0), 1)
|
|
169
|
+
fpt_lt = cdm_long_t_fpt_z(T, threshold, sigma=sigma)
|
|
170
|
+
fpt_st = sigma**2/threshold**2 * cdm_short_t_fpt_z(sigma**2 * T/threshold**2, sigma**2 * 0.1**8/threshold**2)
|
|
171
|
+
fpt_z = (1 - w) * fpt_st + w * fpt_lt
|
|
172
|
+
elif self.threshold_dynamic == 'linear':
|
|
173
|
+
a = threshold - decay*tt
|
|
174
|
+
T_max = min(rt.max(), threshold/decay)
|
|
175
|
+
g_z, T = ie_fpt_linear(threshold, decay, 2*sigma**2, 0.000001, sigma=2*sigma**2, dt=approximation_step, T_max=T_max)
|
|
176
|
+
fpt_z = np.interp(tt, T, g_z)
|
|
177
|
+
elif self.threshold_dynamic == 'exponential':
|
|
178
|
+
a = threshold * np.exp(-decay*tt)
|
|
179
|
+
g_z, T = ie_fpt_exponential(threshold, decay, 2*sigma**2, 0.000001, sigma=2*sigma**2, dt=approximation_step, T_max=rt.max())
|
|
180
|
+
fpt_z = np.interp(tt, T, g_z)
|
|
181
|
+
elif self.threshold_dynamic == 'hyperbolic':
|
|
182
|
+
a = threshold / (1 + decay*tt)
|
|
183
|
+
g_z, T = ie_fpt_hyperbolic(threshold, decay, 2*sigma**2, 0.000001, sigma=2*sigma**2, dt=approximation_step, T_max=rt.max())
|
|
184
|
+
fpt_z = np.interp(tt, T, g_z)
|
|
185
|
+
elif self.threshold_dynamic == 'custom':
|
|
186
|
+
threshold_function2 = lambda t: threshold_function(t)**2
|
|
187
|
+
dt_threshold_function2 = lambda t: 2 * dt_threshold_function(t) * threshold_function(t)
|
|
188
|
+
a = threshold_function(tt)
|
|
189
|
+
g_z, T = ie_fpt_custom(threshold_function2, dt_threshold_function2, 2*sigma**2, 0.000001, sigma=2*sigma**2, dt=approximation_step, T_max=rt.max())
|
|
190
|
+
fpt_z = np.interp(tt, T, g_z)
|
|
191
|
+
|
|
192
|
+
fpt_z = np.maximum(fpt_z, 0.1**14)
|
|
193
|
+
|
|
194
|
+
# Girsanov:
|
|
195
|
+
if s_v == 0:
|
|
196
|
+
# No drift variability
|
|
197
|
+
mu_dot_x0 = drift_vec[:, 0] * np.cos(theta)
|
|
198
|
+
mu_dot_x1 = drift_vec[:, 1] * np.sin(theta)
|
|
199
|
+
|
|
200
|
+
if s_t == 0:
|
|
201
|
+
# No non-decision time variability
|
|
202
|
+
term1 = a * (mu_dot_x0 + mu_dot_x1) / sigma**2
|
|
203
|
+
term2 = 0.5 * (drift_vec[:, 0]**2 + drift_vec[:, 1]**2) * tt / sigma**2
|
|
204
|
+
log_density = term1 - term2 + np.log(fpt_z) - np.log(2*np.pi)
|
|
205
|
+
else:
|
|
206
|
+
# With non-decision time variability
|
|
207
|
+
log_density = np.log(0.1**14) * np.ones(rt.shape[0])
|
|
208
|
+
eps = np.linspace(0, s_t, max(2, int(s_t//0.02)))
|
|
209
|
+
norm2_drift = drift_vec[:, 0]**2 + drift_vec[:, 1]**2
|
|
210
|
+
mu_dot_x = (mu_dot_x0 + mu_dot_x1) / sigma**2
|
|
211
|
+
for i in range(rt.shape[0]):
|
|
212
|
+
if tt[i] - s_t > 0:
|
|
213
|
+
if self.threshold_dynamic == 'fixed':
|
|
214
|
+
integrand = np.exp(- 0.5 * norm2_drift[i] * (tt[i] - eps)/sigma**2) * np.interp(tt[i]-eps, T, fpt_z)/s_t
|
|
215
|
+
density = np.exp(threshold * mu_dot_x[i]) * trapz_1d(integrand, eps) * 0.5/np.pi
|
|
216
|
+
elif self.threshold_dynamic == 'linear':
|
|
217
|
+
integrand = np.exp((threshold - decay * (tt[i] - eps)) * mu_dot_x[i] - 0.5 * norm2_drift[i] * (tt[i] - eps)/sigma**2) * np.interp(tt[i]-eps, T, fpt_z)/s_t
|
|
218
|
+
density = trapz_1d(integrand, eps) * 0.5/np.pi
|
|
219
|
+
elif self.threshold_dynamic == 'exponential':
|
|
220
|
+
integrand = np.exp(threshold*np.exp(-decay * (tt[i] - eps)) * mu_dot_x[i] - 0.5 * norm2_drift[i] * (tt[i] - eps)/sigma**2) * np.interp(tt[i]-eps, T, fpt_z)/s_t
|
|
221
|
+
density = trapz_1d(integrand, eps) * 0.5/np.pi
|
|
222
|
+
elif self.threshold_dynamic == 'hyperbolic':
|
|
223
|
+
integrand = np.exp(threshold/(1 + decay * (tt[i] - eps)) * mu_dot_x[i] - 0.5 * norm2_drift[i] * (tt[i] - eps)/sigma**2) * np.interp(tt[i]-eps, T, fpt_z)/s_t
|
|
224
|
+
density = trapz_1d(integrand, eps) * 0.5/np.pi
|
|
225
|
+
elif self.threshold_dynamic == 'custom':
|
|
226
|
+
integrand = np.exp(threshold_function(tt[i] - eps) * mu_dot_x[i] - 0.5 * norm2_drift[i] * (tt[i] - eps)/sigma**2) * np.interp(tt[i]-eps, T, fpt_z)/s_t
|
|
227
|
+
density = trapz_1d(integrand, eps) * 0.5/np.pi
|
|
228
|
+
|
|
229
|
+
if density > 0.1**14:
|
|
230
|
+
log_density[i] = np.log(density)
|
|
231
|
+
|
|
232
|
+
else:
|
|
233
|
+
# With drift variability
|
|
234
|
+
if s_t == 0:
|
|
235
|
+
# No non-decision time variability
|
|
236
|
+
s_v2 = s_v**2
|
|
237
|
+
x0 = a * np.cos(theta)
|
|
238
|
+
x1 = a * np.sin(theta)
|
|
239
|
+
fixed = 1/(np.sqrt(s_v2/sigma**2 * tt + 1))
|
|
240
|
+
exponent0 = -0.5*drift_vec[:, 0]**2/s_v2 + 0.5*(x0 * s_v2/sigma**2 + drift_vec[:, 0])**2 / (s_v2 * (s_v2/sigma**2 * tt + 1))
|
|
241
|
+
exponent1 = -0.5*drift_vec[:, 1]**2/s_v2 + 0.5*(x1 * s_v2/sigma**2 + drift_vec[:, 1])**2 / (s_v2 * (s_v2/sigma**2 * tt + 1))
|
|
242
|
+
|
|
243
|
+
log_density = 2*np.log(fixed) + exponent0 + exponent1 + np.log(fpt_z) - np.log(2*np.pi)
|
|
244
|
+
else:
|
|
245
|
+
# With non-decision time variability
|
|
246
|
+
log_density = np.log(0.1**14) * np.ones(rt.shape[0])
|
|
247
|
+
eps = np.linspace(0, s_t, max(2, int(s_t//0.02)))
|
|
248
|
+
s_v2 = s_v**2
|
|
249
|
+
for i in range(rt.shape[0]):
|
|
250
|
+
if tt[i] - s_t > 0:
|
|
251
|
+
if self.threshold_dynamic == 'fixed':
|
|
252
|
+
x0 = threshold * np.cos(theta[i])
|
|
253
|
+
x1 = threshold * np.sin(theta[i])
|
|
254
|
+
elif self.threshold_dynamic == 'linear':
|
|
255
|
+
x0 = (threshold - decay * (tt[i]-eps)) * np.cos(theta[i])
|
|
256
|
+
x1 = (threshold - decay * (tt[i]-eps)) * np.sin(theta[i])
|
|
257
|
+
elif self.threshold_dynamic == 'exponential':
|
|
258
|
+
x0 = (threshold * np.exp(-decay * (tt[i]-eps))) * np.cos(theta[i])
|
|
259
|
+
x1 = (threshold * np.exp(-decay * (tt[i]-eps))) * np.sin(theta[i])
|
|
260
|
+
elif self.threshold_dynamic == 'hyperbolic':
|
|
261
|
+
x0 = (threshold / (1 + decay * (tt[i]-eps))) * np.cos(theta[i])
|
|
262
|
+
x1 = (threshold / (1 + decay * (tt[i]-eps))) * np.sin(theta[i])
|
|
263
|
+
elif self.threshold_dynamic == 'custom':
|
|
264
|
+
x0 = threshold_function(tt[i]-eps) * np.cos(theta[i])
|
|
265
|
+
x1 = threshold_function(tt[i]-eps) * np.sin(theta[i])
|
|
266
|
+
fixed = 1/(np.sqrt(s_v2/sigma**2 * (tt[i]-eps) + 1))
|
|
267
|
+
exponent0 = -0.5*drift_vec[i, 0]**2/s_v2 + 0.5*(x0 * s_v2/sigma**2 + drift_vec[i, 0])**2 / (s_v2 * (s_v2/sigma**2 * (tt[i]-eps) + 1))
|
|
268
|
+
exponent1 = -0.5*drift_vec[i, 1]**2/s_v2 + 0.5*(x1 * s_v2/sigma**2 + drift_vec[i, 1])**2 / (s_v2 * (s_v2/sigma**2 * (tt[i]-eps) + 1))
|
|
269
|
+
|
|
270
|
+
integrand = fixed**2 * np.exp(exponent0 + exponent1) * np.interp(tt[i]-eps, T, fpt_z)/s_t
|
|
271
|
+
density = trapz_1d(integrand, eps) * 0.5/np.pi
|
|
272
|
+
if density > 0.1**14:
|
|
273
|
+
log_density[i] = np.log(density)
|
|
274
|
+
|
|
275
|
+
log_density[rt - ndt - s_t <= 0] = np.log(0.1**14)
|
|
276
|
+
log_density = np.maximum(log_density, np.log(0.1**14))
|
|
277
|
+
|
|
278
|
+
return log_density
|
|
279
|
+
|
|
280
|
+
|