@@ -705,7 +705,7 @@ def display_component(component, prefix=""):
705705 else :
706706 print (OUT )
707707
708- def gddm (drift = 0 , noise = 1 , bound = 1 , nondecision = 0 , starting_position = 0 , mixture_coef = .02 , name = "" , parameters = {}, conditions = [], dx = param .dx , dt = param .dt , T_dur = param .T_dur , choice_names = param .choice_names ):
708+ def gddm (drift = 0 , noise = 1 , bound = 1 , nondecision = 0 , starting_position = 0 , mixture_coef = .02 , mixture_distribution = None , name = "" , parameters = {}, conditions = [], dx = param .dx , dt = param .dt , T_dur = param .T_dur , choice_names = param .choice_names ):
709709 """Return a model without the use of PyDDM's object-oriented interface.
710710
711711 PyDDM has two interfaces: this one (the gddm function), and the
@@ -783,10 +783,31 @@ def gddm(drift=0, noise=1, bound=1, nondecision=0, starting_position=0, mixture_
783783 return a vector of the same size as "T". Otherwise, non-decision time
784784 will be assumed to be a single point.
785785
786- - `mixturecoef`: The uniform distribution mixture model, used to allow
787- likelihood fitting. Can not be given by a function, and must be either a
788- constant or a parameter. The uniform mixture model can be disabled by
789- setting this to 0.
786+ - `mixture_coef`: The probability of a "lapse" trial, i.e., the probability
787+ of sampling from a distribution other than the DDM (by default a uniform
788+ distribution). This is used to facilitate likelihood fitting. This can be
789+ either a constant, a parameter, or a function. The mixture model can be
790+ disabled by setting this to 0. If a function, it can accept parameters,
791+ conditions, or "upper", a boolean which allows different mixture
792+ coefficients for the upper and lower boundaries, and return a number 0-1.
793+ If "upper" is not an argument, the return value should be interpreted as
794+ the overall probability of a lapse, i.e., sampling from the mixture
795+ distribution instead of the DDM. If "upper" is an argument, the return
796+ value should be interpreted as the probability of a lapse to the given
797+ side, and the sum of the return values for upper=True and upper=False
798+ should never be greater than 1. By default, the mixture model uses a
799+ uniform distribution, but this can be changed with the
800+ "mixture_distribution" parameter.
801+
802+ - `mixture_distribution`: Use a distribution other than the uniform
803+ distribution as a mixture model. This should still integrate to 1
804+ (i.e. sum to 1/dt). If it integrates to less than 1, these will be
805+ considered undecided trials. If it integrates to more than 1, it will
806+ throw an error. This should accept the argument "T", a vector of times
807+ from 0 to T_dur, and return a vector of the same size as "T". It may
808+ optionally accept the argument "upper", where the distribution may be
809+ specified separately for the upper and lower boundaries. If "upper" is
810+ specified, the two sides must each integrate to 1 on their own.
790811
791812 Other parameters:
792813
@@ -818,14 +839,14 @@ def _parse_dep(val, name, special="xt"):
818839 """Determine whether `val` can be turned into a PyDDM model, and if so, parse relevant information.
819840
820841 `name` is the dependence name for error message outputs.
821- `special` is either "", "x", "t", "T", or "xt", describing what the dependence supports.
842+ `special` is either "", "x", "t", "T", "xt", or "uT" describing what the dependence supports.
822843 """
823844 if val in conditions :
824845 val = eval (f"lambda { val } : { val } " )
825846 if val in parameters .keys ():
826- return "val" ,None ,_fittables [val ]
847+ return "val" ,([],[],[]) ,_fittables [val ]
827848 elif isinstance (val , (int ,float ,np .floating ,np .integer )):
828- return "val" ,None ,val
849+ return "val" ,([],[],[]) ,val
829850 elif hasattr (val , "__call__" ):
830851 sig = inspect .getfullargspec (val )
831852 assert len (sig .kwonlyargs ) == 0 , f"Keyword only args not supported for { name } "
@@ -841,10 +862,11 @@ def _parse_dep(val, name, special="xt"):
841862 _required_parameters .append (arg )
842863 elif arg in conditions :
843864 _required_conditions .append (arg )
844- elif arg in ["x" , "t" , "T" ]:
865+ elif arg in ["x" , "t" , "T" , "upper" ]:
845866 _descr = {"x" : "the vector of particle positions" ,
846867 "t" : "the current time in the simulation" ,
847- "T" : "the vector of all time points in the simulation" }
868+ "T" : "the vector of all time points in the simulation" ,
869+ "upper" : "the side of the distribution (for mixture models)" }
848870 assert arg in special , f"In PyDDM, the '{ arg } ' argument usually indicates { _descr [arg ]} , but this argument cannot be used in the { name } function."
849871 _required_xt .append (arg )
850872 else :
@@ -981,11 +1003,63 @@ def get_nondecision_time(self, conditions):
9811003 return nondecision (** {v : getattr (self , v ) for v in _required_parameters_nd }, ** {v : conditions [v ] for v in _required_conditions_nd })
9821004 overlayobjs .append (OverlayNonDecisionEasy (** {fname :fval for fname ,fval in _fittables .items () if fname in _required_parameters_nd }))
9831005
984- typ , parsed , mixture_coef = _parse_dep (mixture_coef , "mixture_coef" , "" )
1006+ # Split mixture distribution into two parts: the mixture coefficient, and the mixture distribution
1007+ mixture_coef_name = mixture_coef
1008+ _mixc_typ , parsed , mixture_coef = _parse_dep (mixture_coef , "mixture_coef" , ["upper" ])
1009+ _required_parameters_mixc ,_required_conditions_mixc ,_required_u_mixc = parsed
1010+ if isinstance (mixture_coef , Fittable ):
1011+ _required_parameters_mixc = list (set (_required_parameters_mixc + [mixture_coef_name ]))
9851012 # If mixture coefficient is a parameter or value
986- if typ == "val" :
987- overlayobjs .append (OverlayUniformMixture (umixturecoef = mixture_coef ))
988- # If it is a function
989- elif typ == "func" :
990- raise ValueError ("mixture_coef cannot be a function here, please use the full object oriented version of PyDDM for this functionality." )
1013+ if mixture_distribution is None :
1014+ mixture_distribution = lambda T : np .ones (len (T ))/ (np .max (T )+ T [1 ]- T [0 ])
1015+ typ , parsed , mixture_dist = _parse_dep (mixture_distribution , "mixture_distribution" , ["upper" , "T" ])
1016+ if typ == "func" :
1017+ _required_parameters_mix ,_required_conditions_mix ,_required_uT_mix = parsed
1018+ if "T" in _required_uT_mix :
1019+ class OverlayMixtureEasy (Overlay ):
1020+ name = "easy_mixture_model"
1021+ required_parameters = list (set (_required_parameters_mix + _required_parameters_mixc ))
1022+ required_conditions = list (set (_required_conditions_mix + _required_conditions_mixc ))
1023+ def apply (self , solution ):
1024+ assert isinstance (solution , Solution )
1025+ choice_upper = solution .choice_upper
1026+ choice_lower = solution .choice_lower
1027+ m = solution .model
1028+ cond = solution .conditions
1029+ undec = solution .undec
1030+ evolution = solution .evolution
1031+ print (_mixc_typ , mixture_coef , mixture_coef_name )
1032+ if _mixc_typ == "val" :
1033+ if isinstance (mixture_coef , Fittable ):
1034+ mcoef = lambda ** kwargs : kwargs [mixture_coef_name ]
1035+ else :
1036+ mcoef = lambda : mixture_coef
1037+ elif _mixc_typ == "func" :
1038+ mcoef = mixture_coef
1039+ T = m .dt * np .arange (0 , len (choice_upper )) + m .dt / 2
1040+ if "upper" in _required_uT_mix :
1041+ lapses_upper = mixture_distribution (** {v : getattr (self , v ) for v in _required_parameters_mix }, ** {v : cond [v ] for v in _required_conditions_mix }, T = T , upper = True )
1042+ lapses_lower = mixture_distribution (** {v : getattr (self , v ) for v in _required_parameters_mix }, ** {v : cond [v ] for v in _required_conditions_mix }, T = T , upper = False )
1043+ else :
1044+ lapses_upper = mixture_distribution (** {v : getattr (self , v ) for v in _required_parameters_mix }, ** {v : cond [v ] for v in _required_conditions_mix }, T = T )
1045+ lapses_lower = lapses_upper
1046+ if "upper" in _required_u_mixc :
1047+ mix_coef_upper = mcoef (** {v : getattr (self , v ) for v in _required_parameters_mixc }, ** {v : cond [v ] for v in _required_conditions_mixc }, upper = True )
1048+ mix_coef_lower = mcoef (** {v : getattr (self , v ) for v in _required_parameters_mixc }, ** {v : cond [v ] for v in _required_conditions_mixc }, upper = False )
1049+ else :
1050+ mix_coef_upper = .5 * mcoef (** {v : getattr (self , v ) for v in _required_parameters_mixc }, ** {v : cond [v ] for v in _required_conditions_mixc })
1051+ mix_coef_lower = mix_coef_upper
1052+ if np .sum (lapses_upper )* m .dt > 1.0 or np .sum (lapses_lower )* m .dt > 1.0 :
1053+ if np .sum (lapses_upper )* m .dt > 1.01 or np .sum (lapses_lower )* m .dt > 1.01 :
1054+ print ("Renormalising mixture distribution to integrate to 1, this may be a bug in your mixture_distribution function" )
1055+ lapses_upper /= np .sum (lapses_upper )* m .dt
1056+ lapses_lower /= np .sum (lapses_lower )* m .dt
1057+ assert mix_coef_upper + mix_coef_lower <= 1.001 , f"Mixture coefficients cannot be > 1, currently are { mix_coef_upper } and { mix_coef_lower } "
1058+ choice_upper = choice_upper * (1 - (mix_coef_upper + mix_coef_lower )) + lapses_upper * mix_coef_upper * m .dt
1059+ choice_lower = choice_lower * (1 - (mix_coef_upper + mix_coef_lower )) + lapses_lower * mix_coef_lower * m .dt
1060+ return Solution (choice_upper , choice_lower , m , cond , undec , evolution )
1061+ overlayobjs .append (OverlayMixtureEasy (** {fname :fval for fname ,fval in _fittables .items () if fname in list (set (_required_parameters_mix + _required_parameters_mixc ))}))
1062+ else :
1063+ raise ValueError ("Mixture distribution must be a function, or else None for the uniform distribution" )
1064+
9911065 return Model (drift = driftobj , noise = noiseobj , bound = boundobj , IC = icobj , overlay = OverlayChain (overlays = overlayobjs ), dx = dx , dt = dt , T_dur = T_dur , choice_names = choice_names , name = name )
0 commit comments