# Combination of lambda function and plot3d(?)

I'm having trouble understanding why the following code doesn't work like I'd hoped:

def test1():
base_frame = point3d((.5,.5,1), color='red', size=10)
def f(x,y,t): return(sin(pi*x)*sin(pi*y)*e^(-t))
frames = []
for t in [0,1]:
pic = base_frame
pic += plot3d(lambda x,y:f(x,y,t), (x, 0, 1), (y, 0, 1))
#It works if you uncomment the next line:
#pic.save('/tmp/foo.png')
frames += [pic,]
return(frames)


What happens is that both the frames (or all of them if I create more than two) end up containing the last plot. It appears that the plot doesn't get "evaluated" when I want it to. So far, the only mechanism I've found that can force it to be evaluated is inserting a .show() or a .save(), which won't work to well in a larger loop.

Now, I understand that the function "f" I've got here could have been rendered as an expression rather than a function. Then there's no problem. But in the real program, things are more complicated, and (afict) I really need the lambda.

Can anyone help me understand what's happening here, and how I might get this to work?

---------- UPDATE -----------

I realize I haven't been all that clear (in my own mind, as well as in my explanation) of what the problem is here. I think there are two issues (1) Getting the "lambda" thing to work, and (2) getting the plot to "evaluate." Here are three updated test cases, which contain a function that actually requires the lambda: test1() does what I want, but at the price of having to save every frame to a file. test2() and test3() do get the plot3d function to evaluate, but in both of those cases, I lose the "lambda" functionality and so the function being plotted isn't right.

#
# Test file
#

# Make sure I'm using the same function in all three tests.
# I need a function that actually requires the use of lambda
var('x,y,t')
def f(x,y,t):
if x < .5: return(sin(pi*x)*sin(pi*y))
else: return(sin(pi*x)*sin(pi*y)*e^(-t))

# Might as well define the base frame here too
base_frame = point3d((.5,.5,1), color='red', size=10)

def test1():
frames = []
for t in [0,1]:
pic = base_frame
pic += plot3d(lambda x,y:f(x,y,t), (x, 0, 1), (y, 0, 1))
# This "save" appears to force the line above to be "evaluated"
pic.save('/tmp/foo.png')
frames += [pic,]
return(frames)

def test2():
frames = []
for t in [0,1]:
pic = base_frame
def g(x,y): return(f(x,y,t))
pic += plot3d(g(x,y), (x, 0, 1), (y, 0, 1))
frames += [pic,]
return(frames)

def test3():
frames = []
for t in [0,1]:
pic = base_frame
ff = lambda t:lambda x,y:f(x,y,t)
pic += plot3d(ff(t)(x,y), (x, 0, 1), (y ...
edit retag close merge delete

In your situation, can you replace the line "pic += plot3d(lambda x,y:f(x,y,t), (x, 0, 1), (y, 0, 1))" with lines like "def g(x,y): return f(x,y,t); pic += plot3d(g(x,y), ...)"?

Well, I tried it. It appeared to do the same thing as using the lambda. That is, my function evaluated x and y OK, but it didn't force the plot to be evaluated.

That's odd, because for me the two behave differently: if I enter your code in the notebook and then evaluate 'test1()', I see different plots with your version vs. the version with "def g(x,y): ...".

It's possible that I'm not understanding what you suggested correctly. Or, that there's a difference between the command line and notebook? I updated the question, showing what I tried (test2). Let me know if this wasn't what you meant. Note that I'm doing this all in the command line.

In your test2, change the line "pic += ..." to "pic += plot3d(g(x,y), ...)". That change works for me both in the notebook and from the command line. (Right now, you're defining g(x,y) but using it anywhere.)

Sort by » oldest newest most voted

I played with this a little, and I think I've been able to at least focus the problem a little more (using your f above):

sage: lambdas = [lambda x,y:f(x,y,t) for t in [0,1]]
sage: lambdas(.5,.5)
e^(-1)*sin(0.500000000000000*pi)^2
sage: lambdas(.5,.5)
e^(-1)*sin(0.500000000000000*pi)^2


So one aspect of the problem seems to be that the second lambda is overwriting the first..

UPDATE:

Oh wow -- this is a python bug! (Or is it a feature? PEP 227 seems to address something relevant; note especially the "Discussion" and "Examples" sections)

>>> lambdas = [lambda x,y: x*y*t for t in [1,2]]
>>> lambdas(2,3)
12
>>> lambdas(2,3)
12


UPDATE 2:

This morning I realized how to work around this problem: use nested lambda functions!

A python example:

>>> f = lambda t: lambda x,y: x*y*t
>>> f(2)(2,3)
12
>>> f(1)(2,3)
6


And a 3d plot example:

def f(x,y,t): return(sin(pi*x)*sin(pi*y)*e^(-t))
base_frame = point3d((.5,.5,1), color='red', size=10)
L = lambda t: lambda x,y: f(x,y,t)
var('x,y')
(x, y)
frames = [plot3d(L(t)(x,y), (x, 0, 1), (y, 0, 1))+base_frame for t in [0,1]]

frames frames more

I may have spoken too soon. I'm still trying to sort out the two different issues here. If you have a chance, please take a look at the newly updated ticket.

Niles, is this really a Python bug, or is the behavior expected and documented? Lisp-y things sometimes are a little counterintuitive... You might also want to see if it's still in Python 2.7 or 3.x, just out of curiosity...

Even better, I think you could just do: lambda x,y,t=t: f(x,y,t) to capture the value of t when the lambda function is defined.

This is quite puzzling; if you update f to the following, test3 now raises the runtime error (probably test2 too). It seems that the condition x > .5 is being evaluated while x is still a variable instead of a number . . .

def f(x,y,t):
if x not in RR:
raise RuntimeError("checking x too soon")
if x > .5:
return(sin(pi*x)*sin(pi*y))
else:
return(sin(pi*x)*sin(pi*y)*e^(-t))


I also noticed that the plot3d function seems to just call parametric_plot3d in the end, so maybe working with parametric_plot3d directly could help focus too.

At this stage I guess I'm inclined to suggest that you define a full-blown class to handle making these plots -- then the value of t can be an attribute of the class, as well as the appropriate function of x,y and the appropriate plot. If namespace issues are part of the problem, then making each frame a new instance of some general class will, I think, help to avoid them.

more

Well, it's already a class in the real code :-) But I basically agree. I need to rewrite the code with this issue in mind. It doesn't appear that there's going to be one or two line fix.

OK, well I think I've found a way to get the plot to evaluate. Apparently the method json_repr() will do it (with much less overhead than actually saving to a file):

def test0():
var('x,y,t')
def f(x,y,t):
if x < .5: return(sin(pi*x)*sin(pi*y))
else: return(sin(pi*x)*sin(pi*y)*e^(-t))
base_frame = point3d((.5,.5,1), color='red', size=10)
frames = []
for t in [0,1]:
pic = base_frame
pic += plot3d(lambda x,y:f(x,y,t), (x, 0, 1), (y, 0, 1))
# This appears to force the line above to be "evaluated"
foo = pic.json_repr(pic.default_render_params())
frames += [pic,]
return(frames)

more

The following example illustrates the problem Niles saw with using lambda. Notice that the t in the lambda is taken from the current value of t in the global scope:

sage: f=lambda x: (x,t)
sage: t=2
sage: f(1)
(1, 2)
sage: t=3
sage: f(1)
(1, 3)
sage:


See the first few answers of http://stackoverflow.com/questions/938429/scope-of-python-lambda-functions-and-their-parameters for more discussion. Here is one workaround from those answers:

sage: f=lambda x,t=t: (x,t)
sage: t=1
sage: f(1)
(1, 100)

more