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 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+