Continuous Improvement
Let’s see what else we can improve in our four existing components. I can name some things that I’d like to improve.
As soon as we look at the code, we’ll see plenty of things to improve: we generally do. So far, we set them all to use keyword parameters, and they all refer now to parent in their protocol, and we let them refer to the parent by another name internally if desired. I think that is a good idea, because that could help the internal logic make sense.
Issues that come to mind, starting with that thought, include:
- We need a sensible naming convention for the key values of a component, which might be one, two, or possibly more points, or might be a radius and an angle, as in the case of a crank;
- We should be consistent in our use of methods or properties for accessing members of a component;
- We’d like to cache results of a component’s calculations, so that it only does the work once per update;
- It might be desirable not to have to call update on all the components, instead just updating the root component or components;
- The component objects are now part immutable, in the aspects that define their shape, and part mutable, in the aspects that define their current position and orientation;
- The components also have mutable members used in their draw methods;
- Computing position and doing the drawing seem like different capabilities and should quite probably be in separate objects.
Wow. The list is getting longer.
Let’s start with naming. A component in the world is some solid shape made of iron, and in our animation their position and orientation will probably be used in the form of a point representing the “root” of the component, and an angle equivalent. An equivalent form might be to provide two points on the component. Since they are solid, fixing two points is sufficient to settle where it is and its orientation.
In the current program’s drawing code, which is important for checking results but not the final use of the information, I’ve found the two point formulation to be most useful, as we will typically draw the object as a line diagram. Later objects may be more complex.
In the calculation mode, well, it depends. A Wheel has a location and an angle of rotation that is generally the basis of all subsequent calculations.
A Crank amounts to a position on the wheel, specified by a radius from the center, and a “lead angle” specifying how far around the wheel the crank is from the wheel’s internal zero angle. A crank could be 90 degrees in advance of the wheel’s nominal rotation, for example.
A ConRod connects one end to a crank, which drives the rod, and its other end lies along a line at a specified angle, from the crank’s center of rotation (typically the Wheel’s center). Its calculation method wants the radius and angle of the Crank it attaches to. Presently the Crank provides that information, although the ConRod could use the center position and Crank position to compute the values itself. And perhaps it should.
A Piston, as presently defined, has a fixed angle of motion, and one end of it is the end of the ConRod. In principle, its parent could be anything that provides a point along the line upon which the Piston moves.
- An Idea
- I’m glad we have been having this little chat. I’m getting an idea. Let’s assume each component can provide two point values,
startandfinish. For the Wheel, those values would be the wheel center, and the position of the zero index point on the wheel, that is, a point on the rim of the wheel that would be at the same y value, and a positive x value, when the wheel is at angle zero. The <x,0> point. -
Given
startandfinish, we can readily computerandtheta, as the distance between the two points, and the arc-tangent of delta-y over delta-x. -
So, we can either define that every component only returns
startandfinish, and computerandthetawhen we want those values, or we could define that every component can returnrandthetaas well. Note that for most purposes, if we were usingrandtheta, we might also wantstart, because from that we can computefinish. No … probably not. If we wantfinishwe probably don’t wantrandtheta. Anyway, we can get them. -
Let’s work toward that, converting Wheel and Crank to return
startandfinish.
Assess the Terrain
We’ll begin by looking at the relevant existing code, to see how those values are computed and returned now, so as to make everyone work similarly, and to see how they, and the r and theta values are used.
First, I check to be sure the tests all run and there are no edits waiting to be committed. Clean slate.
The Wheel has properties for angle_radians and angle_degrees, because currently we usually set the angle in degrees before update, and fetch radians as needed. We’ll see that happening shortly.
The Wheel also has a method constrained that returns its parent, which will be a point. It is unused. Remove, test, commit.
The Crank uses Wheel and can provide radius, lead angle, and various other values to ConRod.
I’ve formulated a plan for the first try at this: let’s look at ConRod, which is particularly well tested, and cause it to ask for start and finish from Crank, instead of what it asks for now. Mostly that’s going to turn into r and theta, I hope.
class ConRod:
def update(self):
self._start, self._finish = con_rod_ends_complete(
piston_radians=self._piston_radians,
wheel_center=self.crank_origin(),
radius=self._crank.radius(),
lead_radians=self._crank.lead_radians(),
wheel_radians=self.wheel_radians(),
length=self._length
)
def crank_origin(self):
return self._crank.origin()
def wheel_radians(self):
return self._crank.wheel_radians()
Why do you suppose we covered two accesses to _crank and then had two direct accesses to it? Also, we see that we are passing wheel_radians and lead_radians` separately. Are they used separately or are they immediately summed? I hope they are just summed, because that’s the value we can get from the crank’s position vectors.
def con_rod_ends_complete(
*, piston_radians, wheel_center, radius,
lead_radians, wheel_radians, length):
# translate to origin not needed
effective_radians = rotate_to_horizontal(wheel_radians, lead_radians, piston_radians)
start, finish = con_rod_ends(radius, effective_radians, length)
fr, sr = rotate_inverse(start, finish, piston_radians)
return translate_inverse(sr, fr, wheel_center)
def rotate_to_horizontal(wheel_radians, lead_radians, piston_radians):
crank_radians = wheel_radians + lead_radians
return crank_radians - piston_radians
Callooh! Callay! It is summed. Let’s change that signature and use the summed angle.
First two attempts I derail. I’ll try again, changing just rotate_to_horizontal first.
def con_rod_ends_complete(
*, piston_radians, wheel_center, radius,
lead_radians, wheel_radians, length):
# translate to origin not needed
crank_radians = wheel_radians + lead_radians
effective_radians = rotate_to_horizontal(crank_radians, piston_radians)
start, finish = con_rod_ends(radius, effective_radians, length)
fr, sr = rotate_inverse(start, finish, piston_radians)
return translate_inverse(sr, fr, wheel_center)
def rotate_to_horizontal(crank_radians, piston_radians):
return crank_radians - piston_radians
That works. Commit: refactoring to merge lead radians and wheel radians.
Now let’s see if we can use Change Signature on the complete method.
def con_rod_ends_complete(*, piston_radians, wheel_center, radius, crank_radians, length):
# translate to origin not needed
effective_radians = rotate_to_horizontal(crank_radians, piston_radians)
start, finish = con_rod_ends(radius, effective_radians, length)
fr, sr = rotate_inverse(start, finish, piston_radians)
return translate_inverse(sr, fr, wheel_center)
I asked for a default crank_radians of wheel_radians+lead_radians, hoping that that would edit my tests enough to make them run. I do not expect success. Test. The tests do all run. I must inspect them to see why.
PyCharm does not seem to have made that edit. The tests are running but I think they verification is a bit weak.
Also, I just noticed that code and it is apparently swapping start finish into fr, sr. That seems wrong. Made a note of it.
I changed all the tests to have a lead_angle and wheel_angle and add them up to get the crank_angle and the tests all run. The thing is, the tests merely verify that we get a suitable rod connection on any inputs, so, well, any inputs will get verify according to whatever values are put in. Just for extra comfort, I’ll run the graphical display. Perfect. Commit: refactoring to merge lead radians and wheel radians.
Here’s the update:
class ConRod:
def update(self):
self._start, self._finish = (
con_rod_ends_complete(
piston_radians=self._piston_radians,
wheel_center=self.crank_origin(),
radius=self._crank.radius(),
crank_radians=self.wheel_radians(),
length=self._length))
crank_radians isn’t correct. We have lost the lead_radians from the crank. My refactoring wasn’t perfect. I might be wise to back up to this morning’s first state and start over, but I think we can work forward from here.
def update(self):
lead_radians = self._crank.lead_radians()
self._start, self._finish = (
con_rod_ends_complete(
piston_radians=self._piston_radians,
wheel_center=self.crank_origin(),
radius=self._crank.radius(),
crank_radians=self.wheel_radians() + lead_radians,
length=self._length))
This works and I have a drawing with a lead angle showing that it is including the lead angle properly.
It is time for a break. PyCharm has entered a strange state, from which a restart may have recovered. I have saved everything in a save point. I’ll return in the next article to make the next move.
Summary
I think I tried at least three times to find a machine-refactoring approach that fixed up the parameters to include the sum of the wheel angle and lead angle, which is what we’ll be able to compute from the crank when we start working with its start and finish. And then my simple assignment in the update above started getting collection errors “probably due to recursive import”. I tried that three times and restarted PyCharm and finally that worked. So my steam has been reduced, and I’d like a break.
We have made a couple of very small but critical steps here, converting the calculation code to use the combined lead and wheel angle as an input instead of the separate values. It really only used the sum anyway, this just pulled the sum up a couple of levels.
So these are, I think valid small steps on the way to providing only start and finish in all our objects. So far, it has actually gone well. I did stumble a bit but there were no fundamental errors. So far so good!
See you next time!