I have sinned: I allowed an “AI” to write some code for me. I am astonished and ashamed.
I really do think that the current LLM-based “AI” facilities are a net negative in the world. They are already being used to put people out of work. Because what they really compute is not an answer to the question we ask but instead to the question “What would an answer to this question look like”. Because they consume whole cities’ worth of electricity and water. Because in some applications their mistakes can be deadly. And, so far, I see too little recognition of these issues, especially in the oligarchs and techbros who are spending billions to make more and better versions.
And yet, I think that someone in my line of work, which is something about understanding programming and how it can, even should be done, needs to be able to speak from experience about these things, not just from what we have read and heard.
But I honestly feel a bit of shame at actually trying them at all, and I’m extra troubled when they turn out to be useful. Here’s my story:
I’m working toward a scheme, in SLua, for defining something like the power and valve linkage of a locomotive, and then moving the parts in SL to make it seem to work. Our locomotives do the simulation part now, but the code has been written by intensive analysis, and each case is a very custom-crafted program. They are similar but each one is different, sometimes just in some constants but quite often in the code.
I hope to come up with something better for creating these moving objects.
One of the common problems in these linkages is that we know one point on a piece of the linkage and have to compute other points, such as the other end, and the center. In some cases, that’s easy: the CouplingRod that locks driven wheel motion to the drive wheel is always horizontal, so its driven end is just its length away from the driving end, easy-peasy.
But a ConnectingRod connects a rotating wheel to an oscillating piston rod. Because we are working forward from the drive wheel, we know the position of the end on the wheel and we need the position of the end that connects to the piston rod. That problem comes down to “find the position on a line that is a given distance from an external point”.
The solution is obvious with pencil and paper: draw the line, draw a circle around the point of the desired distance, and where the circle intersects the line, one of those two points is the point you want. To analytically compute that value is a bit tricky and the preceding article dealt with that problem.
As I mentioned in that article, I found some code that was probably “AI” generated. I translated it to Lua and vectors and worked through it until I believe I understand it and could perhaps even do it again without looking at the solutions. Even so, my hands felt a little dirty: part of me thinks I should have solved it all by myself.
Frankly, that’s pretty silly. I’ve years of education and years of experience and thousands of pages read and thousands of hours of lectures that got me where I am today, in addition to thousands of hours I’ve spent doing actual programming. But pretty much for everything I’ve ever done, I learned a lot about the solution from others, via study, reading, or searching.
So, I want to argue, this shouldn’t be any different. Anyway, enough about my moral dilemma. I’ve done it again:
There is a common linkage component that requires us to find a point along an arc rather than along a line. Sometimes a large motion needs to be converted to a smaller (or vice versa), so a lever is set up, pivoted somewhere near the middle, and we push one wend and the other end moves a corresponding smaller (or larger) amount. The end of such a lever moves in a circular arc. We have the pushing end of our input rod, so we need to find points at a given distance from the circular arc. Similar problem, different solution.
And this time I actually pushed the “AI” to give me Lua code to solve the problem. Then I modified it to work with my objects and checked its results. Here it is, including comments that the LLM provided:
-- Function to find points on a circle at a specified distance from another point
function findPointsOnCircleAtDistance(circleCenter, circleRadius, specifiedPoint, distanceValue)
local results = {}
-- 1. Define the circles
-- Circle 1: (x - circleX)^2 + (y - circleY)^2 = circleRadius^2
-- Circle 2: (x - pointX)^2 + (y - pointY)^2 = distanceValue^2
-- 2. Subtract the equations of the two circles
-- (x^2 - 2*circleX*x + circleX^2) + (y^2 - 2*circleY*y + circleY^2) - circleRadius^2 = 0
-- (x^2 - 2*pointX*x + pointX^2) + (y^2 - 2*pointY*y + pointY^2) - distanceValue^2 = 0
-- Subtracting the second from the first:
-- (-2*circleX*x + circleX^2 - 2*pointX*x + pointX^2) + (-2*circleY*y + circleY^2 - 2*pointY*y + pointY^2) - circleRadius^2 + distanceValue^2 = 0
-- Rearrange into a linear equation:
-- A*x + B*y + C = 0
local diff = specifiedPoint - circleCenter
local A = 2 * diff.x
local B = 2 * diff.y
local C = circleCenter.x^2 - specifiedPoint.x^2 +
circleCenter.y^2 - specifiedPoint.y^2 -
circleRadius^2 + distanceValue^2
-- 3. Handle cases where B is zero (vertical line) or
-- A is zero (horizontal line)
if math.abs(B) < 0.0001 then
local x = -C / A
local term = circleRadius^2 - (x - circleCenter.x)^2
if term < 0 then
return results
elseif math.abs(term) < 0.0001 then
table.insert(results, vector(x, circleCenter.y))
else
local sqrtTerm = math.sqrt(term)
table.insert(results, vector(x, circleCenter.y + sqrtTerm))
table.insert(results, vector(x, circleCenter.y - sqrtTerm))
end
return results
end
-- 4. Solve for y in terms of x
-- y = (-A*x - C) / B
-- 5. Substitute y back into the first circle equation
-- (x - circleX)^2 + ((-A*x - C) / B - circleY)^2 = circleRadius^2
-- (x - circleX)^2 + ((-A*x - C - B*circleY) / B)^2 = circleRadius^2
-- (x - circleX)^2 + ((-A*x - (C + B*circleY)) / B)^2 = circleRadius^2
local C_prime = C + B * circleCenter.y
-- Now expand and simplify to a quadratic equation:
-- A_quad * x^2 + B_quad * x + C_quad = 0
local A_quad = 1 + (A * A) / (B * B)
local B_quad = -2 * circleCenter.x + (2 * A * C_prime) / (B * B)
local C_quad = circleCenter.x^2 + (C_prime * C_prime) / (B * B) - circleRadius^2
-- 6. Solve the quadratic equation
local discriminant = B_quad * B_quad - 4 * A_quad * C_quad
if discriminant < 0 then
return results -- no real solutions
elseif math.abs(discriminant) < 0.0001 then -- assume tangent
local x = -B_quad / (2 * A_quad)
local y = (-A * x - C) / B
table.insert(results, vector(x, y))
else -- two solutions
local sqrtDisc = math.sqrt(discriminant)
local x1 = (-B_quad + sqrtDisc) / (2 * A_quad)
local y1 = (-A * x1 - C) / B
table.insert(results, vector(x1, y1))
local x2 = (-B_quad - sqrtDisc) / (2 * A_quad)
local y2 = (-A * x2 - C) / B
table.insert(results, vector(x2, y2))
end
return results
end
-- Example Usage
local circleCenter = vector(0, 0)
local circleRadius = 5
local specifiedPoint = vector(6, 0)
local distanceValue = 2
local points = findPointsOnCircleAtDistance(circleCenter, circleRadius,
specifiedPoint, distanceValue)
if #points == 0 then
print("No points found.")
elseif #points == 1 then
print("One point found:")
print(" ", tostring(points[1]))
else
print("Two points found:")
print(" ", tostring(points[1]))
print(" ", tostring(points[2]))
end
local s1 = points[1]
print(s1:dist(specifiedPoint))
Thus far, the code seems to work, and it’s really rather comprehensive, checking the discriminant and all that good stuff. I do not understand the algebra - yet - but I will be working through it, because I don’t want code in my program that I have never understood. This is somewhat silly, I suppose, since there are libraries and compilers and operating systems and all that stuff whose code I certainly have never inspected and understood. But this is in my program, so I want to have understood it at some point, and I would prefer to shape it so that when I need to look at it later, it’s easier to understand.
This morning, I think I’ll just change from the open print result form we have here to an actual test.
function _:featureCircleDistance()
_:describe("points on circle at given distance", function()
_:test("provided example", function()
local circleCenter = vector(0, 0)
local circleRadius = 5
local specifiedPoint = vector(6, 0)
local distanceValue = 2
local points = findPointsOnCircleAtDistance(
circleCenter, circleRadius,
specifiedPoint, distanceValue)
for i, point in points do
_:expect(point:dist(specifiedPoint)).is(distanceValue)
end
end)
end)
end
Test passes. Of course we knew it would … so why write the test?
The reason is that I intend to work until I understand this code, and I am sure that in so doing, I’ll be refactoring it, at least renaming things, and quite possibly making more substantial changes. So I want to be sure that it continues to work.
And, if we were wise, we’d do more than just this one test, such as a test where we’re out of range, where our specified point is inside the circle, and so on.
I find that once I set up a test, it’s easy to do more testing and that makes me more confident. Setting up the first one is a bit tedious but since they all follow the same basic format, it’s almost restful to do it.
As we work with this code, let’s stay alert for times when the tests help us. I’ll bet there will some occasions.
I’ll do some studying and get back to this later. If not right away, certainly before I use it.
Safe paths!