Function algebra for adding or multiplying several python functions and packaging parameters in a single vector

Viewed 288

I am trying to construct a more complex function from the sum and product of multiple simple functions. I want to create a class where I can add a new simple function and its operation to construct a complex function then evaluate the complex function.

For example say I have the functions:

def f1(a, b, c):
    return 2 * c

def f2(a, b, c, d):
    return c + d

And would like to construct something like:

f1(0, 0, 1) + f2(0, 0, 1, 0) * f1(0, 0, 2)
#Out:6

But without typing out the whole expression and with the parameters packaged in a single vector so I can optimise them. I have built a class to do it but it is really quite clumsy and doesn't follow BODMAS (the mathematical order of operations, Bracket, Of, Division, Multiplication, Addition and Subtraction):

class FunctionAlgebra:

    def __init__(self):
        self.f = []
        self.op = []
        self.hpid = []
        self.hyperparameters = []
        self.name = '0'

    def compose(self, f, p, op):
        """
        Compose an expression
        """
        self.hpid = np.concatenate([self.hpid, len(self.f) * np.ones(len(p))])
        self.hyperparameters = np.concatenate([self.hyperparameters, p])
        self.f.append(f)
        self.op.append(op)
        self.name += " {} {}".format(op.__name__, f.__name__)
        self.name = self.name.replace('add', "+").replace("multiply", '*')

    def _evalf_(self, a, b, f, args):
        """
        Evaluate a function
        """
        self.cr = f(a, b, *args)

    def multiply(self):
        """
        Multiply function result by total result
        """
        self.result *= self.cr

    def add(self):
        """
        Add function result to the total result.
        """
        self.result += self.cr

    def call(self, a, b, p):
        """
        Call and evaluate the expression
        """
        self.result = 0
        for i in range(len(self.f)):
            args = p[self.hpid == i]
            self._evalf_(a, b, self.f[i], args)
            self.op[i]()
        return self.result

Testing on the first example the answer should be 6 but is 12 due to no BODMAS:

f = FunctionAlgebra()
f.compose(f1, [1], f.add)
f.compose(f2, [1, 0], f.add)
f.compose(f1, [2], f.multiply)
f.call(0, 0, f.hyperparameters)
#Out:12

Does anyone know a more pythonic way of adding and multiplying functions together and combining their parameters into a single vector?

Thanks,

Robin

1 Answers

You did not implement BODMAS, so all operations are performed sequentially, left to right.

To implement operator precedence, you should:

  • provide an expression somehow (fluent way that you use is not enough)
  • parse the expression (for example with antlr4)
  • and finally, execute operation in required order (e.g. BODMAS)

This is a long (but powerful and correct) way. Python has built-in eval function, so it is possible to use it for your purposes:

class FunctionAlgebra:

    def __init__(self):
        self.parts=[]

    def compose(self, f, p, op):
        self.parts.append({
            'function':f,
            'params':p,
            'operation':op})

    def call(self, *hyperparams):

        #build expression and its input params
        expr = '0'
        inp={}
        i = 0 

        for part in self.parts:
            varname = f'v{i}'
            expr += part['operation']+varname
            inp[varname] = part['function'](*hyperparams, *part['params'])
            i+=1

        print (expr, inp)
        #and eval it
        return eval(expr,inp)


def f1(a, b, c):
    return 2 * c

def f2(a, b, c, d):
    return c + d


f = FunctionAlgebra()
f.compose(f1, [1], '+')
f.compose(f2, [1, 0], '+')
f.compose(f1, [2], '*')

print (f.call(0, 0))

# 0+v0+v1*v2 {'v0': 2, 'v1': 1, 'v2': 4}
# Output 6
Related