0

In my work I frequently come across systems of interdependent equations. I have contrived a toy example as follows. The terminal values w, x, y and z are given:

e(y) = A+B

A(y) = x*log(y)+y^z

B(y) = alpha*y

alpha(y) = x*y+w

We could then consider the function e(y) as the root of an arithmetic tree with the following heirarchy:

enter image description here

Previously, in python I would have done something like this to evaluate the result:

import numpy as np

def root(B, A):
    return B+A

def A(x,y,z):
    return x*np.log(y)+y**z

def B(alpha, y):
    return alpha*y

def alpha(x,y,w):
    return x*y+w

if __name__=='__main__':

    x,y,z,w = 1,2,3,4
    result = root(B(alpha(x,y,w),y), A(x,y,z))

This will give me the right result, but I have come to really despise this way of doing things. It requires me to keep track of which arguments each function needs and how the tree itself is built up. Also, suppose I wanted to modify the tree itself by adding branches and leaves. For example, say I wanted to redefine alpha as v+x+y with the new variable v. I'd have to make a new function and a new call, which is not very efficient as I sometimes need to make pervasive and numerous changes.

I tried different approaches to solve this problem as outlined by this question and this question.

I came across a couple of ideas which looked promising, namely function objects and the Interpreter Pattern. However I was disappointed by the Interpreter Pattern. Suppose I didn't create a parser, and went straight for the underlying composite architecture, wouldn't I still have to do something like this?

root = root_obj(B_obj(alpha_obj(x_obj,y_obj,w_obj),y_obj), A(x_obj,y_obj,z_obj))
root.interpret()

The above would require a lot of added complexity for no added value. My question is as follows: What is a simple and useful object oriented paradigm in which I could define, modify and evaluate a mathematical heirarchy in a dynamic manner?

EDIT

Here's an example of what I would like to achieve:

tree = FunctionTree()
tree.add_nodes(root, A, B, alpha, w, x, y, z)
tree.add_edge(root, [A, B])
tree.add_edge(root, A)
tree.add_edge(A, [x,y,z])
tree.add_edge(B, [alpha, y])
tree.add_edge(alpha, [x, y, w])
tree.evaluate()

Yes, this is less "compact" but it is much more flexible. Imaging having methods for deleting and adding new edges. replacing definitions at nodes and reevaluating the result. I am looking for something like this.

  • 2
    It really depends on what you mean by "compact". Using Expression object is most common way of representing and evaluating equations. Are those not "compact" enough? Why not? Why not implement a simple parser and represent the equations as simple strings that are then parsed? – Euphoric Oct 23 '19 at 13:38
  • Only mention of "compactness" is in end of last sentence. Nothing else in your question implies any relation to "compactness". Could you provide some pseudo-code how you imagine defining or modifying an expression that you would consider compact? – Euphoric Oct 23 '19 at 13:51
  • Okay. In your representation, where do you put the mathematical operations? Also, how would changing the tree look like after it was created? And where are parameters coming from in evaluate method? – Euphoric Oct 23 '19 at 14:08
  • It could be that the nodes of the trees are function objects which are aware of their own arithmetic and arguments. Arguments can either be terminal or non-terminal. functions which have only terminal arguments are closer to the leaves and functions which have only non-terminal arguments are closer to the root. With regards to changing the tree, it would be a simple matter of replacing a node and redefining the connectivity to terminal/nonterminal arguments. – user32882 Oct 23 '19 at 14:15
  • 1
    The tree you draw is a simple example of an Abstract Syntax Tree (en.wikipedia.org/wiki/Abstract_syntax_tree), writing a parser that takes a string representing a mathematical expression and construct its Syntax Tree is pretty straightforward. Is this maybe what you are looking for? – Dtex Oct 23 '19 at 14:41
3

This seems to be a continuation of your previous question. The recommendation made in this answer is still valid. But maybe I was not clear enough.

I'm not python fluent, but:

  • Create a class AbstractExpression.
  • Create a concrete specialisation class for every specific function you have: Function_e, Function_A, Function_B, Function_Alpha. Instances of these class would correspond to your orange boxes.
  • Create a concrete class for the terminal expression. Call it Variable, and imagine that every instance of this class has a name. Instance of this class would correspond to your green circles.
  • For clarity, let's use the pattern with a function eval(context) instead of interpret(context)

Now to the point on which I was not clear enough:

  • Of course, Function_e's constructor would construct a Function_A instance called fA and a Function_B instance. Absolutely no parsing is required here !
  • Of course, Function_A would create Variable instances, vy with the names "y", vz for "z" and vx for "x". Again, no parsing is needed: the class constructor construct the needed objects (you code it).
  • The Function_e's eval() would do what it needs and call fA.eval() where the result of this functions is needed in the formula. I'll insist: absolutely no parsing takes place here! It's your implementation of e that will call a method of your implementation of A
  • in your implementation of fA.eval(), you would call vy.eval(), vz.eval() and vx.eval() in the formula, where you would need each of these variables.

Now we have constructed an interpreter corresponding to your system, (without any parsing), and that is able to calculate the result, if only Variable.eval() could know the values to be used for these parameters. And here enters the context:

  • context would be a dictionary that assigns fixed values to variable names.
  • context is forwarded as single parameter through all the eval() calls explained above.
  • the last implementation needed is eval() for Variable. This would just return the value associated to the variable's name in the context dictionary.

Sorry if I insisted on the absence of parsing. But many web site provide examples of the interpreter pattern without understanding real use cases. So they all are about parsing, which creates significant confusion. You have here a perfect example of use :-)

| improve this answer | |
  • One thing Python specific here that could change the answer is that there's a special function __call__() that (I think) obviates the need to define an exec method. You could then use the root/top level object like any python function. It could even take parameters but I'm not sure if that's helpful here or not. – JimmyJames Oct 23 '19 at 19:28
  • @Christophe I appreciate your answer, but how would my main() method look like with your proposal? Also would I be able to redefine the abstract tree with relative ease? I.e without having to go back into the classes themselves? – user32882 Oct 24 '19 at 5:06
  • @user32882 The abstract tree is the result of the cascaded construction process described above (so taking place in the __init__(self) of the classes). As said, I'm not python fluent, but your main would look like root = Function_e() followed by context = { "x":1, ....} followed by result = root.eval(context) – Christophe Oct 24 '19 at 6:03
  • I think you have the right idea and the fact I am not catching on is mainly due to my own ignorance about design patterns. I will continue to look through Interpreter/Composite to learn more. If/when I am able to implement one or both for this use case, I will accept both your answers. Thanks for your help. – user32882 Oct 24 '19 at 8:11
  • I'd say Composite and a Visitor pattern would be more adequate for what I am trying to achieve. I'll accept your answers though because they have pointed me to the right direction: stackoverflow.com/questions/58571505/… – user32882 Oct 30 '19 at 14:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.