Changing Signatures
Let’s carry on with changing the API of these classes. Where were we?
Oh, right. The ConRod. It’s computing the crank angle up top, which means we should be able to begin to ask the crank for start and finish points instead of the r and theta.
I think we’ll just start doing that and make it work. Oh, I have a neat idea. Let’s see if I can make it work. First, let’s add a method with an extract. We have:
class ConRod:
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))
Extract variable:
def update(self):
lead_radians = self._crank.lead_radians()
crank_radians = self.wheel_radians() + 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=crank_radians,
length=self._length))
Extract method:
def update(self):
crank_radians = self.crank_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=crank_radians,
length=self._length))
def crank_radians(self):
lead_radians = self._crank.lead_radians()
crank_radians = self.wheel_radians() + lead_radians
return crank_radians
Inline:
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.crank_radians(),
length=self._length))
Let’s commit a save point. Then just to make it easier, let’s clean this up:
def crank_radians(self):
lead_radians = self._crank.lead_radians()
crank_radians = self.wheel_radians() + lead_radians
return crank_radians
Two inlines and we have:
def crank_radians(self):
return self.wheel_radians() + self._crank.lead_radians()
I should mention that I am confident that I could just type in some code to convert to using start and finish but I want to do as much as I can in tiny steps. My plan is to convert, inside this object, to using start and finish … and only then to fetch them from the crank. At that point we should be able to remove the r theta methods from Crank. This is certainly more steps than would be possible but they should all be easy ones.
Now, having compacted that method, I’ve decided to rewrite it, like this:
def crank_radians(self):
start, finish = self.get_start_finish()
diff = finish - start
return math.atan2(diff.y, diff.x)
def get_start_finish(self):
start = self.crank_origin()
radius = self._crank.radius()
theta = self._crank.wheel_radians() + self._crank.lead_radians()
finish = start + vector(radius,0).rotate_xy(theta)
return start, finish
Now the ConRod is basing its work on start/finish, not r theta. So we should be able to put a start_finish method into Crank.
class Crank:
def start_finish(self):
start = self._parent.parent
finish = self.constrained(0)
return start, finish
Tests are all green. It has gone so smoothly that I suspect the worst. We already had constrained:
def constrained(self, number):
center = self._parent.parent
r=self._radius
effective_angle = self._leadRadians + self._parent.angle_radians
x = r*math.cos(effective_angle) - 0*math.sin(effective_angle)
y = r*math.sin(effective_angle) + 0*math.cos(effective_angle)
return vector(x, y) + center
Let’s remove some of the unneeded code from these classes. I remove:
class ConRod:
def get_start_finish(self):
start = self.crank_origin()
radius = self._crank.radius()
theta = self._crank.wheel_radians() + self._crank.lead_radians()
finish = start + vector(radius,0).rotate_xy(theta)
return start, finish
By calling the crank:
def crank_radians(self):
start, finish = self._crank.start_finish()
diff = finish - start
return math.atan2(diff.y, diff.x)
After a flurry of inlining, here’s ConRod:
class ConRod:
def __init__(self, *, parent: Crank, length, piston_angle):
self._crank = parent
self._length = length
self._piston_radians = piston_angle * math.pi / 180
# mutable graphic support temps
self.line = None
self.guide = None
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.crank_radians(),
length=self._length))
def crank_radians(self):
start, finish = self._crank.start_finish()
diff = finish - start
return math.atan2(diff.y, diff.x)
def crank_origin(self):
return self._crank.origin()
def init_draw(self, canvas):
start = self._start
finish = self._finish
self.line = canvas.create_line(
start.x, start.y,
finish.x, finish.y,
width=3, fill="red")
wc = self.crank_origin()
tg = vector(2*self._length, 0, 0).rotate_xy(self._piston_radians)
self.guide = canvas.create_line(wc.x, wc.y, wc.x+tg.x, wc.y+tg.y, fill="blue")
one = vector(1,0).rotate_xy(self._piston_radians)
half = wc + one + tg * 0.5
s = 0.1
canvas.create_oval(half.x-s, half.y-s, half.x+s, half.y+s, fill='red')
def draw(self, canvas):
start = self._start
finish = self._finish
canvas.coords(self.line,
start.x, start.y,
finish.x, finish.y)
The rest is just the standard con_rod_ends_complete function that we’ve been using right along, unchanged.
Over in Crank, I remove two or three methods and am left, so far, with this:
class Crank:
def __init__(self, *, parent, radius, lead_degrees=0):
self._parent = parent
self._radius = radius
self._leadRadians = lead_degrees * math.pi / 180.0
def start_finish(self):
start = self._parent.parent
finish = self.constrained(0)
return start, finish
def constrained(self, number):
center = self._parent.parent
r=self._radius
effective_angle = self._leadRadians + self._parent.angle_radians
x = r*math.cos(effective_angle) - 0*math.sin(effective_angle)
y = r*math.sin(effective_angle) + 0*math.cos(effective_angle)
return vector(x, y) + center
def radius(self):
return self._radius
def origin(self):
return self._parent.parent
Someone still uses radius. Find them. Ah, it is in ConRod. Right there in the call to complete:
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.crank_radians(),
length=self._length))
Extract method:
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.crank_radians(),
length=self._length))
def crank_radius(self):
return self._crank.radius()
Re-code crank_radius:
def crank_radius(self):
start, finish = self._crank.start_finish()
return finish.distance(start)
Remove method from Crank. Test. Commit: conrod and crank converted to start/finish.
There’s more we can do. The method constrained(number) needs to be removed, I think, but that’s not for now. And we’d like to make Wheel adhere to the new start/finish scheme.
But for now, we’ve reached a logical conclusion, and there have been a dozen commits, and could have been a few more, most of the work was done with machine refactorings, and the tests stayed green throughout, although a time or two this morning I reverted when I got off track. Overall, very satisfying and a very welcome design improvement along the way to standardizing on start/finish notation. We’ll deal with additional points beyond start and finish if they ever happen.