simetri.helpers.constraint_solver

Geometric constraint solver for points, segments, circles. Uses Sequential Least Squares Programming (SLSQP) to solve the given constraints.

  1"""Geometric constraint solver for points, segments, circles.
  2Uses Sequential Least Squares Programming (SLSQP) to solve the given constraints.
  3"""
  4
  5from dataclasses import dataclass
  6
  7from simetri.graphics.all_enums import ConstraintType as ConstType
  8from simetri.helpers.vector import Vector2D
  9from simetri.geometry.geometry import (
 10    direction,
 11    distance,
 12    is_line,
 13    angle_between_two_lines,
 14    point_to_line_distance
 15)
 16from simetri.geometry.circle import Circle_ as Circle
 17
 18
 19@dataclass
 20class Constraint:
 21    """Constraint class for geometric constraints."""
 22
 23    item1: object
 24    item2: object
 25    type: ConstType
 26    value: float = None
 27    value2: float = None  # used for equal_value_eq
 28
 29    def __post_init__(self):
 30        """Set item sizes for circles and segments."""
 31        self.equation = d_equations[self.type]
 32        if self.type == ConstType.EQUAL_SIZE:
 33            if isinstance(self.item1, Circle):
 34                self.size1 = self.item1.radius
 35            elif is_line(self.item1):
 36                self.size1 = distance(*self.item1)
 37
 38            if isinstance(self.item2, Circle):
 39                self.size2 = self.item2.radius
 40            elif is_line(self.item2):
 41                self.size2 = distance(*self.item2)
 42
 43    def check(self):
 44        """Check the constraint value.
 45
 46        Returns:
 47            float: The result of the constraint equation.
 48        """
 49        return self.equation(self)
 50
 51
 52# Constraint equations
 53# These equations return zero if the constraint is satisfied.
 54# To check if a segment is horizontal use parallel_eq with the x-axis
 55# To check if a segment is vertical use paralell_eq with the y-axis
 56# For concentric circles use distance_eq (center to center dist = 0)
 57# For point on a circle use distance_eq (point to center dist = radius)
 58
 59
 60def distance_eq(constraint):
 61    """Return the difference between the target and current distance.
 62
 63    Args:
 64        constraint (Constraint): The constraint object.
 65
 66    Returns:
 67        float: The difference between the target and current distance.
 68    """
 69    if isinstance(constraint.item1, Circle):
 70        p1 = constraint.item1.center
 71    else:
 72        p1 = constraint.item1
 73
 74    if isinstance(constraint.item2, Circle):
 75        p2 = constraint.item2.center
 76    else:
 77        p2 = constraint.item2
 78
 79    value = constraint.value
 80
 81    return distance(p1, p2) - value
 82
 83
 84def parallel_eq(constraint):
 85    """Return the cross product. If the segments are parallel, the cross product is 0.
 86
 87    Args:
 88        constraint (Constraint): The constraint object.
 89
 90    Returns:
 91        float: The cross product of the vectors.
 92    """
 93    vec1 = Vector2D(*constraint.item1)
 94    vec2 = Vector2D(*constraint.item2)
 95
 96    return vec1.cross(vec2)
 97
 98
 99def perpendicular_eq(constraint):
100    """Return the dot product. If the segments are perpendicular, the dot product is 0.
101
102    Args:
103        constraint (Constraint): The constraint object.
104
105    Returns:
106        float: The dot product of the vectors.
107    """
108    seg1 = constraint.item1
109    seg2 = constraint.item2
110
111    vec1 = Vector2D(*seg1)
112    vec2 = Vector2D(*seg2)
113
114    return vec1.dot(vec2)
115
116
117def equal_size_eq(constraint):
118    """Return the difference between the sizes of the items.
119
120    For segments, item size is the length of the segment.
121    For circles, item size is the radius of the circle.
122
123    Args:
124        constraint (Constraint): The constraint object.
125
126    Returns:
127        float: The difference between the sizes of the items.
128    """
129
130    return constraint.item1.size1 - constraint.item2.size2
131
132
133def outer_tangent_eq(constraint):
134    """Return the difference between the distance of the circles and the sum of the radii.
135
136    If the circles are tangent, the difference is 0.
137
138    Args:
139        constraint (Constraint): The constraint object.
140
141    Returns:
142        float: The difference between the distance of the circles and the sum of the radii.
143    """
144    if is_line(constraint.item1):
145        circle = constraint.item2
146        res = circle.radius -  point_to_line_distance(circle.center, constraint.item1)
147    elif is_line(constraint.item2):
148        circle = constraint.item1
149        res = circle.radius - point_to_line_distance(circle.center, constraint.item2)
150    else:
151        circle_1 = constraint.item1
152        circle_2 = constraint.item2
153
154        dist = distance(circle_1.center, circle_2.center)
155        rad1 = circle_1.radius
156        rad2 = circle_2.radius
157
158        res = dist - (rad1 + rad2)
159
160    return res
161
162def inner_tangent_eq(constraint):
163    """Return the difference between the distance of the circles and the sum of the radii.
164
165    If the circles are tangent, the difference is 0.
166
167    Args:
168        constraint (Constraint): The constraint object.
169
170    Returns:
171        float: The difference between the distance of the circles and the sum of the radii.
172    """
173    circle_1 = constraint.item1
174    circle_2 = constraint.item2
175
176    dist = distance(circle_1.center, circle_2.center)
177    rad1 = circle_1.radius
178    rad2 = circle_2.radius
179
180    return dist - abs(rad1 - rad2)
181
182
183def collinear_eq(constraint):
184    """Return the difference in direction for collinear items.
185
186    Items can be: segments, segment and a circle, or a segment and a point.
187
188    Args:
189        constraint (Constraint): The constraint object.
190
191    Returns:
192        float: The difference in direction.
193    """
194    # for now only segments are implemented
195    a1, b1 = constraint.item1
196    a2, b2 = constraint.item2
197
198    return direction(a1, b1, a2) - direction(a1, b1, b2)
199
200
201def equal_value_eq(constraint):
202    """Return the difference between the values.
203
204    Args:
205        constraint (Constraint): The constraint object.
206
207    Returns:
208        float: The difference between the values.
209    """
210    return constraint.value - constraint.value2
211
212
213def line_angle_eq(constraint):
214    """Return the angle between two segments.
215
216    Args:
217        constraint (Constraint): The constraint object.
218
219    Returns:
220        float: The angle between the two segments.
221    """
222    seg1 = constraint.item1
223    seg2 = constraint.item2
224    return constraint.value - angle_between_two_lines(seg1, seg2)
225
226
227d_equations = {
228    ConstType.COLLINEAR: collinear_eq,
229    ConstType.DISTANCE: distance_eq,
230    ConstType.EQUAL_SIZE: equal_size_eq,
231    ConstType.EQUAL_VALUE: equal_value_eq,
232    ConstType.LINE_ANGLE: line_angle_eq,
233    ConstType.PARALLEL: parallel_eq,
234    ConstType.PERPENDICULAR: perpendicular_eq,
235    ConstType.INNER_TANGENT: inner_tangent_eq,
236    ConstType.OUTER_TANGENT: outer_tangent_eq,
237}
238
239
240def solve(constraints, update_func, initial_guess, bounds=None, tol=1e-04):
241    """Solve the geometric constraints.
242
243    Args:
244        constraints (list): List of Constraint objects.
245        update_func (function): Function that updates the constraint items.
246        initial_guess (list): Initial guess for the solution.
247        bounds (list, optional): Bounds for the solution. Defaults to None.
248        tol (float, optional): Tolerance for the solution. Defaults to 1e-04.
249
250    Returns:
251        OptimizeResult: The optimization result represented as a `OptimizeResult` object.
252    """
253    from scipy.optimize import minimize # this takes too long to import!!!
254
255    def objective(x):
256        """Objective function for the minimization.
257
258        Args:
259            x (list): Current values of the variables.
260
261        Returns:
262            float: The sum of the constraint checks.
263        """
264        update_func(x)
265
266        return sum((constr.check() for constr in constraints))
267
268    def check_constraints(x):
269        """Return constraint results.
270
271        Args:
272            x (list): Current values of the variables.
273
274        Returns:
275            list: List of constraint check results.
276        """
277        update_func(x)
278
279        return [constr.check() for constr in constraints]
280
281    res = minimize(
282        objective,
283        initial_guess,
284        method="SLSQP",
285        bounds=bounds,
286        constraints={"type": "eq", "fun": check_constraints},
287        options={"eps": tol},
288    )
289
290    return res
291
292
293# # Example:
294# # Given 2 circles and a radius, find the position of a circle with the given radius
295# # that is tangent to both circles.
296# x1 = y1 = 0
297# r1 = 40
298# c1 = Circle((x1, y1), r1) # this would be sg.Circle((x1, y1), r1)
299# x2 = 100
300# y2 = 0
301# r2 = 35
302# c2 = Circle((x2, y2), r2)
303
304# # c3 position is estimated at this point
305# # c3 radius is fixed
306# guess = (45, 10)
307# x3 = 45
308# y3 = 45
309# r3 = 50
310# c3 = Circle((x3, y3), r3)
311
312# const1 = Constraint(c1, c3, ConstType.OUTER_TANGENT)
313# const2 = Constraint(c2, c3, ConstType.OUTER_TANGENT)
314
315# def update(x):
316#     '''Update the position of the circle.'''
317#     c3.center = (x[0], x[1])
318
319# bounds = [(35, 100), (35, 100)]
320# print(solve([const1, const2], update, guess, bounds ))
321
322# print(c3.center)
323
324# Apollonius problem
325
326# start with 2 identical circles tangent to each other
327
328# c1 = Circle((0, 0), 50)
329# c2 = Circle((100, 0), 50)
330
331# c3 is the big circle
332
333# c3 = Circle((50, -200), 200)
334
335# const1 = Constraint(c1, c3, ConstType.INNER_TANGENT)
336# const2 = Constraint(c2, c3, ConstType.INNER_TANGENT)
337# # const3 = Constraint(c2, c3, ConstType.DISTANCE, 200)
338
339# def update(x):
340#     '''Update the position of the circle.'''
341#     c3.center = (x[0], x[1])
342
343# guess = (50, -200)
344# bounds = [(49, 51), (-350, -50)]
345# print(solve([const1, const2], update, guess, bounds))
346# print('c3 center:', c3.center)
347
348# # Now we have 3 circles tangent to each other
349# # We will add a circle tangent to the 3 circles
350# # c4 is the circle we are looking for
351
352# c4 = Circle((50, 55), 5)
353
354# const1 = Constraint(c1, c4, ConstType.OUTER_TANGENT)
355# const2 = Constraint(c2, c4, ConstType.OUTER_TANGENT)
356# const3 = Constraint(c3, c4, ConstType.INNER_TANGENT)
357
358# def update(x):
359#     '''Update the position of the circle.'''
360#     c4.center = (x[0], x[1])
361#     c4.radius = x[2]
362
363# guess = (50, 55, 5) # x4, y4, r4
364# bounds = [(49.99, 50.01), (5, 100), (3, 30)]
365
366# print(solve([const1, const2, const3], update, guess, bounds))
367# print(c4.center, c4.radius)
@dataclass
class Constraint:
20@dataclass
21class Constraint:
22    """Constraint class for geometric constraints."""
23
24    item1: object
25    item2: object
26    type: ConstType
27    value: float = None
28    value2: float = None  # used for equal_value_eq
29
30    def __post_init__(self):
31        """Set item sizes for circles and segments."""
32        self.equation = d_equations[self.type]
33        if self.type == ConstType.EQUAL_SIZE:
34            if isinstance(self.item1, Circle):
35                self.size1 = self.item1.radius
36            elif is_line(self.item1):
37                self.size1 = distance(*self.item1)
38
39            if isinstance(self.item2, Circle):
40                self.size2 = self.item2.radius
41            elif is_line(self.item2):
42                self.size2 = distance(*self.item2)
43
44    def check(self):
45        """Check the constraint value.
46
47        Returns:
48            float: The result of the constraint equation.
49        """
50        return self.equation(self)

Constraint class for geometric constraints.

Constraint( item1: object, item2: object, type: simetri.graphics.all_enums.ConstraintType, value: float = None, value2: float = None)
item1: object
item2: object
value: float = None
value2: float = None
def check(self):
44    def check(self):
45        """Check the constraint value.
46
47        Returns:
48            float: The result of the constraint equation.
49        """
50        return self.equation(self)

Check the constraint value.

Returns:

float: The result of the constraint equation.

def distance_eq(constraint):
61def distance_eq(constraint):
62    """Return the difference between the target and current distance.
63
64    Args:
65        constraint (Constraint): The constraint object.
66
67    Returns:
68        float: The difference between the target and current distance.
69    """
70    if isinstance(constraint.item1, Circle):
71        p1 = constraint.item1.center
72    else:
73        p1 = constraint.item1
74
75    if isinstance(constraint.item2, Circle):
76        p2 = constraint.item2.center
77    else:
78        p2 = constraint.item2
79
80    value = constraint.value
81
82    return distance(p1, p2) - value

Return the difference between the target and current distance.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The difference between the target and current distance.

def parallel_eq(constraint):
85def parallel_eq(constraint):
86    """Return the cross product. If the segments are parallel, the cross product is 0.
87
88    Args:
89        constraint (Constraint): The constraint object.
90
91    Returns:
92        float: The cross product of the vectors.
93    """
94    vec1 = Vector2D(*constraint.item1)
95    vec2 = Vector2D(*constraint.item2)
96
97    return vec1.cross(vec2)

Return the cross product. If the segments are parallel, the cross product is 0.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The cross product of the vectors.

def perpendicular_eq(constraint):
100def perpendicular_eq(constraint):
101    """Return the dot product. If the segments are perpendicular, the dot product is 0.
102
103    Args:
104        constraint (Constraint): The constraint object.
105
106    Returns:
107        float: The dot product of the vectors.
108    """
109    seg1 = constraint.item1
110    seg2 = constraint.item2
111
112    vec1 = Vector2D(*seg1)
113    vec2 = Vector2D(*seg2)
114
115    return vec1.dot(vec2)

Return the dot product. If the segments are perpendicular, the dot product is 0.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The dot product of the vectors.

def equal_size_eq(constraint):
118def equal_size_eq(constraint):
119    """Return the difference between the sizes of the items.
120
121    For segments, item size is the length of the segment.
122    For circles, item size is the radius of the circle.
123
124    Args:
125        constraint (Constraint): The constraint object.
126
127    Returns:
128        float: The difference between the sizes of the items.
129    """
130
131    return constraint.item1.size1 - constraint.item2.size2

Return the difference between the sizes of the items.

For segments, item size is the length of the segment. For circles, item size is the radius of the circle.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The difference between the sizes of the items.

def outer_tangent_eq(constraint):
134def outer_tangent_eq(constraint):
135    """Return the difference between the distance of the circles and the sum of the radii.
136
137    If the circles are tangent, the difference is 0.
138
139    Args:
140        constraint (Constraint): The constraint object.
141
142    Returns:
143        float: The difference between the distance of the circles and the sum of the radii.
144    """
145    if is_line(constraint.item1):
146        circle = constraint.item2
147        res = circle.radius -  point_to_line_distance(circle.center, constraint.item1)
148    elif is_line(constraint.item2):
149        circle = constraint.item1
150        res = circle.radius - point_to_line_distance(circle.center, constraint.item2)
151    else:
152        circle_1 = constraint.item1
153        circle_2 = constraint.item2
154
155        dist = distance(circle_1.center, circle_2.center)
156        rad1 = circle_1.radius
157        rad2 = circle_2.radius
158
159        res = dist - (rad1 + rad2)
160
161    return res

Return the difference between the distance of the circles and the sum of the radii.

If the circles are tangent, the difference is 0.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The difference between the distance of the circles and the sum of the radii.

def inner_tangent_eq(constraint):
163def inner_tangent_eq(constraint):
164    """Return the difference between the distance of the circles and the sum of the radii.
165
166    If the circles are tangent, the difference is 0.
167
168    Args:
169        constraint (Constraint): The constraint object.
170
171    Returns:
172        float: The difference between the distance of the circles and the sum of the radii.
173    """
174    circle_1 = constraint.item1
175    circle_2 = constraint.item2
176
177    dist = distance(circle_1.center, circle_2.center)
178    rad1 = circle_1.radius
179    rad2 = circle_2.radius
180
181    return dist - abs(rad1 - rad2)

Return the difference between the distance of the circles and the sum of the radii.

If the circles are tangent, the difference is 0.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The difference between the distance of the circles and the sum of the radii.

def collinear_eq(constraint):
184def collinear_eq(constraint):
185    """Return the difference in direction for collinear items.
186
187    Items can be: segments, segment and a circle, or a segment and a point.
188
189    Args:
190        constraint (Constraint): The constraint object.
191
192    Returns:
193        float: The difference in direction.
194    """
195    # for now only segments are implemented
196    a1, b1 = constraint.item1
197    a2, b2 = constraint.item2
198
199    return direction(a1, b1, a2) - direction(a1, b1, b2)

Return the difference in direction for collinear items.

Items can be: segments, segment and a circle, or a segment and a point.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The difference in direction.

def equal_value_eq(constraint):
202def equal_value_eq(constraint):
203    """Return the difference between the values.
204
205    Args:
206        constraint (Constraint): The constraint object.
207
208    Returns:
209        float: The difference between the values.
210    """
211    return constraint.value - constraint.value2

Return the difference between the values.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The difference between the values.

def line_angle_eq(constraint):
214def line_angle_eq(constraint):
215    """Return the angle between two segments.
216
217    Args:
218        constraint (Constraint): The constraint object.
219
220    Returns:
221        float: The angle between the two segments.
222    """
223    seg1 = constraint.item1
224    seg2 = constraint.item2
225    return constraint.value - angle_between_two_lines(seg1, seg2)

Return the angle between two segments.

Arguments:
  • constraint (Constraint): The constraint object.
Returns:

float: The angle between the two segments.

d_equations = {<ConstraintType.COLLINEAR: 'COLLINEAR'>: <function collinear_eq>, <ConstraintType.DISTANCE: 'DISTANCE'>: <function distance_eq>, <ConstraintType.EQUAL_SIZE: 'EQUAL_SIZE'>: <function equal_size_eq>, <ConstraintType.EQUAL_VALUE: 'EQUAL_VALUE'>: <function equal_value_eq>, <ConstraintType.LINE_ANGLE: 'LINE_ANGLE'>: <function line_angle_eq>, <ConstraintType.PARALLEL: 'PARALLEL'>: <function parallel_eq>, <ConstraintType.PERPENDICULAR: 'PERPENDICULAR'>: <function perpendicular_eq>, <ConstraintType.INNER_TANGENT: 'INNER_TANGENT'>: <function inner_tangent_eq>, <ConstraintType.OUTER_TANGENT: 'OUTER_TANGENT'>: <function outer_tangent_eq>}
def solve(constraints, update_func, initial_guess, bounds=None, tol=0.0001):
241def solve(constraints, update_func, initial_guess, bounds=None, tol=1e-04):
242    """Solve the geometric constraints.
243
244    Args:
245        constraints (list): List of Constraint objects.
246        update_func (function): Function that updates the constraint items.
247        initial_guess (list): Initial guess for the solution.
248        bounds (list, optional): Bounds for the solution. Defaults to None.
249        tol (float, optional): Tolerance for the solution. Defaults to 1e-04.
250
251    Returns:
252        OptimizeResult: The optimization result represented as a `OptimizeResult` object.
253    """
254    from scipy.optimize import minimize # this takes too long to import!!!
255
256    def objective(x):
257        """Objective function for the minimization.
258
259        Args:
260            x (list): Current values of the variables.
261
262        Returns:
263            float: The sum of the constraint checks.
264        """
265        update_func(x)
266
267        return sum((constr.check() for constr in constraints))
268
269    def check_constraints(x):
270        """Return constraint results.
271
272        Args:
273            x (list): Current values of the variables.
274
275        Returns:
276            list: List of constraint check results.
277        """
278        update_func(x)
279
280        return [constr.check() for constr in constraints]
281
282    res = minimize(
283        objective,
284        initial_guess,
285        method="SLSQP",
286        bounds=bounds,
287        constraints={"type": "eq", "fun": check_constraints},
288        options={"eps": tol},
289    )
290
291    return res

Solve the geometric constraints.

Arguments:
  • constraints (list): List of Constraint objects.
  • update_func (function): Function that updates the constraint items.
  • initial_guess (list): Initial guess for the solution.
  • bounds (list, optional): Bounds for the solution. Defaults to None.
  • tol (float, optional): Tolerance for the solution. Defaults to 1e-04.
Returns:

OptimizeResult: The optimization result represented as a OptimizeResult object.