solver: improve redundancy checking of implicit constraints

Related #403
This commit is contained in:
Zheng, Lei 2021-01-10 13:04:14 +08:00
parent e785510c68
commit bc2d5d611f
3 changed files with 139 additions and 39 deletions

View File

@ -1175,7 +1175,8 @@ class BaseMulti(Base):
e = cls._entityDef[0](
solver,partInfo,info.Subname,info.Shape)
params = props + [e0,e]
solver.system.checkRedundancy(obj,partInfo0,partInfo)
solver.system.checkRedundancy(
obj,partInfo0,partInfo,info0.SubnameRef,info.SubnameRef)
h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)):
ret += list(h)
@ -1228,16 +1229,19 @@ class BaseMulti(Base):
if i==idx0:
e0 = cls._entityDef[idx0](
solver,partInfo,info.Subname,info.Shape)
subname0 = info.SubnameRef
info0 = partInfo
else:
e = cls._entityDef[0](solver,partInfo,info.Subname,info.Shape)
if e0 and e:
if idx0:
params = props + [e,e0]
solver.system.checkRedundancy(obj,partInfo,info0)
solver.system.checkRedundancy(
obj,partInfo,info0,info.SubnameRef,subname0)
else:
params = props + [e0,e]
solver.system.checkRedundancy(obj,info0,partInfo)
solver.system.checkRedundancy(
obj,info0,partInfo,subname0,info.SubnameRef)
h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)):
ret += list(h)
@ -1270,7 +1274,8 @@ class BaseCascade(BaseMulti):
params = props + [e1,e2]
else:
params = props + [e2,e1]
solver.system.checkRedundancy(obj,prevInfo,partInfo)
solver.system.checkRedundancy(
obj,prevInfo,partInfo,prev.SubnameRef,info.SubnameRef)
h = func(*params,group=solver.group)
if isinstance(h,(list,tuple)):
ret += list(h)

View File

@ -18,15 +18,12 @@ from .system import System
# plane of the part.
# EntityMap: string -> entity handle map, for caching
# Group: transforming entity group handle
# CstrMap: map from other part to the constrain between this and the other part.
# This is for auto constraint DOF reduction. Only some composite
# constraints will be mapped.
# Update: in case the constraint uses the `Multiplication` feature, only the
# first element of all the coplanar edges will be actually constrainted.
# The rest ElementInfo will be stored here for later update by matrix
# transformation.
PartInfo = namedtuple('SolverPartInfo', ('Part','PartName','Placement',
'Params','Workplane','EntityMap','Group','CstrMap','Update'))
'Params','Workplane','EntityMap','Group','Update'))
class Solver(object):
def __init__(self,assembly,reportFailed,dragPart,recompute,rollback):
@ -315,7 +312,6 @@ class Solver(object):
Workplane = h,
EntityMap = {},
Group = group if group else g,
CstrMap = {},
Update = [])
self.system.log('{}, {}',partInfo,g)

View File

@ -115,6 +115,14 @@ class SystemBase(with_metaclass(System, object)):
self.verbose = obj.Verbose
self.log = logger.info if obj.Verbose else logger.debug
def _cstrKey(cstrType, firstPart, secondPart):
if firstPart > secondPart:
return (cstrType, secondPart, firstPart)
else:
return (cstrType, firstPart, secondPart)
# For skipping invalid constraints
_DummyCstrList = [None] * 6
class SystemExtension(object):
def __init__(self):
@ -126,9 +134,16 @@ class SystemExtension(object):
self.secondInfo = None
self.relax = False
self.coincidences = {}
self.cstrMap = {}
self.elementCstrMap = {}
self.elementMap = {}
self.firstElement = None
self.secondElement = None
def checkRedundancy(self,obj,firstInfo,secondInfo):
def checkRedundancy(self,obj,firstInfo,secondInfo,firstElement,secondElement):
self.cstrObj,self.firstInfo,self.secondInfo=obj,firstInfo,secondInfo
self.firstElement = firstElement
self.secondElement = secondElement
def addSketchPlane(self,*args,**kargs):
_ = kargs
@ -148,34 +163,121 @@ class SystemExtension(object):
h.append(self.addSameOrientation(n1.entity,n,group=group))
return h
def reportRedundancy(self,warn=False):
def reportRedundancy(self,firstPart=None,secondPart=None,count=0,limit=0,implicit=False):
msg = '{} between {} and {}'.format(cstrName(self.cstrObj),
self.firstInfo.PartName, self.secondInfo.PartName)
if warn:
logger.warn('skip redundant {}', msg, frame=1)
firstPart if firstPart else self.firstInfo.PartName,
secondPart if secondPart else self.secondInfo.PartName)
if implicit:
logger.msg('redundant implicit constraint {}, {}', msg, count, frame=1)
elif count > limit:
logger.warn('skip redundant {}, {}', msg, count, frame=1)
else:
logger.debug('auto relax {}', msg, frame=1)
logger.msg('auto relax {}, {}', msg, count, frame=1)
def _countConstraints(self,increment,limit,*names):
first,second = self.firstInfo,self.secondInfo
if not first or not second:
return []
for name in names:
cstrs = first.CstrMap.get(second.Part,{}).get(name,None)
if not cstrs:
if increment:
cstrs = second.CstrMap.setdefault(
first.Part,{}).setdefault(name,[])
else:
cstrs = second.CstrMap.get(first.Part,{}).get(name,[])
cstrs += [None]*increment
count = len(cstrs)
if limit and count>=limit:
self.reportRedundancy(count>limit)
def _populateConstraintMap(
self,cstrType,firstElement,secondElement,increment,limit,item,implicit):
firstPart = self.elementMap[firstElement]
secondPart = self.elementMap[secondElement]
if firstPart == secondPart:
return _DummyCstrList
# A constraint may contain elements belong to more than two parts. For
# exmaple, for a constraint with elements from part A, B, C, we'll
# expand it into two constraints for parts AB and AC. However, we must
# also count the implicit constraint between B and C.
#
# self.cstrMap is a map for counting constraints of the same type
# between pairs of parts. The count is used for checking redundancy and
# auto relaxing. The map is keyed using
#
# tuple(cstrType, firstPartName, secondPartName)
#
# and the value is a list. The item of this list is constraint defined
# (e.g. PlaineAilgnment stores a plane entity as item for auto
# relaxing) , the length of this list is use as the constraint count to
# be used later to decide how to auto relax the constraint.
#
# See the following link for difficulties on auto relaxing with implicit
# constraints. Right now there is no search performed. So the auto relax
# may fail. And the user is required to manually reorder constraints and
# the elements within to help the solver.
#
# https://github.com/realthunder/FreeCAD_assembly3/issues/403#issuecomment-757400349
key = _cstrKey(cstrType,firstPart,secondPart)
cstrs = self.cstrMap.setdefault(key, [])
cstrs += [item]*increment
count = len(cstrs)
if increment and count>=limit:
self.reportRedundancy(firstPart, secondPart, count, limit, implicit)
return cstrs
def countConstraints(self,increment,limit,*names):
count = len(self._countConstraints(increment,limit,*names))
def _countConstraints(self,increment,limit,cstrType,item=None):
first, second = self.firstInfo, self.secondInfo
if not first or not second:
return []
firstElement, secondElement = self.firstElement, self.secondElement
if firstElement == secondElement:
return _DummyCstrList
self.elementMap[firstElement] = first.PartName
self.elementMap[secondElement] = second.PartName
# When counting implicit constraints (see comments in
# _populateConstraintMap() above), we must also make sure to count them
# if and only if they are originated from the same element, i.e. both
# AB and AC involving the same element of A. This will be ture if the
# those constraints are expanded by us, but may not be so if the user
# created them.
#
# self.elementCstrMap is a map keyed using tuple(cstrType, elementName),
# with value of a set of all element names that is involved with the the
# same type of constraint. This set is shared by all element entries in
# the map.
firstSet = self.elementCstrMap.setdefault((cstrType, firstElement), set())
if not firstSet:
firstSet.add(firstElement)
secondSet = self.elementCstrMap.setdefault((cstrType, secondElement),firstSet)
res = _DummyCstrList
if firstSet is not secondSet:
# If the secondSet is different, we shall merge them, and count the
# implicit constraints between the elements of first and second set.
for element in secondSet:
self.elementCstrMap[(cstrType, element)] = firstSet
is_second = element == secondElement
for e in firstSet:
implicit = not is_second or e != firstElement
cstrs = self._populateConstraintMap(
cstrType,e,element,increment,limit,item,implicit)
if not implicit:
# save the result (i.e. the explicit constraint pair of
# the give first and second element) for return
res = cstrs
firstSet |= secondSet
elif secondElement not in firstSet:
# Here means the entry of the secondElement is newly created, count
# the implicit constraints between all elements in the set to the
# secondElement.
for e in firstSet:
implicit = e != firstElement
cstrs = self._populateConstraintMap(
cstrType,e,secondElement,increment,limit,item,implicit)
if not implicit:
res = cstrs
firstSet.add(secondElement)
if res is _DummyCstrList:
self.reportRedundancy(count=len(res), limit=limit)
return res
def countConstraints(self,increment,limit,name):
count = len(self._countConstraints(increment,limit,name))
if count>limit:
return -1
return count
@ -210,7 +312,7 @@ class SystemExtension(object):
# in the previous and the current constraint. The point is taken
# from the element of the second part of the current constraint.
# The projection plane is taken from the element of the first part
# of the current constraint.
# of the current constraint.
#
# This 2D PointOnLine effectively reduce the second PlaneCoincidence
# constraining DOF down to 1.
@ -236,15 +338,12 @@ class SystemExtension(object):
h = []
if self.relax:
dof = 2 if lockAngle else 1
cstrs = self._countConstraints(dof,3,'Alignment')
cstrs = self._countConstraints(dof,3,'Alignment',item=pln1.entity)
count = len(cstrs)
if count > 3:
return
if count == 1:
cstrs[0] = pln1.entity
else:
count = 0
cstrs = None
if d:
h.append(self.addPointPlaneDistance(
@ -255,7 +354,7 @@ class SystemExtension(object):
if count<=2:
n1,n2 = pln1.normal,pln2.normal
if count==2 and not lockAngle:
self.reportRedundancy()
self.reportRedundancy(count=count, limit=count)
h.append(self.addParallel(n2.entity,n1.entity,cstrs[0],group))
else:
self.setOrientation(h,lockAngle,yaw,pitch,roll,n1,n2,group)