Writing a Geometric Solver in Python - Part 3: Fixing our line
So far we have a nice strong framework for expressing geometric constraints as Constraint
objects, and we have a clean way of applying these to our geometric objects. We have clear output when we ask the various objects to repr
themselves, and we have clear error messages when things don’t quite go to plan. Our constraints now support being reciprocal, and if we constraint one object to another that will magically constraint the other object back to the original if we so wish. Where we left it last time is that we needed to update our ConincidentConstraint
object so that it implemented this reciprocal feature, and to do that we needed to upgrade our Line
object so that it could take constraints. The code from the previous article can be found in this gist.
Upgrading our line.
To get us started in the last article we created a very simple line object:
Let’s look at how we might upgrade that Line
to use ConstraintSet
so that we can apply constraints to it. How’s this for starters:
|
|
Line(l)<Point(),Point()>
This is great stuff, we have a line which starts at a point and ends at a point. Let’s try and play around with it a bit.
AttributeError: can't set attribute
Disaster! The issue here is that we didn’t define setters for our start
and end
parameters. But we didn’t have to do that when we defined out Point
object, so what is going on? Let’s revisit that definition of Point
:
So we used ConstrainedValue()
at the class level when we defined our Point
and that looks very neat. Can we do something similar with Line
to specify our Point
parameters? We could define a ConstrainedPoint
object, but this would get very tiresome once we have a large collection of objects. Let’s instead modify ConstrainedValue
so that it accepts a subclass of ConstraintSet
and then use that to specify the parameters of Line
:
|
|
And see how it flies:
AttributeError: 'Line' object has no attribute 'name'
Whoops! Strangely we chose to define the name
property on the Point
class even though _name
is implemented in ConstraintSet
. Let’s extract that up to ConstraintSet
, which will simplify Point
a little.
|
|
And have another go at defining Line
:
|
|
Line(l)<Point(
LinkedValueConstraint<p>
),Point()>
Woohoo! This is exactly what we were after.
Returning to the Constraint
So now we must upgrade our CoincidentConstraint
so that it provides a reciprocal constraint and implements the three methods with introduced to the Constraint
class in the last article. A question we must first answer is what should we call our reciprocal constraint. It seems to me to be equally acceptable to say that a line is coincident with a point as it is to say that a point is coincident with a line. So our natural language tells us that we should use the same constraint object for both the constraint and its reciprocal, which means we need to make our CoincidentConstraint
object apply to a Line
as well as a Point
.
There are other circumstances where this kind of relationship is appropriate. For example if a line is tangent to a circle, then it is equally acceptable to say that the circle is tangent to a line, so it feels like this is a pattern which we may well use again.
Let’s remind ourselves what our existing CoincidentConstraint
class looks like:
|
|
Let’s first of all add the new methods, without considering the addition of the Line
functionality, and we’ll leave the apply_reciprocal_constraint
method empty for the moment.
|
|
We also need to define an __eq__
method to avoid repeating our stack overflow woes from last time, so let’s go ahead and do that now.
In order for our CoincidentConstraint
to apply to a Line
we must allow it to accept a Point
. That’s a little more messy that it might at first appear, as we’ll need to adapt our shiny new __eq__
method to consider this possibility, and the same for our __repr__
method:
|
|
And now let’s modify the validate_object
method so that it checks for the correct object type, and tweak cascade_constraints
so that it cascades as well to a Line
as it does to a Point
:
|
|
Line(l)<Point(
InfluencedConstraint<CoincidentConstraint<p>>
),Point(
InfluencedConstraint<CoincidentConstraint<p>>
)>
Aha! Let’s alter our Line
__repr__
method so that it includes the constraints at the Line
level:
|
|
Line(l)<Point(
InfluencedConstraint<CoincidentConstraint<p>>
),Point(
InfluencedConstraint<CoincidentConstraint<p>>
)>(
CoincidentConstraint<p>
)
Point(
CoincidentConstraint<m>
)
Now we can see the constraints to which the parameters are bound, as well as those to which the object is bound, or we can for the Line
object at least. The Point
object is a lot less forthcoming, and on reflection its kind of annoying having to implement __repr__
on every object we define. Is there a way we can do this at the ConstraintSet
level and just forget about __repr__
for the rest of time. We can if we make an assumption. I’m pretty sure that the assumption is safe, but I’m also pretty sure that I’ve been bitten by every other assumption I’ve ever made. Let’s live dangerously and implement a generic __repr__
method. If it bites us we can re-implement it later.
A More Generic Representation
To write something generic, we need a list of the ConstrainedValue
attributes in our class. Let’s tweak our ConstrainedValue
class so that it keeps track:
|
|
Now we can iterate through that list in our __repr__
method. For the avoidance of doubt, the assumption here being that the only attributes we’re printing out are the ones that are participating in our model, and all of those will be of type ConstraintSet
. Let’s see how that goes:
|
|
And redefine Point
and Line
without the __repr__
method:
|
|
These are starting to look super clean! That apparently duplicated code in the __init__
methods hasn’t escaped my attention, but I don’t want to get distracted. Let’s check that that last change worked:
l: Line(
l.start: Point(
l.start.x: ConstraintSet()<>
l.start.y: ConstraintSet()<>
)
<
InfluencedConstraint<CoincidentConstraint<p>>
>
l.end: Point(
l.end.x: ConstraintSet()<>
l.end.y: ConstraintSet()<>
)
<
InfluencedConstraint<CoincidentConstraint<p>>
>
)
<
CoincidentConstraint<p>
>
q: Point(
q.x: ConstraintSet()<
InfluencedConstraint<CoincidentConstraint<m>>
>
q.y: ConstraintSet()<
InfluencedConstraint<CoincidentConstraint<m>>
>
)
<
CoincidentConstraint<m>
>
This is a really great output. We can really see how the hierarchy of our constraints is building up. So what about that initializer? Well it turns out we can play a pretty similar trick.
Generic Initialization
Firstly let’s describe the behavior we’re after, using our Point
object as an example:
In order to achieve this, in particular the 2nd example, we need to be able to provide a default name. Something like Point1
, when no name is specified, but obviously unique for each point created. Let’s add a generate_name
method to ConstraintSet
which can do this for us:
It’s worth reflecting on how this method works, as its a little subtle. Firstly this method is decorated with @classmethod
This means that the class is passed in as the first parameter instead of the instance. By convention this is called cls
to distinguish from self
in a normal method where the instance is passed in. By using a class method, and the value of cls
we can have a different counter for Point
and Line
. Next try and assign the the value of _counter
to index
, if we’ve not yet initialized it this will throw an AttributeError
, we catch that and set index
to zero. This is an example of “EAFP” or “Easier to ask forgiveness than permission” coding. This rule is not part of PEP20, but it is well established python coding style. Now we set the value of _counter
to one more than index
, which will initialize counter if its not already, and finally construct our default name out of type(self).__name__
and our index
. Let’s give it a quick test. The _
prefix by convention means that this is a private method which shouldn’t be called from outside of the class, but python does nothing to enforce that, so our test is nice and easy to write:
Line0
Line1
Point0
Point1
Perfect. Now let’s write a generic initializer on ConstraintSet
which means we don’t have to write initializers on all our objects. Let’s remind ourselves of our target behavior:
The final behavior is the easiest to implement. We will iterate through the _constraint_sets
we built for our __repr__
method and assign values if matching kwargs
have been passed. We pop
them off kwargs
and then pass the remaining kwargs
to super().__init__
. This last part is important as it preserves our ability to subclass. To pluck an example out of thing air, we might want to create a “DoubleLine” class which draw two lines right next to each other, and takes the distance between the two lines as a parameter:
If we miss out the “pop and pass” part of our initializer, then Line
will never be sent the correct values, and start
and end
would never be set. A first stab at this generic initializer looks like this:
|
|
name
is not a ConstraintSet
so won’t appear in our list, so we must explicitly handle that. It’s tempting to write something like this:
However if we did write this, then the call to super().__init__()
would not have a name
parameter, so that call would immediately overwrite our name
with a _generate_name
value. To prevent this we must check to see if _name
is already defined, and only provide a default value if it is not.
|
|
Lastly we need to consider args
. To maximize code reuse, the best thing to do is to work out which ConstraintSet
each arg
belongs to, and add it explicitly to **kwargs
. The initializers are called from the most specific subclass up to the most general. Our DoubleLine
for example would have its initializer called first, and then the initializer for Line
and finally the initializer for ConstraintSet
. We therefore want to start at the end of our list and work towards the start, so that more specific args
go at the end of the call. In order to make this work we’ll need to split out arg
processing into a separate method so that we can call it further up the initializer. Also, by default, args
is passed to us a tuple
which is immutable, so we’ll need to change it to something we can remove values from. A List
will do:
|
|
Putting these together gives us a mammoth initializer, so I’ve broken out the kwargs
processing into a separate method too, to give anyone reading this code half a chance of following it:
|
|
Redefine Point
and Line
without their initializers…
Let’s test this by revisiting our behaviors:
|
|
Point0: Point(
Point0.x: ConstraintSet()<>
Point0.y: ConstraintSet()<>
)
<>
Point1: Point(
Point1.x: ConstraintSet()<
FixedValueConstraint<1>
>
Point1.y: ConstraintSet()<
FixedValueConstraint<2>
>
)
<>
p: Point(
p.x: ConstraintSet()<
FixedValueConstraint<1>
>
p.y: ConstraintSet()<
FixedValueConstraint<2>
>
)
<>
This is pretty incredible stuff. We’ve managed to abstract all our functionality into our ConstrainedValue
and ConstraintSet
classes so that we have this super clean and easy to use developer interface. The only thing which bugs me slightly. Have another look at this listing:
To my mind, ConstrainedValue(Point)
is intuitive. It clearly says that we’d like a constrained value and that it should be of type Point
. ConstrainedValue(ConstraintSet)
doesn’t have the same feel however, it really isn’t clear what type the value is. Let’s fix that by adding an alias clas to ConstrainedSet
which has a more appropriate name for the end user:
Point0: Point(
Point0.x: Scalar()<
FixedValueConstraint<1>
>
Point0.y: Scalar()<
FixedValueConstraint<2>
>
)
<>
Let’s just quickly check that we’ve not broken anything:
Line0: Line(
Line0.start: Point(
Line0.start.x: Scalar()<>
Line0.start.y: Scalar()<>
)
<
LinkedValueConstraint<Point1>
>
Line0.end: Point(
Line0.end.x: Scalar()<>
Line0.end.y: Scalar()<>
)
<
InfluencedConstraint<CoincidentConstraint<Point0>>
>
)
<
CoincidentConstraint<Point0>
>
Small bug, looks like the LinkedValueConstraint
has nuked the InfluencedConstraint
from Line0.end
. This is because our __set__
method in ConstrainedValue
calls reset_constraints
which probably seemed like a good idea at the time, but now looks more like a bug. Let’s remove that call:
|
|
Line0: Line(
Line0.start: Point(
Line0.start.x: Scalar()<>
Line0.start.y: Scalar()<>
)
<
InfluencedConstraint<CoincidentConstraint<Point0>>
LinkedValueConstraint<Point1>
>
Line0.end: Point(
Line0.end.x: Scalar()<>
Line0.end.y: Scalar()<>
)
<
InfluencedConstraint<CoincidentConstraint<Point0>>
>
)
<
CoincidentConstraint<Point0>
>
Phew! We made it! We now have a super powerful little framework with which we can model constraints. It’s really worth reflecting on what we’ve achieved here. Not only have we implemented all that power, but if we compare the implementation of our Point
with the naive implementation we started with at the beginning of part 1, you’ll see its actually simpler to implement using our framework than without.
In the next article we’ll look at adding some more constraints into the mix. In particular we’ll look at how we can do some simple arithmetic operations with out ConstraintSet
.