Offline, I improved the Linkage collection rather nicely. Now we need a bit more improvement. Seems to be a pattern.

Offline
You may recall that a key idea behind the Linkage class is to offer a method apply that is given a function and applies it to each component of the linkage. The method guarantees that all the components have been updated, and is responsible for passing (some kind of information) to each call of the provided function. Clearly, the particular component will be one of the things provided.

In a previous article, we created a rudimentary version of apply. Yesterday, while hanging out in another world, I extended that method and applied it in our drawing code:

    def apply(self, func):
        s, f = None, None
        for element in self._elements:
            s, f = element.component.update(s,f)
            func(element.component, s, f)

We iterate the Elements, updating each element’s component with the preceding start / finish values, and then call the provided function func, passing the component, and the start / finish of the component itself.

In use, it’s rather nice, at least if one is au courant1 with lambda:

def animate_linkage(components):
    canvas.scale("all", 0,0, 1/scale, 1/scale)
    wheel.turn_by(4)
    components.apply(lambda c, f, s: c.draw(canvas, s, f))
    canvas.scale("all", 0,0, scale, scale)
    root.after(30, animate_linkage, components)

To cause each component to be drawn, all we had to say was:

    components.apply(lambda c, f, s: c.draw(canvas, s, f))

The lambda just means “define a nameless function accepting c, f, and s as parameters, such that when it is called it does c.draw(c,s,f)”. And that’s just what it does. The lambda notation is a standard part of Python, although the back hall chatter is that Guido doesn’t like it much. It’s quite convenient for cases like this, however, pace2 Guido.

However {inline-title}

However, all is not yet well. This code will work in the cases we have so far, where a component is immediately preceded by its parent, starting from the root component, so far a FixedPoint. But soon—by my standards—we’ll have a second ConRod emanating from the same Crank. With the current code in apply, the second rod would get the start / finish of the preceding rod, but it needs the values from the Crank.

Therefore, what we need to do is roughly this:

  1. Keep track of where each component is. The current Elements collection might suffice, but seems inefficient, as we’ll see.

  2. Have saved start / finish values in each Element. (Already the case, currently unused.)

  3. When updating a component, save its returned start / finish in those cells.\
  4. When it is time to process a given component (either to update or to apply the func), look up its parent and use the cached start / finish there, not the returned values from whatever was processed earlier.

Currently the Elements collection in Linkage is a simple array. We can make that work, and if my plan holds, we will make it work, but it seems likely that what we really want is a dictionary, indexed by component, pointing to the corresponding Element, for direct access as we wend our way through the processing.

Note
If I recall correctly, Python guarantees to return keys and values from a dictionary iterator in insertion order. That seems likely to be valuable to us in our present endeavor.

Let’s get to it. I want to look at the Linkage class and think about just how to do it.

class Linkage:
    class Element:
        def __init__(self, component, parent):
            self.component = component
            self.parent = parent
            self.start = None
            self.finish = None

    def __init__(self):
        self._elements = []

    def add(self, component, parent):
        le = Linkage.Element(component, parent)
        self._elements.append(le)

    def apply(self, func):
        s, f = None, None
        for element in self._elements:
            s, f = element.component.update(s,f)
            func(element.component, s, f)

It is tempting “just” to rewrite this, perhaps by intention, and be done with it. Let’s see, though, whether a bit of refactoring might help. I think we’ll be messing about with the parent and the start / finish values when we update. So let’s extract that one line into a method:

    def apply(self, func):
        s, f = None, None
        for element in self._elements:
            f, s = self.update_element(element, f, s)
            func(element.component, s, f)

    def update_element(self, element, f, s):
        s, f = element.component.update(s, f)
        return f, s

PyCharm did that for me. I did personally hand-craft the name update_element, however.

Now I think that when we’re done, we won’t be using the provided s and f parameters. Our mission here is to fetch the correct ones, from the avowed parent component of the current element. Like this:

Dammit, it reversed my parameter. Undo, do again. Dammit again, PyCharm won’t let me control the order of the output pair. I have to actually type a few characters to get what I want:

    def apply(self, func):
        s, f = None, None
        for element in self._elements:
            s, f = self.update_element(element, s, f)
            func(element.component, s, f)

    def update_element(self, element, s, f):
        s, f = element.component.update(s, f)
        return s, f

OK, as I was saying, let’s fetch the correct start / finish values, like this:

    def update_element(self, element, s, f):
        s,f = self.fetch_sf(element.parent)
        s, f = element.component.update(s, f)
        return s, f

That’s what I want. We call this programming “by intention”, or “wishful thinking”. I call the method that I wish I had. Now I “just” implement that method:

    def fetch_sf(self, parent):
        for element in self._elements:
            if element.component == parent:
                return element.start, element.finish
        return None, None

I think this is what we want for that function. Haven’t tested it yet. And I don’t think we have a very good test, now that I think of it. Anyway test. My rudimentary test does fail, so good news there. The message is even roughly what I expect:

    def update(self, start, finish):
>       return start, finish + vector(1,0).rotate_xy(self._angle_radians)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
E       TypeError: unsupported operand type(s) for +: 'NoneType' and 'VtVector'

We’re not yet saving our start/finish values, so they are all None. So:

    def update_element(self, element, s, f):
        s,f = self.fetch_sf(element.parent)
        s, f = element.component.update(s, f)
        element.start = s
        element.finish = f
        return s, f

I expect this to work. Tests pass. Let’s check the animation as well. It works.

We’re green and the animation works. This deserves a commit. First, I’ll move the Linkage class over to its own file: as usual, I’ve developed it inside the test file.

Oh, one more thing, remove those useless parameters: Here’s Linkage, complete:

class Linkage:
    class Element:
        def __init__(self, component, parent):
            self.component = component
            self.parent = parent
            self.start = None
            self.finish = None

    def __init__(self):
        self._elements = []

    def add(self, component, parent):
        le = Linkage.Element(component, parent)
        self._elements.append(le)

    def apply(self, func):
        s, f = None, None
        for element in self._elements:
            s, f = self.update_element(element)
            func(element.component, s, f)

    def update_element(self, element):
        s,f = self.fetch_sf(element.parent)
        s, f = element.component.update(s, f)
        element.start = s
        element.finish = f
        return s, f

    def fetch_sf(self, parent):
        for element in self._elements:
            if element.component == parent:
                return element.start, element.finish
        return None, None

We can do a bit better, I think, but let’s commit the working code: linkage now correctly accesses parent start / finish from elements list.

Now this:

    def apply(self, func):
        for element in self._elements:
            self.update_element(element)
            func(element.component, element.start, element.finish)

That seems better to me: it keeps the start / finish values all under the direct control of the Linkage class. I think I’d like to try improving the update_element method as well. How about this:

    def update_element(self, element):
        parent_s,parent_f = self.fetch_sf(element.parent)
        element.start, element.finish = element.component.update(parent_s, parent_f)
        return element.start, element.finish

I think that combines shorter and more clear in a pleasant way. Commit again: tidying.

Let’s sum up.

Summary

That went smoothly, despite PyCharm’s refusal to let me decide the order of the output start / finish pair. I am somewhat disappointed, but perhaps there was a way to do it that I didn’t see. Anyway no harm done.

The result is what we wanted: the Linkage now caches start and finish values in each element, and fetches the correct values for each component when updating and applying a desired function.

The changes went smoothly, the only test failure being the one I expected / hoped for. I still think we need a more robust test for this capability. Quite probably I should have devised one before starting, but I think things went well enough anyway.

The implementation is not particularly efficient, in that we search the elements list linearly to find a parent. The search is encapsulated in a method call, however, so we can improve the efficiency as and when we want to.

For now, it is time for some iced chai and like that there.

See you next time!



  1. Again with the French! “Up to date”, as you likely know. 

  2. “pace”, pronounced PAH-chay in church latin, means “peace”. Offers a gentle apology to the named party, in this case the eminent Guido von Rossum. First French, now Latin. Will the horrors never cease?