答辩备战材料

任务二 · 单层图形距离计算及可视化
基于 DFX MetaLab + vSDK 的 PCB 跨网络间距与短路检测工具

一、题目在算什么(业务背景)

1.1 一句话定义

这是一个基于 DFX MetaLab 平台和 vSDK 接口开发的 PCB 顶层走线跨网络间距与短路自动检测工具。 程序按任务书先将每条 TOP 线段的物理外轮廓向外扩 0.1mm,再对不同网络的外扩图形执行相交与距离检测, 以动态链接库形式集成在 DFX MetaLab 软件中,通过用户工具菜单触发, 实测约 1.55 秒完成整板 5331 条走线、492 个有效网络的检测, 找出 5400 组唯一违规,并支持表格展示与跳转定位。

1.2 PCB 是什么 / 走线 / 网络

PCB(印制电路板):电子产品里那块布满铜线的板子,连接芯片、电阻、电容等元件。

走线(Line):TOP 层上的铜线。每根走线有起点、终点、线宽、线头类型(圆头/方头)。

网络(Net):电气上连通的所有点的集合。同一网络靠在一起是正常的(本来就连通);不同网络之间必须绝缘,否则短路。

为什么间距是生死攸关的(评委必问业务价值):
  • 制造风险:不同网络铜图形间距不足会降低制造裕量,可能造成蚀刻残铜、绝缘不足或短路。本题统一采用 0.1mm 规则;实际工程阈值应由板厂工艺能力和设计规则确定,不能一概而论。
  • 信号干扰:高速信号线靠太近会产生电磁耦合(串扰),信号失真,设备工作异常。
所以工厂生产前必须用工具逐条检查所有走线间距——这正是我这个工具要干的事。

1.3 违规判定规则(任务书第 5 页第 6 条)

条件含义
① 两图形相交短路(设计错误,两根线重叠了)
② 最小距离 < 0.1mm按本题规则判定为间距不足,制造裕量降低、风险增加

注意:只有不同网络的走线才检测。同一网络的走线靠在一起不算违规。

1.4 为什么要"外扩 0.1mm"

任务书要求先外扩再算,为什么不直接算距离?
因为这是任务书明确规定的数据预处理步骤,不是自行替换判定条件。 每条线保持中心线起点、终点和角度不变,将外轮廓向外扩 0.1mm;圆头和矩形线头均按实际物理轮廓扩展。 后续相交判断和最小距离计算都必须针对外扩后的图形执行。 因此不能简单说成“外扩后相交”等价于原图形距离小于 0.1mm——两条图形都外扩后,几何关系已经改变,仍须按任务书继续执行相交与距离两项检测。
外扩前(细): 外扩后(粗,外扩0.1mm): ╭──────╮ ╭────────╮ ╱ ╲ ╱ ╲ │ ────── │ │ ────── │ ← 线宽 +0.2(两侧各扩0.1) ╲ ╱ ╲ ╱ ╰──────╯ ╰────────╯ ↑中心线不变↑ ↑外轮廓等距外扩↑ 关键:中心线起止坐标和角度不变;宽度增加0.2mm,线头轮廓同步向外扩展

设计思路:任务二的工程化设计原则

本任务的目标是基于 DFX MetaLab 平台和 vSDK 接口,开发一个 PCB 顶层走线跨网络间距与短路自动检测工具, 按任务书规则对 TOP 层不同网络的图形判定"相交或最小距离小于 0.1mm",并以表格形式展示结果、支持点击跳转定位。

四条设计原则

整体设计遵循分层组织、预处理先行、粗筛—精算分离、结果去重可审计四条原则:

六步主线流水线

步骤设计意图
1. 上下文获取按 Job→Pcb→Board→Layer 逐级取得 TOP 层,并校验 LayerSide=1、LayerType=0 确认是正面信号层
2. 线段读取遍历 TOP 层对象只保留 Line 类型,读出起止坐标、线宽和线头类型
3. 外扩分组中心线起止坐标不变,外轮廓向外扩 0.1mm;圆头线段按 vSDK 约定令 LineLength=LineWidth;按 NetID 分入 492 个 ShapeConter 容器
4. AABB 粗筛对所有跨网络线段做轻量比较,只把可能相交或距离不足的网络对提升为候选
5. vSDK 精算对每个候选网络对分别调用 GetTouch 和 GetDistance(0,0.1)
6. 去重输出按 ObjectID 无序对取并集、排序后填入表格

设计动机

为什么外扩在前?任务书明确要求"先外扩再算",这是数据预处理步骤而不是自行替换判定条件。因此不能简化为"原图形距离小于 0.1mm 等价于外扩后相交"。

为什么要粗筛—精算分离?5331 条线产生 13,966,489 对跨网络线段组合,若不筛选则 492 个网络容器两两组合为 120,786 对、241,572 次精确调用。AABB 把它压缩为 1975 个候选网络对、3950 次精确调用,减少 237,622 次。

为什么用 PairKey?无序对键同时解决两个问题:同一对图形无论 SDK 以哪个方向返回都得到同一个键;GetTouch 与 GetDistance 两类结果合并时不会重复计数。

性能与正确性

性能:粗筛边界为 lineWidth/2+0.1+0.05,对 DEMO2 的圆头线段可保证不漏掉阈值内候选,同时允许保留假候选再由精算剔除;精算只处理候选网络对,把理论上的 241,572 次精确调用压缩为 3,950 次,实测约 1.55 秒。
正确性:程序在关键步骤输出 audit 日志,记录线段数、有效网络数、跨网络线段对数、相交命中数、距离命中数和最终唯一违规数,便于现场与原始 ODB++ 文件、独立几何复算相互印证。

工程完整性

每次点击 Run 都会刷新当前 Job、Pcb、Board 上下文,避免继续使用旧工程;并在读取失败、几何失败或资源分配失败时返回明确错误。 ShapeConter、临时 Shape 和结果列表在正常结束和失败路径中统一释放,避免内存泄漏。 interface 与 logic 职责分层,符合任务书"非界面功能写在 logic 类"的要求。

一句话总结

本工具的设计思路不是"调用一个 SDK 接口得到答案",而是把题目要求拆成可验证的工程阶段: 业务规则驱动外扩方式、分组策略和判定口径;空间索引驱动性能;PairKey 去重和 audit 日志驱动正确性;分层结构和资源释放驱动工程完整性。

二、数据怎么导入

2.1 DEMO2.tgz 是什么

ODB++ 格式的 PCB 制造数据包——PCB 行业最通用的数据交换格式之一。 内部是层级目录结构:job → steps/pcb → layers/top(我们要的TOP层)。

2.2 DFX MetaLab 读入 + vSDK 访问的三级指针

用户拖入 DEMO2.tgz 后,软件解析构建内存对象模型。我的代码通过 vSDK 逐级获取上下文:

DEMO2.tgz ↓ (DFX MetaLab 读入) Job (作业:整个PCB项目) ↓ vSDK_Job_GetCurrentPcb Pcb (单块板子) ↓ vSDK_Pcb_GetBoard Board (板子数据:包含所有层) ↓ vSDK_Board_GetLayerByName("TOP") Layer (TOP层:包含所有对象)

2.3 TOP 层校验

不仅按名称找,还校验两个属性确保是正面信号层(任务书第 3 条):

属性含义
LayerSide1正面(顶层)
LayerType0SG(信号层)

三、算法全流程(核心,答辩重头戏)

3.1 整体流程图(背熟这条)

DEMO2.tgz │ (DFX MetaLab 读入) ▼ Board 上下文 │ ▼ 按"TOP" + Side=1 + Type=0 定位 TOP 层 │ ▼ 遍历 9410 个 TOP 对象,筛线段(类型=5) ← 步骤1 筛得 5331 条 LineRecord │ ▼ ① 每条线外扩 0.1mm (CreateShapeByLine) ← 步骤2 ▼ ② 按 NetID 分组进 ShapeConter ← 步骤3 492 个 ShapeConter 容器 │ ▼ ① AABB 粗筛 (4次比较/对) ← 步骤4 ▼ ② GetTouch + GetDistance 精算 ← 步骤5 5400 对违规 (QHash 去重) ← 步骤6 │ ▼ 填入表格 + 点击跳转 (GotoResult) 界面展示

3.2 步骤详解

步骤 1:遍历 TOP 层,筛出所有线段

for (objectIndex = 0; objectIndex < 总数; objectIndex++) {
    GetLayerObjectType(..., objectIndex) → 用0基索引判断类型
    if (类型 == 5 /*Line*/) {
        GetLayerObjectInfosEx(...) → 读起点/终点/符号尺寸/线头名称
        GetLayerObjectNetID(..., objectIndex+1) → 用1基ObjectID读NetID
        存入 LineRecord 容器
    }
}

结果:5331 条线段进入内存。

步骤 2:每条线段外扩 0.1mm

expandedWidth = lineWidth + 0.2;
expandedLength = isRectangle ? capLength + 0.2 : expandedWidth;
CreateShapeByLine(原起点, 原终点, expandedLength, expandedWidth, isRectangle, ...)
关键接口约定:圆头线段的 LineLength 必须与 LineWidth 取相同值(vSDK.h 第879行);矩形线头时 LineLength 表示线头长度。当前 SDK 曾返回状态码3但同时给出有效 Shape,所以程序以“返回值>0 且指针非空”判断可用,并严格按头文件传参。
数据覆盖边界:DEMO2 的 5331 条 TOP 线段全部使用圆形 aperture,因此本赛题数据实际走圆头分支;矩形分支已实现,但不能声称已被 DEMO2 实测覆盖。

步骤 3:按网络分组进 ShapeConter 容器

ShapeConter 是 vSDK 提供的几何容器,专门用来批量计算。按 NetID 分组:

NetID=1 的所有线段 → ShapeConter_1
NetID=2 的所有线段 → ShapeConter_2
...
共 492 个容器

步骤 4:AABB 粗筛(性能关键)

问题:5331 条线中共有 13,966,489 对跨网络线段组合。若不做空间筛选,492 个网络容器两两组合为 C(492,2)=120,786 对,而相交与距离各调用一次,最多需要 241,572 次精确 SDK 调用。

解决:先对 13,966,489 对跨网络线段做轻量 AABB(Axis-Aligned Bounding Box,轴对齐包围盒)比较。判断两个包围盒是否分离只需要 4 项边界比较;只要某条线段对的扩展包围盒可能接近,才把对应网络对加入候选集合:

if (A.maxX < B.minX || B.maxX < A.minX ||
    A.maxY < B.minY || B.maxY < A.minY) {
    // 不可能相交,跳过
}

效果:最终得到 1975 个候选网络对,执行 1975×2=3950 次精确 SDK 调用;相比无筛选的 241,572 次,减少 237,622 次,约减少 98.36%。13,966,489 是轻量线段对比较次数,不是 GetDistance 调用次数。

为什么这次 AABB 粗筛不会漏掉真实违规?
对 DEMO2 实际覆盖的圆头线段,外扩后胶囊形相对中心线的半径为 lineWidth/2+0.1。 距离判定还要求小于 0.1mm,所以代码把每条中心线 AABB 再额外分担 0.05mm,总边界为 lineWidth/2+0.1+0.05。两个图形各承担0.05mm,合计覆盖0.1mm距离阈值。 如果这两个保守AABB仍然分离,则外扩图形在至少一个坐标轴方向上的间隔不小于0.1mm,不可能满足本题违规条件。 因此粗筛允许保留假候选,但对DEMO2的圆头数据不会漏掉阈值内候选,最后仍由vSDK精算确认。

适用边界:DEMO2的5331条TOP线段全部为圆形aperture。矩形线头的图形创建分支虽然已实现,但若未来出现线头长度大于线宽的矩形样本, 为获得通用的不漏检保证,粗筛边界应保守地基于 max(lineWidth,capLength) 计算。答辩时不能把本数据集结论无条件推广到任意矩形线头。

步骤 5:精算(GetTouch + GetDistance)

// 5a. GetTouch:显式取得外扩后相交的图形对
// 5b. GetDistance(0.0, 0.1):取得外扩后最小距离位于 [0.0,0.1) 的图形对
// 5c. 将两类结果按无序 ObjectID 对去重合并
严谨口径:任务书要求“相交或最小距离小于 0.1mm”,因此程序保留两次接口调用并取去重并集。 在 DEMO2 本次实测中,GetTouch 的 3873 对全部包含在 GetDistance(0,0.1) 返回的 5400 对中,故并集仍为 5400;这是本数据集的实测结果,不应脱离接口语义泛化。

步骤 6:去重 + 输出

PairKey = (较小id << 32) | 较大id 组成 64 位无序对键,存入 QHash 去重。 最终输出 5400 对唯一违规,其中外扩后相交 3873 对,不相交但外扩后最小距离小于 0.1mm 的 1527 对。

四、函数是自己写的还是调 SDK 的(必问)

核心原则(背熟):vSDK 提供底层几何能力(读数据、创建图形、计算相交与距离), 提交工程中的 logic 层负责把这些能力组织成完整流程:筛选、外扩、按网络分组、AABB 粗筛、结果去重和审计。 答辩时既不要把几何内核说成自己重写,也不要把工程工作缩减成“只调了一个接口”;应按代码逐项说明实现边界。

4.1 提交代码中实现的工程逻辑(答辩要讲清)

我的函数作用答辩要点
logic::RunDetection主编排函数——决定先做什么后做什么整个流水线的"总指挥"
logic::ReadTopLines读 TOP 层筛线段只保留 Line 类型,校验坐标并处理0基/1基对象编号
logic::BuildExpandedNetContainers外扩 + 分组按 NetID 建立 492 个 ShapeConter 容器
logic::CollectResultPairs收集结果遍历 SDK 结果并存入 QHash 无序对集合
logic::GotoResult跳转定位按任务要求跳到 ObjectID 较小图形的中心坐标

4.2 关键工程设计(应能现场解释)

设计点需要讲明的内容
AABB 粗筛SDK 不负责候选筛选;代码先进行线段包围盒比较,再把命中提升为候选网络对
PairKey 去重将两个 ObjectID 排序后编码为 64 位键,避免正反顺序和两类接口重复计数
ObjectID 1基/0基处理类型遍历用0基索引,NetID与界面对象编号用1基ObjectID
返回码与指针双校验当前SDK的正返回码不只1;必须同时验证返回值>0和Shape指针非空
资源释放路径正常结束、Build失败和Collect失败均释放ShapeConter,临时Shape加入容器后立即销毁

4.3 调 vSDK 库的(诚实承认)

SDK 一共调了 27 个不同的接口,分四类:

类别代表函数关键诚实点
数据读取GetLayerObjectType / GetLayerObjectInfosEx / GetLayerObjectNetID我决定调用顺序与字段选择
几何运算CreateShapeByLine / GetTouch / GetDistance⚠️ 几何运算本身是 SDK 做的,我提供参数
资源管理Create/Delete_ShapeConter我设计释放路径
界面控制Display_SetPartDisplay / Display_Goto按任务书第 15 条调用

4.4 从DFX菜单到检测结果的完整调用链

DFX MetaLab“用户工具”菜单 ↓ 调用导出的 vSDK_Developer(dllpath,parent,pointer) 创建 Interface 非模态窗口并绑定按钮/表格信号 ↓ 用户点击 Run Interface::RunDetection ↓ 清空旧表格、禁用按钮、显示等待光标 logic::RunDetection ├─ RefreshContext:重新取得当前 Job/Pcb/Board ├─ ReadTopLines:定位并读取 TOP 线段 ├─ BuildExpandedNetContainers:外扩并按 NetID 分组 ├─ AABB粗筛:生成候选网络对 ├─ CollectResultPairs:GetTouch/GetDistance并取并集 └─ 生成排序后的 DetectionResult ↓ Interface::FillTable 显示5400行 ↓ 点击表格行 logic::GotoResult → vSDK_Display_Goto

4.5 核心数据结构与复杂度

数据结构作用
LineRecord保存ObjectID、NetID、起终点、线宽、线头长度和线头类型
QVector<LineRecord>顺序保存5331条TOP线段
QHash<int,LineRecord>按ObjectID快速回查线段和中心坐标
QHash<int,void*>NetID到ShapeConter的映射,本数据集共492个
QHash<quint64,bool>保存候选网络无序对和最终违规ObjectID无序对
QVector<DetectionResult>向UI提供最终表格数据

复杂度口径:当前粗筛仍遍历跨网络线段对,理论时间复杂度为 O(L²),本数据集实际完成13,966,489次轻量AABB比较; 精算只处理1975个候选网络对。空间主要用于线段记录、492个网络容器、候选网络对和5400个结果对。若数据规模继续增长,可使用均匀网格、R树或扫描线替代全线段两两粗筛。

4.6 异常处理、资源生命周期与界面线程

上下文刷新:每次点击Run都重新执行 RefreshContext,获取当前Job、Pcb和Board,避免继续使用旧工程上下文。
完整性检查:没有活动Job、找不到TOP、TOP属性不符、对象读取不完整、vSdk.dll或动态接口缺失、几何计算失败时均返回明确错误,由UI弹窗显示。
资源释放:临时Shape加入容器后立即销毁;每个计算结果列表使用后删除;ShapeConter在正常结束、Build失败和Collect失败路径中统一释放。
当前线程模型:检测同步运行在UI线程中。Run期间禁用按钮并显示等待光标,防止重复触发。DEMO2约1.55秒可以接受;若处理更大数据,应迁移到工作线程并通过信号槽更新进度。

五、audit 日志与 0基/1基(关键故事)

5.1 audit 日志是什么

audit = "体检报告"。一次检测跑完后,把所有关键数字记录下来,打印到 Qt Creator 的"应用程序输出"面板。

struct DetectionAudit {
    int lineCount;              // 线段总数
    int networkCount;           // 网络总数
    qint64 crossNetworkPairCount; // 跨网络对数
    int touchPairCount;         // 相交命中数
    int distancePairCount;      // 距离命中数
    int uniqueViolationCount;   // 最终违规数
};

实际输出:Distance detection audit: lines=5331 nets=492 crossNetPairs=13966489 touchHits=3873 distanceHits=5400 uniqueViolations=5400

5.2 0基 / 1基是什么

对象A 对象B 对象C 0基序号(index): 0 1 2 ← C/C++数组习惯 1基ID(ID): 1 2 3 ← 日常习惯(第1个、第2个) ↑差1 ↑差1 ↑差1 同一个对象,在两套体系下数字差 1

5.3 vSDK 的坑:两套体系混用

接口函数参数名用几基
GetLayerObjectType(..., int IObjectIndex)IObjectIndex0基(Index 这个词)
GetLayerObjectNetID(..., int kLayerObjectId)kLayerObjectId1基(Id 这个词)

5.4 我踩的坑(错误代码)

line.objectId = objectIndex;  // ❌ 错!存成了 0基
GetLayerObjectNetID(board, layer, line.objectId, line.netId);  // ❌ 错!要 1基

结果:每条线查到的是"前一个对象"的网络,所有网络归属全错位。 本该同网络的线被分到不同网络 → 产生虚假的"跨网络违规"。 网络数虚增:本该 492,变成 515。

5.5 怎么发现的——audit 立功

阶段audit 输出判断
第一次(bug版)nets=515❌ 不对!应为 492
改成 1基后nets=492✅ 对上了!bug 定位成功

5.6 正确的修复

line.objectId = objectIndex + 1;  // ✅ 改成 1基!
GetLayerObjectNetID(board, layer, line.objectId, line.netId);  // ✅

这个 1基的 objectId 贯穿全程:查 NetID、AddShape 编号、表格显示、GotoResult 比较,都用它,保证一致。

"开发过程中我特别注重可观测性。我在检测流程里埋了 audit 日志,记录线段数、网络数、相交命中、距离命中等中间数字。 第一次跑完,audit 显示网络数为 515。结合接口参数语义、对象抽样对照以及修复后的逐线 NetID 统计,我判断网络归属存在系统性错位。需要注意:ODB++ 全局 netlist 的 527 个定义网络与 TOP 线段实际引用的 NetID 不是同一统计口径,不能直接用 527 推导 492。 回头查代码,定位到 GetLayerObjectNetID。我发现 vSDK 头文件里,有的参数叫 IObjectIndex(0基序号),有的叫 kLayerObjectId(1基ID),两套标识混用。 改成 1基后,网络数精确降到 492,违规数从 6093 修正到 5400。这个经历让我深刻体会到:工程问题不能靠猜,要用数据说话。audit 日志就是我用的数据。"

六、正确性如何验证(含 ODB++ 独立解析)

6.1 原始几何数据复核:TOP 线段数为 5331

将 DEMO2.tgz 解压后直接解析 ODB++ 的 TOP/features 文件,不经过插件的对象读取流程,重新统计原始图形记录:

ODB++ 原始文件: 5331 条线段
代码 audit: 5331 条线段
→ 数量完全一致,并对首条线段坐标进行抽样核对;这为读取完整性提供了强证据,但不把单一计数夸大为“所有字段 100% 正确”。

首条线段坐标也对得上:
ODB++ 原始:(96.361, 3.543)→(97.015, 2.889)
代码读出: (96.3607, 3.5433)→(97.015, 2.88899)(精度差异)

6.2 网络数 492 vs 527(口径不同,都合理)

来源网络数口径
代码 audit492TOP 层有走线的网络数
ODB++ netlist 文件527全板定义的网络数(含其他层、过孔)

527 是 ODB++ 全局 netlist 中定义的网络编号数;492 是对 vSDK 返回的 5331 条 TOP 线段 NetID 去重后的数量。两者统计对象不同,因此不能用 527−492=35 直接断言这些网络只存在于哪一层。后续算法应使用与 TOP 线段一一对应的 492 个有效 NetID 分组。

6.3 5400 组结果的逐对复核

插件审计:5331 条线段、492 个有效网络、13,966,489 对跨网络线段组合;GetTouch 命中 3873 对,GetDistance 命中 5400 对,最终唯一违规 5400 对。

独立几何复算:对同一批线段记录分别使用解析几何算法和 Shapely/GEOS 进行逐 ObjectID 对复算,两种方法均得到 3873 对相交、1527 对距离不足、合计 5400 对,且 ObjectID 对集合完全一致。集合 SHA-256 为:
b79a5ad42d3653ea94405fe7cface580566def508ef2bd335aca9d66f4d9fd1

证据边界:ODB++ 原始 features 独立证明几何记录数量和坐标;每条 TOP 线段对应的 NetID 采用 DFX MetaLab/vSDK 导入模型返回值。不能宣称仅靠全局 cadnet/netlist 文件就直接推出 492。

6.4 答辩时的验证顺序

先展示插件 audit 的六个关键数字,再说明原始 ODB++ 对 9410 个 TOP 对象和 5331 条 L 记录的复核,最后说明解析几何与 Shapely/GEOS 的逐对结果集合一致。验证脚本属于开发期自查材料,不是任务书要求的提交文件;现场以可复现的统计口径和结果集合为主。

七、答辩话术集锦(按评委提问分类)

🎯 开场自述(30 秒)

"我做的是一个 PCB 走线间距自动检测工具。PCB 顶层有 5331 条走线,分属 492 个网络。 按任务书要求,程序先将每条线的物理外轮廓向外扩0.1mm,再检测不同网络外扩图形是否相交或最小距离小于0.1mm,最终得到5400对唯一违规。 算法分六步:先用 vSDK 读入 TOP 层并筛出线段;再按任务书将每条线的外轮廓向外扩 0.1mm; 随后按 NetID 分组,用 AABB 对跨网络线段进行粗筛,再对候选网络容器调用 SDK 完成相交和距离精算,最后按 ObjectID 无序对去重。 性能上,13,966,489 次轻量线段对筛选最终只产生 1975 个候选网络对,精确 SDK 调用从理论上的 241,572 次降至 3950 次,实测约 1.55 秒; 正确性上,原始 ODB++ 复核得到 5331 条 TOP 线段,解析几何与 Shapely/GEOS 逐对复算均得到相同的 5400 组 ObjectID 对。"

📌 Q1:这些算法都是你自己写的吗?

整体算法的设计和编排是我自己写的,但底层的几何运算调用了 vSDK 接口

具体来说,AABB 粗筛、网络分组策略、PairKey 去重、资源释放路径设计、ObjectID 基址处理这些都是我写的。

实际的相交判断和距离计算调用了 SDK 的 GetTouchGetDistance, 这两个是 vSDK 提供的核心几何引擎,我没必要也不可能自己重写一遍工业级的几何运算库。 我的工作是把它们正确地组织起来,解决业务问题。

📌 Q2:那你核心贡献是什么?

我的核心贡献有三个:

第一,流程设计。 SDK 给的是零件,我把它们组装成一条完整的检测流水线——从读数据、外扩、分组、粗筛、精算到输出。

第二,性能优化。 13,966,489 是跨网络线段对的轻量 AABB 比较规模;492 个网络容器无筛选时需做 120,786 个网络对、241,572 次精确接口调用。AABB 将其压缩为 1975 个候选网络对、3950 次精确调用,实测约 1.55 秒。由于没有保存未优化版本的同机基准,答辩时不声称“从分钟级降到秒级”。

第三,bug 定位与正确性保障。 我通过 audit 日志独立定位了 ObjectID 1基/0基的错误(网络数从 515 修正到 492), 以及 SDK 返回码 3 的容错处理。这些问题需要结合vSDK头文件、培训材料、参数命名和实际运行结果交叉确认,不能只凭单一接口返回值判断。

📌 Q3:为什么 ObjectID 用 1基,而遍历用 0基?

SDK 里有两套对象标识,我从头文件参数命名区分出来的:
GetLayerObjectType 参数叫 IObjectIndex(0基序号)
GetLayerObjectNetID 参数叫 kLayerObjectId(1基ID)

所以我循环用 0基 objectIndex 调用 GetLayerObjectType, 但存进 LineRecord 的 objectIdobjectIndex+1。 这个 1基的值贯穿全程:查 NetID、AddShape 编号、表格显示、跳转比较都用它,保证一致。
追问:你怎么发现的?
通过 audit 日志发现异常:改之前按线段 NetID 去重得到 515。随后核对头文件参数名和对象编号语义,并对样本对象逐项检查,确认 GetLayerObjectType 使用 0 基索引,而 GetLayerObjectNetID 使用 1 基 ObjectID。 改成 objectIndex+1 后,网络数稳定为 492,违规数从 6093 修正为 5400。这里不把全局 netlist 的 527 当作直接判据。

📌 Q4:为什么圆头线段的 LineLength 要等于 LineWidth?

这是 vSDK 的硬性约定。头文件 vSDK.h 第 879 行注释明确写: IsRectangle:false 为圆头线段,LineLengthLineWidth 赋相同值

几何含义:圆头线段是一个胶囊形(中心线 + 两端半圆),SDK 用 LineWidth 同时作为宽度和两端半圆的直径。 LineLength 参数只在矩形线头时才用。所以圆头时两者必须相等。

头文件明确要求圆头线段的 LineLengthLineWidth 取相同值。调试中曾观察到返回码 3;在当前 SDK 版本中,只要返回值大于 0 且 Shape 指针有效,该次创建仍可使用,因此最终代码同时检查返回值和指针,并严格按头文件约定传参,不把返回码 3 简单描述为“硬失败”。

📌 Q5:为什么要做 AABB 粗筛?直接两两算不行吗?

性能考虑。5331 条线产生 13,966,489 对跨网络线段组合,程序只在这一层做代价很低的 AABB 比较,并将命中的线段对提升为候选网络对。

如果完全不筛选,492 个网络容器两两组合为 120,786 对;GetTouch 与 GetDistance 各调用一次,共 241,572 次精确 SDK 调用。AABB 最终只保留 1975 个候选网络对,即 3950 次精确调用,减少 237,622 次(约 98.36%)。

注意:crossNetPairs=13966489 是轻量线段对比较数,不是 GetDistance 调用数。

AABB 是计算几何里最经典的 broad phase(粗筛)技术,代价极低(只需 4 次坐标比较), 能高效剔除绝大多数不相交的组合。这借鉴了游戏引擎和物理引擎里的碰撞检测思路。

📌 Q6:5400 这个数字怎么验证是对的?

我采用了三层证据链。

第一层,原始数据:直接解析 ODB++ TOP/features,得到 9410 个对象、其中 5331 条 L 线段,并抽样核对坐标;这与插件读取结果一致。

第二层,网络口径:492 是 5331 条 TOP 线段通过 vSDK 得到的 NetID 去重数;527 是全局 cadnet/netlist 的定义网络数。二者统计对象不同,不能用全局文件直接证明 492。

第三层,逐对几何:将同一批线段记录分别交给解析几何算法与 Shapely/GEOS 复算,两种方法都得到 3873 对外扩后相交、1527 对距离不足,合计 5400 对,而且 ObjectID 对集合完全一致。插件 audit 也为 5400。 因此我的结论不是“凭感觉相信 5400”,而是运行结果、原始数据复核和两套独立几何计算相互印证。

📌 Q7:为什么用 GetDistance 而不是 GetTouch?

两个都用,因为任务书明确要求判断“相交或最小距离小于 0.1mm”。
GetTouch 显式取得相交对;GetDistance(0,0.1) 取得距离位于 [0,0.1) 的图形对;最后按 ObjectID 无序对取并集。
在 DEMO2 的实测结果中,3873 个 GetTouch 对全部包含在 5400 个 GetDistance 对中,所以去重并集仍为 5400,其中距离不足但不相交的有 1527 对。保留 GetTouch 既对应任务书的相交条件,也便于 audit 分类。

📌 Q8:外扩后几何怎么变?圆头和方头有区别吗?

任务书要求:线段起点、终点坐标不变,外轮廓向外扩 0.1mm。

圆头线段:外扩后仍为胶囊形,线宽增加0.2mm,两端圆头半径各增加0.1mm。DEMO2 的5331条TOP线段均为圆形 aperture,实际全部走这一分支。
矩形线头:宽度与线头长度均增加0.2mm。该分支已在代码中实现,但DEMO2没有矩形线头样本,所以答辩时应明确“已实现但未被本数据集覆盖”。

代码实现:调用 CreateShapeByLine,起止坐标保持原值;宽度统一改为 lineWidth+0.2。圆头线段按 SDK 约定令扩展后的 LineLength=expandedWidth;矩形线头则令 LineLength=capLength+0.2。因此两种线头都按各自物理轮廓向外扩 0.1mm。

📌 Q9:代码量多少?

logic.cpp     542 行  ← 核心算法
logic.h        88 行
interface.cpp 269 行  ← UI交互
interface.h    72 行
interface.ui  149 行(XML)
任务书要求非界面相关功能写在 logic 类。工程采用逻辑层与界面层分离:logic 负责读取、外扩、分组、检测和跳转数据;Interface 负责按钮、表格和消息提示。这里更准确的说法是“职责分层”,不夸大为严格完整的 MVC 架构。

📌 Q10:最尖锐——这代码是不是 AI 写的?

如果评委问到,我会依据比赛规则如实说明辅助工具的使用情况,不回避也不夸大个人完成范围。

更重要的是,我能够现场说明并验证全部提交内容:包括 ObjectID 的 0基/1基差异、圆头和矩形线头的外扩参数、492 与 527 的统计口径、AABB 为什么不会漏掉阈值内候选、5400 对结果如何逐对复核,以及如何在 Qt Creator 中重新构建并在 DFX MetaLab 中演示。 答辩重点不是一句“谁写的”,而是提交者是否真正理解、能够复现、验证和维护自己的提交成果。

📌 Q11:GetDistance 不是你写的,你只是调了一下?

GetDistance 这个函数实现确实是 SDK 的,但我做的远不止"调一下":

第一,调用参数是我设计的——我传 DMin=0、DMax=0.1,这来自我对任务书"小于 0.1mm"的理解(开区间)。

第二,调用对象是我组织的——GetDistance 接收两个 ShapeConter 容器, 这 492 个容器是我按网络分组、逐条外扩、塞进 ShapeConter 的。

第三,调用时机经过空间筛选——13,966,489 对跨网络线段只做轻量 AABB 比较,由此得到 1975 个候选网络对。随后对每个候选网络对各调用一次 GetTouch 和 GetDistance,共 3950 次;不是对 1400 万条线段对逐一调用 SDK。

第四,结果是我处理的——SDK 返回的原始结果列表,我遍历、用 PairKey 去重、组装成 DetectionResult、填进表格。

所以 GetDistance 只是我流水线里的一环,前面的数据准备和后面的结果处理都是我写的。

📌 Q12:性能怎么样?还能优化吗?

当前在本机一次记录中的检测耗时约为 1.552 秒。主要优化点:
✅ 已做:AABB 粗筛,精确 SDK 调用从理论上的 241,572 次降为 3950 次,减少 237,622 次
✅ 已做:按网络分组,同网络线段不进入违规检测
可做:网格分箱(Grid Partitioning)替代两两遍历,降到 O(n·k)
可做:多线程并行(不同网络对独立计算)

📌 Q13:DLL 是怎么被 DFX MetaLab 加载的?

入口函数:vSDK_Developer(dllpath, parent, pointer)——这是 extern "C" 导出的 C 函数, DFX MetaLab 通过 QLibrary 加载 DLL 后调用它。函数作用:创建 Interface 窗口实例,设置位置、窗口属性、显示。

另外通过遍历顶层窗口的 objectName 判断窗口是否已存在,避免重复打开多个 Distance Detection 窗口。 这是 Qt 非模态窗口单例的常用做法。

📌 Q14:点击运行时还做了什么?

任务书第 15 条要求点击运行时还要做三件事,我在 RunDetection 开头都做了:
vSDK_Display_SetPartDisplay(false)  // 关闭元件显示
vSDK_Display_SetSelectMode(4)       // 选择模式→对象模式
vSDK_Display_SetObjectMode(3)       // 绘制模式→轮廓模式

📌 Q15:表格坐标格式怎么保证的?

任务书第 12 条要求:坐标为图形中心点,六位小数,圆括号包裹,逗号分隔,单位 mm 不显示。
我的 FormatPosition 函数:"(%1,%2)" + 'f', 6 位小数。 中心点取 ((startX+endX)/2, (startY+endY)/2),外扩不改变中心,所以中心点稳定。

📌 Q16:AABB粗筛为什么不会漏检?

对DEMO2全部为圆头的线段,外扩图形半径是 lineWidth/2+0.1;距离阈值0.1mm由两个图形各承担0.05mm,所以代码使用 lineWidth/2+0.1+0.05 扩展中心线AABB。若两个保守AABB仍分离,外扩图形不可能相交或小于0.1mm。 粗筛可以产生假候选,但本数据集不会漏掉真实候选。该证明以DEMO2的圆形aperture为边界;对任意矩形线头不能无条件照搬。

📌 Q17:约1.552秒是怎么测出来的?

这是本机一次运行记录中的观察值,用于说明当前数据规模下大约是秒级响应,不是严格性能基准。 当前提交代码没有用 QElapsedTimer 输出正式统计,所以答辩时不把1.552秒说成跨机器稳定值。 如果做正式基准,应在相同硬件和Release构建下预热,多次运行并报告平均值、最小值和波动范围。

📌 Q18:没有导入DEMO2或SDK接口失败怎么办?

每次Run先刷新当前Job、Pcb和Board;没有活动Job、TOP不存在或属性错误时立即停止。 对象读取不完整、vSdk.dll或动态接口缺失、Shape创建、容器加入、相交/距离计算失败都会返回具体错误字符串,Interface通过消息框提示,不会把部分结果当成完整结果展示。 失败路径同时释放已经创建的ShapeConter和结果列表。

📌 Q19:为什么每次表格顺序稳定?

QHash本身不保证遍历顺序,所以代码不会直接按QHash顺序输出。 候选网络PairKey和最终违规PairKey都会先取出为列表并执行 std::sort,再按升序组装DetectionResult。 PairKey内部先将两个ID排序,因此同一对图形无论SDK以哪个方向返回,键和值及最终表格顺序都保持确定。

📌 Q20:工程怎么构建?为什么最终只提交五个开发文件?

开发环境按任务书使用Qt Creator、Qt 5.15.2和MinGW 64位,由官方 emptyExampleWithUI 模板工程生成动态链接库,再由DFX MetaLab用户工具加载。 最终提交的五个文件是 logic.h/.cppinterface.h/.cppinterface.ui:logic保存非界面算法,interface与ui保存交互和布局。 模板入口、vSDK封装、资源和构建配置属于官方初始化工程及运行环境依赖,不是本题要求重复提交的自定义功能源码。现场应能从模板工程重新构建并演示绑定。

📌 Q21:两位参赛者如何分工?

这一题必须按真实工作填写,不要临场虚构。答辩前补全并相互确认:
高旭辉:____________________________
王宇轩:____________________________
共同完成:需求核对、DEMO2现场运行、5400结果验证和答辩复盘等________________。

即使有主要分工,两个人都应能解释完整主链:5331条线→492个网络→13,966,489对跨网络线段→1975个候选网络对→3950次精算→5400组唯一违规。

八、速记卡(打印带去答辩)

🔑 核心数字(背熟)

指标数值说明
线段数5331TOP 层线段总数(ODB++ 独立验证一致 ✅)
有效网络数4925331 条 TOP 线段的 vSDK NetID 去重数;全局 netlist 定义数为 527
违规数5400外扩后相交 3873 对 + 不相交但距离<0.1mm 的 1527 对
阈值0.1mm对外扩后图形采用严格小于;接口区间为 [0.0,0.1)
外扩值0.1mm中心线不变;宽度增加 0.2mm,线头轮廓同步外扩
跨网络线段对13,966,489轻量 AABB 比较规模,不是 SDK 调用次数
候选网络对1975粗筛后进入精算的网络对
精确 SDK 调用39501975 对 × GetTouch/GetDistance 各一次;减少 237,622 次
实测耗时约1.552秒一次 Qt 应用程序输出记录,不同机器会有波动

🔑 三个关键决策(必答)

┌──────────────────────────────────────────────────────────┐ │ 1. ObjectID 1基 vs 0基 │ │ - GetLayerObjectType 要 0基 (IObjectIndex) │ │ - GetLayerObjectNetID 要 1基 (kLayerObjectId) │ │ - 破案关键: audit 显示 nets=515(应492) → 定位到基址错 │ ├──────────────────────────────────────────────────────────┤ │ 2. 圆头线段 LineLength = LineWidth │ │ - vSDK.h:879 硬性约定 │ │ - 状态码与Shape指针双校验,不能把3简单当硬失败 │ ├──────────────────────────────────────────────────────────┤ │ 3. AABB 粗筛 + SDK 精算 │ │ - 粗筛: 4 次坐标比较剔除空间不相交对 │ │ - 1975个候选网络对,共3950次精确SDK调用 │ │ - 两类结果按ObjectID无序对取并集;本数据集并集为5400 │ └──────────────────────────────────────────────────────────┘

🔑 分工清单(自己写的 vs 调库的)

┌─────────────────────────────────────────────────────┐ │ 工程中实现的逻辑 │ vSDK 提供的底层能力 │ ├──────────────────────────┼──────────────────────────┤ │ ✅ 整体流程编排 │ 🔧 几何运算内核 │ │ (RunDetection) │ (GetTouch/GetDistance)│ │ ✅ AABB 粗筛算法 │ 🔧 图形创建 │ │ (4次比较判包围盒) │ (CreateShapeByLine) │ │ ✅ 网络分组策略 │ 🔧 数据读取 │ │ (按NetID分492容器) │ (GetLayerObject*) │ │ ✅ PairKey 去重 │ 🔧 资源管理 │ │ (位运算64位键) │ (Create/Delete) │ │ ✅ ObjectID 基址处理 │ 🔧 界面控制 │ │ (1基 vs 0基) │ (Display_*) │ │ ✅ 返回码容错(=3也算成功)│ │ │ ✅ 资源释放三路径 │ │ └──────────────────────────┴──────────────────────────┘

🚫 绝对不能说的两句话

❌ "算法都是 SDK 做的,我就是调了一下" —— 这等于自认没贡献

❌ "全部都是我自己写的,SDK 没参与" —— 一查代码就穿帮

正确姿态:SDK 提供底层能力,我提供算法设计与工程组织。 在本题指定的DFX MetaLab与vSDK开发环境中,复用平台提供的几何内核并完成数据组织、候选筛选和结果管理,是合理的工程分工;关键是能否正确使用接口解决业务问题并验证结果

🎬 现场演示脚本(约60—90秒)

步骤操作同时要说清楚
1在DFX MetaLab导入DEMO2.tgz数据由平台负责读入,插件通过vSDK访问当前内存对象
2打开“工具→用户工具→Distance Detection”DLL入口创建非模态Interface窗口,并避免重复打开
3指出初始Total=0、表格为空窗口打开时不提前计算,符合任务书要求
4点击Run先刷新上下文,关闭元件显示,切换对象选择与轮廓模式
5展示Total=5400和六列表格六列名称、中心坐标格式和ObjectID均符合任务书
6展示Qt应用程序输出依次读出5331、492、13,966,489、3873、5400、5400
7点击任意一行结果跳转目标取两个ObjectID较小者的中心坐标
8回到流程图或核心代码用“读取→外扩→分组→粗筛→精算→去重”收束演示
现场异常备用话术:如果临时环境、路径或DFX状态导致演示无法运行,不要反复乱点。先说明错误提示来自哪一层,再用保存的audit输出和验证证据解释预期结果;同时明确现场环境问题不等于算法结论。最好在答辩前用最终提交源码重新构建并完整走一遍上述流程。

⏱ 5 分钟时间分配建议

时间内容
0:00–0:30自我介绍 + 一句话定义
0:30–2:00核心数据链 + 四模块讲解(用流程图)
2:00–4:00重点讲 Q3(ObjectID 1基/0基)+ Q5(AABB),最能体现你懂的两个决策
4:00–5:00留给评委提问(把上面的 qa 答辩话术用上)

🛡 防身护身符

被问到不确定的,别硬编。说"这个我当时主要关注了 XX,你说的这个点我可以后续深入研究"——比胡说强百倍。

诚实是最大的加分项。评委欣赏能说清楚"我没做什么"的人,远胜过"我什么都做了"的吹嘘。