class OverlayNone(Overlay): """No overlay. An identity function for Solutions. Example usage: | overlay = OverlayNone() """ name = "No overlay" required_parameters = [] @staticmethod def _test(v): pass @staticmethod def _generate(): yield OverlayNone() @accepts(Self, Solution) @returns(Solution) def apply(self, solution): return solution @accepts(Self, NDArray(d=1, t=Number)) @returns(NDArray(d=1, t=Number)) def apply_trajectory(self, trajectory, **kwargs): return trajectory
class OverlayNonDecision(Overlay): """Add a non-decision time This shifts the reaction time distribution by `nondectime` seconds in order to create a non-decision time. Example usage: | overlay = OverlayNonDecision(nondectime=.2) """ name = "Add a non-decision by shifting the histogram" required_parameters = ["nondectime"] @staticmethod def _test(v): assert v.nondectime in Number(), "Invalid non-decision time" @staticmethod def _generate(): yield OverlayNonDecision(nondectime=0) yield OverlayNonDecision(nondectime=.5) yield OverlayNonDecision(nondectime=-.5) @accepts(Self, Solution) @returns(Solution) @ensures("set(return.corr.tolist()) - set(solution.corr.tolist()).union({0.0}) == set()") @ensures("set(return.err.tolist()) - set(solution.err.tolist()).union({0.0}) == set()") @ensures("solution.prob_undecided() <= return.prob_undecided()") def apply(self, solution): corr = solution.corr err = solution.err m = solution.model cond = solution.conditions undec = solution.undec evolution = solution.evolution shifts = int(self.nondectime/m.dt) # truncate newcorr = np.zeros(corr.shape, dtype=corr.dtype) newerr = np.zeros(err.shape, dtype=err.dtype) if shifts > 0: newcorr[shifts:] = corr[:-shifts] newerr[shifts:] = err[:-shifts] elif shifts < 0: newcorr[:shifts] = corr[-shifts:] newerr[:shifts] = err[-shifts:] else: newcorr = corr newerr = err return Solution(newcorr, newerr, m, cond, undec, evolution) @accepts(Self, NDArray(d=1, t=Number), Unchecked) @returns(NDArray(d=1, t=Number)) def apply_trajectory(self, trajectory, model, **kwargs): shift = int(self.nondectime/model.dt) if shift > 0: trajectory = np.append([trajectory[0]]*shift, trajectory) elif shift < 0: if len(trajectory) > abs(shift): trajectory = trajectory[abs(shift):] else: trajectory = np.asarray([trajectory[-1]]) return trajectory
class OverlayNonDecisionGamma(Overlay): """Add a gamma-distributed non-decision time This shifts the reaction time distribution by an amount of time specified by the gamma distribution with shape parameter `shape` (sometimes called "k") and scale parameter `scale` (sometimes called "theta"). The distribution is then further shifted by `nondectime` seconds. Example usage: | overlay = OverlayNonDecisionGamma(nondectime=.2, shape=1.5, scale=.05) """ name = "Add a gamma-distributed non-decision time" required_parameters = ["nondectime", "shape", "scale"] @staticmethod def _test(v): assert v.nondectime in Number(), "Invalid non-decision time" assert v.shape in Positive0(), "Invalid shape parameter" assert v.shape >= 1, "Shape parameter must be >= 1" assert v.scale in Positive(), "Invalid scale parameter" @staticmethod def _generate(): yield OverlayNonDecisionGamma(nondectime=.3, shape=2, scale=.01) yield OverlayNonDecisionGamma(nondectime=0, shape=1.1, scale=.1) @accepts(Self, Solution) @returns(Solution) @ensures("np.sum(return.corr) <= np.sum(solution.corr)") @ensures("np.sum(return.err) <= np.sum(solution.err)") @ensures( "np.all(return.corr[0:int(self.nondectime//return.model.dt)] == 0)") def apply(self, solution): # Make sure params are within range assert self.shape >= 1, "Invalid shape parameter" assert self.scale > 0, "Invalid scale parameter" # Extract components of the solution object for convenience corr = solution.corr err = solution.err dt = solution.model.dt # Create the weights for different timepoints times = np.asarray(list(range(-len(corr), len(corr)))) * dt weights = scipy.stats.gamma(a=self.shape, scale=self.scale, loc=self.nondectime).pdf(times) if np.sum(weights) > 0: weights /= np.sum(weights) # Ensure it integrates to 1 newcorr = np.convolve(corr, weights, mode="full")[len(corr):(2 * len(corr))] newerr = np.convolve(err, weights, mode="full")[len(corr):(2 * len(corr))] return Solution(newcorr, newerr, solution.model, solution.conditions, solution.undec, solution.evolution) @accepts(Self, NDArray(d=1, t=Number), Unchecked) @returns(NDArray(d=1, t=Number)) def apply_trajectory(self, trajectory, model, **kwargs): ndtime = scipy.stats.gamma(a=self.shape, scale=self.scale, loc=self.nondectime).rvs() shift = int(ndtime / model.dt) if shift > 0: np.append([trajectory[0]] * shift, trajectory) elif shift < 0: if len(trajectory) > abs(shift): trajectory = trajectory[abs(shift):] else: trajectory = np.asarray([trajectory[-1]]) return trajectory
class OverlayNonDecisionUniform(Overlay): """Add a uniformly-distributed non-decision time. The center of the distribution of non-decision times is at `nondectime`, and it extends `halfwidth` on each side. Example usage: | overlay = OverlayNonDecisionUniform(nondectime=.2, halfwidth=.02) """ name = "Uniformly-distributed non-decision time" required_parameters = ["nondectime", "halfwidth"] @staticmethod def _test(v): assert v.nondectime in Number(), "Invalid non-decision time" assert v.halfwidth in Positive0(), "Invalid halfwidth parameter" @staticmethod def _generate(): yield OverlayNonDecisionUniform(nondectime=.3, halfwidth=.01) yield OverlayNonDecisionUniform(nondectime=0, halfwidth=.1) @accepts(Self, Solution) @returns(Solution) @ensures("np.sum(return.corr) <= np.sum(solution.corr)") @ensures("np.sum(return.err) <= np.sum(solution.err)") def apply(self, solution): # Make sure params are within range assert self.halfwidth >= 0, "Invalid st parameter" # Extract components of the solution object for convenience corr = solution.corr err = solution.err m = solution.model cond = solution.conditions undec = solution.undec evolution = solution.evolution # Describe the width and shift of the uniform distribution in # terms of list indices shift = int(self.nondectime / m.dt) # Discretized non-decision time width = int(self.halfwidth / m.dt) # Discretized uniform distribution half-width offsets = list(range(shift - width, shift + width + 1)) # Create new correct and error distributions and iteratively # add shifts of each distribution to them. Use this over the # np.convolution because it handles negative non-decision # times. newcorr = np.zeros(corr.shape, dtype=corr.dtype) newerr = np.zeros(err.shape, dtype=err.dtype) for offset in offsets: if offset > 0: newcorr[offset:] += corr[:-offset] / len(offsets) newerr[offset:] += err[:-offset] / len(offsets) elif offset < 0: newcorr[:offset] += corr[-offset:] / len(offsets) newerr[:offset] += err[-offset:] / len(offsets) else: newcorr += corr / len(offsets) newerr += err / len(offsets) return Solution(newcorr, newerr, m, cond, undec, evolution) @accepts(Self, NDArray(d=1, t=Number), Unchecked) @returns(NDArray(d=1, t=Number)) def apply_trajectory(self, trajectory, model, **kwargs): ndtime = np.random.rand() * 2 * self.halfwidth + (self.nondectime - self.halfwidth) shift = int(ndtime / model.dt) if shift > 0: np.append([trajectory[0]] * shift, trajectory) elif shift < 0: if len(trajectory) > abs(shift): trajectory = trajectory[abs(shift):] else: trajectory = np.asarray([trajectory[-1]]) return trajectory
class OverlayChain(Overlay): """Join together multiple overlays. Unlike other model components, Overlays are not mutually exclusive. It is possible to transform the output solution many times. Thus, this allows joining together multiple Overlay objects into a single object. It accepts one parameter: `overlays`. This should be a list of Overlay objects, in the order which they should be applied to the Solution object. One key technical caveat is that the overlays which are chained together may not have the same parameter names. Parameter names must be given different names in order to be a part of the same overlay. This allows those parameters to be accessed by their name inside of an OverlayChain object. Example usage: | overlay = OverlayChain(overlays=[OverlayNone(), OverlayNone(), OverlayNone()]) # Still equivalent to OverlayNone | overlay = OverlayChain(overlays=[OverlayPoissonMixture(pmixturecoef=.01, rate=1), | OverlayUniformMixture(umixturecoef=.01)]) # Apply a Poission mixture and then a Uniform mixture """ name = "Chain overlay" required_parameters = ["overlays"] @staticmethod def _test(v): assert v.overlays in List( Overlay), "overlays must be a list of Overlay objects" @staticmethod def _generate(): yield OverlayChain(overlays=[OverlayNone()]) yield OverlayChain(overlays=[ OverlayUniformMixture(umixturecoef=.3), OverlayPoissonMixture(pmixturecoef=.2, rate=.7) ]) yield OverlayChain(overlays=[ OverlayNonDecision(nondectime=.1), OverlayPoissonMixture(pmixturecoef=.1, rate=1), OverlayUniformMixture(umixturecoef=.1) ]) def __init__(self, **kwargs): Overlay.__init__(self, **kwargs) object.__setattr__(self, "required_parameters", []) object.__setattr__(self, "required_conditions", []) for o in self.overlays: self.required_parameters.extend(o.required_parameters) self.required_conditions.extend(o.required_conditions) assert len(self.required_parameters) == len( set(self.required_parameters) ), "Two overlays in chain cannot have the same parameter names" object.__setattr__(self, "required_conditions", list(set( self.required_conditions))) # Avoid duplicates def __setattr__(self, name, value): if "required_parameters" in self.__dict__: if name in self.required_parameters: for o in self.overlays: if name in o.required_parameters: return setattr(o, name, value) return Overlay.__setattr__(self, name, value) def __getattr__(self, name): if name in self.required_parameters: for o in self.overlays: if name in o.required_parameters: return getattr(o, name) else: return Overlay.__getattribute__(self, name) def __repr__(self): overlayreprs = list(map(repr, self.overlays)) return "OverlayChain(overlays=[" + ", ".join(overlayreprs) + "])" @accepts(Self, Solution) @returns(Solution) def apply(self, solution): assert isinstance(solution, Solution) newsol = solution for o in self.overlays: newsol = o.apply(newsol) return newsol @accepts(Self, NDArray(d=1, t=Number)) @returns(NDArray(d=1, t=Number)) @paranoidconfig(unit_test=False) def apply_trajectory(self, trajectory, **kwargs): for o in self.overlays: trajectory = o.apply_trajectory(trajectory=trajectory, **kwargs) return trajectory