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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.