Skip to content

constraints -- Geometric constraints on primitives

This modules defines the constraints definitions and the solver tools

Constraints can be any object referencing variables and implementing the following signature to guide the solver in the resolution:

    class SomeConstraint:

            # name the attributes referencing the solver variables to change
            slvvars = 'some_primitive', 'some_point'
            # function returning the squared error in the constraint

            #  for a coincident constraint for instance it's the squared distance
            #  the error must be a contiguous function of the parameters, and it's squared for numeric stability reasons.
            #  the solver internally use an iterative approach to optimize the sum of all fit functions.
            def fit((self):
                    ...

This modules defines the constraints definitions and the solver tools

Constraints can be any object referencing variables and implementing the following signature to guide the solver in the resolution:

class SomeConstraint:

    # name the attributes referencing the solver variables to change
    slvvars = 'some_primitive', 'some_point'
    # function returning the squared error in the constraint

    #  for a coincident constraint for instance it's the squared distance
    #  the error must be a contiguous function of the parameters, and it's squared for numeric stability reasons.
    #  the solver internally use an iterative approach to optimize the sum of all fit functions.
    def fit(self):
        ...

isconstraint(obj)

Return True if obj match the constraint signature

Source code in madcad/constraints.py
48
49
50
def isconstraint(obj):
	''' Return True if obj match the constraint signature '''
	return hasattr(obj, 'fit') and hasattr(obj, 'slvvars')

Solver

solve(constraints, fixed=(), *args, **kwargs)

Short hand to use the class Problem

Source code in madcad/constraints.py
196
197
198
def solve(constraints, fixed=(), *args, **kwargs):
	''' Short hand to use the class Problem '''
	return Problem(constraints, fixed).solve(*args, **kwargs)

Problem(constraints, fixed=())

Class to holds data for a problem solving process. it is intended to be instantiated for each different probleme solving, an instance is used multiple times only when we want to solve on top of the previous results, using exactly the same probleme definition (constraints and variables)

Therefore the solver protocol is the following
  • constraints define the problem
  • each constraint refers to variables it applies on constraints have the method fit() and a member 'slvvars' that can be

    1. An iterable of names of variable members in the constraint object
    2. A function returning an iterable of the actual variables objects (that therefore must be referenced refs and not primitive types)
    
  • each variable object can redirect to other variable objects if they implements such a member 'slvvars'

  • primitives can also be constraints on their variables, thus they must have a method fit() (but no member 'primitives' here)
  • primitives can implement the optional solver methods for some constraints, such as 'slv_tangent'
Source code in madcad/constraints.py
216
217
218
219
220
221
222
223
224
225
226
227
228
def __init__(self, constraints, fixed=()):
	self.constraints = set()
	self.slvvars = {}
	self.dim = 0
	for cst in constraints:
		# cst can contains objects that are not constraints
		if hasattr(cst, 'fit'):
			self.register(cst)
	for prim in fixed:
		self.unregister(prim)
	for v in self.slvvars.values():
		if isinstance(v, tuple):	self.dim += 1
		else:						self.dim += len(v)

constraints = set() instance-attribute

slvvars = {} instance-attribute

dim = 0 instance-attribute

register(obj)

Register a constraint or a variable object

Source code in madcad/constraints.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def register(self, obj):
	''' Register a constraint or a variable object '''
	if hasattr(obj, 'fit'):
		self.constraints.add(obj.fit)
	# register object's variables
	if hasattr(obj, 'slvvars'):
		if callable(obj.slvvars):
			for var in obj.slvvars():
				if isinstance(var, (float, int)):		raise TypeError("primitive types (float,int) are not allowed when 'slvvars' is a callable")
				self.register(var)
		else:
			for varname in obj.slvvars:
				var = getattr(obj, varname)
				if isinstance(var, (float,int)):
					self.slvvars[(id(obj), varname)] = (obj, varname)
				else:
					self.register(var)
	elif isinstance(obj, tuple):
		for p in obj:
			self.register(p)
	else:
		if id(obj) not in self.slvvars:
			self.slvvars[id(obj)] = obj

unregister(obj)

Unregister all variables from a constraint or a variable object

Source code in madcad/constraints.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def unregister(self, obj):
	''' Unregister all variables from a constraint or a variable object '''
	if hasattr(obj, 'slvvars'):
		if callable(obj.slvvars):
			for var in obj.slvvars():
				self.unregister(var)
		for varname in obj.slvvars:
			var = getattr(obj, varname)
			if isinstance(var, (float,int)):
				del self.slvvars[(id(obj), varname)]
			else:
				self.unregister(var)
	elif isinstance(obj, tuple):
		for p in obj:
			self.unregister(p)
	else:
		if id(obj) in self.slvvars:
			del self.slvvars[id(obj)]

state()

Source code in madcad/constraints.py
273
274
275
276
277
278
279
280
def state(self):
	x = np.empty(self.dim, dtype='f8')
	i = 0
	for v in self.slvvars.values():
		l = len(v)
		x[i:i+l] = v
		i += l
	return x

place(x)

Source code in madcad/constraints.py
282
283
284
285
286
287
def place(self, x):
	i = 0
	for v in self.slvvars.values():
		l = len(v)
		for j in range(l):	v[j] = x[i+j]
		i += l

fit()

Source code in madcad/constraints.py
289
290
291
292
293
def fit(self):
	residuals = typedlist(float)
	for fit in self.constraints:
		residuals.extend(fit())
	return residuals

evaluate(x)

Source code in madcad/constraints.py
295
296
297
def evaluate(self, x):
	self.place(x)
	return self.fit()

solve(precision=1e-06, method='trf', maxiter=None, afterset=None)

Source code in madcad/constraints.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def solve(self, precision=1e-6, method='trf', maxiter=None, afterset=None):
	#nprint(self.slvvars)
	if afterset:	evaluate = lambda x: afterset(x) or self.evaluate(x)
	else:			evaluate = self.evaluate
	res = least_squares(evaluate, self.state(), 
			xtol=precision, 
			gtol=precision**3,
			ftol=precision**2,
			method=method, 
			max_nfev=maxiter,
			)

	self.place(res.x)
	#print(res)
	if res.cost < precision:
		return res
	elif not res.sucess:
		raise SolveError(res.message)
	else:
		raise SolveError('no solution found')

Constraints definitions

Distance(*args, **kwargs)

Bases: Constraint

Makes two points distant of the fixed given distance

Source code in madcad/constraints.py
37
38
39
40
41
def __init__(self, *args, **kwargs):
	for i,name in enumerate(self.__slots__):
		setattr(self, name, args[i] if i < len(args) else None)
	for name, arg in kwargs.items():
		setattr(self, name, arg)

__slots__ = ('p1', 'p2', 'd', 'along', 'location') class-attribute instance-attribute

slvvars = ('p1', 'p2') class-attribute instance-attribute

fit()

Source code in madcad/constraints.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def fit(self):
	if isinstance(self.p1, vec3) and isinstance(self.p2, vec3):
		if self.along:
			if isinstance(self.along, vec3):	a = self.along
			else:						a = self.along.direction
			return (dot(self.p1-self.p2, a) - self.d) ** 2,
		else:
			return (distance(self.p1, self.p2) - self.d) **2,
	elif isinstance(self.p1, vec3):
		return (length(noproject(self.p2.origin-self.p1, self.p2.direction)) - self.d) **2,
	elif isinstance(self.p2, vec3):
		return (length(noproject(self.p1.origin-self.p2, self.p1.direction)) - self.d) **2,
	else:
		d1 = self.p1.direction
		d2 = self.p2.direction
		if dot(d1,d2) < 0:	d2 = -d2
		return length2(cross(d1,d2)), (length(noproject(self.p1.origin-self.p2.origin, d1+d2)) - self.d) **2

display(scene)

Source code in madcad/constraints.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def display(self, scene):
	if isinstance(self.p1, vec3) and isinstance(self.p2, vec3):
		return scene.display(scheme.note_distance(
				self.p1, self.p2, 
				project=self.along,
				text='{:.5g}\n{:+.1g}'.format(self.d, sqrt(self.fit()[0])),
				))
	elif isinstance(self.p1, vec3):
		return scene.display(scheme.note_distance(
				self.p1, self.p1 + noproject(self.p2.origin-self.p1, self.p2.direction),
				text='{:.5g}\n{:+.1g}'.format(self.d, sqrt(self.fit()[0])),
				))
	elif isinstance(self.p2, vec3):
		return scene.display(scheme.note_distance(
				self.p2, self.p2 + noproject(self.p1.origin-self.p2, self.p1.direction), 
				text='{:.5g}\n{:+.1g}'.format(self.d, sqrt(self.fit()[0])),
				))
	else:
		p = mix(self.p1.origin, self.p2.origin, 0.5)
		d1 = self.p1.direction
		d2 = self.p2.direction
		if dot(d1,d2) < 0:	d2 = -d2
		d = d1 + d2
		p1 = self.p1.origin
		p2 = self.p2.origin
		return scene.display(scheme.note_distance(
				p + noproject(p1-p, d),
				p + noproject(p2-p, d),
				text='{:.5g}\n{:+.1g}'.format(self.d, sqrt(self.fit()[0])),
				))

Radius(*args, **kwargs)

Bases: Constraint

Gets the given Arc with the given fixed radius

Note: Only ArcCentered are supported yet.

Source code in madcad/constraints.py
37
38
39
40
41
def __init__(self, *args, **kwargs):
	for i,name in enumerate(self.__slots__):
		setattr(self, name, args[i] if i < len(args) else None)
	for name, arg in kwargs.items():
		setattr(self, name, arg)

__slots__ = ('arc', 'radius', 'location') class-attribute instance-attribute

slvvars = ('arc',) class-attribute instance-attribute

fit()

Source code in madcad/constraints.py
160
161
def fit(self):
	return (self.arc.radius - self.radius) **2,

display(scene)

Source code in madcad/constraints.py
163
164
165
166
167
168
169
170
171
def display(self, scene):
	r = self.arc.radius
	center, z = self.arc.axis
	x = dirbase(z)[0]
	return scene.display(scheme.note_leading(
				center+x*r, 
				r*x, 
				text='R{:.5g}\n{:+.1g}'.format(r, self.radius - self.arc.radius),
				))

Angle(*args, **kwargs)

Bases: Constraint

Gets two segments with the given fixed angle between them

Source code in madcad/constraints.py
37
38
39
40
41
def __init__(self, *args, **kwargs):
	for i,name in enumerate(self.__slots__):
		setattr(self, name, args[i] if i < len(args) else None)
	for name, arg in kwargs.items():
		setattr(self, name, arg)

__slots__ = ('s1', 's2', 'angle') class-attribute instance-attribute

slvvars = ('s1', 's2') class-attribute instance-attribute

fit()

Source code in madcad/constraints.py
130
131
132
133
134
def fit(self):
	d1 = self.s1.direction
	d2 = self.s2.direction
	a = atan2(length(cross(d1,d2)), dot(d1,d2))
	return (a - self.angle)**2,

display(scene)

Source code in madcad/constraints.py
136
137
138
139
140
141
def display(self, scene):
	return scene.display(scheme.note_angle(
				(self.s1.origin, -self.s1.direction),
				(self.s2.origin, -self.s2.direction),
				text='{:.5g}°\n{:+.1g}'.format(degrees(self.angle), degrees(sqrt(self.fit()[0]))),
				))

Tangent(*args, **kwargs)

Bases: Constraint

Makes to curves tangent in the given point The point moves as well as curves.

The curves primitives must have a member slv_tangent(p) returning the tangent vector at the nearest position to p

Source code in madcad/constraints.py
37
38
39
40
41
def __init__(self, *args, **kwargs):
	for i,name in enumerate(self.__slots__):
		setattr(self, name, args[i] if i < len(args) else None)
	for name, arg in kwargs.items():
		setattr(self, name, arg)

__slots__ = ('c1', 'c2', 'p', 'size') class-attribute instance-attribute

slvvars = ('c1', 'c2', 'p') class-attribute instance-attribute

fit()

Source code in madcad/constraints.py
64
65
def fit(self):
	return length2(cross(self.c1.slv_tangent(self.p), self.c2.slv_tangent(self.p))),

OnPlane(*args, **kwargs)

Bases: Constraint

Puts the given points on the fixed plane given by its normal axis

Source code in madcad/constraints.py
37
38
39
40
41
def __init__(self, *args, **kwargs):
	for i,name in enumerate(self.__slots__):
		setattr(self, name, args[i] if i < len(args) else None)
	for name, arg in kwargs.items():
		setattr(self, name, arg)

__slots__ = ('axis', 'pts') class-attribute instance-attribute

slvvars()

Source code in madcad/constraints.py
176
177
def slvvars(self):
	return self.pts

fit()

Source code in madcad/constraints.py
178
179
180
181
def fit(self):
	s = 0
	for p in self.pts:
		yield dot(p-self.axis[0], self.axis[1]) **2