#!/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) # 坐标 -> 相邻坐标
edge_map = {} # (start, end) -> feature_index
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)
edge_map[(start, end)] = idx
edge_map[(end, start)] = idx
visited_coords = set()
short_polyline_seeds = [] # 存储每根短 polyline 的一个起点用于删除
# BFS 遍历每个连通组件
for coord in graph:
if coord in visited_coords:
continue
queue = deque([coord])
current_visited = set()
current_feature_indices = set()
while queue:
curr = queue.popleft()
if curr in current_visited:
continue
current_visited.add(curr)
for neighbor in graph[curr]:
edge_key = (curr, neighbor)
if edge_key in edge_map:
feat_idx = edge_map[edge_key]
if feat_idx not in current_feature_indices:
current_feature_indices.add(feat_idx)
if neighbor not in current_visited:
queue.append(neighbor)
visited_coords.update(current_visited)
if not current_feature_indices:
continue
# 计算当前 polyline 总长度
total_length = sum(valid_features[i]['length'] for i in current_feature_indices)
if total_length < min_length:
# 找一个端点作为选择依据(度为1的点)
endpoints = [pt for pt in current_visited if len(graph[pt]) == 1]
seed = endpoints[0] if endpoints else next(iter(current_visited))
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("get_select_count")
current_count = int(self.incam.COMANS)
# 一次性删除所有选中的图形
if current_count > 0:
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 > 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):
""" 主执行函数:客户阻抗线连续性检查(分层处理 + 四步清理)
改进版:不合并到 tmp_total,逐层独立判断 temp_layer_n 是否为空
并且:仅将实际处理过的层纳入结果提示
"""
job = self.JOB
step = 'edit'
unthrough_lay = [] # 存在与 ball pad 导通风险的层
backup_layers = []
temp_layers = []
self.ico.ClearAll()
self.zkLineIndex = defaultdict(lambda: defaultdict(list))
######## 1. 创建备份层 ##################
for idx, zklay in enumerate(self.zkLayerList):
zkLay_bak = f'{zklay}_bak'
layer_n = f'tmp_{zklay}'
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)
######## 2. 分层处理:复制非line/arc图形到独立临时层,并删末端覆盖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}")
has_copied = self.selectun_separate(backup_layer, original_zklay, temp_layer_n)
if not has_copied:
continue
unique_ends = self.get_unique_endpoints_single_layer(step, backup_layer)
if not unique_ends:
print(f"{backup_layer}: 无唯一端点,跳过 surface 删除")
else:
self.delete_surfaces_covering_points(unique_ends, temp_layer_n)
######## 3. 检查是否连接到背面 Ball Pad(逐层判断)########
for backup_layer in backup_layers:
original_zklay = backup_layer.replace('_bak', '')
temp_layer_n = self.zkLineIndex[original_zklay]['tempLayer']
self.ico.ClearAll()
self.ico.DispWork(backup_layer)
self.ico.DispLayer(temp_layer_n)
# 设置过滤器:只选 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={temp_layer_n},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))
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='警告',
text=f'{layer_list_str}层存在阻抗线不连续,此阻抗线与背面 Ball Pad 导通:\n\n'
'1. 人为确认是否为分流设计;\n'
'2. 如为分流设计,需与客户确认是否设计异常,与客户EQ此组阻抗通过测量科邦控阻值,如客户不同意,内部策划列难点评估测科邦',
bitmap='warning',
buttons=['OK'],
defaultButton='OK'
)
sys.exit(0)
# sys.exit(0)
######### 4. 清理阶段:三大删除操作(仅对非空 temp_layer_n 执行)########
self.ico.ClearAll()
processed_empty = [] # 被处理过且清空的层
processed_valid = [] # 被处理过但仍保留线路的层
skipped_layers = [] # temp_layer_n 为空,跳过的层
for backup_layer in backup_layers:
original_zklay = backup_layer.replace('_bak', '')
temp_layer_n = self.zkLineIndex[original_zklay]['tempLayer']
current_step = 'edit'
# --- 判断是否跳过 ---
temp_info = self.ico.GetFeatureFullInfo(current_step, temp_layer_n)
if not temp_info or len(temp_info) == 0:
print(f"跳过处理:{backup_layer} 对应 {temp_layer_n} 为空")
skipped_layers.append(original_zklay)
continue # 完全跳过,不执行任何操作,也不做判断
#############################################################
# 从现在开始:只有 temp_layer_n 非空的层才会进入下面流程
#############################################################
self.ico.ClearAll()
self.incam.COM("sel_clear_feat")
self.ico.DispWork(backup_layer)
self.ico.DispLayer(temp_layer_n)
# --- 步骤1: 删除与 temp_layer_n 接触的所有整根 polyline ---
print(f"[{backup_layer}] 步骤1: 删除与 {temp_layer_n} 接触的整根 polyline...")
self.ico.ResetFilter()
self.incam.COM(f"sel_ref_feat,layers={temp_layer_n},use=filter,mode=touch,pads_as=shape,"
"f_types=line;pad;surface;arc;text,polarity=positive;negative")
features_touch = self.ico.GetFeatureFullInfo(current_step, layer=backup_layer, mode='select')
touch_start_points = set()
for feat in features_touch:
if feat['type'] in ('line', 'arc'):
xs, ys = round(feat['x0'], 6), round(feat['y0'], 6)
touch_start_points.add((xs, ys))
if touch_start_points:
self.incam.COM("sel_clear_feat")
# print("11111111111")
for x, y in touch_start_points:
self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.3f},y={y:.3f},tol=1")
self.incam.COM("get_select_count")
selected_count = int(self.incam.COMANS)
if selected_count > 0:
self.incam.COM("sel_delete")
print(f"{backup_layer}: 删除了 {selected_count} 根与 {temp_layer_n} 接触的 polyline")
# sys.exit(0)
# '''
# --- 步骤2: 删除长度 < 8mm 的短 polyline ---
print(f"[{backup_layer}] 步骤2: 删除长度 < 8mm 的短 polyline...")
self.cleanup_short_polylines(step=current_step, backup_layer=backup_layer, min_length=8.0)
# --- 步骤3: 删除两端都不连接 ball pad 的孤立段 ---
print(f"[{backup_layer}] 步骤3: 删除孤立段(不连 ball pad)...")
remaining_features = self.ico.GetFeatureFullInfo(step=current_step, layer=backup_layer)
delete_seeds = []
for feat in remaining_features:
if feat['type'] not in ('line', 'arc'):
continue
xs, ys = round(feat['x0'], 6), round(feat['y0'], 6)
xe, ye = round(feat['x1'], 6), round(feat['y1'], 6)
start_ok = self.selectBallPad(original_zklay, xs, ys)
end_ok = self.selectBallPad(original_zklay, xe, ye)
if not (start_ok or end_ok):
delete_seeds.append((xs, ys))
if delete_seeds:
self.incam.COM("sel_clear_feat")
for x, y in delete_seeds:
self.incam.COM(f"sel_polyline_feat,operation=select,x={x:.3f},y={y:.3f},tol=1")
self.incam.COM("get_select_count")
current_count = int(self.incam.COMANS)
if current_count > 0:
self.incam.COM("sel_delete")
print(f"{backup_layer}: 删除了 {len(delete_seeds)} 根孤立段")
# --- 步骤4: 仅当该层被实际处理过时,才判断其清理后状态 ---
remaining_after = self.ico.GetFeaturesPro(job=job, step=current_step, layer=backup_layer)
has_valid_line = any(f['type'] in ('line', 'arc') for f in remaining_after)
if has_valid_line:
processed_valid.append(original_zklay)
else:
processed_empty.append(original_zklay)
# 注意:这里不在 continue 路径上 -> 只有处理过的层才走到这里
######## 5. 输出分类提示信息(仅包含实际处理过的层)########
msg_parts = []
if processed_empty:
msg_parts.append(
f'{", ".join(processed_empty)} 存在阻抗线不连续,同组无可测阻抗线,与客户EQ此组阻抗通过测量科邦卡控阻值,如客户不同意,内部策划列难点评估测科邦'
)
if processed_valid:
msg_parts.append(
f'{", ".join(processed_valid)} 存在阻抗线不连续,同组存在可测阻抗线,EQ客户Y型阻抗线follow同组可测阻抗线进行调整'
)
if not msg_parts:
messageBox.showMessage(
message="本次未处理任何阻抗层(所有 tmp_zklay 均为空)",
bitmap='warning'
)
else:
messageBox.showDialog(
title='结果',
text='\n\n'.join(msg_parts),
bitmap='warning',
buttons=['OK'],
defaultButton='OK'
)
sys.exit()
# '''
if __name__ == "__main__":
app = QApplication(sys.argv)
analyzer = chk_Conti_zkline()
从开发的角度说明是如何实现这个代码功能的,使用什么功能判断、什么功能实现。一定要有逻辑