Spin Me Right 'Round
Time to start on the rotation part of this idea.
Last night, I drew the picture below and posted it on Mastodon, with the caption “I hope this works”.

I think it was Michael Henos who said “hope is not a strategy”, and it certainly isn’t a very good one if it is a strategy at all. Fortunately, my actual strategy includes lots of testing and also animating some pictures so that I can see whether this notion works. I hope (sorry, I meant to say “estimate”) that we’ll get an end-to-end working version this morning. It won’t be clean, nor well integrated, but we should at least have enough tests to be confident that the scheme works.
In my book, programmer estimates aren’t much different from hope. We don’t know what we’ll encounter along the path. It could be smooth and clear; it could be swampland; it could be heavy brush full of snakes. What an estimate means varies by individual and situation, but for me, it means that if things go well, and I don’t get distracted, we’ll probably end up somewhere near the anticipated result. It could happen.
We are down to the rotation part of my plan for solving the connecting rod geometry, which goes like this:
- Translate the given problem to the origin (0,0) if necessary;
- Rotate by the negative of the piston angle to get the motion axis horizontal;
- Solve the problem for horizontal motion;
- Rotate the result back to the piston angle;
- Translate the result back to its true origin.
My analysis, done with drawings and hand calculations, convinces me that this scheme should work. I could be wrong: history suggests that I frequently am. We’ll write tests to verify results, as many as we seem to need.
Let’s get started. Where are we now? We have one test of the evolving “complete” solution:
def test_at_90(self):
origin = vector(0,0)
wheel_theta = 0
tilt_theta = 90*math.pi/180
radius = 1
length = 5
start, finish = con_rod_ends_complete(
tilt_theta, origin,
radius, wheel_theta, length)
print(f'\n{start=}\n{finish=}')
root24 = math.sqrt(24)
assert start.x == pytest.approx(1)
assert start.y == pytest.approx(0)
assert finish.x == pytest.approx(0)
assert finish.y == pytest.approx(root24)
Let’s use the terms that I used in the drawing, wheel angle, lead angle, and piston angle. I’ll call the variables radians if in radians, otherwise degrees.
By the time I add in lead_radians, we have way too many variables. I think I’ll use explicit names. And I need to change the function’s signature anyway.
We can require explicit names with Python’s * initial parameter. Here’s the function as it stands:
def con_rod_ends_complete(*,
piston_radians, origin,
radius, lead_radians,
wheel_radians, length):
use_radians = -piston_radians + wheel_radians + lead_radians
start, finish = con_rod_ends_with_origin(origin, radius, use_radians, length)
return start.rotate_xy(piston_radians), finish.rotate_xy(piston_radians)
I’ve added in the use of lead_radians. According to my theory, subject to the usual terms and conditions, this ought to work. Somehow, I don’t believe myself. Anyway this test will surely still run, because I passed in a zero for piston_radians. And it does. Let’s commit, just because we can.
We need more tests. Probably the biggest issue with them is that they are hard to hand calculate. Could we verify rather than provide explicit answers? Yesterday’s experiment with that wasn’t entirely satisfying, because inside the verify function, pytest’s assertion comments seemed not to work. I’ll try again. In any case we need to verify things like the distance between the two result points, and we should verify that the finish point is actually at the correct angle. I think we can do that.
Here’s the test and a new verify_constraints method. I think every solution should verity according to that function.
def test_at_90(self):
origin = vector(0,0)
wheel_radians = 0
piston_radians = 90*math.pi/180
lead_radians = 0
radius = 1
length = 5
start, finish = con_rod_ends_complete(
piston_radians=piston_radians, origin=origin,
radius=radius, lead_radians=lead_radians,
wheel_radians=wheel_radians, length=length)
root24 = math.sqrt(24)
assert start.x == pytest.approx(1)
assert start.y == pytest.approx(0)
assert finish.x == pytest.approx(0)
assert finish.y == pytest.approx(root24)
self.verify_constraints(start, finish, origin, length, piston_radians)
def verify_constraints(self, start, finish, origin, length, piston_radians):
assert finish.distance(start) == pytest.approx(length)
zero_based = finish - origin
finish_radians = math.atan2(zero_based.y, zero_based.x)
assert finish_radians == pytest.approx(piston_radians)
I start creating tests with greater and greater complexity. I’m ignoring the actual coordinates and counting on the verify function.
I’ve tested a few variations, using values that let me at least somewhat verify the actual values by checking on graph paper or comparing to equivalent but different inputs. Everything is passing.
This is a good place to take a break. Commit. What remains, to be done next, is to embed this function into a new ConRod object, test that a bit, and then draw some convincing animations, at which point I’ll really believe it’s working.
This has gone very smoothly. Very simple next steps, supported by tests. Looking at the code, one might feel that it’s so simple that all this care wasn’t needed. Since I interfered with the canine the first time around, I must respectfully disagree: I needed to be careful.
And, of course, I’ll really only be convinced when I can see the animation working.
Maybe next time! Today, we got to the point I expected, an end-to-end (apparently) working function. Now we “just” have to use it.
See you next time!