Unexpected behavior in switch_func when using joint probabilities

Issue #54 closed
Alon Kellner created an issue

While experimenting with Lea I stumbled upon some unexpected behavior when returning joint probabilities for switch_func.

Here is the simplest example that I devised that triggers this behavior:

>>> import lea
>>> lea.vals(0).switch_func(lambda value: lea.joint(lea.vals(1)))
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "LeaPlayground\venv\lib\site-packages\lea\lea.py", line 1224, in __str__
    return self.get_alea().__str__()
  File "LeaPlayground\venv\lib\site-packages\lea\lea.py", line 1288, in get_alea
    new_alea = self.new(sorting=sorting)
  File "LeaPlayground\venv\lib\site-packages\lea\lea.py", line 1358, in new
    new_alea = Alea.pmf(self._calc(),prob_type=prob_type,sorting=sorting)
  File "LeaPlayground\venv\lib\site-packages\lea\lea.py", line 1399, in _calc
    return tuple(self.gen_vp())
  File "LeaPlayground\venv\lib\site-packages\lea\slea.py", line 58, in _gen_vp
    for (vd,pd) in lea_v._gen_vp():
  File "LeaPlayground\venv\lib\site-packages\lea\clea.py", line 63, in _gen_vp
    for vps in Clea.prod(tuple(lea_arg.gen_vp for lea_arg in self._lea_args)):
  File "LeaPlayground\venv\lib\site-packages\lea\clea.py", line 59, in prod
    for x in gs[-1]():
TypeError: 'NoneType' object is not callable

I tried digging through the code to find the problem, here is line 63 in clea.py that calls the gen_vp attribute which had None values for each lea_arg:

for vps in Clea.prod(tuple(lea_arg.gen_vp for lea_arg in self._lea_args)):

This is as far as I got with my limited understanding of the code.

Here is a little script that shows that it is the combination of both the switch_func and the joint probability that causes the behavior:

import lea
from lea import leaf

working_switch_1 = leaf.die().switch({}, lea.vals((0, 0), (0, 1), (1, 0), (1, 1)))
print "working_switch_1:\n" + str(working_switch_1)

working_switch_2 = leaf.die().switch_func(lambda value: lea.vals((0, 0), (0, 1), (1, 0), (1, 1)))
print "working_switch_2:\n" + str(working_switch_2)

working_switch_3 = leaf.die().switch({}, lea.joint(lea.vals(0, 1), lea.vals(0, 1)))
print "working_switch_3:\n" + str(working_switch_3)

faulty_switch = leaf.die().switch_func(lambda value: lea.joint(lea.vals(0, 1), lea.vals(0, 1)))
print "faulty_switch:\n" + str(faulty_switch)

My assumption is that it is a bug, perhaps there is something obvious I am missing that explains why returning joint probabilities for switch_func is nonsense, am I missing something?

Comments (11)

  1. Pierre Denis repo owner

    Hi Alon,

    Thank for reporting this issue, which is very interesting indeed.

    I understand what happens but it's a bit difficult to explain. Let me try... There are two kinds of instance in Lea:

    1. Alea instance; these are actual probability distributions storing the mapping {value -> Prob(value)}; these are returned by lea.pmf, lea.vals, ... methods,
    2. other Lea instances: these model treatments, expressions, ... applied on probability distributions; these are returned by arithmetic expressions, lea.joint, lea.switch, etc.

    The point is that the non-Alea instances are not evaluated directly to Alea instances. This is done on-demand, by calling .calc(), or implicitly when you ask to display them. This is sometimes referred as "lazy evaluation". In the present case, the switch_func wrongly assumes that the passed function (or lambda) returns an Alea instance. This is faulty indeed. Here is a workaround to make the evaluation by yourself:

    vals(0).switch_func(lambda value: joint(vals(1)).calc())
    

    I will try to make a fix ASAP. I’ll post the status on the present issue page. Thanks for the preciseness of your report!

  2. Alon Kellner reporter

    Thank you Pierre for your quick and in-depth response, it was very informative.

    Combining your response with the theory of the Statues algorithm makes a lot of sense to me, I now understand the issue.

    The workaround is fine for now, I wait with anticipation for the permanent fix 😁

  3. Pierre Denis repo owner

    Hi Alon,

    I’ve analyzed the issue more deeply. My first idea was to add a call to .calc() in Slea, the class that implements the switch_func construct. This would convert any object returned by the passed function into Alea instances, which would fix your issue and other potential ones. But, … further thinking makes me anticipate more severe problems. Calling .calc() on a non-Alea instance X gets rid of dependencies of X with other Lea instances. This may lead to wrong results, especially for conditional probabilities using .given() method.

    I’ve then investigated then how to handle non-Alea instances possibly returned by the passed function. The problem turns out to be harder than expected. The (incomplete) implementation I’ve tried adds significant complexity. Also, it may slow down the simple cases, which are now working well. The price to pay is high for cases that are very special, if not contrived.

    For these reasons, I think that the best fix consists in adding a requirement to force the function passed to switch_func to return Alea instances only. This maybe checked easily. So, instead of a cryptic exception message, you would have something clearer:

    lea.vals(0).switch_func(lambda value: lea.joint(lea.vals(1)))
    # -> lea.Error: the function passed to switch_func shall return Alea instances only, e.g. lea.pmf(...) or lea.vals(...)
    

    Then, you have to add the .calc() as I explained or use a simpler constructs, like

    lea.vals(0).switch_func(lambda value: lea.pmf({(1,):1}))
    lea.vals(0).switch_func(lambda value: lea.vals((1,)))
    lea.vals(0).switch_func(lambda value: (1,))
    

    which are all equivalent (the last one using automatic coercion of (1,) to Alea instance).

    I hope this is fine for you. Let me know if something is unclear or arguable.

  4. Alon Kellner reporter

    Thank you Pierre, I appreciate the time and attention you dedicated for my issue, your explanation is approachable and sincere.

    It seems to me that this solution is a compromise, yet I trust your judgement that this is the optimal one.

    Either way this functionality is not critical for my application, the indicative error message is more than enough. As far as I am concerned this issue is resolved.

  5. Pierre Denis repo owner

    Great! Lea 3.2.3 is just released with the fix described above. I've also added a warning note on the wiki.

  6. Log in to comment