Skip to content

Commit

Permalink
Merge pull request #94 from lschwetlick/remove_logspace
Browse files Browse the repository at this point in the history
removes logspace option
  • Loading branch information
otizonaizit authored Sep 19, 2024
2 parents 7cd7411 + c694a1d commit 43fecc9
Show file tree
Hide file tree
Showing 6 changed files with 38 additions and 52 deletions.
2 changes: 0 additions & 2 deletions psignifit/_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ def threshold(self, percentage_correct: np.ndarray, unscaled: bool = False, retu
thresholds: stimulus values for all provided percentage_correct (if return_ci=False)
(thresholds, ci): stimulus values along with confidence intervals
For the sigmoids in logspace this also returns values in the linear
stimulus level domain.
"""
percentage_correct = np.asarray(percentage_correct)
sigmoid = self.configuration.make_sigmoid()
Expand Down
43 changes: 15 additions & 28 deletions psignifit/_sigmoids.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
""" All sigmoid functions.
If you add a new sigmoid type, add it to the CLASS_BY_NAME constant
and to the _LOGSPACE_NAMES, if it expects stimulus on an exponential scale.
"""
from typing import Optional, TypeVar

Expand Down Expand Up @@ -32,7 +31,7 @@
class Sigmoid:
""" Base class for sigmoid implementation.
Handels logarithmic input and negative output
Handles logarithmic input and negative output
for the specific sigmoid implementations.
Sigmoid classes should derive from this class and implement
Expand All @@ -48,20 +47,16 @@ class Sigmoid:
psi(X_(1-alpha)) = 0.95 = 1-alpha
psi(X_(alpha)) = 0.05 = alpha
"""
logspace = False
negate = False

def __init__(self, PC=0.5, alpha=0.05, negative=False, logspace=False):
def __init__(self, PC=0.5, alpha=0.05, negative=False):
"""
Args:
PC: Percentage correct (sigmoid function value) at threshold
alpha: Scaling parameter
negative: Flip sigmoid such percentage correct is decreasing.
logspace: Expect log-scaled stimulus correct
"""
self.alpha = alpha
self.negative = negative
self.logspace = logspace
if negative:
self.PC = 1 - PC
else:
Expand All @@ -71,8 +66,7 @@ def __eq__(self, o: object) -> bool:
return (isinstance(o, self.__class__)
and o.PC == self.PC
and o.alpha == self.alpha
and o.negative == self.negative
and o.logspace == self.logspace)
and o.negative == self.negative)

def __call__(self, stimulus_level: N, threshold: N, width: N) -> N:
""" Calculate the sigmoid value at specified stimulus levels.
Expand All @@ -86,8 +80,6 @@ def __call__(self, stimulus_level: N, threshold: N, width: N) -> N:
Returns:
Percentage correct at the stimulus values.
"""
if self.logspace:
stimulus_level = np.log(stimulus_level)
value = self._value(stimulus_level, threshold, width)

if self.negative:
Expand All @@ -107,8 +99,6 @@ def slope(self, stimulus_level: N, threshold: N, width: N, gamma: N = 0, lambd:
Returns:
Slope at the stimulus values.
"""
if self.logspace:
stimulus_level = np.log(stimulus_level)

slope = (1 - gamma - lambd) * self._slope(stimulus_level, threshold, width)

Expand Down Expand Up @@ -143,10 +133,7 @@ def inverse(self, perc_correct: N, threshold: N, width: N,
perc_correct = 1 - perc_correct

result = self._inverse(perc_correct, threshold, width)
if self.logspace:
return np.exp(result)
else:
return result
return result

def _value(self, stimulus_level: np.ndarray, threshold: np.ndarray, width: np.ndarray) -> np.ndarray:
raise NotImplementedError("This should be overwritten by an implementation.")
Expand Down Expand Up @@ -192,9 +179,6 @@ def assert_sanity_checks(self, n_samples: int, threshold: float, width: float):
threshold_stimulus_level = threshold
if self.negative:
stimulus_levels = 1 - stimulus_levels
if self.logspace:
threshold_stimulus_level = np.exp(threshold)
stimulus_levels = np.exp(stimulus_levels)

# sigmoid(threshold_stimulus_level) == threshold_percent_correct
np.testing.assert_allclose(self(threshold_stimulus_level, threshold, width), self.PC)
Expand Down Expand Up @@ -308,6 +292,16 @@ def _inverse(self, perc_correct: np.ndarray, threshold: np.ndarray, width: np.nd
return (t1icdf(perc_correct) - t1icdf(self.PC)) * width / C + threshold


class Weibull(Gumbel):
""" Sigmoid based on the Weibull function.
IMPORTANT: All the sigmoids in `psignifit` work in linear space. This sigmoid class is an
alias for the `Gumbel` class. It is left to the user to transform stimulus values to
logarithmic space.
"""
pass


_CLASS_BY_NAME = {
'norm': Gaussian,
'gauss': Gaussian,
Expand All @@ -317,14 +311,9 @@ def _inverse(self, perc_correct: np.ndarray, threshold: np.ndarray, width: np.nd
'tdist': Student,
'student': Student,
'heavytail': Student,
'weibull': Gumbel,
'logn': Gaussian,
'weibull': Weibull,
}

_LOGSPACE_NAMES = [
'weibull',
'logn'
]

ALL_SIGMOID_NAMES = set(_CLASS_BY_NAME.keys())
ALL_SIGMOID_NAMES |= {'neg_' + name for name in ALL_SIGMOID_NAMES}
Expand Down Expand Up @@ -354,7 +343,5 @@ def sigmoid_by_name(name, PC=None, alpha=None):
if name.startswith('neg_'):
name = name[4:]
kwargs['negative'] = True
if name in _LOGSPACE_NAMES:
kwargs['logspace'] = True

return _CLASS_BY_NAME[name](**kwargs)
5 changes: 1 addition & 4 deletions psignifit/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ class fp_error_handler(np.errstate):
pass


def check_data(data: np.ndarray, logspace: Optional[bool] = None) -> np.ndarray:
def check_data(data: np.ndarray) -> np.ndarray:
""" Check data format, type and range.
Args:
data: The data matrix with columns levels, number of correct and number of trials
logspace: Data should be used logarithmically. If None, no test on logspace is performed.
Returns:
data as float numpy array
Raises:
Expand All @@ -47,7 +46,5 @@ def check_data(data: np.ndarray, logspace: Optional[bool] = None) -> np.ndarray:
if not np.allclose(ntrials, ntrials.astype(int)):
raise PsignifitException('The number of trials column contains non'
' integer numbers!')
if logspace is True and levels.min() < 0:
raise PsignifitException(f'Sigmoid {data.sigmoid} expects non-negative stimulus level data.')

return data
5 changes: 3 additions & 2 deletions psignifit/demos/demo_001.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@
# 'tdist' Student t-distribution with df=1 for heavy tailed functions.
# ==================== ================================================
#
# For positive data on a log-scale, you may want to use one of the following:
# For positive data on a log-scale, we define the 'weibull' sigmoid class. Notice that it is left
# to the user to transform the stimulus level in logarithmic space, and the threshold and width
# back to linear space. 'weibull' is therefore just an alias for 'gumbel'.
#
# ==================== ================================================
# 'logn' Cumulative log-normal distribution.
# 'weibull' Weibull distribution.
# ==================== ================================================
#
Expand Down
18 changes: 8 additions & 10 deletions psignifit/psignifit.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,15 @@

def psignifit(data: np.ndarray, conf: Optional[Configuration] = None,
return_posterior: bool = False, **kwargs) -> Result:
"""
Main function for fitting psychometric functions function
""" Fit a psychometric function to experimental data.
This function is the user interface for fitting psychometric functions to data.
Notice that the parameters of the psychometric function are always fit in linear space, even
for psychometric function that are supposed to work in a logarithmic space, like the Weibull
function. It is left to the user to transform the stimulus level to logarithmic space before
calling this function.
pass your data in the n x 3 matrix of the form:
[x-value, number correct, number of trials]
Expand Down Expand Up @@ -51,7 +55,7 @@ def psignifit(data: np.ndarray, conf: Optional[Configuration] = None,
"Can't handle conf together with other keyword arguments!")

sigmoid = conf.make_sigmoid()
data = check_data(data, logspace=sigmoid.logspace)
data = check_data(data)

levels, ntrials = data[:, 0], data[:, 2]
if conf.verbose:
Expand All @@ -60,13 +64,7 @@ def psignifit(data: np.ndarray, conf: Optional[Configuration] = None,

stimulus_range = conf.stimulus_range
if stimulus_range is None:
if sigmoid.logspace:
stimulus_range = (levels[levels > 0].min(), levels.max())
else:
stimulus_range = (levels.min(), levels.max())
if sigmoid.logspace:
stimulus_range = (np.log(stimulus_range[0]), np.log(stimulus_range[1]))
levels = np.log(levels)
stimulus_range = (levels.min(), levels.max())

width_min = conf.width_min
if width_min is None:
Expand Down
17 changes: 11 additions & 6 deletions psignifit/tests/test_sigmoids.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,11 @@
ALPHA = 0.05


# list of all sigmoids (after having removed aliases)
LOG_SIGS = ('weibull', 'logn', 'neg_weibull', 'neg_logn')


def test_ALL_SIGMOID_NAMES():
TEST_SIGS = (
'norm', 'gauss', 'neg_norm', 'neg_gauss', 'logistic', 'neg_logistic',
'gumbel', 'neg_gumbel', 'rgumbel', 'neg_rgumbel',
'logn', 'neg_logn', 'weibull', 'neg_weibull',
'weibull', 'neg_weibull',
'tdist', 'student', 'heavytail', 'neg_tdist', 'neg_student', 'neg_heavytail')
for name in TEST_SIGS:
assert name in _sigmoids.ALL_SIGMOID_NAMES
Expand All @@ -37,7 +33,6 @@ def test_sigmoid_by_name(sigmoid_name):
s = _sigmoids.sigmoid_by_name(sigmoid_name, PC=PC, alpha=ALPHA)
assert isinstance(s, _sigmoids.Sigmoid)

assert (sigmoid_name in LOG_SIGS) == s.logspace
assert sigmoid_name.startswith('neg_') == s.negative


Expand All @@ -53,3 +48,13 @@ def test_sigmoid_sanity_check(sigmoid_name):
sigmoid.assert_sanity_checks(n_samples=100,
threshold=THRESHOLD_PARAM,
width=WIDTH_PARAM)


@pytest.mark.parametrize('sigmoid_name', _sigmoids.ALL_SIGMOID_NAMES)
def test_sigmoid_roundtrip(sigmoid_name):
sigmoid = _sigmoids.sigmoid_by_name(sigmoid_name, PC=PC, alpha=ALPHA)
x = 0.5
y = sigmoid(x, THRESHOLD_PARAM, WIDTH_PARAM)
reverse_x = sigmoid.inverse(y, THRESHOLD_PARAM, WIDTH_PARAM)
assert np.isclose(x, reverse_x, atol=1e-6)

0 comments on commit 43fecc9

Please sign in to comment.