高斯-牛顿法用于齐纳二极管
使用高斯-牛顿法(非线性最小二乘法)计算齐纳二极管指数模型的参数
引言
该代码实现了高斯-牛顿法来求解非线性最小二乘问题,并将其应用于齐纳二极管的 IZ(VZ) 模型。
背景
最近,我一直在考虑一个意想不到的效应,即齐纳二极管电流对微控制器的输入级 (ADC) 的吸收;齐纳二极管本应只用于钳位可能的过高电压,而不应像实际上那样明显地改变输入信号。不幸的是,二极管的数据手册没有提供关于 IZ(VZ) 的详细信息,所以我不得不对其进行测量,然后,无可救药地被吸引,尝试对其进行建模。
本文描述了这个过程。
免责声明
- 本文不试图解释甚至介绍高斯-牛顿算法(例如,它在维基百科上已经得到了很好的详细介绍)。它只是将其用于一个目的。
- 使用高斯-牛顿方法是因为它很契合(抱歉用这个双关语)。虽然我确信有更高级的方法来计算非线性最小二乘,但我并不关心。我编写代码的过程很有趣。
实验数据
实验数据是使用图 1 中所示的简单电路收集的:一个可调电压发生器(实验室电源)与一个 992 欧姆
的电阻和一个 3V3
的齐纳二极管串联。电压发生器为电路供电,为齐纳二极管提供正向和反向电压。
在这种电路中,电阻和齐纳二极管流过的电流是相同的
IZ = IR = VR/R
因此,测量 VZ 和 VR 就可以获得所有需要的数据。
然而,额外测量 VTOT 并非仅仅是浪费(我的)时间,因为它为收集到的 {VZ, IZ} 对提供了“冗余检查”。
数据文件
共收集了 123
个数据点,VTOT 的范围从约 -21 V
到(约)18 V
。
对应的值可在 data.csv 文件中找到。图 2 显示了数据的部分视图。
实验曲线
收集到的数据用图 3 绘制的曲线表示。
显示了齐纳二极管的特性行为:正向电压(本例中约为 0.7 V
)下指数级增长的电流,反向击穿电压(约为标称值 -3.3 V
)下指数级增长(但斜率非常不同,更平缓)的(负)电流。
在中区,电流非常小。
模型
选择的 IZ(VZ) 模型是“指数二极管”。
在本文中,我只考虑方程 (1),该方程应仅在 V > -VB 范围内有效,其中 VB 是(齐纳)二极管的“击穿电压”。
图 0,位于页面顶部,实际上显示了该方程在无效范围(V <= -VB)下的“灾难性失效”。
由于该齐纳二极管是 3V3
的,其标称 VB 为 3.3 V
。然而,本次测试的目的是“实验性地”确定方程 (1) 的有效范围。
计划
该计划包括迭代应用高斯-牛顿方法(非线性最小二乘)到实验数据的子范围(尾部),直到获得令人满意的拟合,以确定齐纳二极管的“实验击穿电压”(VBE)。
(这个结果将是另一篇文章的起点,在其中需要找到 V < -VBE 范围内的方程 (2) 的拟合。)
细节
- 在第 0 步,考虑所有实验数据来拟合模型(VStart = Vmin = V0)。计算相应的方差。
- 在第 1 步,排除第一个实验点(VStart = V1),对剩余数据尾部进行拟合,计算相应的方差。
- 在第 2 步,排除第一个和第二个实验点(VStart = V2),对剩余数据尾部进行拟合和方差计算。
- ...
- 在第 83 步,停止所有操作,因为现在数据尾部相当小了。
最后,绘制方差(Vstart)图以估算 VBE。
高斯-牛顿算法
高斯-牛顿算法是一种求解非线性最小二乘问题的迭代方法(详情请参阅维基百科页面:高斯-牛顿算法[^])。
迭代步骤 n+1
,即在步骤 n+1
时模型估计参数的值是
在我们的例子中,在将方程 (1) 重写为如下形式之后
我们有模型函数
以及迭代步骤分量
其中明确显示了参数(2
个分量)和残差(N
个分量)的矢量性质,以及雅可比矩阵(N x 2
个元素)的矩阵性质。
幸运的是,JTJ 乘积是一个 2x2
矩阵:其逆矩阵的计算是微不足道的。
代码
代码位于单个源文件 gn2.cpp 中 - 它使用 g++-7 编译,但任何现代 C++ 编译器都应该可以完成。
它包含一个名为 GN
的类,该类在 (main
函数中) 使用所有实验数据值(以及参数的初始估计)进行构造。
main
首先“任意地”设置 20
次迭代步骤,然后通过 offset
参数定义的尾部数据,迭代调用 GN::compute
方法。
for ( size_t offset = 0; offset <= (N - 40); ++offset)
{
cout << "----------------------\n";
cout << "starting with item:\n" << offset << endl << endl;
auto [ best_pe, best_var] = gn.compute(steps, offset);
cout << "best pe:\n" << best_pe << endl;
cout << "best variance:\n" << best_var << endl << endl;
}
cout << "done." << endl << endl;
对于每个范围,都会报告
- 在标准输出上,最佳计算出的方差以及相应的参数估计。
- 在一个名为
gn_<offset>.log
的日志文件中(例如gn_009.log
),记录了计算的所有细节(是的,大约会生成80
个日志文件 :-))。
控制台输出摘录
...
----------------------
starting with item:
9
best pe:
2.10572e-17
43.5169
best variance:
0.000195843
...
相应日志文件 gn_009.log 的摘录
experimental data
ex:
-3.773
-3.753
[...]
ey:
-0.0174294
-0.0164012
[...]
parameter estimates starting value
epe
5.18e-20
51.2
####################
step 0
r:
0
0
[...]
variance:
0.000197485
j:
0 0
0 0
[...]
jt:
0 0 0 0 [...]
[...]
jtr:
6.88719e+13
1.81736e-06
jtj:
7.32544e+35 2.96725e+16
2.96725e+16 0.00120204
inv_jtj:
1.30385e-32 -3.21856e-13
-3.21856e-13 7.94587e+06
identity:
1 0
0 1
dpe:
3.13055e-19
-7.72628
new parameter estimates:
3.64855e-19
43.4737
[...]
其中 identity = (JTJ)-1 · (JTJ) 是一个“一致性检查”,而 dpe
是参数估计值的增量(delta)。
方差图
在图上收集并绘制了针对不同范围计算出的方差。
该图再次证实了方程 (1) 对于电压 V < -VB 的不适用性。
它还指示了击穿电压的实验值 VBE。事实上,模型在以下情况下“表现良好”:
V > -VBE ≅ -2.8 V
关注点
我到处都使用 std::vector
,但在这种特殊应用中,我发现 std::array
结合模板为矩阵计算提供了强大而简洁的工具,例如,考虑
template <size_t R, size_t C>
array< array < double, R>, C > transpose
( const array < array< double, C >, R > & m, size_t offset)
{
array< array < double, R >, C > t{0}; // the transposed matrix
for (size_t r = offset; r<R; ++r)
for ( size_t c = 0; c < C; ++c)
t[c][r] = m[r][c];
return t;
}
在我看来,这是一种几乎可以自文档化的转置操作实现。
您可以通过丢弃 offset
参数来实现更简洁的实现,我在这里使用它是为了提高性能而牺牲了通用性。
历史
- 2019年6月30日 - 首次发布