简介:集成电路仿真大作业Python代码资源,以参数调整和神经网络设计训练为主线,面向微电子、集成电路专业学生及需要借助Python完成电路仿真优化与建模的开发者。压缩包共70个文件,大小约1.69MB,包含核心Python源码(.py)、电路仿真过程数据(.sp/.st0/.lis/.mat/.mt0等)、训练数据(.csv)、模型文件(.mdl)以及项目说明文档(.pdf/.doc),能够还原从电路元参数设定、优化算法到神经网络训练验证的完整过程。已有138人学习,适合课程设计参考、实验复现或作为进一步研究的起点。代码覆盖使用PySpice等库建模、调用SciPy优化参数、利用TensorFlow/Keras搭建网络等环节;通过配套数据与说明,可深入理解数据预处理、超参数调节和模型评估的具体实现,为后续科研或工程实践提供可迁移的框架。
1. 拿到这个集成电路仿真 Python 大作业的 .rar 时,第一感觉是乱
拿到的压缩包顶层是__MACOSX和.DS_Store,里面夹着iccad/、main.py、Two_stage_opamp.py、DE.py、nn_test_xmh.py,还有x.mat、data.csv和penalty.mat。如果你以为解压后直接python main.py就能出结果,大概率会卡在路径、依赖或者调用顺序上。这个集成电路仿真大作业的真实主线是:用 SPICE 类工具搭两级运算放大器,跑出不同参数组合下的性能数据,再通过差分进化(DE)算法自动调参,最后训练神经网络替代重复仿真。它解决的是一整条“仿真-优化-拟合”链路,而不是某一段脚本。适合正在上模拟 IC 课程、需要交大作业,或者刚接触 Python 做电路代理建模的同学。你需要分清楚哪些文件是数据生成器、哪些是优化器、哪些是训练入口,下面按实际执行顺序拆开讲。
2. 两级运放仿真与数据生成:PySpice 建模和 SPICE 参数提取
在开始调参之前,得先让仿真器稳定地产出数据。项目里的Two_stage_opamp.py就是这个入口,它把两级运放网表封装成 Python 函数,输入是几个设计变量,输出是低频增益、单位增益带宽(GBW)和相位裕度(PM)。
2.1 为什么是两级运放
两级运放是模拟电路课程设计的“标配”:第一级提供高增益的差分放大,第二级提供更大的输出摆幅和驱动能力。和单级放大器相比,它的性能指标更丰富,适合做成优化问题;和三级结构相比,它又没有那么多稳定性风险,适合在一个学期内完成。设计变量一般包括输入对管宽度wp、尾电流itail、第二级负载管宽度、密勒补偿电容Cc、调零电阻Rz。如果只调晶体管尺寸而不动补偿网络,相位裕度往往连 45 度都到不了,后续 DE 算法的惩罚值会一直居高不下。所以数据生成脚本里一定要把Cc和Rz也作为变量参与扫描。
2.2 用 PySpice 搭网表并做 AC 扫描
常见做法是直接用 PySpice 在 Python 里建网表,而不是手写 SPICE 文本,因为每个管子的宽长比都是 Python 变量,后面接优化器非常方便。下面是simulate_opamp的核心骨架,整体思路和原项目中的Two_stage_opamp.py一致,只是做了函数化封装:
# simulate_opamp.py 核心片段 import numpy as np from PySpice.Spice.Netlist import Circuit from PySpice.Unit import * def simulate_opamp(wp=60e-6, L=0.5e-6, cc=2e-12, rz=3e3, itail=50e-6): circuit = Circuit('two_stage_opamp') # 0.18um 工艺简化模型(Level 1),足够做趋势研究 circuit.model('nch', 'nmos', level=1, kn=180e-6, vto=0.45, lambda_=0.02) circuit.model('pch', 'pmos', level=1, kp=60e-6, vto=-0.45, lambda_=0.03) vdd = 1.8 circuit.V('vdd', 'vdd', circuit.gnd, vdd) # 尾电流源 M5,栅极接偏置电压 circuit.MOSFET('M5', 'tail', 'bias', 'vss', 'vss', model='nch', width=10e-6, length=L) circuit.V('bias', 'bias', circuit.gnd, 0.8) # 输入对管 M1/M2,差分正负输入 vip/vin circuit.MOSFET('M1', 'd1', 'vin', 'tail', 'tail', model='nch', width=wp, length=L) circuit.MOSFET('M2', 'd2', 'vip', 'tail', 'tail', model='nch', width=wp, length=L) # PMOS 有源负载 M3/M4 circuit.MOSFET('M3', 'd1', 'd1', 'vdd', 'vdd', model='pch', width=wp, length=L) circuit.MOSFET('M4', 'd2', 'd1', 'vdd', 'vdd', model='pch', width=wp, length=L) # 第二级放大管 M6 和补偿网络 circuit.MOSFET('M6', 'out', 'd2', 'vss', 'vss', model='nch', width=120e-6, length=L) circuit.C('cc', 'out', 'd2', cc) circuit.R('rz', 'out', 'd2', rz) circuit.R('load', 'out', 'vdd', 50e3) # 交流激励:从 vip 注入 1V 小信号 circuit.SinusoidalVoltageSource('vin_ac', 'vip', circuit.gnd, amplitude=1) circuit.V('vicm', 'vin', circuit.gnd, 0.9) simulator = circuit.simulator(temperature=25, nominal_temperature=25) analysis = simulator.ac(start_frequency=1@u_Hz, stop_frequency=1@u_GHz, number_of_points=101, variation='dec') return analysis这段代码里的关键参数:wp是输入对管的沟道宽度,决定第一级跨导和增益;L是沟道长度,长度增加会让输出阻抗变大,但带宽下降;Cc是密勒补偿电容,直接影响主极点和相位裕度;Rz是调零电阻,用于消除右半平面零点。跑完 AC 扫描后,analysis['out']是复数电压增益序列,用20 * np.log10(np.abs(vout))能算增益曲线,再插值找到增益下降 3dB 处的频率就是 GBW。注意 Level 1 模型精度有限,但作为大作业和训练神经网络的样本来源已经足够;真正流片前还需要换成 BSIM 模型。
2.3 批量采样并导出 data.csv
要喂给神经网络,必须把“参数 -> 性能指标”组织成结构化表格。我一般会在外层做 300~500 次随机采样,每次从合理范围内抽取一组参数,调用上面的函数,把结果写进同一个 DataFrame。如果仿真不收敛,就把该行性能值置为None,后续统一处理。
# 批量生成训练数据 import pandas as pd rows = [] rng = np.random.default_rng(42) for i in range(500): wp = rng.uniform(40e-6, 120e-6) cc = rng.uniform(1e-12, 5e-12) rz = rng.uniform(0, 5e3) itail = rng.uniform(20e-6, 80e-6) try: analysis = simulate_opamp(wp=wp, cc=cc, rz=rz, itail=itail) gain_db = compute_gain_db(analysis) # 由 util.py 提供 gbw_mhz = compute_gbw(analysis) # 由 util.py 提供 pm_deg = compute_pm(analysis) # 由 util.py 提供 rows.append({'wp': wp, 'cc': cc, 'rz': rz, 'itail': itail, 'gain_db': gain_db, 'gbw_mhz': gbw_mhz, 'pm_deg': pm_deg}) except Exception: rows.append({'wp': wp, 'cc': cc, 'rz': rz, 'itail': itail, 'gain_db': None, 'gbw_mhz': None, 'pm_deg': None}) df = pd.DataFrame(rows) df.to_csv('data.csv', index=False)这里用均匀随机采样而不是网格采样,是因为 5 个变量做网格扫描会指数爆炸,而随机 500 组已经能覆盖设计空间的主要区域。保留失败行也是有意义的:训练时剔除它们,同时观察失败比例是否过高,如果超过 30%,说明抽样范围里有很多不收敛的参数组合,需要收窄范围。util.py里通常是这些后处理函数,包括从 AC 数据里提取增益、带宽、相位裕度的插值计算。
| 参数 | 范围 | 影响趋势 |
|---|---|---|
wp | 40~120 µm | 增大 gm,提升增益,但寄生电容增大,带宽下降 |
L | 0.3~0.8 µm | 沟长增大,输出阻抗上升,增益上升,带宽下降 |
Cc | 1~5 pF | 主极点内推,相位裕度改善,但 GBW 下降 |
Rz | 0~5 kΩ | 调右半平面零点,过大引入低频零极点 |
itail | 20~80 µA | 增大偏置电流提升 GBW,但功耗上升 |
3. DE 算法调参:把仿真目标函数和约束写成可优化问题
有仿真器之后,手动调参数会非常累。项目中的DE.py使用了差分进化算法,这是一种对非光滑、非连续目标函数很友好的全局优化方法。相比遗传算法,DE 不涉及二进制编码,直接在实数域上做变异和交叉,实现成本低,收敛速度通常也更快。
3.1 为什么选择差分进化
模拟电路参数优化常见的方案有网格搜索、随机搜索、贝叶斯优化和遗传算法。网格搜索在高维空间里点数量爆炸;随机搜索无法利用历史搜索信息;贝叶斯优化需要高斯过程拟合,对陡峭区域和仿真失败点比较敏感。而差分进化只需要给定参数范围,就能用种群迭代逼近全局最优,非常适合“每个个体都要完整跑一次 SPICE 仿真”的场景。scipy 里自带differential_evolution函数,控制参数少,基本只需要设置边界、种群大小和最大迭代次数。
3.2 定义目标函数和惩罚值
在 DE 中,目标函数返回一个标量,越小越好。我们不能直接写“相位裕度大于 60 度”,而是要把约束转成惩罚。项目中的penalty.mat应该就是记录迭代过程中惩罚值的矩阵,方便事后画收敛曲线。下面是我会用的目标函数设计:
# objective.py 目标函数定义 import numpy as np def evaluate_design(x): # x = [wp, cc, rz, itail] wp, cc, rz, itail = x gain_db, gbw_mhz, pm_deg = run_simulate_and_extract( wp, cc, rz, itail) if gain_db is None: return 1e6 # 仿真失败时给一个巨大的惩罚 # 目标指标 gain_ref = 60.0 # dB gbw_ref = 50.0 # MHz pm_ref = 60.0 # deg penalty = 0.0 # 未达标部分按相对偏差平方累加,达标不再奖励 penalty += max(0.0, (gain_ref - gain_db) / gain_ref) ** 2 penalty += max(0.0, (gbw_ref - gbw_mhz) / gbw_ref) ** 2 penalty += max(0.0, (pm_ref - pm_deg) / pm_ref) ** 2 return penalty逻辑说明:max(0, ...)保证达标项不贡献惩罚,未达标项按相对偏差平方叠加。这样避免量纲差异——增益是 dB 量级,带宽是 MHz 量级,如果不归一化,带宽项会被吃掉。仿真失败时直接返回1e6,DE 算法因为惩罚值巨大而自动丢弃该个体。注意这里每个个体都要跑一次仿真,所以目标函数要尽量精简;run_simulate_and_extract里不应该再打印调试信息或重复读文件。
3.3 运行差分进化并保存惩罚历史
# DE.py 主程序 from scipy.optimize import differential_evolution from scipy.io import savemat import numpy as np bounds = [(40e-6, 120e-6), # wp (1e-12, 5e-12), # cc (0, 5e3), # rz (20e-6, 80e-6)] # itail result = differential_evolution( evaluate_design, bounds, seed=7, maxiter=80, popsize=25, updating='deferred', workers=-1, disp=False ) print('最优参数:', result.x) print('最小惩罚:', result.fun) # 保存最终种群的惩罚值,便于分析收敛情况 savemat('penalty.mat', {'penalty': result.population_energies})参数说明:popsize=25表示每个维度有 25 个种群个体,4 维就是 100 个;maxiter=80是最大迭代代数,总仿真次数约 100×80=8000 次,需要根据单次仿真时长调整,如果一次仿真 2 秒,这就要 4 个多小时,所以通常会把 AC 扫描点数减少到 50,或先把仿真脚本并行化;updating='deferred'让种群在每代结束后统一更新,配合workers=-1使用多核并行效果更好。result.population_energies是最终种群所有个体对应的惩罚值,存成.mat文件后,可以用scipy.io.loadmat读回,结合 matplotlib 绘制惩罚值分布柱状图,直观看到有多少个体逼近零惩罚。
| 方法 | 需要梯度 | 处理失败点 | 高维表现 | 实现复杂度 |
|---|---|---|---|---|
| 网格搜索 | 否 | 容易,直接跳过 | 差 | 低 |
| 随机搜索 | 否 | 容易 | 一般 | 低 |
| 贝叶斯优化 | 否 | 需要额外处理 | 较好 | 中 |
| 差分进化 | 否 | 自然处理 | 好 | 低-中 |
4. 神经网络替代仿真:x.mat 与 data.csv 驱动的训练流程
当 DE 找到一组候选参数之后,我们还想知道“如果参数稍微偏一点,性能会不会崩”,或者想在全设计空间快速筛选。这时最合适的做法是训练一个前馈神经网络作为代理模型。项目里的nn_test_xmh.py和erfenlei.py就是在做这件事,x.mat很可能是从 MATLAB 导出的输入特征矩阵,data.csv则是上面生成的仿真数据表。
4.1 数据清洗与归一化
训练之前要检查三点:是否存在全为空的行、输入输出是否都做了标准化、训练/验证/测试集合是否按同一种方式切分。下面以data.csv为例:
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler df = pd.read_csv('data.csv') df = df.dropna(subset=['gain_db', 'gbw_mhz', 'pm_deg']) # 输入特征和输出指标 X = df[['wp', 'cc', 'rz', 'itail']].values y = df[['gain_db', 'gbw_mhz', 'pm_deg']].values scaler_x = StandardScaler().fit(X) scaler_y = StandardScaler().fit(y) X_s = scaler_x.transform(X) y_s = scaler_y.transform(y) X_train, X_val, y_train, y_val = train_test_split( X_s, y_s, test_size=0.2, random_state=42)这里用dropna剔除仿真失败的行;如果失败行太多,就要回到第 2 章收窄参数范围。StandardScaler把输入输出都变成零均值单位方差,否则wp是微米量级、cc是皮法量级,数值相差太多,梯度下降会非常不稳定。random_state=42保证每次切分一致,方便复现实验。
4.2 用 Keras 搭一个回归网络
对于这种表格型特征,不需要卷积神经网络,最简单的三层全连接网络(也就是经典的 BP 网络结构)就能达到不错的效果。输入是 4 个设计参数,输出是 3 个性能指标,中间层用 ReLU 激活,输出层不接激活函数:
from tensorflow import keras from tensorflow.keras import layers model = keras.Sequential([ layers.Input(shape=(4,)), layers.Dense(64, activation='relu'), layers.Dropout(0.1), layers.Dense(64, activation='relu'), layers.Dense(3) ]) model.compile( optimizer=keras.optimizers.Adam(learning_rate=3e-3), loss='mse', metrics=['mae'] ) history = model.fit( X_train, y_train, epochs=300, batch_size=32, validation_data=(X_val, y_val), callbacks=[keras.callbacks.EarlyStopping(patience=30)] )为什么要两层 64 神经元?因为输入只有 4 个特征,输出 3 个,问题并不复杂,层数太多反而容易过拟合。Dropout(0.1)是轻量正则;EarlyStopping在验证 loss 连续 30 个 epoch 不下降时停止,避免白跑。学习率初始设为3e-3,如果训练震荡明显,降到1e-3;如果收敛太慢,试着调大 batch size。训练完成后,用model.predict对新参数组合做预测时,要记得先对输入做scaler_x.transform,对输出做scaler_y.inverse_transform才能得到真实量纲的增益和带宽。
4.3 超参数选择的几种常见组合
我整理了一个适用于这个数据规模的经验表,可以在自己的机器上先跑这几个组合:
| 学习率 | 神经元数量 | batch_size | 结果表现 |
|---|---|---|---|
| 3e-3 | 64-64 | 32 | 收敛快,可能有震荡 |
| 1e-3 | 64-64 | 32 | 稳定,推荐默认 |
| 1e-3 | 32-32 | 64 | 参数少,方差大 |
| 1e-4 | 128-128 | 16 | 收敛慢,但精度略好 |
实际训练时,我习惯先跑1e-3 + 64-64 + batch32,看验证 loss 曲线如果连续 50 轮没有明显下降,就换成1e-4做 fine-tune。不要把 epoch 设成固定 3000,一定要配合早停。
4.4 二分类版本:erfenlei.py 在做什么
除了回归预测连续性能,erfenlei.py通常是把问题转化为“该组参数是否满足所有指标”。这个脚本会计算每个样本是否达标,例如增益大于 60dB 且相位裕度大于 60°,生成 0/1 标签,然后用同样的输入特征训练一个二分类器。这样做的好处是 DE 可以先快速过滤掉大概率不达标的区域,再用精确仿真验证保留点:
# erfenlei.py 示例:二分类标签生成与逻辑回归 from sklearn.linear_model import LogisticRegression meet = (df['gain_db'] > 60) & (df['gbw_mhz'] > 50) & (df['pm_deg'] > 60) y_cls = meet.astype(int).values clf = LogisticRegression(max_iter=500) clf.fit(scaler_x.transform(X), y_cls) # 预测时使用 clf.predict_proba() 取正类概率这里二分类的好处是可以理解参数空间里“合格区域”的大小。如果正类样本占比非常低,说明抽样范围离目标方向偏了,需要把 DE 的最优解附近作为新的采样中心继续生成数据,形成“仿真 -> 训练 -> 再采样”的主动学习闭环。
5. 验证与排错:确保仿真和预测对得上
训练完神经网络,最重要的不是 loss 有多低,而是预测值和 SPICE 仿真结果是否一致。这一章讲几个我实际踩过的坑和验证手段。
5.1 用散点图和 R2 检查回归效果
一个合格代理模型在验证集上的 R2 应该在 0.95 以上。如果低于这个数,要么数据量太少,要么某个特征没参与扫描。下面这段代码对每个输出画“真实值 vs 预测值”:
import matplotlib.pyplot as plt from sklearn.metrics import r2_score y_pred_s = model.predict(X_val) # 反标准化回真实量纲 y_pred = scaler_y.inverse_transform(y_pred_s) y_true = scaler_y.inverse_transform(y_val) labels = ['Gain (dB)', 'GBW (MHz)', 'Phase Margin (deg)'] for i in range(3): plt.figure(figsize=(4, 4)) plt.scatter(y_true[:, i], y_pred[:, i], s=8, alpha=0.5) plt.plot([y_true[:, i].min(), y_true[:, i].max()], [y_true[:, i].min(), y_true[:, i].max()], 'r--') plt.xlabel('SPICE Simulated ' + labels[i]) plt.ylabel('NN Predicted ' + labels[i]) print(f'{labels[i]}: R2 = {r2_score(y_true[:, i], y_pred[:, i]):.3f}') plt.savefig(f'check_{i}.png', dpi=150)如果散点图出现弯曲的“弓形”,大概率是输出没有真正覆盖两端,要检查数据生成时参数边界是否太窄。如果某个指标 R2 特别差,优先看它在数据生成时是否因为仿真失败被剔除太多,导致样本量不足。
5.2 几个容易踩的坑
首先是路径问题:.rar里有__MACOSX和.DS_Store,在 Windows 上解压后目录结构有时会多出一层,导致open('x.mat')失败。我建议把所有数据文件直接放到项目根目录,用os.path.join(os.path.dirname(__file__), 'data.csv')这种绝对路径写法,避免终端工作目录不一致。第二个坑是读取x.mat时,scipy.io.loadmat读出来的通常是字典,里面可能有__header__和__globals__等键,要先用.keys()查看实际矩阵名,再决定用哪个键当输入数据。第三个坑是 DE 和神经网络都要固定seed,否则每次跑出来的参数和结果都不同,最后报告里无法复现。
最后一个实用技巧是:在神经网络训练结束后,挑 10~20 组在训练中没有见过的参数,重新跑一遍simulate_opamp,对比预测和仿真。因为随机切分和标准化已经保证了数据隔离,但重新采样能验证模型在真实设计空间外推时的可靠性。把这 20 个点的平均绝对误差写进大作业的结论页,比贴训练 loss 曲线更有说服力。
本文还有配套的精品资源,点击获取