@@ -2637,3 +2637,259 @@ def __exit__(self, exc_type, exc_value, traceback):
26372637 os .rmdir (path )
26382638 except OSError :
26392639 pass
2640+
2641+
2642+ class _FuncInfo (object ):
2643+ """
2644+ Class used to store a function.
2645+
2646+ """
2647+
2648+ def __init__ (self , function , inverse , bounded_0_1 = True , check_params = None ):
2649+ """
2650+ Parameters
2651+ ----------
2652+
2653+ function : callable
2654+ A callable implementing the function receiving the variable as
2655+ first argument and any additional parameters in a list as second
2656+ argument.
2657+ inverse : callable
2658+ A callable implementing the inverse function receiving the variable
2659+ as first argument and any additional parameters in a list as
2660+ second argument. It must satisfy 'inverse(function(x, p), p) == x'.
2661+ bounded_0_1: bool or callable
2662+ A boolean indicating whether the function is bounded in the [0,1]
2663+ interval, or a callable taking a list of values for the additional
2664+ parameters, and returning a boolean indicating whether the function
2665+ is bounded in the [0,1] interval for that combination of
2666+ parameters. Default True.
2667+ check_params: callable or None
2668+ A callable taking a list of values for the additional parameters
2669+ and returning a boolean indicating whether that combination of
2670+ parameters is valid. It is only required if the function has
2671+ additional parameters and some of them are restricted.
2672+ Default None.
2673+
2674+ """
2675+
2676+ self .function = function
2677+ self .inverse = inverse
2678+
2679+ if callable (bounded_0_1 ):
2680+ self ._bounded_0_1 = bounded_0_1
2681+ else :
2682+ self ._bounded_0_1 = lambda x : bounded_0_1
2683+
2684+ if check_params is None :
2685+ self ._check_params = lambda x : True
2686+ elif callable (check_params ):
2687+ self ._check_params = check_params
2688+ else :
2689+ raise ValueError ("Invalid 'check_params' argument." )
2690+
2691+ def is_bounded_0_1 (self , params = None ):
2692+ """
2693+ Returns a boolean indicating if the function is bounded in the [0,1]
2694+ interval for a particular set of additional parameters.
2695+
2696+ Parameters
2697+ ----------
2698+
2699+ params : list
2700+ The list of additional parameters. Default None.
2701+
2702+ Returns
2703+ -------
2704+
2705+ out : bool
2706+ True if the function is bounded in the [0,1] interval for
2707+ parameters 'params'. Otherwise False.
2708+
2709+ """
2710+
2711+ return self ._bounded_0_1 (params )
2712+
2713+ def check_params (self , params = None ):
2714+ """
2715+ Returns a boolean indicating if the set of additional parameters is
2716+ valid.
2717+
2718+ Parameters
2719+ ----------
2720+
2721+ params : list
2722+ The list of additional parameters. Default None.
2723+
2724+ Returns
2725+ -------
2726+
2727+ out : bool
2728+ True if 'params' is a valid set of additional parameters for the
2729+ function. Otherwise False.
2730+
2731+ """
2732+
2733+ return self ._check_params (params )
2734+
2735+
2736+ class _StringFuncParser (object ):
2737+ """
2738+ A class used to convert predefined strings into
2739+ _FuncInfo objects, or to directly obtain _FuncInfo
2740+ properties.
2741+
2742+ """
2743+
2744+ _funcs = {}
2745+ _funcs ['linear' ] = _FuncInfo (lambda x : x ,
2746+ lambda x : x ,
2747+ True )
2748+ _funcs ['quadratic' ] = _FuncInfo (np .square ,
2749+ np .sqrt ,
2750+ True )
2751+ _funcs ['cubic' ] = _FuncInfo (lambda x : x ** 3 ,
2752+ lambda x : x ** (1. / 3 ),
2753+ True )
2754+ _funcs ['sqrt' ] = _FuncInfo (np .sqrt ,
2755+ np .square ,
2756+ True )
2757+ _funcs ['cbrt' ] = _FuncInfo (lambda x : x ** (1. / 3 ),
2758+ lambda x : x ** 3 ,
2759+ True )
2760+ _funcs ['log10' ] = _FuncInfo (np .log10 ,
2761+ lambda x : (10 ** (x )),
2762+ False )
2763+ _funcs ['log' ] = _FuncInfo (np .log ,
2764+ np .exp ,
2765+ False )
2766+ _funcs ['log2' ] = _FuncInfo (np .log2 ,
2767+ lambda x : (2 ** x ),
2768+ False )
2769+ _funcs ['x**{p}' ] = _FuncInfo (lambda x , p : x ** p [0 ],
2770+ lambda x , p : x ** (1. / p [0 ]),
2771+ True )
2772+ _funcs ['root{p}(x)' ] = _FuncInfo (lambda x , p : x ** (1. / p [0 ]),
2773+ lambda x , p : x ** p ,
2774+ True )
2775+ _funcs ['log{p}(x)' ] = _FuncInfo (lambda x , p : (np .log (x ) /
2776+ np .log (p [0 ])),
2777+ lambda x , p : p [0 ]** (x ),
2778+ False ,
2779+ lambda p : p [0 ] > 0 )
2780+ _funcs ['log10(x+{p})' ] = _FuncInfo (lambda x , p : np .log10 (x + p [0 ]),
2781+ lambda x , p : 10 ** x - p [0 ],
2782+ lambda p : p [0 ] > 0 )
2783+ _funcs ['log(x+{p})' ] = _FuncInfo (lambda x , p : np .log (x + p [0 ]),
2784+ lambda x , p : np .exp (x ) - p [0 ],
2785+ lambda p : p [0 ] > 0 )
2786+ _funcs ['log{p}(x+{p})' ] = _FuncInfo (lambda x , p : (np .log (x + p [1 ]) /
2787+ np .log (p [0 ])),
2788+ lambda x , p : p [0 ]** (x ) - p [1 ],
2789+ lambda p : p [1 ] > 0 ,
2790+ lambda p : p [0 ] > 0 )
2791+
2792+ def __init__ (self , str_func ):
2793+ """
2794+ Parameters
2795+ ----------
2796+ str_func : string
2797+ String to be parsed.
2798+
2799+ """
2800+
2801+ if not isinstance (str_func , six .string_types ):
2802+ raise ValueError ("'%s' must be a string." % str_func )
2803+ self ._str_func = six .text_type (str_func )
2804+ self ._key , self ._params = self ._get_key_params ()
2805+ self ._func = self ._parse_func ()
2806+
2807+ def _parse_func (self ):
2808+ """
2809+ Parses the parameters to build a new _FuncInfo object,
2810+ replacing the relevant parameters if necessary in the lambda
2811+ functions.
2812+
2813+ """
2814+
2815+ func = self ._funcs [self ._key ]
2816+
2817+ if not self ._params :
2818+ func = _FuncInfo (func .function , func .inverse ,
2819+ func .is_bounded_0_1 ())
2820+ else :
2821+ m = func .function
2822+ function = (lambda x , m = m : m (x , self ._params ))
2823+
2824+ m = func .inverse
2825+ inverse = (lambda x , m = m : m (x , self ._params ))
2826+
2827+ is_bounded_0_1 = func .is_bounded_0_1 (self ._params )
2828+
2829+ func = _FuncInfo (function , inverse ,
2830+ is_bounded_0_1 )
2831+ return func
2832+
2833+ @property
2834+ def func_info (self ):
2835+ """
2836+ Returns the _FuncInfo object.
2837+
2838+ """
2839+ return self ._func
2840+
2841+ @property
2842+ def function (self ):
2843+ """
2844+ Returns the callable for the direct function.
2845+
2846+ """
2847+ return self ._func .function
2848+
2849+ @property
2850+ def inverse (self ):
2851+ """
2852+ Returns the callable for the inverse function.
2853+
2854+ """
2855+ return self ._func .inverse
2856+
2857+ @property
2858+ def is_bounded_0_1 (self ):
2859+ """
2860+ Returns a boolean indicating if the function is bounded
2861+ in the [0-1 interval].
2862+
2863+ """
2864+ return self ._func .is_bounded_0_1 ()
2865+
2866+ def _get_key_params (self ):
2867+ str_func = self ._str_func
2868+ # Checking if it comes with parameters
2869+ regex = '\{(.*?)\}'
2870+ params = re .findall (regex , str_func )
2871+
2872+ for i , param in enumerate (params ):
2873+ try :
2874+ params [i ] = float (param )
2875+ except ValueError :
2876+ raise ValueError ("Parameter %i is '%s', which is "
2877+ "not a number." %
2878+ (i , param ))
2879+
2880+ str_func = re .sub (regex , '{p}' , str_func )
2881+
2882+ try :
2883+ func = self ._funcs [str_func ]
2884+ except (ValueError , KeyError ):
2885+ raise ValueError ("'%s' is an invalid string. The only strings "
2886+ "recognized as functions are %s." %
2887+ (str_func , list (self ._funcs )))
2888+
2889+ # Checking that the parameters are valid
2890+ if not func .check_params (params ):
2891+ raise ValueError ("%s are invalid values for the parameters "
2892+ "in %s." %
2893+ (params , str_func ))
2894+
2895+ return str_func , params
0 commit comments