I’m making a small improvement to the ArcRod. I have a good reason for doing it, if I’m not mistaken. Then some renames, possibly improving things.

Given the pictures I drew yesterday:

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

It seems to me that working with angle A isn’t ideal, because we are interested in line a, which runs from the start of the ArcRod to its finish. So angle B is much more direct in terms of what we’re really up to. In the fullness of time, when we permit an arbitrary location for the target circle, we’ll have to rotate our solution to the angle between the ArcRod and horizontal. Coming soon to a computer near you. And we will think of the end point of the ArcRod as being   a   units away from ArcRod’s start.

So I have tweaked the algorithm in ArcRod to use angle B. (Probably there should be some kind of typewriter picture or some other clue as a comment.) The new method goes like this:

def update_both(self, ar_start, ar_finish):
    a = self._radius
    b = self._length
    c = ar_finish.distance(self._position)
    A = opposite_angle(a, b, c)
    offset = vector(b, 0).rotate_xy(A)
    result = ar_finish + offset
    return ar_finish, result

I swapped a and b, making b the radius of the target arc. (I do not like the names, and we’ll work on that in a moment.) I also do not like that the letters no longer match my drawing. Steady on. Be all that as it may, the new scheme is calculating the angle at the start of the ArcRod and since it is an inner angle, we just rotate directly, no longer needing to complement it. (It’s a confident angle, it doesn’t need complements. Sorry. (Yes, I know it’s “compliments”. Sorry again.))

Where was I? Oh, right, anyway this code continues to pass the test, which, allowing for rounding, goes like this:

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) == pytest.approx(length)
    assert position.distance(finish) == pytest.approx(radius)

Before we go any further, let’s work on the names. Let’s call the circle that the ArcRod reaches toward, the target. Change Signature and rename the members, all machine refactorings:

class ArcRod:
    def __init__(self, *, length, target_position, target_radius):
        self._length = length
        self._target_position = target_position
        self._target_radius = target_radius

    def update_both(self, ar_start, ar_finish):
        a = self._target_radius
        b = self._length
        c = ar_finish.distance(self._target_position)
        A = opposite_angle(a, b, c)
        offset = vector(b, 0).rotate_xy(A)
        result = ar_finish + offset
        return ar_finish, result

Now let’s do better than the ABCs here. The first argument to opposite_angle is the opposite side to the angle computed, and the other two sides are the adjacent sides, to that angle. Let’s rename locally for now:

    def update_both(self, ar_start, ar_finish):
        opposite_side = self._target_radius
        adjacent_side_1 = self._length
        adjacent_side_2 = ar_finish.distance(self._target_position)
        angle = opposite_angle(opposite_side, adjacent_side_1, adjacent_side_2)
        offset = vector(adjacent_side_1, 0).rotate_xy(angle)
        result = ar_finish + offset
        return ar_finish, result

Better. The function name isn’t right, somehow. And the input parameters, why do they say ar_? They are the start and finish of the ArcRod’s parent, typically a crank. Let’s rename, using a convention I’ve been trying, p for parent:

    def update_both(self, _p_start, p_finish):
        opposite_side = self._target_radius
        adjacent_side_1 = self._length
        adjacent_side_2 = p_finish.distance(self._target_position)
        angle = opposite_angle(opposite_side, adjacent_side_1, adjacent_side_2)
        offset = vector(adjacent_side_1, 0).rotate_xy(angle)
        result = p_finish + offset
        return p_finish, result

result is a weak name. While we think about that, let’s fix up the opposite_angle function:

def opposite_angle(a, b, c):
    return math.acos((b * b + c * c - a * a) / (2 * b * c))

The parameters are easy enough, I think:

def opposite_angle(opp, adj1, adj2):
    return math.acos((adj1*adj1 + adj2*adj2 - opp*opp) / (2*adj1*adj2))

I went with short names because it’s a long and presumably well-known expression. I could be wrong.

What is the name of this function? opposite_angle doesn’t seem right. I guess it is the angle opposite to the opposite side parameter.

I hate those short names already. But if I make them long, I’ll hate the expression. Don’t know. How about this?

def opposite_angle(side_a, side_b, side_c):
    a_squared = side_a * side_a
    b_squared = side_b * side_b
    c_squared = side_c * side_c
    b_c = side_b * side_c
    cos_angle_opposite_side_a = (b_squared + c_squared - a_squared) / (2 * b_c)
    return math.acos(cos_angle_opposite_side_a)

This is nearly silly but might actually be helpful at 3 AM when the beeper goes off. In any case, we’ve spent enough time on it. if not too much. Something about converting a one-line function to six lines seems off. I don’t know. You do you, I’m pausing here.

Let’s test and commit: tidying.

Our update_both function was intended, I think, to provide two answers, not just one, with the real update method selecting the right one given the inputs. I think we need some additional input hint so that we can select the right point on the circle. Some kind of angle might be ideal.

So we have work to do. Today, I just wanted to do that one change, so that we compute the angle we actually want and use it to get our end point. And that led to some renaming, which leaves me somewhat more pleased but not entirely satisfied. Naming is hard. That’s why I named our most recent cat “Kitty”, peace be upon her.

For now, that’ll do. I have a Time Police novel to read, and it’s time for an iced chai.

See you next time!