The ArcRod from yesterday defines the center and one end of a longer rod in ‘real life’. I propose what would be called a hack if we did something like it in code. I stumble, but recover. Small steps FTW.

On our diagram, note the long blueish line marked “arc rod”:

diagram of linkage

We’re working our way to the right-hand end of that rod, and then finally to the angled reddish rod that connects the two horizontal rods, the piston and valve rods. As you can see in the operating picture so far, we only have the stubby left hand end of that long rod defined:

arc rod

Strictly speaking, we want a rod with one start, where it connects to the angled red rod from the BellCrank, and two finishes, one where it connects now, to the reverser linkage, and the other way out at the other end. But our present scheme does not provide for three results from a Component, just two, their start and finish.

So I propose a hack. We’re going to add another Component, called I don’t know maybe ExtensionRod, that, given the start/finish of some rod, component, starts at the same start and finishes at the same angle, but at some given length. Essentially we’re going to weld an extension to the start end of the ArcRod we just added, extending off in the other direction from the finish.

I am not entirely in love with this idea: it’s clearly a hack. But our entire scheme, at present, relies on each component only proceeding from the start/finish of its parent, and returning a start/finish of its own. Suddenly providing a third output just doesn’t fit the design.

This often happens as we evolve a design: it supports everything we’re doing for a while and then suddenly we have something that needs doing that is possible, but clearly just a bit filthy. Our design allows this new thing, but it’s like going around the block the wrong way to get next door: it just isn’t right.

Our design seems to need improvement. Right now, we don’t know what that improvement would be. So we go around the block the wrong way. I reckon that if it just happens once, we let it go. If it happens multiple times, our code that goes around the block becomes more irritating and we do something about it. What might we do?

  1. Live with it. If it’s not too awful, and we still don’t see a good solution, let it be.

  2. Package it. Rather than just living with it, we might create a function or object to contain the round-the-block solution, so that we can use it more readily and safely. That’s better.

  3. Improve the existing design. Sometimes, after we’ve done it a few times and can see the commonalities, we can improve the existing design to encompass the new requirement right in the original objects. This is often better than the separate packaging of the around-the-block solution, in clarity, efficiency, or convenience.

Note
Somewhere, but not here, I should write about two (or more?) forms of “generalizing” a solution. There’s the kind where we just add more and more features to some object, and the kind where we change things around so that the object becomes more capable without becoming larger or messier. By my lights, the latter is better. It’s also relatively rare.

I’ve been reviewing Ruby configuration for my sins and RMV seems to be an example of the former form. It appears that RMV has about a thousand command words. I just wanted to be sure that my Ruby would run on another computer. I didn’t want to read a book about it, nor write a book to make it happen.

But I digress …

What was I saying? Right, we want a new component, ExtensionRod, that is given a length (positive or negative, I believe) and given the start/finish of its parent, returns the same start, and a finish which is the given length from the start, at the same angle as the input start-finish.

Begin with a test, I guess. I really think I could just code this up but no, that way lies degrada … it’s not that hard to create a test file:

class TestExtensionRod:
    def test_exists(self):
        xr = ExtensionRod(length=-4)

As usual, I’ll keep the class with the test for a while.

I evolve to this, which passes:

class ExtensionRod:
    def __init__(self, *, length):
        self._target = vector(length, 0)


class TestExtensionRod:
    def test_exists(self):
        xr = ExtensionRod(length=-4)
        assert xr._target == vector(-4, 0)

We’re saving that vector because we plan to rotate it to get the result. Let’s make the ExtensionRod a Component subclass and deal with that first.

class ExtensionRod(Component):
    def __init__(self, *, length):
        self._target = vector(length, 0)

    def draw(self, canvas, start, finish):
        pass

    def init_draw(self, canvas, start, finish):
        pass

    def update(self, parent_start, parent_finish):
        pass

PyCharm adds the pass methods for me upon request, after I put in the superclass Component. We’ll test update. (We’ll also worry about the order of those methods but alphabetic will do for now.)

    def test_horizontal(self):
        s = vector(1,0)
        f = vector(0,0)
        xr = ExtensionRod(length=-4)
        start, finish = xr.update(s, f)
        assert start == s
        assert finish == vector(-4, 0)

I begin with this “implementation”:

    def update(self, parent_start, parent_finish):
        return vector(0,0), vector(0,0)

Why? Because I want to see the test fail. That’s not required: I just feel like going in small steps.

Expected :v_Vector((1, 0, 0))
Actual   :v_Vector((0, 0, 0))

Fix:

    def update(self, parent_start, parent_finish):
        return parent_start, vector(0,0)

Should fail on the -4, 0 now:

Expected :v_Vector((-4, 0, 0))
Actual   :v_Vector((0, 0, 0))

Perfect. Let’s continue in tiny steps and just return _target:

    def update(self, parent_start, parent_finish):
        return parent_start, self._target

This should pass (but the code isn’t completely right. Patience, prudence!)

Test passes. We could commit. Let’s do, it’s a handy save point. Commit: initial ExtensionRod.

Why am I going in such tiny steps? Not from any rule or even guideline. It’s just that I can see the intermediate points along the path I’m forging, so why not stop at each of them. It doesn’t cost me any substantial time, and it notches lots of little successes. It feels good.

Now a test that shows we’re not rotating the Extension to the right position.

! ! ! ! Writing this next test, I realize that the code is already wrong.

    def test_vertical(self):
        s = vector(0,1)
        f = vector(0,0)
        xr = ExtensionRod(length=-4)
        start, finish = xr.update(s, f)
        assert start == s
        assert finish == vector(0, -4)

The idea was that the finish is at distance length from the start, not from the finish. The answers are wrong, they’re not what I want.

I’ve sketched a couple of examples, and I’m hoping they’re enough, but I’m now a bit uncertain. I’m also glad that I’ve been taking tiny steps: less to fix up.

Here’s the first test fixed:

    def test_horizontal(self):
        s = vector(1,0)
        f = vector(0,0)
        xr = ExtensionRod(length=-4)
        start, finish = xr.update(s, f)
        assert start == s
        assert finish == vector(5, 0)

The idea is that the new finish is supposed to be 4 units away from the parent start, in the opposite direction from the orientation of the parent-start to parent-finish line.

I could fake it here and then make it work, but I think it’s better just to make it work.

I have to use my close_enough function because the zeroes come back as 4 times ten to the minus infinity or something, but these tests pass:

    def test_horizontal(self):
        s = vector(1,0)
        f = vector(0,0)
        xr = ExtensionRod(length=-4)
        start, finish = xr.update(s, f)
        assert start == s
        close_enough(finish, vector(5, 0))

    def test_vertical(self):
        s = vector(0,1)
        f = vector(0,0)
        xr = ExtensionRod(length=-4)
        start, finish = xr.update(s, f)
        assert start == s
        close_enough(finish, vector(0, 5))

With this code:

def close_enough(result, expected):
    assert result.x == pytest.approx(expected.x, abs=1e-3)
    assert result.y == pytest.approx(expected.y, abs=1e-3)

class ExtensionRod:
    def update(self, parent_start, parent_finish):
        dif = parent_finish - parent_start
        theta = math.atan2(dif.y, dif.x)
        rotated = self._target.rotate_xy(theta)
        return parent_start, parent_start + rotated

OK, I think we’re good. Tests passing. Commit: save point.

Let’s reflect and break: this is enough for you to read and for me to write.

Reflection

I feel that deciding to test saved me. I could be wrong: it’s possible that I’d just have typed in the right code and looked at the picture (not even drawn yet) and been satisfied, but there would have been more work to do the drawing, and we have reason to suspect that I’d have still done it wrong. And then we’d be debugging instead of seeing.

I’d have done well to have created my test sketches sooner. And I think I’ll do a few more before continuing: I’d like to test at least the 45 degree angles. I’m sure they work but the tests will be more convincing.

I must have three or four copies of close_enough in various files now. I know of two for sure, because I copied one into this file. I’m not sure how to consolidate those. One possibility is to put a method into the vector class, but when it’s in the tests it compiles into good pytest assertions and if I put it in the vector class, it’ll just throw regular assertions. Maybe it’s OK as it is.

There are at least two instances of differencing two vectors, using atan2 to get the angle of the line and then rotating some vector by that angle. That could definitely be moved to the vector class in some useful way.

So opportunities to improve things are showing up. I’ve made suitable notes.

For now, the progress is decent. I think the class is ready for drawing. Next time we’ll maybe check it a bit more and then draw the picture.

This is going to work!

See you next time!