The upcoming component needs DESIGN. I hate when that happens. Let’s talk about this. Something special happens!

The next component on my agenda is a kind of connecting rod that connects, not to a straight line like the current ConRod, but instead to an arc of a circle. The component that it drives is typically a “bell crank”, which is used to change the direction of motion of a push-pull. You’ll see what I mean in the inevitable diagram or demonstration.

We’ll have a fixed-length rod whose start end is on a crank, and we’ll have another component in mind, with a fixed position (or at least a given position) and a diameter. In essence, at the moment of computation, we’ll have the centers of two circles, and their radii, and our mission is to find the points where the two circles intersect, and then select the “right” one, since there are typically two1 points where two circles intersect.

I am frankly not ready to build this, because I have not worked out the math. Of course it’s out there on what’s left of the Internet, and I could perhaps even solve it alone: it’s just two equations in two unknowns, with a bunch of square roots and stuff. My experience with the simpler case of a circle and a line tells me that I will need to be very careful with the math, and that will require some offline searching, drawing, hen-scratching, and scribbling out of errors.

We could, of course, build up the framework of the object, and almost certainly could hand-craft some examples with easy solutions, but even that will require some of the aforementioned scribbling. I really need to do some design away from PyCharm.

This is unusual for me. Not that I don’t look things up: I do that all the time. But generally, I look something up and type in the example or something like the example, and move along. I do far less “up front” design than is often recommended in the “book larnin’” we’ve all seen. I do that on purpose, to find out what happens when we start from an overly simple, possibly wrong design, and slowly manipulate the code, “evolving” to a better design. I try to work on the very low end of up front design, specifically to show that we can generally recover readily from design mistakes.

But here, I don’t currently have even a poor way of solving the problem. And I am hot to step away, grab my iPad, and start searching and … mathing? Mathematicizing? Mathematicalizing? Whatever, doing math.

Tell you what, let’s set up the tests and a starting class, just to see what we can do when we know essentially nothing.

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

class TestArcRod:
    def test_exists(self):
        arc_rod = ArcRod(length=5)

That much is clear and the test works. I hit a speed bump, however, when thinking of the other parameters to the class creation. Ideally, we could have a bell crank on some kind of pivot or slot, such that things might be adjusted somehow, within some reasonable range. However, the parent of an ArcRod will be the object whose finish is its start, typically a crank. We do not cater to components with more than one parent, more than one object upon which they depend. I’d like to have that capability, but now is not the kind. Table that notion and provide a target crank origin and radius. We’ll call them position and radius for now. We’re here to sketch in the shape of things.

So, this:

class ArcRod:
    def __init__(self, *, length, position, radius):
        self._length = length
        self._position = position
        self._radius = radius

class TestArcRod:
    def test_exists(self):
        length = 5
        position = vector(5,5)
        radius = 1
        arc_rod = ArcRod(length=length, position=position, radius=radius)
        assert arc_rod._length == length

We can at least sketch an actual test:

    def test_update(self):
        length = 5
        position = vector(6,0)
        radius = 2
        arc_rod = ArcRod(length=length, position=position, radius=radius)
        ar_start = vector(None, None)
        ar_finish = vector(0,0)
        start, finish = arc_rod.update_both(ar_start, ar_finish)
        assert False

Our actual update is named update. I’m supposing that ArcRod has a method update_both that returns both solutions to the geometry, and the update method does a bit more work to sort out which one to use.

Let’s draw a picture of the situation:

Two cards, one with sketch of layout with arc, one detailed with triangles, sides, and equations

From the two (2!) pictures I just drew on cards, it seems that we have two triangles with three known sides, and two known vertices. Side-side-side is solvable for the angles. Law of Cosines (no, I didn’t remember that, I looked it up.)

I code this, with a print to see how I’m doing:

    def update_both(self, ar_start, ar_finish):
        a = self._length
        b = self._radius
        c = ar_finish.distance(self._position)
        cos_A = (b*b + c*c - a*a)/(2*b*c)
        A = math.acos(cos_A)
        A_degrees = math.degrees(A)
        print(f'{a=} {b=} {c=} {cos_A=} {A_degrees=}')
        return None, None

That prints 51.something, which seems credible. That’s the angle from the bell crank to the intersection point. Doesn’t that, plus the bell crank origin, give me enough information to compute that point?

I think it does. Let’s beef up the test and then try it.

    def test_update(self):
        length = 5
        position = vector(6,0)
        radius = 2
        arc_rod = ArcRod(length=length, position=position, radius=radius)
        ar_start = vector(None, None)
        ar_finish = vector(0,0)
        start, finish = arc_rod.update_both(ar_start, ar_finish)
        assert finish.distance(start) == length
        assert position.distance(finish) == radius

If we have the right answer, the distance between the start and finish of the arc rod will be its length, and the distance between the bell crank origin and finish will be the bell crank radius. This test is passing, with this code:

    def update_both(self, ar_start, ar_finish):
        a = self._length
        b = self._radius
        c = ar_finish.distance(self._position)
        cos_A = (b*b + c*c - a*a)/(2*b*c)
        A = math.acos(cos_A)
        offset = vector(b, 0).rotate_xy(math.pi - A)
        result = self._position + offset
        return ar_finish, result

We compute the inner angle of the triangle, and the rotation we need is the complement, thus the math.pi-A. We rotate a vector whose length is our radius to that angle, and add the resulting vector to our position, and that’s the intersection point.

And the test runs, without even needing the usual fudge factor. The result point is (4.75, 1.561etc), which is at least credible.

I deserve a break, and so do you. But what has just happened?

The Stones Were Right

If you try sometimes you’ll find, you get what you need. I honestly had no intention of coming up with anything like a solution. I just wanted to lay in a test that would fail but succeed when the code was in place. I drew those two sketches above and put in the side values. That made me think “hey, a side-side-side is solvable”. I looked up the method, though doubtless 60 years ago I’d have remembered it, slapped in some rough code to get the cosine and the angle. Then I remembered that if I would rotate the radius to that angle, it would tell me where it was. And that turns out to solve the problem—for this case.

We’re far from done, but we’re further along than I had expected to be. And this solution makes me wonder whether we might be able to use an approach like the ConRod, where we translate and rotate to an easy position, solve the easy problem, and rotate and translate back. I don’t see why not.

This is better than finding a perfect leaf on the sidewalk or a really interesting insect. I just kind of wandered into what looks like a promising approach, and what is certainly at least a good foundation of test and code. I didn’t seek this out: I really just kind of stumbled into it. I just love when that happens!

See you next time!



  1. Or zero, if the radii just won’t reach, or, rarely, just one point of intersection if the radii are just right. Since we’ll be designing these linkages, we’ll see two points unless something has gone wrong.