Skip to content

Commit 5b575e4

Browse files
committedAug 8, 2023
move from pystan to cmdstanpy
1 parent 6c2258f commit 5b575e4

11 files changed

+274
-126
lines changed
 

‎docs/modeling.ipynb

+172-76
Large diffs are not rendered by default.

‎docs/symp_talk_uit2019.ipynb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1776,7 +1776,7 @@
17761776
"name": "python",
17771777
"nbconvert_exporter": "python",
17781778
"pygments_lexer": "ipython3",
1779-
"version": "3.7.6"
1779+
"version": "3.8.3"
17801780
},
17811781
"rise": {
17821782
"enable_chalkboard": true,

‎pypillometry/baseline.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import scipy.signal as signal
99
import scipy
1010
import math
11+
import os
12+
import cmdstanpy
1113

1214
import scipy.interpolate
1315
from scipy.interpolate import interp1d, splrep, splev
@@ -290,7 +292,11 @@ def prominence_to_lambda(w, lam_min=1, lam_max=100):
290292
# load or compile model
291293
vprint(10, "Compiling Stan model")
292294

293-
sm = StanModel_cache(stan_code_baseline_model_asym_laplac)
295+
fname="stan/baseline_model_asym_laplac.stan"
296+
fpath=os.path.join(os.path.split(__file__)[0], fname)
297+
sm = cmdstanpy.CmdStanModel(stan_file=fpath)
298+
sm.compile()
299+
#sm = StanModel_cache(stan_code_baseline_model_asym_laplac)
294300

295301
## put the data for the model together
296302
data={
@@ -307,8 +313,8 @@ def prominence_to_lambda(w, lam_min=1, lam_max=100):
307313

308314
## variational optimization
309315
vprint(10, "Optimizing Stan model")
310-
opt=sm.vb(data=data)
311-
vbc=opt["mean_pars"]
316+
opt=sm.variational(data=data)
317+
vbc=opt.stan_variable("coef")
312318
meansigvb=np.dot(B, vbc)
313319
vprint(10, "Done optimizing Stan model")
314320

@@ -350,8 +356,8 @@ def prominence_to_lambda(w, lam_min=1, lam_max=100):
350356

351357
## variational optimization
352358
vprint(10, "2nd Optimizing Stan model")
353-
opt=sm.vb(data=data2)
354-
vbc2=opt["mean_pars"]
359+
opt=sm.variational(data=data2)
360+
vbc2=opt.stan_variable("coef")
355361
meansigvb2=np.dot(B2, vbc2)
356362
vprint(10, "Done 2nd Optimizing Stan model")
357363

@@ -475,7 +481,7 @@ def baseline_pupil_model(tx,sy,event_onsets, fs=1000, lp1=2, lp2=0.2):
475481
event_onsets_ix=np.argmin(np.abs(np.tile(event_onsets, (sy.size,1)).T-tx), axis=1)
476482

477483
# set up a single regressor
478-
x1=np.zeros(sy.size, dtype=np.float)
484+
x1=np.zeros(sy.size, dtype=float)
479485
x1[event_onsets_ix]=1
480486
kernel=pupil_kernel(4, fs=fs)
481487
x1=np.convolve(x1, kernel, mode="full")[0:x1.size]

‎pypillometry/convenience.py

-21
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
def nprange(ar):
1212
return (ar.min(),ar.max())
1313

14-
15-
import pystan
1614
import pickle
1715
from hashlib import md5
1816

@@ -68,25 +66,6 @@ def trans_logistic_vec(x, a, b, inverse=False):
6866

6967
return x
7068

71-
def StanModel_cache(model_code, model_name=None, **kwargs):
72-
"""Use just as you would `stan`"""
73-
code_hash = md5(model_code.encode('ascii')).hexdigest()
74-
if model_name is None:
75-
cache_fn = 'cached-model-{}.pkl'.format(code_hash)
76-
else:
77-
cache_fn = 'cached-{}-{}.pkl'.format(model_name, code_hash)
78-
try:
79-
sm = pickle.load(open(cache_fn, 'rb'))
80-
except:
81-
sm = pystan.StanModel(model_code=model_code)
82-
with open(cache_fn, 'wb') as f:
83-
pickle.dump(sm, f)
84-
else:
85-
print("Using cached StanModel")
86-
return sm
87-
88-
89-
9069
def plot_pupil_ipy(tx, sy, event_onsets=None, overlays=None, overlay_labels=None,
9170
blinks=None, interpolated=None,
9271
figsize=(16,8), xlab="ms", nsteps=100):

‎pypillometry/fakedata.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def generate_pupil_data(event_onsets, fs=1000, pad=5000, baseline_lowpass=0.2,
101101

102102
### real events regressor
103103
## scaling
104-
event_ix=(np.array(event_onsets)/1000.*fs).astype(np.int)
104+
event_ix=(np.array(event_onsets)/1000.*fs).astype(int)
105105
#a, b = (myclip_a - my_mean) / my_std, (myclip_b - my_mean) / my_std
106106
delta_weights=stats.truncnorm.rvs(-1/response_fluct_sd,np.inf, loc=1, scale=response_fluct_sd, size=event_ix.size)
107107
x1=np.zeros_like(sy)

‎pypillometry/preproc.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def detect_blinks_zero(sy, min_duration, blink_val=0):
139139
-------
140140
np.array (nblinks x 2) containing the indices of the start/end of the blinks
141141
"""
142-
x=np.r_[0, np.diff((sy==blink_val).astype(np.int))]
142+
x=np.r_[0, np.diff((sy==blink_val).astype(int))]
143143
starts=np.where(x==1)[0]
144144
ends=np.where(x==-1)[0]-1
145145
if sy[0]==blink_val: ## first value missing?
@@ -206,7 +206,7 @@ def blink_onsets_mahot(sy, blinks, smooth_winsize, vel_onset, vel_offset, margin
206206
onset=max(0, onset-margin[0]) # avoid overflow to the left
207207

208208
# find start of "reversal period" and move forward until it drops back
209-
offset_ix=np.argmin(np.abs(((offsets-endl<0)*np.iinfo(np.int).max)+(offsets-endl)))
209+
offset_ix=np.argmin(np.abs(((offsets-endl<0)*np.iinfo(int).max)+(offsets-endl)))
210210
while(offset_ix<(len(offsets)-1) and offsets[offset_ix+1]-1==offsets[offset_ix]):
211211
offset_ix+=1
212212
offset=offsets[offset_ix]

‎pypillometry/pupil.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def pupil_kernel(duration=4000, fs=1000, npar=10.1, tmax=930.0):
102102
sampled version of h(t) over the interval [0,`duration`] with sampling rate `fs`
103103
"""
104104
n=int(duration/1000.*fs)
105-
t = np.linspace(0,duration, n, dtype = np.float)
105+
t = np.linspace(0,duration, n, dtype = float)
106106
h=pupil_kernel_t(t,npar,tmax)
107107
#h = t**(npar) * np.exp(-npar*t / tmax) #Erlang gamma function Hoek & Levelt (1993)
108108
#hmax=np.exp(-npar)*tmax**npar ## theoretical maximum
@@ -159,7 +159,7 @@ def pupil_build_design_matrix(tx,event_onsets,fs,npar,tmax,max_duration="estimat
159159
h=pupil_kernel(duration=max_duration, fs=fs, npar=npar, tmax=tmax) ## pupil kernel
160160

161161
# event-onsets for each event
162-
x1 = np.zeros((event_onsets.size, tx.size), dtype=np.float) # onsets
162+
x1 = np.zeros((event_onsets.size, tx.size), dtype=float) # onsets
163163

164164
# event-onsets as indices of the txd array
165165
evon_ix=np.argmin(np.abs(np.tile(event_onsets, (tx.size,1)).T-tx), axis=1)
@@ -174,7 +174,7 @@ def pupil_build_design_matrix(tx,event_onsets,fs,npar,tmax,max_duration="estimat
174174
# h=pupil_kernel(duration=max_duration, fs=fs, npar=npar, tmax=tmax) ## pupil kernel
175175
#
176176
# # event-onsets for each event
177-
# x1 = np.zeros((event_onsets.size, tx.size), dtype=np.float) # onsets
177+
# x1 = np.zeros((event_onsets.size, tx.size), dtype=float) # onsets
178178
#
179179
# # event-onsets as indices of the txd array
180180
# evon_ix=np.argmin(np.abs(np.tile(event_onsets, (tx.size,1)).T-tx), axis=1)

‎pypillometry/pupildata.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,15 @@ def __init__(self,
165165
sometimes, when the eyetracker loses signal, no entry in the EDF is made;
166166
when this option is True, such entries will be made and the signal set to 0 there
167167
"""
168-
self.sy=np.array(pupil, dtype=np.float)
168+
self.sy=np.array(pupil, dtype=float)
169169
if sampling_rate is None and time is None:
170170
raise ValueError("you have to specify either sampling_rate or time-vector (or both)")
171171

172172
if time is None:
173173
maxT=len(self)/sampling_rate*1000.
174174
self.tx=np.linspace(0,maxT, num=len(self))
175175
else:
176-
self.tx=np.array(time, dtype=np.float)
176+
self.tx=np.array(time, dtype=float)
177177

178178
if sampling_rate is None:
179179
self.fs=np.round(1000./np.median(np.diff(self.tx)))
@@ -216,9 +216,9 @@ def __init__(self,
216216
self.sy=nsy
217217

218218
if event_onsets is None:
219-
self.event_onsets=np.array([], dtype=np.float)
219+
self.event_onsets=np.array([], dtype=float)
220220
else:
221-
self.event_onsets=np.array(event_onsets, dtype=np.float)
221+
self.event_onsets=np.array(event_onsets, dtype=float)
222222

223223
# check whether onsets are in range
224224
for onset in self.event_onsets:
@@ -253,12 +253,12 @@ def __init__(self,
253253
self.response_estimated=False
254254

255255
## initialize blinks
256-
self.blinks=np.empty((0,2), dtype=np.int)
257-
self.blink_mask=np.zeros(len(self), dtype=np.int)
256+
self.blinks=np.empty((0,2), dtype=int)
257+
self.blink_mask=np.zeros(len(self), dtype=int)
258258

259259
## interpolated mask
260-
self.interpolated_mask=np.zeros(len(self), dtype=np.int)
261-
self.missing=np.zeros(len(self), dtype=np.int)
260+
self.interpolated_mask=np.zeros(len(self), dtype=int)
261+
self.missing=np.zeros(len(self), dtype=int)
262262
self.missing[self.sy==0]=1
263263

264264
self.original=None
@@ -364,8 +364,8 @@ def sub_slice(self, start: float=-np.inf, end: float=np.inf, units: str="sec"):
364364
slic.event_onsets=slic.event_onsets[keepev]
365365
slic.event_labels=slic.event_labels[keepev]
366366
## just remove all detected blinks (need to rerun `detect_blinks()`)
367-
slic.blinks=np.empty((0,2), dtype=np.int)
368-
slic.blink_mask=np.zeros(len(slic), dtype=np.int)
367+
slic.blinks=np.empty((0,2), dtype=int)
368+
slic.blink_mask=np.zeros(len(slic), dtype=int)
369369
return slic
370370

371371
def summary(self) -> dict:
@@ -596,8 +596,8 @@ def _plot(self, plot_range, overlays, overlay_labels, units, interactive, highli
596596
overlays=(ov[startix:endix] for ov in overlays)
597597

598598
if interactive:
599-
blinks=np.empty((0,2), dtype=np.int)
600-
interpolated=np.empty((0,2), dtype=np.int)
599+
blinks=np.empty((0,2), dtype=int)
600+
interpolated=np.empty((0,2), dtype=int)
601601
if highlight_blinks:
602602
blinks=[]
603603
for sblink,eblink in self.blinks:
@@ -966,7 +966,7 @@ def blinks_detect(self, min_duration:float=20, blink_val:float=0,
966966
blinks=helper_merge_blinks(blinks_vel, blinks_zero)
967967
obj.blinks=np.array([[on,off] for (on,off) in blinks if off-on>=min_duration_ix])
968968

969-
obj.blink_mask=np.zeros(self.sy.size, dtype=np.int)
969+
obj.blink_mask=np.zeros(self.sy.size, dtype=int)
970970

971971
for start,end in obj.blinks:
972972
obj.blink_mask[start:end]=1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Stan model for pupil-baseline estimation
2+
//
3+
functions{
4+
// asymmetric laplace function with the mu, sigma, tau parametrization
5+
real my_skew_double_exponential_lpdf(real y, real mu, real sigma, real tau) {
6+
return log(tau) + log1m(tau)
7+
- log(sigma)
8+
- 2 * ((y < mu) ? (1 - tau) * (mu - y) : tau * (y - mu)) / sigma;
9+
}
10+
11+
// zero-centered asymmetric laplace function with the mu, lambda, kappa parametrization
12+
real skew_double_exponential2_lpdf(real y, real lam, real kappa) {
13+
return log(lam) - log(kappa+1/kappa)
14+
+ ((y<0) ? (lam/kappa) : (-lam*kappa))*(y);
15+
}
16+
}
17+
data{
18+
int<lower=1> n; // number of timepoints in the signal
19+
vector[n] sy; // the pupil signal
20+
21+
int<lower=1> ncol; // number of basis functions (columns in B)
22+
matrix[n,ncol] B; // spline basis functions
23+
24+
int<lower=1> npeaks; // number of lower peaks in the signal
25+
int<lower=1> peakix[npeaks]; // index of the lower peaks in sy
26+
vector<lower=0>[npeaks] lam_prominences; // lambda-converted prominence values
27+
28+
real<lower=0> lam_sig; // lambda for the signal where there is no peak
29+
real<lower=0,upper=1> pa; // proportion of allowed distribution below 0
30+
}
31+
32+
transformed data{
33+
vector[n] lam; // lambda at each timepoint
34+
real<lower=0> kappa; // calculated kappa from pa
35+
kappa=sqrt(pa)/sqrt(1-pa);
36+
37+
lam=rep_vector(lam_sig, n);
38+
for(i in 1:npeaks){
39+
lam[peakix[i]]=lam_prominences[i];
40+
}
41+
}
42+
parameters {
43+
vector[ncol] coef; // coefficients for the basis-functions
44+
}
45+
46+
transformed parameters{
47+
48+
}
49+
50+
model {
51+
{
52+
vector[n] d;
53+
54+
coef ~ normal(0,5);
55+
d=sy-(B*coef); // center at estimated baseline
56+
for( i in 1:n ){
57+
d[i] ~ skew_double_exponential2(lam[i], kappa);
58+
}
59+
}
60+
}

‎requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
numpy
22
scipy
3-
pystan
3+
cmdstanpy
44
matplotlib
55
pandas
66
typing

‎setup.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,19 @@
1818
long_description=long_description,
1919
long_description_content_type="text/markdown",
2020
url="https://github.com/ihrke/pypillometry",
21-
packages=setuptools.find_packages(),
21+
packages=setuptools.find_namespace_packages(),#where="pypillometry", exclude="tests"),
2222
classifiers=[
2323
"Programming Language :: Python :: 3",
2424
"License :: OSI Approved :: MIT License",
2525
"Operating System :: OS Independent",
2626
],
27+
#include_package_data=True,
28+
package_dir={"pypillometry": "pypillometry"},
29+
package_data={
30+
"pypillometry": [],
31+
"pypillometry.stan": ['*.stan'],
32+
"pypillometry.tests": []
33+
},
2734
install_requires=requirements,
28-
python_requires='>=3.6',
35+
python_requires='>=3.10',
2936
)

0 commit comments

Comments
 (0)
Please sign in to comment.