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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy