I thought I might have done something, well, pretty silly. That particular concern turns out not to be the case.

Content Warning
Title refers to cinematic violence. Do not click unless you want to know just where “Silly Wabbit” comes from in my chaotic mind.1

As I was saying, I think I’ve done something rather silly, if my memory is accurate, which assumes facts not in evidence. I think that I carefully built a dictionary for rapid access to parent values during apply, and then searched the dictionary linearly. Let’s find out:

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):
        for element in self._elements:
            self.update_element(element)
            func(element.component, element.start, element.finish)

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

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

Ah! Good news, I guess. My memory is not accurate. I have not put in the dictionary yet: it’s still a list. If we were to review the relevant article we might see me deciding not to go to the dictionary right away. Nonetheless, having thought of it, let’s do it now.

We’ll convert _elements to a dictionary with key equal to the component. Then when we go to fetch_sf for a given parent, which is of course some component (or None) we can just look it up directly.

Possibly, if I were the kind of person who uses “agentic AI”, I’d ask the agent to do this work for me. Frankly, I’d rather have an agent that cleans the bathroom and leaves the coding to me. I enjoy coding.

I think we need to change quite a few separate lines here, and I can’t think of any clever machine refactorings that will help. We are green, no pending commits. So here we go:

class Linkage:
    def __init__(self):
        self._elements = dict()

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

    def fetch_sf(self, parent):
        element = self._elements.get(
            parent, 
            Linkage.Element(None, None))
        return element.start, element.finish

This runs green. If I just use self._elements[parent], the tests fail because we have objects with parent None, which is not a valid key. So this seems nice. We’ll commit: Linkage uses dictionary for elements avoiding linear search.

I think in the code above, we could refer to self.Element rather than explicitly mention Linkage. That might be better. I’ll do it. Commit: tidying.

What’s Next?

My notes are all used up: anything that I noticed needed work has been dealt with. That doesn’t mean everything is perfect, but it is probably a good indication that we can move on to bigger things.

We have almost all the linkage components needed to do the fundamental bits of a locomotive. Not all the valve control linkage, which is daunting, but almost everything that actually drives the wheels—with one remaining exception.

There is a rod extending from the crank that links all the drive wheels together. It is called the Side Rod according to this diagram. Its motion is pretty straightforward, as one end follows the crank directly, and the other end extends straight horizontally from there.

I’m glad I remembered that. The one after this, a bell crank, is difficult. This one should be easy. Let’s see if we can write a test.

class TestSideRod:
    def test_side_rod(self):
        assert False

Fails! Perfect!

class SideRod:
    def __init__(self, *, length):
        self._length = length


class TestSideRod:
    def test_side_rod(self):
        side_rod = SideRod(length=3)

Passes! Commit: initial SideRod.

The fact that now I need to look at the other classes to see what I have to implement tells me that we are due to have an Abstract Base Class for the components. I have made a note on a card. For now, I’m pretty sure we want an update. Yes, with parameters start and finish, which are vectors. This will be easy.

    def test_update(self):
        length = 3
        side_rod = SideRod(length=length)
        crank_start = vector(-99,-99)
        crank_finish = vector(2,3)
        start, finish = side_rod.update(crank_start, crank_finish)
        assert start == crank_finish
        assert finish == crank_finish + vector(length, 0)

We ignore the crank start, which is basically the wheel center, and our finish is basically length away from the finish of the crank, flat. What can I say, this one is really simple. The update:

class SideRod:
    def update(self, _start, finish):
        return finish, finish + vector(self._length, 0)

Test passes. Commit: update working.

I’m still working in the test file, as is my habit when a class is just a-bornin’.

I think I’d like to see one of these babies drawn on the screen. Let’s put one into the drawing linkage. That will fail and drive out the init and draw.

...
piston = Piston(length=piston_length, angle=tilt_angle)
components.add(piston, con_rod)
side_rod = SideRod(length=2)    # <===
components.add(side_rod, crank) # <===
components.apply(lambda c, s, f: c.init_draw(canvas, s, f))

Running the draw main should fail looking for init_draw:

AttributeError: 'SideRod' object has no attribute 'init_draw'

Perfect. The draw methods are also easy:

    def init_draw(self, canvas, start, finish):
        self._cached_line = canvas.create_line(
            start.x, start.y, finish.x, finish.y,
            width=5, fill="blue"
        )
        
    def draw(self, canvas, start, finish):
        canvas.coords(self._cached_line,
                      start.x, start.y,
                      finish.x, finish.y)

And we have a lovely picture:

So that’s nice. We’ll sum up and move along to iced chai, perhaps a bit of granola bar, and some Peter F Hamilton.

Summary

Existing tests supported changing implementation details in Linkage, resulting in faster, more direct, simpler, and more understandable code.

A new simple object, SideRod, was easy to test, implement and draw, and it triggered a recognition that we might benefit from an abstract base class. We’ll look into that, probably next time. And coming up, the BellCrank, which is going to require serious geometric thinking, as it works like ConRod except that the target end follows an arc, not a straight line. That promises to be a bit challenging, hopefully just a bit.

Today things went very smoothly, which I attribute not only to my own mellow yet edgy personality, but mostly to having decent tests at the beginning, and a really easy new class to build at the end. The trick, I suppose, would be to devise ways to make all our changes easy. Is that possible? Probably more possible than we realize.

A pleasant morning’s effort, about 90 minutes including various interruptions, conversations, and observation of the gas people plumbing the next door house with some amazing tunneling tool and a giant coil of yellow-orange tubing.

See you next time!



  1. In the title, I quote here, not Elmer Fudd, not the Trix cereal ads, but O-Ren Ishii, shortly before being deservedly but horribly dispatched by The Bride, Beatrix Kiddo, in Tarantino’s ultra-violent film, Kill Bill