在整车碰撞模型的搭建过程中,在转向系统中涉及到万向铰的创建。采用手动创建如下:创建过程步骤多,选点容易出错,多新手不友好还费时。通过二次开发手段,通过工程师手动选取节点后,后续动作均全自动生成,不仅可大大提高效率还可以确保不出错。效果如下:from ansa import basefrom ansa import constantsfrom ansa import calcimport mathimport randomfrom typing import List, Tuple, Union, Iterable, Dict, Optional
class Vector(object): @classmethod def point2Point(cls, point1:Union[List[float], Tuple[float], base.Entity], point2:Union[List[float], Tuple[float], base.Entity], unit:bool=True)->List[float]: """ 计算两点构成的向量。 Args: point1: 第一个点坐标,可以是列表、元组或 Entity 对象 point2: 第二个点坐标,可以是列表、元组或 Entity 对象 unit: 是否返回单位向量,默认为 True Returns: List[float]: 两点构成的向量,若 unit 为 True 则返回单位向量 """ if isinstance(point1, base.Entity):point1 = point1.position if isinstance(point2, base.Entity):point2 = point2.position vect:List[float] = [p2-p1 for p1, p2 in zip(point1, point2)] return cls.unit(vect) if unit else vect @staticmethod def unit(vect:Union[List[float], Tuple[float]])->List[float]: """ 将一个向量单位化。 Args: vect: 输入向量,可以是列表或元组 Returns: List[float]: 单位向量 """ return calc.Normalize(vect) @staticmethod def distance(point1:Union[List[float], Tuple[float], base.Entity], point2:Union[List[float], Tuple[float], base.Entity] )->float: """ 计算两点的距离。 Args: point1: 第一个点坐标,可以是列表、元组或 Entity 对象 point2: 第二个点坐标,可以是列表、元组或 Entity 对象 Returns: float: 两点之间的距离 """ if isinstance(point1, base.Entity):point1 = point1.position if isinstance(point2, base.Entity):point2 = point2.position return math.dist(point1, point2) @staticmethod def move(point:Union[List[float], Tuple[float], base.Entity], vect:Union[List[float], Tuple[float]], dist:float)->List[float]: """ 沿指定向量移动点。 Args: point: 点坐标,可以是列表、元组或 Entity 对象 vect: 移动方向向量 dist: 移动距离 Returns: List[float]: 移动后的点坐标 """ if isinstance(point, base.Entity):point = point.position return list(map(lambda p, v: p+dist*v, point, vect)) @staticmethod def getPointCenter(points:List[Union[List[float], Tuple[float], base.Entity]])->Tuple[float]: """ 计算输入点集 合的中心点。 Args: points: 点集 合,可以是列表、元组或 Entity 对象 Returns: Tuple[float]: 中心点坐标 (x, y, z) """ if not points: return (0.0, 0.0, 0.0) xSum, ySum, zSum = 0.0, 0.0, 0.0 for point in points: if isinstance(point, base.Entity): point = point.position xSum += point[0] ySum += point[1] zSum += point[2] count:int = len(points) return (xSum/count, ySum/count, zSum/count)
class JointBase(object): """ 关节基础类。 定义关节的通用属性和行为。 """ DECK:int = constants.LSDYNA def __init__(self, entType:str, nameCN:str=None): """ 初始化关节基础类。 Args: entType: 实体类型名称 nameCN: 中文名称(可选) """ self.entType:str = entType self.nameCN:str = nameCN self.masterNodes:List[base.Entity] = [] if nameCN: print(f'[INFO] 初始化关节类型:{self.nameCN}') def getMasterNodes(self): """ 获取主节点列表。 Returns: List[base.Entity]: 主节点列表 """ ... @classmethod def createRbe2(cls, nodes:List[base.Entity], masterNode:base.Entity=None)->base.Entity: """ 创建 RBE2 刚性连接。 Args: nodes: 节点列表 masterNode: 主节点,若为 None 则自动创建位于重心处的节点 Returns: base.Entity: 创建的 CONSTRAINED_NODAL_RIGID_BODY 实体 """ print('[INFO]创建RBE2') setEnt:base.Entity = base.CreateEntity(cls.DECK, 'SET') base.AddToSet(setEnt, nodes) setEnt.set_entity_values(cls.DECK, {'Name': f'SET_NODE_{setEnt._id:010d}'}) if not masterNode: cog = base.cog(setEnt) masterNode = base.CreateEntity(cls.DECK, 'NODE', {'X': cog[0], 'Y': cog[1], 'Z':cog[2]}) base.AddToSet(setEnt, masterNode) fields = {'NSID':setEnt._id, 'MASTER':masterNode._id, 'No.of.Nodes':len(nodes)+1} return base.CreateEntity(cls.DECK, 'CONSTRAINED_NODAL_RIGID_BODY', fields)class UniversalJoint(JointBase): """ 万向铰关节类。 用于创建万向铰类型的连接。 """ def __init__(self): """ 初始化万向铰关节。 """ super().__init__('CONSTRAINED_JOINT_UNIVERSAL', nameCN='万向铰') self.pickTypes:Tuple[str] = ('ELEMENT_SHELL', 'ELEMENT_SOLID') self.pickNodeResultFlag:int = 0 def getNodesBy2Axis(self): """ 获取两轴上的节点。 通过用户交互选择两个轴上的所有节点,并提取端点。 """ print(f'[INFO] 请选择{self.nameCN}第一轴上的所有节点') axis1Nodes:List[base.Entity] = base.PickNodes(self.DECK, self.pickTypes) if not axis1Nodes: print(f'[WARNING] 未选择{self.nameCN}第一轴上的任何节点') self.pickNodeResultFlag = 1 return None print(f'[INFO] 请选择{self.nameCN}第二轴上的所有节点') axis2Nodes:List[base.Entity] = base.PickNodes(self.DECK, self.pickTypes) if not axis2Nodes: print(f'[WARNING] 未选择{self.nameCN}第二轴上的任何节点') self.pickNodeResultFlag = 2 return None self.axis1Node1, self.axis1Node2, _ = self.getVertixByAxis(axis1Nodes) if not self.axis1Node1 or not self.axis1Node2: print(f'[WARNING] 未选择{self.nameCN}第一轴上的一端点未被选中') self.pickNodeResultFlag = 3 return None self.axis2Node1, self.axis2Node2, _ = self.getVertixByAxis(axis2Nodes) if not self.axis2Node1 or not self.axis2Node2: print(f'[WARNING] 未选择{self.nameCN}第二轴上的一端点未被选中') self.pickNodeResultFlag = 4 return None self.pickNodeResultFlag = 0 return None @classmethod def getVertixByAxis(cls, nodes:List[base.Entity])->Tuple[List[base.Entity], List[base.Entity], bool]: """ 根据轴上的节点获取两个端点簇。 Args: nodes: 轴上的节点列表 Returns: Tuple[List, List, bool]: (簇1点列表, 簇2点列表, 是否只有一个簇) """ obj = ClusterPoints(nodes) return obj.run() @classmethod def createCenterNode(cls, nodes:List[base.Entity])->base.Entity: """ 创建位于节点集 合重心处的节点。 Args: nodes: 节点列表 Returns: base.Entity: 创建的节点实体 """ cog:Tuple[float] = Vector.getPointCenter(nodes) return base.CreateEntity(cls.DECK, 'NODE', {'X': cog[0], 'Y': cog[1],'Z': cog[2]}) @classmethod def getNode4(cls, node1:base.Entity, node2:base.Entity, node3:base.Entity)->base.Entity: """ 计算并创建第四个节点。 通过向量运算确定第四个节点的位置,用于构建万向铰。 算法原理: 1. vect1 = node1 → node2:确定第一轴方向 2. vect2 = node1 → node3:确定第二轴的参考方向 3. vect3 = vect1 × vect2:叉积得到垂直于两向量所在平面的法向量 4. vect4 = vect1 × (vect3/|vect3|):再次叉积,得到同时垂直于第一轴和法向量的向量,即第二轴方向 5. dist = |node1 - node3|:计算参考距离 6. node4 = node1 + dist × vect4:沿第二轴方向移动参考距离得到第四个节点 Args: node1: 原点节点 node2: 定义第一轴方向的节点 node3: 定义第二轴参考方向的节点 Returns: base.Entity: 创建的第四个节点实体 """ vect1:Tuple[float] = Vector.point2Point(node1, node2) vect2:Tuple[float] = Vector.point2Point(node1, node3) vect3:Tuple[float] = calc.CrossProduct(vect1, vect2) vect4:Tuple[float] = Vector.unit(calc.CrossProduct(vect1, Vector.unit(vect3))) dist:float = Vector.distance(node1, node3) pos:List[float] = Vector.move(node1, vect4, dist) return base.CreateEntity(cls.DECK, 'NODE', {'X': pos[0], 'Y': pos[1],'Z': pos[2]}) def run(self, pickFlag:int=2)->None: """ 执行万向铰创建流程。 Args: pickFlag: 选择模式标志,默认为2 """ if pickFlag==2: self.getNodesBy2Axis() node3:base.Entity = self.createCenterNode(self.axis1Node1) axis1MasterPos2:Tuple[float] = Vector.getPointCenter(self.axis1Node2) axis2MasterPos1:Tuple[float] = Vector.getPointCenter(self.axis2Node1) axis2MasterPos2:Tuple[float] = Vector.getPointCenter(self.axis2Node2) cog:Tuple[float] = Vector.getPointCenter([node3, axis1MasterPos2, axis2MasterPos1, axis2MasterPos2]) node1:base.Entity = base.CreateEntity(self.DECK, 'NODE', {'X': cog[0], 'Y': cog[1],'Z': cog[2]}) node2:base.Entity = base.CreateEntity(self.DECK, 'NODE', {'X': cog[0], 'Y': cog[1],'Z': cog[2]}) node4:base.Entity = self.getNode4(node1, node3, axis2MasterPos1) rbe1:base.Entity = self.createRbe2(self.axis1Node1+self.axis1Node2+[node3], node1) rbe2:base.Entity = self.createRbe2(self.axis2Node1+self.axis2Node2+[node4], node2) fields:Dict[str, any] = {'N1': node1, 'N2': node2, 'N3': node3, 'N4': node4} return base.CreateEntity(self.DECK, self.entType, fields)def main(): """ 主函数。 创建并运行万向铰。 """ jointObj = UniversalJoint() jointObj.run()
if __name__ == '__main__': main()
class ClusterPoints(object): """ 基于点的聚类分析工具。 使用二分K-Means算法将一组点分成两个簇。 """ def __init__(self, points:Iterable[base.Entity], singleClusterThrehold:float = 7.5, maxIterations:int=20 ): """ 初始化聚类分析器。 Args: points: 输入的实体点集 合 singleClusterThrehold: 簇间最小间隙阈值,用于判断是否为同一簇 maxIterations: 最大迭代次数 """ self.inputValidatedCheck(points) self.singleClusterThrehold = singleClusterThrehold self.maxIterations = maxIterations self.distCache:Dict[Tuple[int, int], float] = {} def inputValidatedCheck(self, points:Iterable[base.Entity])->None: """ 输入验证和标准化。 Args: points: 输入的点集 合 Raises: TypeError: 输入不是列表或元组时抛出 ValueError: 点数量少于2时抛出 """ if not isinstance(points, list) and not isinstance(points, tuple): raise TypeError('输入必须是一个列表或元组') if len(points)<2: raise ValueError('至少需要两个点才能进行聚类分析') self.points:List[base.Entity] = [point for point in points if isinstance(point, base.Entity)] self.numOfPoints:int = len(self.points) return None def getDistance(self, idx1:int, idx2:int)->float: """ 获取或计算两点间的距离。 Args: idx1: 第一个点的索引 idx2: 第二个点的索引 Returns: float: 两点之间的距离 """ if idx1==idx2: return 0.0 if idx1>idx2: idx1, idx2 = idx2, idx1 key = (idx1, idx2) try: dist = self.distCache[key] except KeyError: dist = math.dist(self.points[idx1].position, self.points[idx2].position) self.distCache[key] = dist return dist def calcCog(self, indices:Iterable[int])->Optional[Tuple[float]]: """ 快速计算输入下标对应节点构成的簇的中心点。 Args: indices: 节点索引集 合 Returns: Optional[Tuple[float]]: 中心点坐标 (x, y, z),若索引为空则返回 None """ if not indices: return None xSum, ySum, zSum = 0.0, 0.0, 0.0 for idx in indices: pos = self.points[idx].position xSum += pos[0] ySum += pos[1] zSum += pos[2] count:int = len(indices) return (xSum/count, ySum/count, zSum/count) def calcDistToCenter(self, center1:Tuple[float], center2:Tuple[float], )->Tuple[List[float]]: """ 批量计算所有点到输入的名义中心点的距离。 Args: center1: 第一个中心点坐标 center2: 第二个中心点坐标 Returns: Tuple[List[float]]: (到center1的距离列表, 到center2的距离列表) """ distancesToCenter1:List[float] = [] distancesToCenter2:List[float] = [] for idx in range(self.numOfPoints): if self.farthestPointIdx1==idx: distancesToCenter1.append(0.0) distancesToCenter2.append(math.dist(self.points[idx].position, center2)) elif self.farthestPointIdx2==idx: distancesToCenter1.append(math.dist(self.points[idx].position, center1)) distancesToCenter2.append(0.0) else: distancesToCenter1.append(math.dist(self.points[idx].position, center1)) distancesToCenter2.append(math.dist(self.points[idx].position, center2)) return (distancesToCenter1, distancesToCenter2) def calcClusterStats(self, indices:List[int])->Tuple[float]: ''' 快速计算簇的统计信息:平均距离、最小距离、最大距离 Args: indices: 簇中点的索引 Return: (平均距离, 最小距离, 最大距离) ''' numOfIndices:int = len(indices) if numOfIndices<2: return 0.0, 0.0, 0.0 totalDist, minDist, maxDist = 0.0, math.inf, 0.0 count:int = 0 if numOfIndices<=50: for i, i_idx in enumerate(indices): for j in range(i+1, numOfIndices): j_idx = indices[j] dist = self.getDistance(i_idx, j_idx) totalDist += dist minDist = min(minDist, dist) maxDist = max(maxDist, dist) count += 1 else: sampleSize:int = min(500, numOfIndices*(numOfIndices-1)//2) for _ in range(sampleSize): i = random.choice(indices) j = random.choice(indices) while i==j: j=random.choice(indices) dist = self.getDistance(i, j) totalDist += dist minDist = min(minDist, dist) maxDist = max(maxDist, dist) count += 1 return (totalDist/count if count>0 else 0.0, minDist if minDist<math.inf else 0.0, maxDist) def findFarthestPointsByBest(self)->Tuple[int]: """ 全局遍历获取距离最远的两个点。 Returns: Tuple[int]: 距离最远两个点的索引 """ maxDistance:float = -1.0 point1Idx, point2Idx = 0, 1 for i in range(self.numOfPoints): for j in range(i+1, self.numOfPoints): dist = self.getDistance(i, j) if dist>maxDistance: maxDistance = dist point1Idx, point2Idx = i, j return point1Idx, point2Idx def findFarthestPointToPoint(self, startIdx:int)->int: """ 找到离起点最远的点。 Args: startIdx: 起始点索引 Returns: int: 距离起始点最远的点索引 """ farthestFromStart:int = startIdx maxDist:float = -1.0 for i in range(self.numOfPoints): if i==startIdx:continue dist = self.getDistance(startIdx, i) if dist>maxDist: maxDist = dist farthestFromStart = i return farthestFromStart def findFarthestPoints(self)->Tuple[int]: """ 使用随机采样和三角形不等式找到近似最远的两个点。 算法原理: 1. 随机选择一个点作为起点 2. 找到离起点最远的点作为第一个候选点 3. 找到离第一个候选点最远的点作为第二个候选点 4. 重复几次找到更好的近似 Returns: Tuple[int]: 距离最远的两个点的索引 """ if self.numOfPoints<2: return (0, 0) if self.numOfPoints<=100: return self.findFarthestPointsByBest() bestDist:float = -1.0 bestPair:Tuple[int, int] = (0, 1) for _ in range(min(5, self.numOfPoints//10)): startIdx:int = random.randint(0, self.numOfPoints-1) farthestFromStart:int = self.findFarthestPointToPoint(startIdx) farthestFromFarthest:int = self.findFarthestPointToPoint(farthestFromStart) dist = self.getDistance(farthestFromStart, farthestFromFarthest) if dist>bestDist: bestDist = dist bestPair = (farthestFromStart, farthestFromFarthest) return bestPair def initCluster(self): """ 初始化簇分配。 将点分配给距离最近的最远点对应的簇。 """ self.cluster1Indices = [self.farthestPointIdx1] self.cluster2Indices = [self.farthestPointIdx2] for idx in range(self.numOfPoints): if idx == self.farthestPointIdx1 or idx==self.farthestPointIdx2:continue dist1 = self.getDistance(idx, self.farthestPointIdx1) dist2 = self.getDistance(idx, self.farthestPointIdx2) if dist1<dist2: self.cluster1Indices.append(idx) else: self.cluster2Indices.append(idx) return None def iterOpti(self): """ 使用优化后的迭代策略进行簇分配优化。 迭代计算中心点并将点重新分配到最近的簇,直到收敛或达到最大迭代次数。 """ for _ in range(self.maxIterations): center1 = self.calcCog(self.cluster1Indices) center2 = self.calcCog(self.cluster2Indices) if center1 is None or center2 is None: self.farthestPointIdx1, self.farthestPointIdx2 = self.findFarthestPoints() self.cluster1Indices = [self.farthestPointIdx1] self.cluster2Indices = [self.farthestPointIdx2] continue distanceToCenter1, distanceToCenter2 = self.calcDistToCenter(center1, center2) cluster1Indices = [self.farthestPointIdx1] cluster2Indices = [self.farthestPointIdx2] for i in range(self.numOfPoints): if i==self.farthestPointIdx1 or i==self.farthestPointIdx2:continue if distanceToCenter1[i]<distanceToCenter2[i]: cluster1Indices.append(i) else: cluster2Indices.append(i) if (set(self.cluster1Indices)==set(cluster1Indices)) and (set(self.cluster2Indices)==set(cluster2Indices)): break self.cluster1Indices = cluster1Indices self.cluster2Indices = cluster2Indices return None def rebuildResult(self): """ 构建返回结果。 根据索引构建对应的实体点列表,确保两个簇都不为空。 """ self.cluster1Points = [self.points[i] for i in self.cluster1Indices] self.cluster2Points = [self.points[i] for i in self.cluster2Indices] if not self.cluster1Points and self.cluster2Points: self.cluster1Points.append(self.cluster2Points.pop()) elif self.cluster1Points and not self.cluster2Points: self.cluster2Points.append(self.cluster1Points.pop()) return None def isOneCluster(self): """ 判断是否只有一个簇。 根据簇内平均距离和簇间平均距离判断两个簇是否应该合并为一个簇。 Returns: Tuple[List, List, bool]: (所有点, 空列表, True) 如果是一个簇,否则返回 None """ if self.numOfPoints<=50: avgDist1 = self.calcClusterStats(self.cluster1Indices)[0] avgDist2 = self.calcClusterStats(self.cluster2Indices)[0] interClusterAvg = 0.0 if self.cluster1Points and self.cluster2Points: sampleSize:int = min(100, len(self.cluster1Indices)*len(self.cluster2Indices)) for _ in range(sampleSize): i = random.choice(self.cluster1Indices) j = random.choice(self.cluster2Indices) interClusterAvg += self.getDistance(i, j) interClusterAvg /= sampleSize intraClusterAvg = max(avgDist1, avgDist2) if interClusterAvg<intraClusterAvg: allPoints = self.cluster1Points + self.cluster2Points return allPoints, [], True else: center1 = self.calcCog(self.cluster1Indices) center2 = self.calcCog(self.cluster2Indices) if center1 and center2: centerDist = math.dist(center1, center2) size1 = len(self.cluster1Indices) size2 = len(self.cluster2Indices) if (max(size1, size2)) > 3*min(size1, size2) and centerDist<self.singleClusterThrehold: allPoints = self.cluster1Points + self.cluster2Points return allPoints, [], True return None def run(self): """ 执行聚类分析。 流程: 1. 找到近似最远的两个点作为初始种子点 2. 初始化簇分配 3. 迭代优化簇分配 4. 构建返回结果 5. 判断是否只有一个簇 Returns: Tuple[List, List, bool]: (簇1点列表, 簇2点列表, 是否只有一个簇) """ if self.numOfPoints==2: dist = self.getDistance(0, 1) if dist<self.singleClusterThrehold: return [self.points[0], self.points[1]], [], True else: return [self.points[0]], [self.points[1]], False self.farthestPointIdx1, self.farthestPointIdx2 = self.findFarthestPoints() self.initCluster() self.iterOpti() self.rebuildResult() result = self.isOneCluster() return result if result else (self.cluster1Points, self.cluster2Points, False)