Which Way to Go?
There are two possible solutions for the ArcRod’s position. We need to specify which one we want. Result promising but I’m sent back to the drawing board.
Since the ArcRod’s target is a circle, and we have its center and radius as part of the object, I propose to provide an angle as an additional creation parameter. That angle should be set to point roughly to the part of the circle where we want our solution to be. Internally we’ll convert that angle to a point on the circle at the provided radius, and we’ll from our two solutions, we’ll select the position that is closest to that point.
I expect this to work nicely, and it shouldn’t be terribly inconvenient to use.
I would like to have a test for this. At this point, we have a few tests for the base calculations in ArcRod. They all check both results. Here’s one example out of our three similar tests:
def test_update_90(self):
length = 5
position = vector(0,6)
radius = 2
arc_rod = ArcRod(length=length, target_position=position, target_radius=radius)
parent_finish = vector(0,0)
start, finish_1, finish_2 = arc_rod.update_both(parent_finish)
assert finish_1 != finish_2
assert finish_1.distance(start) == pytest.approx(length)
assert position.distance(finish_1) == pytest.approx(radius)
assert finish_2.distance(start) == pytest.approx(length)
assert position.distance(finish_2) == pytest.approx(radius)
All three test return and check both values from update_both. In due time, we want our object to return only the desired one.
The method update_both is probably poorly named, now that I think of it. The ArcRod code looks like this:
def update(self, parent_start, parent_finish):
start, finish_1, finish_2 = self.update_both(parent_finish)
return start, finish_1
def update_both(self, p_finish):
axis = self._target_position - p_finish
angle = math.atan2(axis.y, axis.x)
offset_1, offset_2 = self.target_offsets(p_finish)
rotated_1 = offset_1.rotate_xy(angle)
rotated_2 = offset_2.rotate_xy(angle)
return p_finish, p_finish + rotated_1, p_finish + rotated_2
def target_offsets(self, p_finish):
opposite_side = self._target_radius
adjacent_side_1 = self._length
adjacent_side_2 = p_finish.distance(self._target_position)
angle = opposite_angle(opposite_side, adjacent_side_1, adjacent_side_2)
offset_1 = vector(adjacent_side_1, 0).rotate_xy(angle)
offset_2 = vector(adjacent_side_1, 0).rotate_xy(-angle)
return offset_1, offset_2
It seems to me that while computing and carrying both possibilities forward was useful in development, it makes more sense to drop the undesirable result as soon as we can. That said, I’m not sure just how this will go, so it might be best to work forward from where we are and then move the decision around.
And my clever scheme of setting the direction toward the desired position? That needs to take into account the angle between the rod and the circle, and that varies, because the ArcRod’s start is on a crank and moves around. The angle variable in update_both is different on each call, though it will not vary much.
In update_both, we rotate our two possible offsets by angle. Will it suffice to rotate our target offset as well, and then compare?
Let’s sneak up on this. My rough scheme is to change the creation signature of ArcRod to include the angle, compute a raw offset based on that and radius and save it. Then in update_both, adjust that offset and compare the two we have.
Then, for now, I’ll put the selected one first, swapping offset_ and offset_2 if needed. Then I can see what happens in the tests and then select one. In fact, let’s print the values in the 90 test and make the program select the other one.
No. New test, based on 90. I think I can write it:
def test_update_90_select(self):
length = 5
position = vector(0,6)
radius = 2
arc_rod_a = ArcRod(
length=length,
target_position=position,
target_radius=radius,
target_angle=0)
parent_finish = vector(0,0)
start, finish_a1, finish_a2 = arc_rod_a.update_both(parent_finish)
arc_rod_b = ArcRod(
length=length,
target_position=position,
target_radius=radius,
target_angle=180)
start_, finish_b1, finish_b2 = arc_rod_b.update_both(parent_finish)
assert finish_b1 == finish_a2
assert finish_b2 == finish_a1
If we set the angle to 180 instead of zero we should reverse the answers. Once that’s working we’ll enhance the test to be sure we got the one we wanted. This seems hard enough for now.
Now to adjust the init to compute and cache a target point:
class ArcRod:
def __init__(self, *, length, target_position, target_radius, target_angle):
self._length = length
self._target_position = target_position
self._target_radius = target_radius
r_vector = vector(target_radius, 0)
self._target_offset = r_vector.rotate_xy(target_angle * math.pi / 180)
self._cached_line = None
And then to use the new target offset …
def update_both(self, p_finish):
axis = self._target_position - p_finish
angle = math.atan2(axis.y, axis.x)
offset_1, offset_2 = self.target_offsets(p_finish)
rotated_1 = offset_1.rotate_xy(angle)
rotated_2 = offset_2.rotate_xy(angle)
rotated_target = self._target_offset.rotate_xy(angle)
d1 = rotated_target.distance(rotated_1)
d2 = rotated_target.distance(rotated_2)
if d1 > d2:
rotated_1, rotated_2 = rotated_2, rotated_1
return p_finish, p_finish + rotated_1, p_finish + rotated_2
We rotate the target offset the same as we do the two candidates. We get the distance to each and if d1 is further away, we swap the results. Let’s see what the test does. I’m hopeful that it will run now.
It does not. I need more info. Some prints will be useful. First prints, in the test, verify that the values are not swapped. I wonder why and add prints now to update_both.
That tells me that my computed offset isn’t where I thought it would be I think if I put 90 and 270 into the test it would pass. Let’s see. Yes, it does pass. The question becomes “what shall we do about this?”
I think the test will also pass if I use 0 and 180 as before, and do not rotate the target offset. Trying that.
- Note
- I’m just kind of hammering here. That’s not good. Need to settle in and think this out. But not yet. I’m hammering.
The test does pass with the rotate taken out. Now let’s examine the prints and see if we got the one we intended.
The test describes a crank at 90 degrees right above the origin. My intention is that 180 should select the negative (left) value and 0 the positive (right) one.
That is not the case, and I notice that the offset we compare with is nothing like the two computed ones. For example, in the zero angle case:
rotated_1=v_Vector((-1.5612494995995998, 4.75, 0))
rotated_2=v_Vector((1.5612494995996002, 4.75, 0))
rotated_target=v_Vector((2.0, 0.0, 0))
d1=5.936749784048373
d2=4.770220330509022
In the 180 case the rotated target is <-2,0>, so really no better than what we had before.
Tentative Conclusion
We’ll consider this to be a spike and reset. Then I’ll go back to the drawing board and do some geometry.
Oops, I should have saved that test. Fortunately, I have it here in the article, so I’ll paste it back in and leave it failing on my machine. It won’t even compile, since I didn’t save the changed signature. That may be just as well. I think this scheme can be made to work, but I’m not entirely sure of that, so we might need different input parameters.
ANd I”ll literally go back and draw some pictures and figure out some geometry that will do the job for us. I’ll show the pictures in the net article if they’re useful.
So: was this morning bad or good? I feel just fine. I think that the scheme will work and that it is “just” a matter of getting the right value into the target offset. It would have been great if my first cut had worked but when you're I’m rotating and translating all around things don’t always go as you'd I’d wish.
See you soon!