B.Mr.W.
B.Mr.W.

Reputation: 19628

Pass in any variable while guaranteeing predefined relationship

I have a simple formula taking basic arithmetic calculations given several inputs.

a = 1
b = 2
c = a + b #3=1+2 
d = 4
e = c + d #7=3+4

graph of relationship

In theory, the relationships should always hold true. And I want to write a function that user can modify any variable and the rest will be auto updated (update priority has been predefined if there are more than one alternative eg. update the most right node first).

def f():
    #default state
    state = {'a':1, 'b':2, 'c':3, 'd':4, 'e':7}
    ...
    return state

f(a=0) == {'a':0, 'b':2, 'c':2, 'd':4, 'e':6}
f(c=4) == {'a':1, 'b':3, 'c':4, 'd':4, 'e':8}
f(b=2, c=4) == {'a':2, 'b':2, 'c':4, 'd':4, 'e':8}

graph of modified relationship for c=4

I tried to use **kwargs and *args to allow the user to pass in any variable but have to hard code the update logic based on which variable got modified. Any better ideas?

P.S.: this example is for demonstration purpose; the real problem involves much more variables and the mathematical relationship is also more difficult (logarithm, exponential, ..)

Upvotes: 0

Views: 199

Answers (3)

smichr
smichr

Reputation: 19029

I wonder if this (using SymPy) gets at the desired behavior:

from sympy.abc import *
from sympy import *
ed = Tuple(
(Eq(d, a+b+c),c),
(Eq(g, e+f),f),
(Eq(h,d+g),g))

def indep(ed):
    free = set()
    dep = set()
    for i,_ in ed:
        assert i.lhs.is_Symbol
        free |=i.rhs.free_symbols
        dep |= {i.lhs}
    return free - dep

def F(x, v, eq):
    eq = list(eq)
    free = indep(eq)
    if x in free:
        # leaf
        return [i[0] for i in eq], free
    else:
        for i,y in eq:
            if i.lhs == x:
                return [i[0] for i in eq] + [Eq(x, v)], free - {y}
    assert None, 'unknown parameter'

def update(x, v, ed):
    eqs, ex = F(x, v, ed)
    sol = solve(eqs, exclude=ex, dict=True)
    assert len(sol) == 1
    return Dict(sol), list(ordered(ex))

ed represents the list of equations and the defined "dependent/rightmost" variable for each equation, however you want to define it.

So if you want to update g = 10 you would have the following values:

>>> update(g, 10, ed)
({d: a + b + c, f: 10 - e, g: 10, h: a + b + c + 10}, [a, b, c, e])

The variables on the right are the ones that you would have to specify in order to get known values for all others

>>> _[0].subs(dict(zip(_[1],(1,1,1,1))))
{d: 3, f: 9, g: 10, h: 13}

Upvotes: 1

niyaz
niyaz

Reputation: 33

Problem Analysis

  • Two numbers get combined with a mathematical operators, resulting in another number
    • Resultant numbers depend on a relationship,

These relationships and numbers are illustrated as a tree structure.

  • Hence, there are two types of cells (@Copperfield):
    • Free cells, not depending on a relationship, dangling as leaves in the tree.
    • Inner cells, depending on a relationship between two cells, we will call them nodes.
      • Resulting tree never makes cycles.

Assumptions and Rationale

In his comments, @B.Mr.W. says, each relationship is formed by mathematical operators and there can be more than one nodes pointing to another node.

I assume he has relations like d = a - b * c.
Evaluation of such expressions / relations has to follow operator precedence.
And, such expressions will be anyhow resolved to d = a - (b*c).

Evaluation of such expressions would result in sub-relationships which are again binary.

Note: These binary sub-relationships remain anonymous.

Requirements

  1. Create new Leaf cells storing a number.
  2. Create new Node cells by define relationships between other cells.
  3. A Change to any cell should update all related cells, without affecting the relationship. That is, a cascading effect along the related branches.
  4. Requirement 3. has to follow an ordering preference. The right-side node is preferred by default.

Solution Analysis

  1. Both types of cells can be combined within or with other-type using a mathematical operator. (Implemented in class Cell)

  2. I follow Python Data Model to solve this problem and interface style becomes:

# create new Leaf nodes with values:
a = Leaf(2)
b = Leaf(3)
c = Leaf(4)

# define relationships
d = a + b*c       # (b*c) becomes anonymous
e = d - Leaf(3)   # Leaf(3) is also anonymous

( ( 2 + ( 3 * 4 ) ) - 3 ) => { 'a':2, 'b':3, 'c':4, 'd':14, 'e':11 }

# get values of known nodes
a(), b(), c(), d(), e()

# update values
a(1) => { 'a':1, 'b':3, 'c':4, 'd':13, 'e':10 }
b(2) => { 'a':1, 'b':2, 'c':4, 'd':9, 'e':6 }
c(3) => { 'a':1, 'b':2, 'c':3, 'd':7, 'e':4 } 

Code:

"""Super-type of Leaves and Nodes."""
class Cell:
    """Arithematic operation handlers between Leaves/Nodes"""
    def __add__(self, other):
        return Node(leftCell=self, rightCell=other, op='+')
    def __sub__(self, other):
        return Node(leftCell=self, rightCell=other, op='-')
    def __mul__(self, other):
        return Node(leftCell=self, rightCell=other, op='*')
    def __truediv__(self, other):
        return Node(leftCell=self, rightCell=other, op='/')
    
    """for clean presentation of float values"""
    @staticmethod
    def cleanFloat(val:int|float):
        if isinstance(val, float):
            rounded = round(val)
            if abs(rounded-val)<0.011:
                return rounded
            else:
                return round(val, 2)
        return val

"""Leaves will contain values only"""
class Leaf(Cell):
    def __init__(self, val:int|float):
        self.val = val

    """Getter/Setter for Leaf val"""
    @property
    def val(self):
        return self.cleanFloat(self.__val)
    @val.setter
    def val(self, val:int|float):
        self.__val = self.cleanFloat(val)
    
    """Getter/Setter of Leaf object."""
    def __call__(self, val:int|float=None):
        if val == None:
            return self.val
        else:
            self.val = val

    def __str__(self):
        return f"{self.val}"


"""Nodes contain left/right child, an arithematic operation and preferred side for update"""
class Node(Cell):
    def __init__(self, leftCell:Cell, rightCell:Cell, 
                 op:str, prefSide:str='right'):
        self.leftCell = leftCell
        self.rightCell = rightCell
        self.op = op
        self.prefSide = prefSide

    """
    Preferred and the other cells for reverse path operation required during update.
    These properties will help clean retrieval.
    """
    @property
    def preferredCell(self):
        match self.prefSide:
            case 'left' : return self.leftCell
            case 'right': return self.rightCell
    @property
    def unPreferredCell(self):
        match self.prefSide:
            case 'left' : return self.rightCell
            case 'right': return self.leftCell

    """Getter/Setter for Nodes"""
    def __call__(self, val :int|float = None):
        if val == None:
            match self.op:
                case '+':
                    nodeVal =  self.leftCell() + self.rightCell()
                case '-':
                    nodeVal =  self.leftCell() - self.rightCell()
                case '*':
                    nodeVal =  self.leftCell() * self.rightCell()
                case '/':
                    nodeVal =  self.leftCell() / self.rightCell()
                case _:
                    raise
            return self.cleanFloat(nodeVal)
        else:
            match self.op:
                case '+':
                    self.preferredCell( val - self.unPreferredCell() )
                case '*':
                    self.preferredCell( val / self.unPreferredCell() )
                case '-':
                    match self.prefSide:
                        case 'left'  : 
                            self.preferredCell( val + self.unPreferredCell() )
                        case 'right' : 
                            self.preferredCell( self.unPreferredCell() - val )
                case '/':
                    match self.prefSide:
                        case 'left ' : 
                            self.preferredCell( val * self.unPreferredCell() )
                        case 'right' : 
                            self.preferredCell( self.unPreferredCell() / val )
                case _:
                    raise
        
    def __str__(self):
        return f"( {str(self.leftCell)} {self.op} {str(self.rightCell)} )"
                

if __name__ == '__main__':
    
    def createTree():
        # create new Leaf nodes having values
        a = Leaf(2)
        b = Leaf(3)
        c = Leaf(4)
        d = Leaf(5)
        e = Leaf(6)
        
        # define relationships 
        f = a + b
        g = d / f - c        # (d / f) becomes anonymous, higher precedence
        h = Leaf(9) / e * g  
        # here, (Leaf(9)/e) creates the anonymous node, left to right associativity
        return (a,b,c,d,e,f,g,h)
    
    (a,b,c,d,e,f,g,h) = createTree()    
    
    def treeDict():
        return f"{{ 'a':{a()}, 'b':{b()}, 'c':{c()}, 'd':{d()}, 'e':{e()}, 'f':{f()}, 'g':{g()}, 'h':{h()} }}"
        

    print('\nget values of known cells:')
    print(f"{h} => {treeDict()}\n")
    
    print('each cell expanded (take care of anonymous cells):')
    print(f"'a':{a}\n'b':{b}\n'c':{c}\n'd':{d}\n'e':{e}\n'f':{f}\n'g':{g}\n'h':{h}\n")
    

    print('update values:')
    a(1)
    print( f"a(1) => {treeDict()}")
    
    b(2)
    print( f"b(2) => {treeDict()}")
    
    c(3)
    print( f"c(3) => {treeDict()}")

    f(10)
    print(f"f(10) => {treeDict()}")
    
    g(10)
    print(f"g(10) => {treeDict()}")

    h(100)
    print(f"h(100) => {treeDict()}")

    print('\nchange ordering preference: g.prefSide = "left"')
    (a,b,c,d,e,f,g,h) = createTree()    
    g.prefSide = 'left'
    g(1)
    print(f"g(1) => {treeDict()}")

    print('\nchange ordering preference: g.prefSide = "left"')
    (a,b,c,d,e,f,g,h) = createTree()    
    g.prefSide = 'left'
    h(0)
    print(f"h(0) => {treeDict()}")

    print("\nAccessing Anonymous Cells:")
    print(f"h.leftCell() : {h.leftCell()}")
    print(f"h.leftCell.leftCell() : {h.leftCell.leftCell()}")

Example Tree structure

Upvotes: 2

netanel perry
netanel perry

Reputation: 36

You indeed can represent the state of the problem as a directional graph, where each edge between the variables contains an arithematic expression to use on the other node to update its state.

Than, use the BFS algorithem starting at the variable that you try to change and it will modify all of the varialbes that are linked to it and so forward.

If you have to change multiple varialbes, run the bfs multiple times, starting at each variable. Than, if the one of the variable you ran the BFS on changes (and does not contain the wanted value), than you know the wanted state is not possible (because the variables are co-dependent).

So each time you add a state to the problem, you have to modify only the edges connected to it (and from it).

Upvotes: 2

Related Questions