Reading Before Refactoring: What My Stirling Engine Code Actually Taught Me
Thereās a moment in every engineering studentās semester when you realize your MATLAB code has become sentientānot in the fun AI way, but in the āI have no idea what half of these functions do anymoreā way. Today was that moment for my Stirling engine simulation.
The request seemed simple: refactor StirlingCycle.m because it had grown unwieldy. But instead of diving straight into surgical code changes, Claude did something that surprised me. It read the entire file first.
The Code Archaeology Phase
Before suggesting a single change, Claude walked through 400+ lines of thermodynamic calculations, slider-crank kinematics, and Schmidt analysis functions. The file had accumulated layers like sedimentary rockāeach representing a different debugging session or feature request.
That reading phase revealed something Iād completely missed: three separate functions were calculating piston displacement using slightly different assumptions. One used full slider-crank kinematics. Another used a simplified sinusoidal approximation Iād added during an early debugging session. A thirdāburied in the Schmidt analysis sectionāused the exact kinematic equations but with hardcoded parameters instead of pulling from the config struct.
If Claude had jumped straight to āletās extract this into a separate file,ā it would have preserved all three approaches. The refactored code would have been just as inconsistent as the original, just spread across more files.
Hereās what one of those problematic functions looked like:
function vol = schmidtExpansionVolume(theta, params)
% Simplified sinusoidal - added 10/15 during late-night debug
stroke = params.powerBore * 0.8; % Why 0.8? No idea anymore
vol = params.clearanceVol + (stroke/2) * (1 - cos(theta));
end
That 0.8 factor was a mystery even to me. The comment admitted as much. The real slider-crank calculation elsewhere in the file used actual geometryācrank length, connecting rod length, proper kinematic equations. This simplified version was a debugging artifact Iād forgotten to remove, producing subtly different volume curves than the rest of the simulation.
Test-First Refactoring: Catching What Iād Hidden
What happened next changed how Iāll approach refactoring forever. Instead of just rewriting the code to ālook cleaner,ā Claude suggested writing comprehensive tests firstātests that would capture exactly what the current code produces.
The first test compared the output of all three displacement calculations across a full 360-degree cycle:
function test_displacement_consistency()
params = loadDefaultParams();
angles = linspace(0, 2*pi, 360);
kinematic = arrayfun(@(a) calculatePistonPosition(a, params, true), angles);
sinusoidal = arrayfun(@(a) simplifiedDisplacement(a, params), angles);
schmidt = arrayfun(@(a) schmidtExpansionVolume(a, params), angles);
% These should match within tolerance
assert(max(abs(kinematic - sinusoidal)) < 0.001, 'Kinematic vs sinusoidal mismatch');
end
The test failed immediately. Maximum deviation: 0.023 meters. Not huge in absolute terms, but enough to throw off the P-V diagram integration by several percent. The work output calculation used the kinematic version while the Schmidt analysis used the sinusoidal approximation. Iād been comparing apples to oranges for weeks.
Thatās when I understood what my āclamped volume calculationsā were actually doing. During debugging, Iād noticed the P-V diagram wasnāt closing properlyāthe cycle didnāt return to its starting point. Instead of finding the root cause (inconsistent displacement functions), Iād added volume clamping at the cycle boundaries:
if abs(theta) < 0.01 || abs(theta - 2*pi) < 0.01
vol = params.clearanceVol; % Force closure
end
This made the diagram look correct while masking the underlying physics violation. A test-first approach would have caught this immediately because Iād have had to articulate: āThe P-V diagram should close naturally without forced clamping.ā
What the Refactoring Actually Produced
With the tests in place, the refactoring became straightforward. One canonical displacement function. One set of geometric parameters. The Schmidt analysis now calls the same kinematics as everything else.
The refactored piston position functionānow the only oneālooks like this:
function pistonPosition = calculatePistonPosition(crankAngle, params, isPower)
if isPower
angle = crankAngle;
crankLength = params.powerCrankLength;
rodLength = params.powerRodLength;
else
angle = crankAngle + params.phaseShift;
crankLength = params.displacerCrankLength;
rodLength = params.displacerRodLength;
end
beta = asin(crankLength * sin(angle) / rodLength);
pistonPosition = rodLength * cos(beta) - crankLength * cos(angle);
end
Clean slider-crank kinematics, used everywhere. The mysterious 0.8 factor is gone. The volume clamping is gone tooāand the P-V diagram still closes properly, because the physics is now consistent.
The Broader Lesson
Writing tests before refactoring forced me to articulate what the code should do. That articulation revealed my mental model was wrong. I thought I had one displacement calculation with some cleanup code around the edges. I actually had three competing calculations and duct tape hiding the conflicts.
Five minutes of readingāreally understanding the existing codeārevealed problems I would have preserved if Iād jumped straight to restructuring. The tests gave me confidence that the refactored code actually works, not just that it looks cleaner.
Tomorrow Iāll run the full simulation and compare output against theoretical Schmidt analysis predictions. If they donāt match within tolerance, at least Iāll know exactly which function to examine.
Sometimes the most productive coding session is the one where you write zero new features and just make the existing code honest.