Let’s push on with the arc-following idea. I think we’re just about ready to put it into and object and thus into play, but I could be wrong.
I have quite a few tests for the arc code now, with names “trivial case”, “close and equal radii”, “calculate h”, “calculate 3 per Bourke”, “function returns both values”, “function at 45”, and “function returns empty on errors”.
I think the most interesting aspects of this arc segment so far include:
Let me elaborate just a bit:
I had that testing focus, I’m sure, because at that point I was working through the geometry, square root of the hippopotamus kind of thing. So I had those numbers and checked for them. But getting them becomes increasingly tedious.
I finally realized that we could test for criteria like these: there are two points returned; they are not equal; each one is the specified distance from the first point and the other specified distance from the second point. In short: check that the result meets my desires, not some specific values. Those tests are simple and can be handled by a single checking function.
The best way I know to understand provided code is to surround it with tests, and then refactor it, breaking out small bits that make sense, and figuring them out one at a time. The process is tedious, and I, for one, often do not enjoy it. After all, we have this program, let’s just use it.
Even going that way, I suspect it would have taken me as long or longer to understand the LLM code, and quite likely I’d have stopped with less real understanding, out of boredom.
I do not favor LLMs, for various moral reasons, and because I program for the joy of discovery and I see no point letting some LLM have all the fun. I have not used them enough to have a strong sense of whether they really make me more productive. And, if they do, how do I feel about that?
That would be a dangerous place to be, I think, but if the test are solid enough, it might be OK.
Oh, right. Well, we have a very nicely tested function and helper:
function circle_intersections(p0, r0, p1, r1)
local d = p1:dist(p0)
if d >= r0 + r1 then
return {}
end
local a = calculate_a(p0, r0, p1, r1)
local h = math.sqrt(r0*r0 - a*a)
local dif = p1 - p0
local p2 = p0 + dif * a / d
local pb_x0 = p2.x + (p1.y - p0.y) * h / d
local pb_y0 = p2.y - (p1.x - p0.x) * h / d
local pb_x1 = p2.x - (p1.y - p0.y) * h / d
local pb_y1 = p2.y + (p1.x - p0.x) * h / d
return {vector(pb_x0, pb_y0), vector(pb_x1, pb_y1)}
-- local step = vector(dif.y, dif.x) * h / d
-- return {p2+step, p2-step}
end
function calculate_a(p0, r0, p1, r1)
local d = p0:dist(p1)
return (d*d + r0*r0 - r1*r1) / (2*d)
end
What we need is another object like our MainRod, PistonRod, SideRod, and DriveWheel, to let us build up mechanisms.
There is a log of commonality between this as yet unnamed object and the MainRod. The MainRod is driven in a circle at its starting end, and places its driven end at a given fixed distance, constrained to be on a line. This new rod is driven in a circle at its starting end and places its driven end at a given fixed distance, constrained to be on an arc.
Seems likely that there will be a lot of commonality between these two objects. We could spend some time looking at MainRod and thinking how to extend it. We won’t do that. We will look at it, but we’ll instead create a new object, following MainRod where it makes sense, and plugging in the arc code where that makes sense. Then, if there is enough commonality, we’ll refactor to eliminate it.
Let’s call it an ArcFollowingRod, for now, just to get off the dime.
We need a test. We’ll make the test feature separate from the one we used for the circle code: they’re related but not the same.
function _:featureArcFollowerRod()
_:describe("ArcFollowerRod", function()
_:test("exists", function()
local wheel = DriveWheel{x=10, y=10, r=1}
local rod = ArcFollowerRod{parent=wheel, radius=1, length=5, }
end)
end)
end
I can already tell that this will require some guessing to set up something that will work, and we’ll need to select which of the two results we want, and so on. This is enough to require the basic class:
ArcFollowerRod = class()
function ArcFollowerRod:init(parms)
self._parent = parms.parent or error('expected parent')
self._length = parms.length or error('expected length')
self._radius = parms.radius or error('expected radius')
end
I just copied MainRod for this much. Test should run. Perfect. Ship it!
What more does it need? It needs the x and y of the pivot point for the arc, and its radius. I think we want the pivot below the center of the drive wheel, so that the push is basically direct against the pivoting part, and I think we want the radius of the arc to be a bit larger than that of the drive wheel, because if it were the same size it would tend to go around and around, not back and forth. If it were smaller, something would break. I’ll guess.
function verify_results(points)
_:expect(#points).is(2)
end
_:test("initial config", function()
local wheel = DriveWheel{x=10, y=10, r=1}
local rod = ArcFollowerRod{
parent=wheel, radius=1, length=5,
pivot_x = 15, pivot_y = 8, pivot_r=1.5}
points = rod:driven_points()
verify_results(points)
end)
This may be too big a bite. We’ll see. Well, actually we can pass this trivially:
function ArcFollowerRod:driven_points()
return {1, 2}
end
This is a legitimate move in the way we work. It is often called “fake it till you make it”. You provide a dummy answer that passes the elementary test. Then you make a harder test that fails, then you make it pass. So we’ll do that. I think I need to move the circle functions to a more accessible location: currently they are local functions inside the CircieCircle tests.
This is slightly more tricky than one might like. If we just move them to top level and make them local functions (because global accesses are very slow), we have to order them so that the called functions are defined before the calling functions.
An alternative is to define them into a table of functions and refer to them that way. That’s probably a better way of packaging things. For now, I’ve just arranged them so things compile.
Now let’s make the test harder:
_:test("initial config", function()
local wheel = DriveWheel{x=10, y=10, r=1}
local rod = ArcFollowerRod{
parent=wheel, radius=1, length=5,
pivot_x = 15, pivot_y = 8, pivot_r=1.5}
points = rod:driven_points()
verify_results(points,
vector(10,10),1,
vector(15,8),1.5)
end)
Now I’m using the same verify as in the circle code. This will fail miserably, sine we aren’t even giving it vector inputs. It does. Opportunity to improve. Let’s just create the values and call our circle code:
function ArcFollowerRod:driven_points()
local p0 = self:driven_end()
local p1 = vector(self._pivot_x, self._pivot_y)
return circle_intersections(p0, self._radius, p1, self._pivot_radius)
end
Now we just need the driven end, the DriveWheel end. That’s drive_point()
. I expect trouble but we’ll just call that and see how our test fails.
function ArcFollowerRod:driven_points()
local p0 = self._parent:driven_end()
local p1 = vector(self._pivot_x, self._pivot_y)
return circle_intersections(p0, self._radius, p1, self._pivot_radius)
end
Run the tests. Some typos and there is no drive_point
method on DriveWheel. That’s on MainRod. It does its own computations of some things. This code needs a good refactoring. Anyway … I think DriveWheel needs drive_point
, so I’ll provide it:
function DriveWheel:drive_point(radius)
return self:position() + self:adjusted_offset(vector(self._radius, 0))
end
This code really needs refactoring. Objects aren’t able to rely on others to do the right thing. This happens as we learn what we need. I’m not upset, just recognizing what needs to happen. Let’s test and see how we’re doing.
That member is named r
not radius
. Inconsistent naming bites me again. We need to look at the names and align them more sensible. Also the types, sometimes we’re using vectors and sometimes separate x and y. Again: we need a bit of refactoring here.
My test passes. Honestly I do not believe it. Let’s add some printing to the verify.
Nothing prints. Why? Length is zero. I’ve returned nothing.
I’m about at my point of tiredness here. but let’s get some more info out of the circle code.
I quickly see that I am not passing the right values, I need to pass the length of the rod.
function ArcFollowerRod:driven_points()
local p0 = self._parent:drive_point()
local p1 = vector(self._pivot_x, self._pivot_y)
return circle_intersections(p0, self._length, p1, self._pivot_radius)
end
Test. I like the values printed and the verify fails. Here’s the output. The numbered lines are my solution points and their distances from the centers. Those look right to me.
Feature: ArcFollowerRod
ci <20, 10, 0> 5 <15, 8, 0> 1.5
#points 2
1 <15.0250838994676, 9.499790251330998, 0>
5.04991861215129 1.4999999999999993
Actual: 5.04991861215129, Expected: 1 +/- 0.001.
Test: 'initial config'.
2 <16.052502307428952, 6.9312442314276215, 0>
6.786018431199479 1.5000000000000013
Actual: 6.786018431199479, Expected: 1 +/- 0.001.
Test: 'initial config'.
ArcFollowerRod, 7 Tests: Pass 5, Fail 2, Ignored 0.
I actually do expect … oh have I called it with the wrong expectations? Yes, I have. With that in place:
Feature: ArcFollowerRod
ci <20, 10, 0> 5 <15, 8, 0> 1.5
#points 2
1 <15.0250838994676, 9.499790251330998, 0>
5.04991861215129 1.4999999999999993
2 <16.052502307428952, 6.9312442314276215, 0>
6.786018431199479 1.5000000000000013
Actual: 6.786018431199479, Expected: 5 +/- 0.1.
Test: 'initial config'.
ArcFollowerRod, 7 Tests: Pass 6, Fail 1, Ignored 0.
The first one is good and the second is not. That’s weird, because the circle-circle code is supposedly well tested and working.
Let’s create a test for that code and see if we can replicate this problem … looking at the ci
line above, those are the values going into the function. THose don’t make much sense to me. I expect a point near 10,10 for the first point and 15,8 for the second. With a radius of the wheel of 1, x and y shouldn’t deviate from 10,10 by more than 1, i.e. 9-11.
I find the problem: I passed r
to the DriveWheel and should have passed radius
. It doesn’t error when it doesn’t get radius
. Fix that.
I fix that and clean up all the lines that didn’t supply radius. Now it’s time for a break. I shall return perhaps even today.
I’m hoping I’ve just given it an impossible configuration. We’ll see.
I think my second point is inside the radius of my first point’s circle. I’m not sure just what is supposed to happen in that case. Let’s adjust that a bit.
Ah. My test values are not valid at all. I’m testing from 10, 10, need to test from the drive position.
When I give it a point outside the big circle, my test passes:
Feature: ArcFollowerRod
p0 drivepoint <11, 10, 0>
ci <11, 10, 0> 5 <17, 8, 0> 1.5
#points 2
1 <15.50870649213401, 7.838619476402029, 0> 5.000000000000001 1.5000000000000016
2 <15.903793507865991, 9.02388052359797, 0> 5.000000000000002 1.5000000000000009
ArcFollowerRod, 7 Tests: Pass 7, Fail 0, Ignored 0.
distance 5 and 1.5 is what I expect and what I get. But it really should work if the target is inside as well, it seems to me. Try another test:
_:test("inside config", function()
local wheel = DriveWheel{x=10, y=10, radius=1}
local pivot = vector(15,10)
local rod = ArcFollowerRod{
parent=wheel, radius=1, length=5,
pivot_x = pivot.x, pivot_y = pivot.y, pivot_radius=1.5}
local start = rod:starting_point()
points = rod:driven_points()
verify_results(points,
start, 5,
pivot, 1.5)
end)
That has the pivot inside the big circle and works at least at the initial angle. I put the original 15, 8 point back in and it passes as well. I’m not sure what happened but it seems to be fixed.
Well enough. Ship it, come back tomorrow.
Safe paths!