Chris
Chris

Reputation: 1321

Different ValueTrackers returning same value in Manim

I'm attempting to place multiple DecimalNumbers in Manim, each with a random starting value, so that when I use a ValueTracker on each of them they will seemingly output arbitrary arrangements. Yet when I implement this code:

all_d, all_t = [], []

for tile in tiles:
    start_number = random()
    k = ValueTracker(start_number)
    updateFunction = lambda i: i.set_value(k.get_value())
    d = DecimalNumber(num_decimal_places=2).add_updater(updateFunction)
    d.move_to(tile)
    all_d.append(d)
    all_t.append(k)

print([k.get_value() for k in all_t]) # OUTPUT ValueTracker starting values

self.play(*[Create(d) for d in all_d])
self.play(*[k.animate.set_value(0) for k in all_t], run_time = 5)

The print statement outputs random numbers: [0.27563412131212717,..., 0.9962578393535727], but all of the DecimalNumbers on the screen output the same value, starting with 1.00 and ending with 0.00 synchronously. And if I add the same print statement after all of my code, it results in all 0's: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0].

I've tried adding: k.set_value(start_number) after initializing the ValueTracker, but that proved fruitless. I've also tried using this code instead:

all_d, all_t = [], []

for i, tile in enumerate(tiles):
    start_number = random()
    d = DecimalNumber(num_decimal_places=2)
    d.move_to(tile)
    all_d.append((i, d, start_number))
    all_t.append(ValueTracker(start_number))

self.play(*[all_t[i].animate.set_value(start_number + 1) for i, _, start_number in all_d],
          *[UpdateFromFunc(d, lambda m: m.set_value(all_t[i].get_value())) for i, d, _ in all_d],
          run_time = 5)

But all of them still match up, and they go from 1.00 to 2.00.

How can I remedy this?

Upvotes: 2

Views: 281

Answers (1)

JLDiaz
JLDiaz

Reputation: 1453

It is a tricky problem related to the fact that your lambda is accessing to an external variable. In particular, look at these two lines, inside your for loop:

        # for ...
            k = ValueTracker(start_number)
            updateFunction = lambda i: i.set_value(k.get_value())
            # ...
            all_t.append(k)

Inside lambda you use k, and that k is no more than a reference to an external variable. While that lambda is being defined, the value of k is not read. It is read however when the lambda is ejecuted later by manim, as an updater function.

The problem is that this lambda, as well as all the lambdas created in the loop, refer to the same k, because that k is kind of a "global" variable for the lambdas, since it is external to them.

The value of k changes in each iteration of the loop, but that does not affect to the lambdas, until they are run. At that time k is a reference to the last ValueTracker() created in the loop. All other ValueTrackers are stored in the list all_t, but k refers only to the last one.

This explains why, when you loop onall_t to print the values you get different ones, but the updaters (the lambdas) use all the same one (the last one in the list).

Solution

The solution looks like a hack, but it actually makes perfect sense if you think on it:

            k = ValueTracker(start_number)
            updateFunction = lambda i, k=k: i.set_value(k.get_value())

Note the small addition k=k in the lambda definition. What does this do?

  • It declares a new parameter named k for the lambda. So when you use k inside it no longer refers to the external variable, but to the local variable (parameter) for that particular lambda. Each lambda has his own k
  • It gives it a default value, because when the lambda is called as an updater, manim will not pass any value to k, only to i
  • The default value is the value of k at that iteration of the loop. Python evaluates the expression that assigns the default value, and the result of that evaluation is what is used as default.

In other words, in k=k, the k at the left is a local variable for the lambda. The k at the right is replaced for the current value of k and assigned to the internal k as default value.

This solves your problem.

Upvotes: 3

Related Questions