Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matlab test #66

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions psignifit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

# import here the main function
from .psignifit import psignifit
from ._configuration import Configuration
from ._pooling import pool_blocks
from . import _sigmoids
from .psigniplot import plot_bias_analysis
Expand Down
16 changes: 10 additions & 6 deletions psignifit/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from . import _sigmoids
from ._utils import PsignifitException
from ._typing import ExperimentType, Prior

from ._matlab import config_from_matlab

_PARAMETERS = {'threshold', 'width', 'lambda', 'gamma', 'eta'}

Expand Down Expand Up @@ -36,11 +36,10 @@ class Configuration:
"""
beta_prior: int = 10
CI_method: str = 'project'
confP: Tuple[float, float, float] = (.95, .9, .68)
confidence_intervals: Tuple[float, float, float] = (.95, .9, .68)
dekuenstle marked this conversation as resolved.
Show resolved Hide resolved
estimate_type: str = 'MAP'
experiment_type: str = ExperimentType.YES_NO.value
experiment_choices: Optional[int] = None
fast_optim: bool = False
fixed_parameters: Optional[Dict[str, float]] = None
grid_set_type: str = 'cumDist'
instant_plot: bool = False
Expand All @@ -64,10 +63,15 @@ class Configuration:
def __post_init__(self):
self.check_attributes()

@classmethod
def from_matlab_options(cls, option_dict: Dict[str, Any], **kwargs):
python_configs = config_from_matlab(option_dict, **kwargs)
return cls(**python_configs)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a helper function in the MATLAB tests

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this class function but exposed psignifit.config_from_matlab. This might be handy for users switching from MATLAB to Python


@classmethod
def from_dict(cls, config_dict: Dict[str, Any]):
config_dict = config_dict.copy()
return cls(confP=tuple(config_dict.pop('confP')),
return cls(confidence_intervals=tuple(config_dict.pop('confidence_intervals')),
**config_dict)

def as_dict(self) -> Dict[str, Any]:
Expand Down Expand Up @@ -138,7 +142,7 @@ def check_experiment_type(self, value):
if not (is_valid or is_nafc):
raise PsignifitException(
f'Invalid experiment type: "{value}"\nValid types: {valid_values},' +
', or "2AFC", "3AFC", etc...')
' or "2AFC", "3AFC", etc...')
if is_nafc:
self.experiment_choices = int(value[:-3])
self.experiment_type = ExperimentType.N_AFC.value
Expand Down Expand Up @@ -223,4 +227,4 @@ def make_sigmoid(self) -> _sigmoids.Sigmoid:
self.sigmoid.alpha = self.width_alpha
return self.sigmoid
else:
return _sigmoids.sigmoid_by_name(self.sigmoid, PC=self.thresh_PC, alpha=self.width_alpha)
return _sigmoids.sigmoid_by_name(self.sigmoid, PC=self.thresh_PC, alpha=self.width_alpha)
83 changes: 83 additions & 0 deletions psignifit/_matlab.py
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file should go to the tests folder

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above -> Matlab compatiblitiy can be useful for people switching from matlab

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
_CONFIG_KEYS_MAT2PY = {
'sigmoidName': 'sigmoid',
'expType': 'experiment_type',
'expN': 'experiment_choices',
'estimateType': 'estimate_type',
'confP': 'confidence_intervals',
'instantPlot': 'instant_plot',
'maxBorderValue': 'max_bound_value',
'moveBorders': 'move_bounds',
'dynamicGrid': None,
'widthalpha': 'width_alpha',
'threshPC': 'thresh_PC',
'CImethod': 'CI_method',
'gridSetType': 'grid_set_type',
'fixedPars': 'fixed_parameters',
'nblocks': 'pool_max_blocks',
'useGPU': None,
'poolMaxGap': None,
'poolMaxLength': None,
'poolxTol': None,
'betaPrior': 'beta_prior',
'verbose': 'verbose',
'stimulusRange': 'stimulus_range',
'fastOptim': None,
}
_CONFIG_KEYS_MAT_IGNORE = ('theta0')
_MATLAB_PARAMETERS = ['threshold', 'width', 'lambda', 'gamma', 'eta']


def param_matlist2pydict(param_list):
""" Transform parameter list from matlab-psignifit to the dict of python-psignifit. """
return {k: v
for k, v in zip(_MATLAB_PARAMETERS, param_list)
if v is not None}


def param_pydict2matlist(param_dict):
""" Transform parameter dict from python-psignifit to the list of matlab-psignifit. """
return [param_dict[p] for p in _MATLAB_PARAMETERS]


def _exptype_mat2py(mat_type):
mat2py = {
'YesNo': 'yes/no',
'equalAsymptote': 'equal asymptote',
}
if mat_type in mat2py:
return mat2py[mat_type]
else:
return mat_type


_CONFIG_VALUES_MAT2PY = {
'fixedPars': param_matlist2pydict,
'expType': _exptype_mat2py,
}


def config_from_matlab(matlab_config, raise_matlab_only=True):
""" Transform an option dict for matlab-psignifit to the configs
expected by python-psignifit.
"""
py_config = {}
for mat_key, mat_value in matlab_config.items():
if mat_key in _CONFIG_KEYS_MAT_IGNORE:
continue
elif mat_key not in _CONFIG_KEYS_MAT2PY:
raise ValueError(f"Unknown psignifit option '{mat_key}'.")

py_key = _CONFIG_KEYS_MAT2PY[mat_key]
if py_key is None:
if raise_matlab_only:
raise ValueError(f"Psignifit option '{mat_key}' is supported only in the matlab version.\n"
"Remove this from the configuration to use the python psignifit.")
else:
continue

if mat_key in _CONFIG_VALUES_MAT2PY:
py_config[py_key] = _CONFIG_VALUES_MAT2PY[mat_key](mat_value)
else:
py_config[py_key] = mat_value

return py_config
4 changes: 4 additions & 0 deletions psignifit/_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def masked_parameter_bounds(grid: Dict[str, Optional[np.ndarray]], mesh_mask: np
new_bounds = dict()
mask_indices = mesh_mask.nonzero()
for axis, (parameter_name, parameter_values) in enumerate(sorted(grid.items())):
if parameter_values is None:
new_bounds[parameter_name] = None
continue
dekuenstle marked this conversation as resolved.
Show resolved Hide resolved

indices = mask_indices[axis]
left, right = 0, len(parameter_values) - 1
if len(indices) > 0:
Expand Down
5 changes: 3 additions & 2 deletions psignifit/_priors.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ def setup_priors(custom_priors, bounds, stimulus_range, width_min, width_alpha,
priors.update(custom_priors)
check_priors(priors, stimulus_range, width_min)

for parameter, prior in priors.items():
priors[parameter] = normalize_prior(prior, bounds[parameter])
for parameter, bound in bounds.items():
if bound:
priors[parameter] = normalize_prior(priors[parameter], bound)
return priors


10 changes: 5 additions & 5 deletions psignifit/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def default(self, obj):

@dataclasses.dataclass
class Result:
parameter_estimate: Dict[str, float]
parameter_fit: Dict[str, float]
dekuenstle marked this conversation as resolved.
Show resolved Hide resolved
configuration: Configuration
confidence_intervals: Dict[str, List[Tuple[float, float]]]
data: Tuple[List[float], List[float], List[float]]
Expand Down Expand Up @@ -100,9 +100,9 @@ def threshold(self, percentage_correct: np.ndarray, unscaled: bool = False, retu
if unscaled: # set asymptotes to 0 for everything.
lambd, gamma = 0, 0
else:
lambd, gamma = self.parameter_estimate['lambda'], self.parameter_estimate['gamma']
new_threshold = sigmoid.inverse(percentage_correct, self.parameter_estimate['threshold'],
self.parameter_estimate['width'], lambd, gamma)
lambd, gamma = self.parameter_fit['lambda'], self.parameter_fit['gamma']
new_threshold = sigmoid.inverse(percentage_correct, self.parameter_fit['threshold'],
self.parameter_fit['width'], lambd, gamma)
if not return_ci:
return new_threshold

Expand All @@ -127,7 +127,7 @@ def slope(self, stimulus_level: np.ndarray) -> np.ndarray:
Returns:
Slopes of the psychometric function at the stimulus levels.
"""
stimulus_level, param = np.asarray(stimulus_level), self.parameter_estimate
stimulus_level, param = np.asarray(stimulus_level), self.parameter_fit
sigmoid = self.configuration.make_sigmoid()
return sigmoid.slope(stimulus_level, param['threshold'], param['width'], param['gamma'], param['lambda'])

Expand Down
2 changes: 1 addition & 1 deletion psignifit/demos/demo_003.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
# The most important result are the fitted parameters of the psychometric
# function. They can be found in a dictionary format.

print(res.parameter_estimate)
print(res.parameter_fit)

# %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# For each of these parameters, also the confidence interval is contained
Expand Down
4 changes: 2 additions & 2 deletions psignifit/demos/demo_004.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@
# First see that the only parameter whose fit changes by this is the
# beta-variance parameter eta (the 5th)

print('Fit with beta prior = 1: ', res1.parameter_estimate)
print('Fit with beta prior = 200: ', res200.parameter_estimate)
print('Fit with beta prior = 1: ', res1.parameter_fit)
print('Fit with beta prior = 200: ', res200.parameter_fit)

# Now we have a look at the confidence intervals
# TODO: uncomment once confidence intervals are implemented
Expand Down
23 changes: 14 additions & 9 deletions psignifit/psignifit.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,31 +83,36 @@ def psignifit(data: np.ndarray, conf: Optional[Configuration] = None,
bounds.update(conf.bounds)
if conf.fixed_parameters is not None:
for param, value in conf.fixed_parameters.items():
bounds[param] = (value, value)

if value is not None:
bounds[param] = (value, value)
priors = setup_priors(custom_priors=conf.priors, bounds=bounds,
stimulus_range=stimulus_range, width_min=width_min, width_alpha=conf.width_alpha,
beta_prior=conf.beta_prior, threshold_perc_correct=conf.thresh_PC)
fit_dict, posteriors, grid = _fit_parameters(data, bounds, priors, sigmoid, conf.steps_moving_bounds,
conf.max_bound_value, conf.grid_steps)

grid_values = [grid_value for _, grid_value in sorted(grid.items())]
intervals = confidence_intervals(posteriors, grid_values, conf.confP, conf.CI_method)
intervals_dict = {param: interval_per_p.tolist()
for param, interval_per_p in zip(sorted(grid.keys()), intervals)}
grid_none_ix = tuple(ix for ix, (param, value) in enumerate(sorted(grid.items())) if value is None)
grid_params = [param for param, value in grid.items() if value is not None]
grid_values = [grid[param] for param in grid_params]
intervals = confidence_intervals(np.squeeze(posteriors, axis=grid_none_ix), grid_values, conf.confidence_intervals, conf.CI_method)
intervals_dict = {param: intervals[ix].tolist() for ix, param in enumerate(grid_params)}
dekuenstle marked this conversation as resolved.
Show resolved Hide resolved
marginals = marginalize_posterior(grid, posteriors)

if conf.verbose:
_warn_marginal_sanity_checks(marginals)

if fit_dict['gamma'] is None: # equal asymptotes
fit_dict['gamma'] = fit_dict['lambda']
intervals_dict['gamma'] = intervals_dict['lambda']
dekuenstle marked this conversation as resolved.
Show resolved Hide resolved

if not return_posterior:
posteriors = None

return Result(parameter_estimate=fit_dict,
return Result(parameter_fit=fit_dict,
configuration=conf,
confidence_intervals=intervals_dict,
parameter_values={k: v.tolist() for k, v in grid.items()},
prior_values={param: priors[param](values).tolist() for param, values in grid.items()},
parameter_values={k: v.tolist() for k, v in grid.items() if v is not None},
prior_values={param: priors[param](values).tolist() for param, values in grid.items() if values is not None },
marginal_posterior_values={k: v.tolist() for k, v in marginals.items()},
posterior_mass=posteriors,
data=data.tolist())
Expand Down
12 changes: 6 additions & 6 deletions psignifit/psigniplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def plot_psychmetric_function(result: Result, # noqa: C901, this function is to
y_label='Proportion Correct'):
""" Plot oted psychometric function with the data.
"""
params = result.parameter_estimate
params = result.parameter_fit
data = np.asarray(result.data)
config = result.configuration

Expand Down Expand Up @@ -88,7 +88,7 @@ def plot_block_residuals(result: Result, ax: matplotlib.axes.Axes = plt.gca()) -


def _plot_residuals(x_values: np.ndarray, x_label: str, result: Result, ax: matplotlib.axes.Axes = plt.gca()):
params = result.parameter_estimate
params = result.parameter_fit
data = result.data
sigmoid = result.configuration.make_sigmoid()

Expand Down Expand Up @@ -178,7 +178,7 @@ def plot_marginal(result: Result,
ci_x = np.r_[CI[0], x[(x >= CI[0]) & (x <= CI[1])], CI[1]]
ax.fill_between(ci_x, np.zeros_like(ci_x), np.interp(ci_x, x, marginal), color=line_color, alpha=0.5)

param_value = result.parameter_estimate[parameter]
param_value = result.parameter_fit[parameter]
ax.plot([param_value] * 2, [0, np.interp(param_value, x, marginal)], color=line_color)

if plot_prior:
Expand Down Expand Up @@ -229,12 +229,12 @@ def plot_prior(result: Result,

parameter_keys = ['threshold', 'width', 'lambda']
sigmoid_x = np.linspace(stimulus_range[0], stimulus_range[1], 10000)
sigmoid_params = {param: result.parameter_estimate[param] for param in parameter_keys}
sigmoid_params = {param: result.parameter_fit[param] for param in parameter_keys}
for i, param in enumerate(parameter_keys):
prior_x = params[param]
weights = convolve(np.diff(prior_x), np.array([0.5, 0.5]))
cumprior = np.cumsum(priors[param] * weights)
x_percentiles = [result.parameter_estimate[param], min(prior_x), prior_x[-cumprior[cumprior >= .25].size],
x_percentiles = [result.parameter_fit[param], min(prior_x), prior_x[-cumprior[cumprior >= .25].size],
prior_x[-cumprior[cumprior >= .75].size], max(prior_x)]
plt.subplot(2, 3, i + 1)
plt.plot(params[param], priors[param], lw=line_width, c=line_color)
Expand All @@ -261,7 +261,7 @@ def plot_2D_margin(result: Result,
if result.posterior_mass is None:
ValueError("Expects posterior_mass in result, got None. You could try psignifit(return_posterior=True).")

parameter_indices = {param: i for i, param in enumerate(sorted(result.parameter_estimate.keys()))}
parameter_indices = {param: i for i, param in enumerate(sorted(result.parameter_fit.keys()))}
other_param_ix = tuple(i for param, i in parameter_indices.items()
if param != first_param and param != second_param)
marginal_2d = np.sum(result.posterior_mass, axis=other_param_ix)
Expand Down
Binary file not shown.
Loading