Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b73cedc

Browse files
authored
Merge pull request #7464 from alvarosg/string-func-parser
[MRG+2] ENH: _StringFuncParser to get numerical functions callables from strings
2 parents 424d3b0 + c143e75 commit b73cedc

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed

lib/matplotlib/cbook.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/matplotlib/tests/test_cbook.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,3 +515,64 @@ def test_flatiter():
515515

516516
assert 0 == next(it)
517517
assert 1 == next(it)
518+
519+
520+
class TestFuncParser(object):
521+
x_test = np.linspace(0.01, 0.5, 3)
522+
validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'cbrt',
523+
'log', 'log10', 'log2', 'x**{1.5}', 'root{2.5}(x)',
524+
'log{2}(x)',
525+
'log(x+{0.5})', 'log10(x+{0.1})', 'log{2}(x+{0.1})',
526+
'log{2}(x+{0})']
527+
results = [(lambda x: x),
528+
np.square,
529+
(lambda x: x**3),
530+
np.sqrt,
531+
(lambda x: x**(1. / 3)),
532+
np.log,
533+
np.log10,
534+
np.log2,
535+
(lambda x: x**1.5),
536+
(lambda x: x**(1 / 2.5)),
537+
(lambda x: np.log2(x)),
538+
(lambda x: np.log(x + 0.5)),
539+
(lambda x: np.log10(x + 0.1)),
540+
(lambda x: np.log2(x + 0.1)),
541+
(lambda x: np.log2(x))]
542+
543+
bounded_list = [True, True, True, True, True,
544+
False, False, False, True, True,
545+
False,
546+
True, True, True,
547+
False]
548+
549+
@pytest.mark.parametrize("string, func",
550+
zip(validstrings, results),
551+
ids=validstrings)
552+
def test_values(self, string, func):
553+
func_parser = cbook._StringFuncParser(string)
554+
f = func_parser.function
555+
assert_array_almost_equal(f(self.x_test), func(self.x_test))
556+
557+
@pytest.mark.parametrize("string", validstrings, ids=validstrings)
558+
def test_inverse(self, string):
559+
func_parser = cbook._StringFuncParser(string)
560+
f = func_parser.func_info
561+
fdir = f.function
562+
finv = f.inverse
563+
assert_array_almost_equal(finv(fdir(self.x_test)), self.x_test)
564+
565+
@pytest.mark.parametrize("string", validstrings, ids=validstrings)
566+
def test_get_inverse(self, string):
567+
func_parser = cbook._StringFuncParser(string)
568+
finv1 = func_parser.inverse
569+
finv2 = func_parser.func_info.inverse
570+
assert_array_almost_equal(finv1(self.x_test), finv2(self.x_test))
571+
572+
@pytest.mark.parametrize("string, bounded",
573+
zip(validstrings, bounded_list),
574+
ids=validstrings)
575+
def test_bounded(self, string, bounded):
576+
func_parser = cbook._StringFuncParser(string)
577+
b = func_parser.is_bounded_0_1
578+
assert_array_equal(b, bounded)

0 commit comments

Comments
 (0)