#!/bin/env python # 必须写在第一行
-- coding: utf-8 --
#################################################
Author: songwenhua
Function:客户阻抗线连续性检查
Date: 2025-11-05
v1.00 songwenhua 用户需求号: 2482 任务ID:2100
/home/incam/Desktop/scripts/shendan/check_Y_zk_line
LOAD_MODE__
import os
import re
import sys
from py39COM import Gateway, InCAM
from py39Tools import TableWidget
from ICO import ICO
from ICNET import ICNET
from XMLParse import XMLParsePlus
from messageBox import messageBox
from EqHelper import EqHelper
from PyQt5.QtWidgets import QWidget, QApplication, QComboBox
from collections import defaultdict, deque
from functools import partial
import math
from img import apprcc_rc
from pprint import pprint
import time
class chk_Conti_zkline:
def __init__(self): # self.setWindowFlags(Qt.WindowStaysOnTopHint) self.JOB = os.environ.get('JOB', None) self.STEP = os.environ.get('STEP', None) # self.chklist = os.environ.get('chklist', None) # --启动pycharm.sh时,里面有export INCAM_DEBUG=yes设置此环境变量 INCAM_DEBUG = os.getenv('INCAM_DEBUG', None) # 接口定义 if INCAM_DEBUG == 'yes': # 通过genesis gateway命令连结pid进行会话,不用在genesis环境下运行,直接用gateway的方式,可在pycharm环境下直接debug self.incam = Gateway() # 方法genesis_connect通过查询log-genesis文件获取的料号名 self.JOB = self.incam.job_name self.STEP = self.incam.step_name self.pid = self.incam.pid else: self.incam = InCAM() self.pid = os.getpid() self.ico = ICO(incam=self.incam) self.icNet = ICNET(incam=self.incam) self.jobName = self.ico.SimplifyJobName(jobName=self.JOB) self.dbSite = self.ico.GetDBSite(JOB=self.JOB) self.SITE = self.ico.GetSite(JOB=self.JOB) self.getinfomation_dict = defaultdict(defaultdict) self.ballPadIndexList = defaultdict(list) self.layerMatrix = self.ico.GetLayerMatrix() self.zkLineIndex = defaultdict(defaultdict) self.step_list = self.ico.GetStepList() layer_list = self.ico.GetLayerList() self.zk_siglay = {} self.zkLayerList = self.getZKLayer() # 获取阻抗线层别 self.run() def getZKLayer(self): """ 获取阻抗线层别 :return: 阻抗线层别 """ zkLayerList = list() layerList = self.layerMatrix['allLay'] # self.ico.GetLayerList() for lay in layerList: for t in ('s', 'ss', 'gsg', 'gssg'): # pattern_tmp = re.compile(r'^((?:un-)?(\d+|[tb])zk)-(\d+\.?\d*)-(%s)-?(\d+\.?\d*)?-?(\d+\.?\d*)?-?(\d+\.?\d*)?$' % t)#存在un开头的阻抗层 pattern_tmp = re.compile(r'^((\d+|[tb])zk)-(\d+\.?\d*)-(%s)-?(\d+\.?\d*)?-?(\d+\.?\d*)?-?(\d+\.?\d*)?$' % t) matchObj = re.match(pattern_tmp, lay) if matchObj: matchList = matchObj.groups() zkLayerList.append(lay) self.getinfomation_dict[lay]['layer'] = matchList[0] print(zkLayerList) if len(zkLayerList) > 0: SignalLayer_list = self.layerMatrix['sigAllLay'] for i in range(len(SignalLayer_list)): if i == 0: self.zk_siglay['tzk'] = SignalLayer_list[0] elif i == (len(SignalLayer_list) - 1): self.zk_siglay['bzk'] = SignalLayer_list[-1] else: self.zk_siglay[str(i + 1) + 'zk'] = SignalLayer_list[i] # self.zk_siglay['un-' + str(i + 1) + 'zk'] = SignalLayer_list[i]#还要加一个un-开头的对应线路层! print("zk_siglay : %s" % self.zk_siglay) for lay in self.getinfomation_dict.keys(): for key in self.zk_siglay: if self.getinfomation_dict[lay]['layer'] == key: self.getinfomation_dict[lay]['sigLayerNum'] = str(SignalLayer_list.index(self.zk_siglay[key]) + 1) self.getinfomation_dict[lay]['sigLayerName'] = str(self.zk_siglay[key]) break else: messageBox.showMessage(message='没有识别到阻抗线层别,请规范命名(eg:tzk-gssg-100)', bitmap='critical') exit() return zkLayerList def get_unique_endpoints_single_layer(self, step, backup_layer): """ 仅从单个 backup_layer 提取端点(仅本层内统计) """ self.ico.ClearLayer() self.ico.DispWork(backup_layer) features = self.ico.GetFeatureFullInfo(step, layer=backup_layer) if not features: return [] layer_endpoints = [] for feat in features: ftype = feat['type'] if ftype in ('line', 'arc'): xs, ys = feat['x0'], feat['y0'] xe, ye = feat['x1'], feat['y1'] layer_endpoints.append((round(xs, 6), round(ys, 6))) layer_endpoints.append((round(xe, 6), round(ye, 6))) point_count = defaultdict(int) for pt in layer_endpoints: point_count[pt] += 1 unique_ends = [pt for pt, cnt in point_count.items() if cnt == 1] unique_ends.sort(key=lambda p: (p[0], p[1])) print(f"{backup_layer}: 找到 {len(unique_ends)} 个唯一端点") return unique_ends def selectun_separate(self, backup_layer, zkLay, target_temp_layer): """ 在原始信号层上选出与 backup_layer 接触的所有非 line/arc 图形, 复制到指定的 target_temp_layer 并合并成 surface。 """ try: zk2SigLay = self.getinfomation_dict[zkLay]['sigLayerName'] except KeyError as e: return False self.ico.ClearAll() self.incam.COM("adv_filter_reset") self.incam.COM(f"display_layer, name={zk2SigLay}, display=yes") self.incam.COM(f"work_layer, name={zk2SigLay}") self.incam.COM("set_filter_type,filter_name=,lines=no,pads=yes,surfaces=yes,arcs=no,text=yes") self.incam.COM("set_filter_polarity,filter_name=,positive=yes,negative=yes") self.incam.COM(f"sel_ref_feat,layers={backup_layer},use=filter,mode=touch,pads_as=shape," "f_types=line;pad;surface;arc;text,polarity=positive;negative,include_syms=,exclude_syms=") self.incam.COM("get_select_count") count = int(self.incam.COMANS) if count == 0: return False # 复制到专属临时层 self.incam.COM(f"sel_copy_other, dest=layer_name, target_layer={target_temp_layer}, " "invert=no, dx=0, dy=0, size=0, x_anchor=0, y_anchor=0") # 切换到目标临时层并合并 self.ico.ClearAll() self.ico.DispWork(target_temp_layer) self.ico.TrySelContResize([2.54, 5, 10]) return True def selectBallPad(self, zkLay, x, y): """ 判断指定坐标点是否与背面ball pad导通 :param zkLay: 阻抗层名称 :param x: 检查点的x坐标 :param y: 检查点的y坐标 :return: bool 是否导通 """ # 阻抗层对应的线路层 zk2SigLay = self.getinfomation_dict[zkLay]['sigLayerName']#self.getinfomation_dict得到的是原始阻抗层,而不是_bak备份阻抗层 sigBot = self.layerMatrix['sigAllLay'][-1] #线路层最后一层,背面 # 设置工作环境 self.ico.ClearAll() self.ico.ResetFilter() self.incam.COM(f'affected_layer, name={sigBot}, mode=single, affected=yes') self.incam.COM(f'display_layer, name={zk2SigLay}, display=yes') self.incam.COM(f'work_layer, name={zk2SigLay}') # 执行选择 self.incam.COM("sel_clear_feat") #清除之前的选择 self.incam.COM("clear_highlight") self.incam.COM(f'sel_board_net_feat, operation=select, x={x}, y={y}, tol=1, use_ffilter=no') # 获取选中特征 featureFullInfo_sigBot = self.ico.GetFeatureFullInfo(step=self.STEP, layer=sigBot, mode='select') # 检查背面ball pad # 对于背面sig选到的物体,做个筛选,如果是pad且是.smd属性,说明选到了ball pad selectPadList = [info for info in featureFullInfo_sigBot if info['type'] == 'pad' and (info['attr'] == '.smd' or info['attr'] == '.smd,.bga' or info['attr'] == '.smd,.lga' or info['attr'] == '.smd,.smt_pad' or info['attr'] == '.smd,.test_pad')] # 20241209 新增ball pad属性是.smd.bga和.smd.lga return len(selectPadList) > 0 # 如果找到ball pad则返回True def cleanup_short_polylines(self, step, backup_layer, min_length=8.0): """ 删除总长度 < min_length 的整根 polyline(由多个 line 与arc 组成) 利用 feature['length'] 避免重复计算几何距离 """ features = self.ico.GetFeatures(step, backup_layer) # 提取所有 line 和 arc valid_features = [f for f in features if f['type'] in ('line', 'arc')] if not valid_features: return # #######构建拓扑图 端点->线段#### graph = defaultdict(list) # 坐标 -> 相邻坐标 coord_to_feats = defaultdict(list) # 坐标 -> (index, feature) for idx, feat in enumerate(valid_features): xs = round(feat['xs'], 3) ys = round(feat['ys'], 3) xe = round(feat['xe'], 3) ye = round(feat['ye'], 3) start = (xs, ys) end = (xe, ye) graph[start].append(end) graph[end].append(start) coord_to_feats[start].append((idx, feat)) coord_to_feats[end].append((idx, feat)) visited_coords = set() short_polyline_seeds = [] # 存储每根短 polyline 的一个起点用于删除 ##########BFS 遍历每个连通组件########### for coord in list(graph.keys()): if coord in visited_coords: continue queue = deque([coord]) current_visited_coords = set() current_feat_indices = set() while queue: curr = queue.popleft() if curr in current_visited_coords: continue current_visited_coords.add(curr) for neighbor in graph[curr]: # 查找连接 curr和neighbor 的 feature found = False for feat_idx, feat in coord_to_feats[curr]: if feat_idx in current_feat_indices: continue # 判断是否连接 curr 和 neighbor s = (round(feat['xs'],3), round(feat['ys'],3)) e = (round(feat['xe'],3), round(feat['ye'],3)) connected = (curr == s and neighbor == e) or (curr == e and neighbor == s) if connected: current_feat_indices.add(feat_idx) found = True break if not found: continue # 没有可用边 if neighbor not in current_visited_coords: queue.append(neighbor) # 标记这些点已被全局访问 visited_coords.update(current_visited_coords) if not current_feat_indices: continue ################计算当前 polyline 总长度########### total_length = sum(valid_features[i]['length'] for i in current_feat_indices) if total_length < min_length: # 找一个端点作为选择依据(度为1的点) endpoints = [pt for pt in current_visited_coords if len(graph[pt]) == 1] seed = endpoints[0] if endpoints else next(iter(current_visited_coords)) short_polyline_seeds.append(seed) ##########删除:选中所有短 polyline######### if short_polyline_seeds: self.incam.COM("sel_clear_feat") for x, y in short_polyline_seeds: # self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.3f},y={y:.3f},tol=1") self.incam.COM(f"sel_polyline_feat,operation=select,x={x},y={y},tol=35.775,cyclic=yes,clear_prev=no") self.incam.COM("sel_delete") def delete_surfaces_covering_points(self, points, layer_new): """ 删除临时层中包含给定点的 surface :param points: list of (x, y) """ self.ico.ClearLayer() self.ico.DispWork(layer_new) self.incam.COM("sel_clear_feat") # 初始清空 tol = 0.05 # 匹配点的 filter 容差 selected_count = 0 for x, y in points: #用 FilterAreaXY 查找落在点附近的图形 # self.incam.COM("sel_clear_feat") # 临时清空 # x1, y1 = x - tol, y - tol # x2, y2 = x + tol, y + tol # count_in_area = self.ico.FilterAreaXY( # isInside='yes', # intersect='yes', # x1=x1, # y1=y1, # x2=x2, # y2=y2 # ) # self.incam.COM(f"sel_single_feat, operation=select, x={x}, y={y}, tol=0.1") self.incam.COM(f"sel_single_feat,operation=select,x={x},y={y},tol=35.775,cyclic=yes,clear_prev=no") self.incam.COM("get_select_count") current_count = int(self.incam.COMANS) # if current_count > selected_count: # selected_count = current_count # print(f"在点 ({x:.3f}, {y:.3f}) 处选择了图形") # 一次性删除所有选中的图形 if current_count > 0: self.incam.COM("sel_delete") # print(f"成功删除了 {current_count} 个覆盖末端点的 surface") # else: # print("未找到覆盖末端点的 surface") @staticmethod def extract_start_points(data): """ 支持输入为 str 或 list[str],统一处理并提取每个图形的 #OB 起点 """ if isinstance(data, list): # 如果是列表,用换行符拼接成字符串 content = "\n".join(data) elif isinstance(data, str): content = data else: raise TypeError("Input must be str or list of str") start_points = [] # 按 '#数字 #S' 分割出各个图形块 blocks = re.split(r'#(\d+)\s+#S', content)[1:] # 每两项:编号 + 内容 for i in range(0, len(blocks), 2): block_content = blocks[i+1] match = re.search(r'#OB\s+([-\d.]+)\s+([-\d.]+)', block_content) if match: x = float(match.group(1)) y = float(match.group(2)) start_points.append((x, y)) return start_points def get_coords(self, feature): """ 从任意图元中快速提取一个坐标点 (x, y) 特别处理 surface 的 orig 字段 """ # 处理 surface 的 orig if feature.get('type') == 'surface' and isinstance(feature.get('orig'), list): pattern = r'#O[BS]\s+([-\d.]+)\s+([-\d.]+)' for line in feature['orig']: match = re.search(pattern, line) if match: x = float(match.group(1)) y = float(match.group(2)) return round(x, 3), round(y, 3) # 返回第一个有效坐标即可 return None def run(self): """ 主执行函数:完成客户阻抗线连续性检查(分层处理版本) """ job = self.JOB step = 'edit' unthrough_lay = [] # 告警层记录 backup_layers = [] valid_touch_points = set() final_tmp_layer = 'tmp_total' # 最终合并用的临时层 temp_layers = [] # 存储每个 tmp_n 层名列表 self.ico.ClearAll() self.zkLineIndex = defaultdict(lambda: defaultdict(list)) ######## 创建备份层 ################## for idx, zklay in enumerate(self.zkLayerList): zkLay_bak = f'{zklay}_bak' layer_n = f'tmp_{idx + 1}' # 对应的独立临时层 temp_layers.append(layer_n) self.zkLineIndex[zklay]['zkLaybak'] = zkLay_bak self.zkLineIndex[zklay]['tempLayer'] = layer_n # 新增映射 # 删除旧备份层和临时层 self.ico.DelLayer(layer_list=[zkLay_bak, layer_n]) self.ico.DispWork(zklay) self.incam.COM(f'sel_copy_other,dest=layer_name,target_layer={zkLay_bak},invert=no,dx=0,dy=0,size=0,x_anchor=0,y_anchor=0') backup_layers.append(zkLay_bak) # 清空最终 tmp 层 self.ico.CreateOrEmptyLay(layer_list=[final_tmp_layer]) ######## 分层处理:复制 → 合并 → 删除末端覆盖 surface ######## for backup_layer in backup_layers: original_zklay = backup_layer.replace('_bak', '') temp_layer_n = self.zkLineIndex[original_zklay]['tempLayer'] print(f"处理 {backup_layer} => 使用临时层 {temp_layer_n}") # 步骤1: 复制非 line/arc 图形到 temp_layer_n has_copied = self.selectun_separate(backup_layer, original_zklay, temp_layer_n) if not has_copied: continue # 步骤2: 获取该 bak 层的唯一端点(仅本层内统计) unique_ends = self.get_unique_endpoints_single_layer(step, backup_layer) if not unique_ends: print(f"{backup_layer}: 无唯一端点,跳过 surface 删除") continue # 步骤3: 删除 temp_layer_n 中覆盖这些端点的 surface self.delete_surfaces_covering_points(unique_ends, temp_layer_n) ######## 合并所有 temp_layer_n 到 final_tmp_layer ######## self.ico.ClearAll() # 确保目标层存在且为空(可选) self.ico.CreateOrEmptyLay(layer_list=[final_tmp_layer]) for temp_layer in temp_layers: tmp_info = self.ico.GetFeatureFullInfo(step, temp_layer) if not tmp_info: continue # --- 开始复制该层 --- self.incam.COM(f"display_layer,name={temp_layer},display=yes") # 显示源层 self.incam.COM(f"work_layer,name={temp_layer}") # 设置为工作层 self.incam.COM("sel_all_feat") # 选择所有图形 # 将选中图形复制到 final_tmp_layer self.incam.COM(f"sel_copy_other,dest=layer_name,target_layer={final_tmp_layer}," "invert=no,dx=0,dy=0,size=0,x_anchor=-11.5,y_anchor=-11.5,subsystem=1-Up-Edit") print(f"已将 {temp_layer} 的内容复制到 {final_tmp_layer}") self.ico.ClearAll() # 清理选择状态 ######## 统一进行 ball pad 导通判断(基于合并后的 final_tmp_layer)######## for backup_layer in backup_layers: original_zklay = backup_layer.replace('_bak', '') self.ico.ClearAll() self.ico.DispWork(backup_layer) self.ico.DispLayer(final_tmp_layer) # 设置过滤器:只选 line self.incam.COM("adv_filter_reset") self.incam.COM("set_filter_type, lines=yes, pads=no, surfaces=no, arcs=no, text=no") self.incam.COM("set_filter_polarity, positive=yes, negative=yes") # 选择与 final_tmp_layer 接触的 line self.incam.COM(f"sel_ref_feat,layers={final_tmp_layer},use=filter,mode=touch,pads_as=shape," "f_types=line;pad;surface;arc;text,polarity=positive;negative,include_syms=,exclude_syms=") features = self.ico.GetFeatureFullInfo(step, layer=backup_layer, mode='select') if not features: continue connection_to_ballpad = False problem_coords = [] test_points = [(round(feat['x0'], 6), round(feat['y0'], 6)) for feat in features if feat['type'] in ('line', 'arc')] test_points = list(set(test_points)) for xs, ys in test_points: if self.selectBallPad(original_zklay, xs, ys): connection_to_ballpad = True problem_coords.append((xs, ys)) else: valid_touch_points.add((xs, ys)) if connection_to_ballpad: unthrough_lay.append({ 'layer': backup_layer, 'coords': problem_coords }) ######## 统一弹窗提示 ###### if unthrough_lay: # 提取所有出问题的原始层名(去掉 '_bak' 后缀) problem_layers = [item['layer'].replace('_bak', '') for item in unthrough_lay] layer_list_str = ', '.join(problem_layers) messageBox.showDialog( title='阻抗线与 Ball Pad 导通警告', text=f'{layer_list_str}层存在阻抗线不连续且与背面 Ball Pad 导通:\n\n' '1. 请确认是否为分流设计;\n' '2. 如为客户设计,请 EQ 客户确认是否允许;\n' '3. 如不允许,需内部评估并通知测量科邦控阻值。', bitmap='warning', buttons=['OK'], defaultButton='OK' ) sys.exit(0) ######## 清理 & 删除短 polyline 和孤立段 ######## for backup_layer in backup_layers: original_zklay = backup_layer.replace('_bak', '') self.ico.ClearAll() self.incam.COM("sel_clear_feat") current_step = 'edit' self.ico.DispWork(backup_layer) # 删除与 final_tmp_layer 接触的整根 polyline for x, y in valid_touch_points: self.incam.COM(f"sel_polyline_feat,operation=select,x={x},y={y},tol=35.775,cyclic=yes,clear_prev=no") self.incam.COM("sel_delete") # self.ico.ClearLayer() # 删除长度 < 8mm 的短 polyline self.cleanup_short_polylines(step=current_step, backup_layer=backup_layer, min_length=8.0) # 删除两端都不导通 ball pad 的孤立段 final_features = self.ico.GetFeaturesPro(job=job, step=current_step, layer=backup_layer) for feat in final_features: if feat['type'] not in ('line', 'arc'): continue xs, ys = round(feat['xs'], 3), round(feat['ys'], 3) xe, ye = round(feat['xe'], 3), round(feat['ye'], 3) start_ok = self.selectBallPad(original_zklay, xs, ys) end_ok = self.selectBallPad(original_zklay, xe, ye) if not (start_ok or end_ok): self.incam.COM(f"sel_polyline_feat,operation=select,x={xs},y={ys},tol=35.775,cyclic=yes,clear_prev=no") self.incam.COM("sel_delete") ######## 判断剩余内容并提示用户 ######## empty_layers = [] valid_layers = [] for backup_layer in backup_layers: remaining = self.ico.GetFeaturesPro(job=job, step='edit', layer=backup_layer) has_valid = any(f['type'] in ('line', 'arc') for f in remaining) if has_valid: valid_layers.append(backup_layer) else: empty_layers.append(backup_layer) msg_parts = [] if empty_layers: msg_parts.append(f'{", ".join(empty_layers)}存在阻抗线不连续,同组无可测阻抗线,与客户EQ此组阻抗通过测量科邦卡控阻值,如客户不同意,内部策划列难点评估测科邦') if valid_layers: msg_parts.append(f'{", ".join(valid_layers)}存在阻抗线不连续,同组存在可测阻抗线,EQ客户Y型阻抗线follow同组可测阻抗线进行调整') if msg_parts: messageBox.showDialog( title='提示', text='\n\n'.join(msg_parts), bitmap='warning', buttons=['OK'], defaultButton='OK' ) # 清理临时层 self.ico.ClearLayer() # self.ico.DelLayer(layer_list=temp_layers + [final_tmp_layer] + backup_layers) sys.exit()
if name == “main”:
app = QApplication(sys.argv)
analyzer = chk_Conti_zkline()
在run函数中的删除部分我想要实现:第一:就在备份层阻抗线路删除与temp_toal层touch的整根线路polyline(注意polyline是一根很长线,由很多短的line组成)self.incam.COM(f"sel_polyline_feat,operation=select,x={feature[‘xs’]},y={feature[‘ys’]}")命令能够通过一个坐标点选中一根polyline
第二:删除整根线长小于8的polyline;第三:删除不与背面ball pad导通的polyline;
第四:判断经过删除的备份层是否为空,是与不是都输出messagebox提示并结束整个代码
如果有必要也可改动selectun函数与get_unique_endpoints等其他函数代码
最新发布