Skip to content

Commit fb1e258

Browse files
committed
Clean up and add readme
1 parent fecc9cc commit fb1e258

11 files changed

+115
-47
lines changed

README.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# constraint-solver
2+
3+
Pure python geometric constraint solver
4+
5+
See `gcs/solve_elements.py` for a description of the
6+
mechanics behind this package. Essentially, gcs implements
7+
a constraint solver that breaks a problem into fully
8+
constrained sets of equations/variables and solves each
9+
set in order of its graph dependencies.
10+
11+
This algorithm was loosely inspired by some light research
12+
I did on CAD constraint solvers, but the idea to split
13+
equations/constraints into fully constrained sets was
14+
original.
15+
16+
## Installing
17+
18+
This repo contains conda environment definitions that
19+
work with this package (`environment.yml` and
20+
`environment-windows.yml`).
21+
22+
To install on windows (remove `sh` if not using bash and
23+
use `create-windows-env.bat`):
24+
25+
```bash
26+
sh create_windows-env.sh
27+
pip install -e .
28+
```
29+
30+
## Running
31+
32+
A couple sample scripts exist in `test/`.
33+
34+
- `test/make_constraint_graph.py`: shows how to set up a
35+
solver, solve it, and view its geometry and constraint graph
36+
- `test/problem2_cstr_del_add`: tests adding/deleting constraints
37+
on a sample problem. Makes an mpl animation showing the solver
38+
being updated as the constraint controlling the circle's radius
39+
changes

gcs/constraint_graph.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,21 @@
55

66

77
def create_solver_graph(gs, cmap=cm.rainbow):
8-
"""Return an igraph graph that represents a solved solver"""
8+
"""
9+
Returns an igraph graph that represents a solved solver
10+
11+
Vertices are colored based on which constrained set
12+
they belong to. Groups of variables/equations with
13+
the same color belong to the same equation set and
14+
will be solved together
15+
16+
Parameters
17+
----------
18+
gs
19+
Geom solver instance
20+
cmap
21+
Color map to use to color graph vertices
22+
"""
923
var_list = list(gs.vars)
1024
eqn_list = list(gs.eqns)
1125

gcs/constraint_solver.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
from __future__ import print_function
2-
31
import scipy.optimize as opt
42

3+
from .solve_elements import EqnSet
4+
55

6-
def solve_numeric(eqn_set, ftol=1.0e-10):
6+
def solve_numeric(eqn_set: EqnSet, ftol=1.0e-10):
77
"""
88
Solve an equation set numerically
99
1010
Parameters
1111
----------
1212
eqn_set: EqnSet
1313
the equation set to solve
14+
ftol
15+
solver tolerance
1416
1517
Returns
1618
-------

gcs/constraints_signed.py

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
77
"""
88

9-
from __future__ import division
10-
119
from math import hypot, atan2
1210

1311
# --------------------------------------------------------------------

gcs/constraints_unsigned.py

-2
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
77
"""
88

9-
from __future__ import division
10-
119
from math import hypot, atan2
1210

1311
# --------------------------------------------------------------------

gcs/eqn_set_splitting_proto.py

+2-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
1-
from __future__ import print_function, division
1+
#note: these functions are not used, see equation_solving instead
22

3-
# from operator import methodcaller # TODO: can use this for sort key
4-
5-
# for sorted insertion: heapq, bisect, blist, sortedcontainers...
63
from blist import sortedlist
74

8-
# from sortedcontainers import SortedList as sortedlist
9-
10-
from equation_solving import EqnSet
5+
from .solve_elements import EqnSet
116

127

138
# original

gcs/equation_solving.py

+16-16
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def split_equation_set(eqn_set):
2424
"""Split an equation set up into smaller solvable equation sets"""
2525

2626
# used for tiebreaker of priority key
27-
nEq = len(eqn_set.eqns) + 1
27+
n_eq = len(eqn_set.eqns) + 1
2828

2929
solve_sets = set()
3030

@@ -34,7 +34,7 @@ def split_equation_set(eqn_set):
3434

3535
# Initialize priority queue with the equations in the input set
3636
pq = sortedlist(
37-
[EqnSet().add(eqn) for eqn in eqn_set.eqns], key=methodcaller("key", nEq)
37+
[EqnSet().add(eqn) for eqn in eqn_set.eqns], key=methodcaller("key", n_eq)
3838
)
3939

4040
while pq:
@@ -51,7 +51,7 @@ def split_equation_set(eqn_set):
5151
p.discard(eqn_set)
5252

5353
# delete any empty eqn sets and re-sort the pq
54-
pq = sortedlist(filter(None, pq), key=methodcaller("key", nEq))
54+
pq = sortedlist(filter(None, pq), key=methodcaller("key", n_eq))
5555

5656
unique_eqn_combos = set(frozenset(eqs.eqns | eqs.vars) for eqs in pq)
5757

@@ -73,23 +73,23 @@ def split_equation_set(eqn_set):
7373
underconstrained_set.set_solved()
7474
solve_sets.add(underconstrained_set)
7575

76-
# while unsolved_eqns:
77-
# eqn = unsolved_eqns.pop()
78-
# eqn_set = EqnSet()
76+
# while unsolved_eqns:
77+
# eqn = unsolved_eqns.pop()
78+
# eqn_set = EqnSet()
7979
#
80-
# connected_eqns = {eqn}
80+
# connected_eqns = {eqn}
8181
#
82-
# while connected_eqns:
83-
# eqn = connected_eqns.pop()
84-
# unsolved_eqns.discard(eqn)
85-
# eqn_set.add(eqn)
82+
# while connected_eqns:
83+
# eqn = connected_eqns.pop()
84+
# unsolved_eqns.discard(eqn)
85+
# eqn_set.add(eqn)
8686
#
87-
# for var in eqn.vars:
88-
# connected_eqns.update(eqn for eqn in var.eqns
89-
# if eqn not in eqn_set.eqns)
87+
# for var in eqn.vars:
88+
# connected_eqns.update(eqn for eqn in var.eqns
89+
# if eqn not in eqn_set.eqns)
9090
#
91-
# eqn_set.set_solved()
92-
# solve_sets.add(eqn_set)
91+
# eqn_set.set_solved()
92+
# solve_sets.add(eqn_set)
9393

9494
return solve_sets
9595

gcs/geom2d.py

+31-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .solve_elements import Eqn, Var
88
from . import constraints_unsigned as cstr
99

10-
# form . import constraints_signed as cstr
10+
# from . import constraints_signed as cstr
1111

1212
try:
1313
from ccad import model
@@ -19,7 +19,7 @@
1919
# ----------------------------------------------------------
2020

2121

22-
class Geometry(object):
22+
class Geometry:
2323
def __init__(self, name):
2424
self.vars = []
2525
self.name = name
@@ -37,14 +37,16 @@ def delete(self):
3737

3838
class Point(Geometry):
3939
def __init__(self, name, x=0.0, y=0.0):
40+
super().__init__(name)
41+
4042
self.x = Var(name + ".x", x)
4143
self.y = Var(name + ".y", y)
44+
4245
self.vars = [self.x, self.y]
43-
self.name = name
4446

4547
def plot(self, ax=None):
4648
ax = ax or plt.gca()
47-
plt.scatter(x=(self.x.val,), y=(self.y.val,))
49+
ax.scatter(x=(self.x.val,), y=(self.y.val,))
4850

4951
def draw(self, view):
5052
self.geom = model.vertex([self.x.val, self.y.val, 0])
@@ -54,12 +56,13 @@ def draw(self, view):
5456
# TODO: add Line (instead of / in addition to, line segment)
5557

5658

57-
class Line_Segment(Geometry):
59+
class LineSegment(Geometry):
5860
def __init__(self, name, x1=0.0, y1=0.0, x2=0.0, y2=0.0):
61+
super().__init__(name)
62+
5963
self.p1 = Point(name + ".p1", x1, y1)
6064
self.p2 = Point(name + ".p2", x2, y2)
6165
self.vars = [self.p1.x, self.p1.y, self.p2.x, self.p2.y]
62-
self.name = name
6366

6467
def plot(self, ax=None):
6568
ax = ax or plt.gca()
@@ -79,6 +82,8 @@ def draw(self, view):
7982

8083
class Circle(Geometry):
8184
def __init__(self, name, cx=0.0, cy=0.0, cr=1.0):
85+
super().__init__(name)
86+
8287
self.p = Point(name + ".p", cx, cy)
8388
self.r = Var(name + ".r", cr)
8489
self.vars = [self.p.x, self.p.y, self.r]
@@ -101,12 +106,15 @@ def draw(self, view):
101106

102107

103108
class Constraint(object):
104-
def __init__(self):
109+
def __init__(self, name):
110+
self.name = name
105111
self.equations = []
106112

107113

108114
class SetVar(Constraint):
109115
def __init__(self, name, var, val):
116+
super().__init__(name)
117+
110118
self.var = var
111119
self.val = val
112120

@@ -115,6 +123,8 @@ def __init__(self, name, var, val):
115123

116124
class HorzDist(Constraint):
117125
def __init__(self, name, p1, p2, d):
126+
super().__init__(name)
127+
118128
self.p1 = p1
119129
self.p2 = p2
120130
self.d = d
@@ -126,6 +136,8 @@ def __init__(self, name, p1, p2, d):
126136

127137
class VertDist(Constraint):
128138
def __init__(self, name, p1, p2, d):
139+
super().__init__(name)
140+
129141
self.p1 = p1
130142
self.p2 = p2
131143
self.d = d
@@ -137,6 +149,8 @@ def __init__(self, name, p1, p2, d):
137149

138150
class LineLength(Constraint):
139151
def __init__(self, name, L, d):
152+
super().__init__(name)
153+
140154
self.L = L
141155
self.d = d
142156

@@ -151,6 +165,8 @@ def __init__(self, name, L, d):
151165

152166
class AnglePoint3(Constraint):
153167
def __init__(self, name, p1, p2, p3, a):
168+
super().__init__(name)
169+
154170
self.p1 = p1
155171
self.p2 = p2
156172
self.p3 = p3
@@ -169,6 +185,8 @@ def __init__(self, name, p1, p2, p3, a):
169185

170186
class TangentLineCircle(Constraint):
171187
def __init__(self, name, L, C):
188+
super().__init__(name)
189+
172190
self.L = L
173191
self.C = C
174192

@@ -185,6 +203,8 @@ def __init__(self, name, L, C):
185203

186204
class PointOnCircle(Constraint):
187205
def __init__(self, name, p, C):
206+
super().__init__(name)
207+
188208
self.p = p
189209
self.C = C
190210

@@ -200,6 +220,8 @@ def __init__(self, name, p, C):
200220
class GroundPoint(Constraint):
201221
# note: untested
202222
def __init__(self, name, p):
223+
super().__init__(name)
224+
203225
self.p = p
204226
self.gx = p.x.val
205227
self.gy = p.y.val
@@ -212,6 +234,8 @@ def __init__(self, name, p):
212234

213235
class CoincidentPoint2(Constraint):
214236
def __init__(self, name, p1, p2):
237+
super().__init__(name)
238+
215239
self.p1 = p1
216240
self.p2 = p2
217241

gcs/geom_solver.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from .system_solver import Solver
33

44

5-
class GCS(object):
5+
class GCS:
66
"""Geometric constraint solver (GCS)"""
77

88
__slots__ = (

gcs/sample_problems.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def problem2():
1919
p2 = g2d.Point("p2", 2.0, 2.0)
2020
p3 = g2d.Point("p3", 3.0, 3.0)
2121
c1 = g2d.Circle("c1", 0.0, 0.0, 1.0)
22-
L1 = g2d.Line_Segment("L1", 1.0, 1.0, 3.0, 3.0)
22+
L1 = g2d.LineSegment("L1", 1.0, 1.0, 3.0, 3.0)
2323

2424
geometry = (p0, p1, p2, p3, c1, L1)
2525

gcs/solve_elements.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
from __future__ import division
2-
31
"""
42
Classes that act as nodes in a system-of-equations solve-graph.
53
64
Var nodes are connected to Eqn nodes, and vice versa. Eqn nodes
75
are grouped together to make EqnSets, and when an A* search is
86
performed, constrained equation sets are found which can be solved.
97
10-
As the search is performed, the nodes track the implicit heirarchy:
8+
As the search is performed, the nodes track the implicit hierarchy:
119
each variable has one equation set that solves for it, and a variable
1210
may be required to be solved for before an EqnSet can be solved.
1311
@@ -238,16 +236,16 @@ def degrees_of_freedom(self):
238236
"""The number of degrees of freedom of this set (#var - #eq)"""
239237
return len(self.vars) - len(self.eqns)
240238

241-
def key(self, nEq=None):
239+
def key(self, n_eq=None):
242240
"""
243241
Sorting key: value of key is higher for sets with more deg of freedom
244-
245-
`nEq` is meant to be the total number of equations in a larger set
242+
243+
`n_eq` is meant to be the total number of equations in a larger set
246244
and should be at least `len(self.eqns) + 1`, and should be the same
247245
for the key for all equation sets being used
248246
"""
249247
# last term is tie-breaker to value sets with more equations
250-
return -self.degrees_of_freedom() + (len(self.eqns) / nEq if nEq else 0.0)
248+
return -self.degrees_of_freedom() + (len(self.eqns) / n_eq if n_eq else 0.0)
251249

252250
def is_constrained(self):
253251
"""Does the number of equations equal the number of variables?"""

0 commit comments

Comments
 (0)