Bell Crank is kind of an inflection point. Let’s see what we should do next.

My informal list of components that we need for locomotives is complete with Bell Crank. I’m sure we’ll need more, but I’ll have to experiment to see which additional issues arise. So this is a good time to look around and see what needs doing. Let’s brainstorm, or anyway brain breeze to see what we might do.

  1. ArcRod error? I thought I saw the ArcRod thrashing, unable to find a solution. Probably bad setup but we need more tests.
  2. More complex demo linkage. Building a more complex demo would be impressive, and also likely to turn up things that are hard to to, or impossible.
  3. Abstract Base Class. Duck typing is fine, but Python does permit us to define an abstract base class for objects. If we do that for our components, we’ll get compiler help when we forget things. Not that we’ll ever forget anything. What was I saying? Oh, right.
  4. Type hinting. Python has been improving the type hinting, and it might be a good time to work with that a bit, to learn how it works, see what it’s good for.
  5. Export state. One way this thing might be used would be to set up a linkage, run it, and export the linkage state at points around the cycle, for use in some other animation system.
  6. Export. I suppose one might want to export the actual linkage information, again for possible use in some other system. I have no real use case for this.
  7. General code improvement. We know this is always on the agenda, but at a pause point such as we’re at now, it’s a particularly good time to look for rough spots in the code. I’m sure there are some.
  8. Better base types. We throw numbers around as lengths, coordinates, angles in degrees, angles in radians. Our vector type is borrowed from somewhere on the Internet. We might benefit from some stronger base types.
  9. Error handling. This is a hard one. It always is. There are at least two situations to consider: detecting bad arguments when defining a linkage, detecting that a linkage can’t possibly work, and dealing with run-time errors. (I did say “at least two”.)

Quite a list for offhand.

I think I’d like to start with the abstract base class. That will give us at least a bit of a look at all our code.

ABC

The general plan will be to define Component, an abstract base class for all our linkage components, then cause them all to inherit from it.

Since abstract base classes generally have no behavior, we can’t really TDD it, but we have lots of other tests that will have to keep working, and we can fiddle around with classes, temporarily removing required methods to see that they get diagnosed properly, and so on.

from abc import ABC, abstractmethod

class Component(ABC):
    @abstractmethod
    def draw(self, canvas, start, finish):
        pass
    @abstractmethod
    def init_draw(self, canvas, start, finish):
        pass
    @abstractmethod
    def update(self, parent_start, parent_finish):
        pass

That seems to be all there is to it. Now I’ll make some class inherit from Component.

class BellCrank(Component):

I renamed update to update_x and get this error running the tests:

E       TypeError: Can't instantiate abstract class BellCrank without an implementation for abstract method 'update'

There is also a warning flag on the file, but since some of those are kind of picky, I tend to ignore them. I wonder if there is a way to tweak those to make them higher priority or something. I’ll look into that. Anyway, the error does get detected, just not as soon as I might prefer.

I think PyCharm will offer to create the necessary methods if I do the right incantation. I’ll look into that as well.

Once that’s done, it’s just a matter of a few minutes to make all the components inherit from Component. They get cute little icons showing inheritance on the key methods. Tests are green. Commit: abstract base class Component used in all components.

Now it occurs to me that we could probably add some type hinting to Element and Linkage. Let’s see about that.

I went hog-wild:

from typing import Callable, Any, Optional

from src.component import Component


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

        def dump(self):
            if hasattr(self.component, 'dump'):
                self.component.dump(self.start, self.finish)

    def __init__(self):
        self._elements = dict()
        self._previous = None

    @property
    def parent(self):
        return self._previous
    @parent.setter
    def parent(self, component):
        self._previous = component

    def add_one(self, component:Component, parent:Component=None):
        le = self.Element(component, parent or self._previous)
        self._elements[component] = le
        self._previous = component
        return component

    def add_l(self, component:Component, parent:Component=None):
        self.add_one(component, parent)
        return self

    def add(self, *args:Component):
        for arg in args:
            self.add_one(arg)
        return self

    def apply(self, func: Callable[[Component, Any ,Any], Any]):
        for element in self._elements.values():
            self.update_element(element)
            element.dump()
            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:Component):
        element = self._elements.get(parent, self.Element(None, None))
        return element.start, element.finish

There’s a bit of cuteness in here, and I’m not in love with it. I originally typed the Element parameters as Component, but we see that in the fetch_sf method just above, we create an Element with no component or parent, sort of a null Element. So I had to do the Optional thing that you see on Element.

And in apply I just put in that Callable mess for completeness, but it is close to right, if not very helpful.

I can’t say that things are vastly better but PyCharm did highlight that I had not set SideRod to be a Component, which was due to the fact that I had never moved that class to the ‘src’ tree. Done and done.

In general, PyCharm now tends to underline more things that aren’t right, and maybe its guesses about what I want are better.

Note
I am still not using the optional “AI” features and do not intend to, certainly not on a regular basis. But PyCharm is quite good even without any fake artificial intelligence. I think it was implemented by people with real intelligence. Nice!

So, there we are. A little bit of typing and we have abstract base classes for our Components. Since there are only three required methods, that’s no big deal here, but it is the right thing to do as a code base grows. So good for us.

And then the type hinting in Linkage and Element was possible, and actually found a problem in the code, where SideRod had not been move to the production side of the code tree. Actually useful.

A pleasant little morning effort. I’ll stop while I’m ahead.

See you next time!