【问题标题】:Why does this LinqPad program produce different results on the second run?为什么这个 LinqPad 程序在第二次运行时会产生不同的结果?
【发布时间】:2019-08-16 16:22:34
【问题描述】:

我正在 LinqPad 中开发一个简单的 PID 控制器:

PID 类

class PidController
{
    public float Proportional { get; set; }
    public float Integral { get; set; }
    public float Derivative { get; set; }
    public float SetPoint { get; set; }
    public float Kp { get; set; }
    public float Ki { get; set; }
    public float Kd { get; set; }

    float _lastError;
    DateTime _lastTime = DateTime.Now;

    public PidController(float kp, float ki, float kd)
    {
        Kp = kp; Ki = ki; Kd = kd;
    }

    public float GetControlValue(float actual)
    {
        var currentTime = DateTime.Now;
        var deltaTime = (float)(currentTime - _lastTime).TotalSeconds;
        var error = SetPoint - actual;

        Proportional = error;
        Integral = Integral + error * deltaTime;
        Derivative = (error - _lastError) / deltaTime;

        _lastError = error;

        return Kp * Proportional + Ki * Integral + Kd * Derivative;
    }
}

为了测试和调整,控制器将控制这个简单的过程:

受控过程

class SimpleProcess
{
    private DateTime _lastTime = DateTime.Now;
    private float _output;

    public float Output { get { UpdateOutput(); return _output; }}
    public float Input { get; set; }

    private void UpdateOutput()
    {
        var deltaTime = (float)(DateTime.Now - _lastTime).TotalSeconds;
        _output += Input * deltaTime;
    }
}

...使用这个主循环:

主程序循环

void Main()
{
    var pid = new PidController(1f, 0f, 0f) { SetPoint = 100f };
    var proc = new SimpleProcess();

    // pid.Dump();
    // proc.Dump();

    var values = new List<ProcessValue>();

    for (int i = 0; i < 50; i++)
    {
        var actual = proc.Output;
        var controlValue = pid.GetControlValue(actual);
        proc.Input = controlValue;

        var value = new ProcessValue
        {
            index = i,
            timestamp = DateTime.Now.ToString("ss.fff"),
            p = pid.Proportional,
            i = pid.Integral,
            d = pid.Derivative,
            input = controlValue,
            output = actual
        };

        values.Add(value);

        Thread.Sleep(100);
    }

    values.Dump();
}

public class ProcessValue
{
    public int index;
    public string timestamp;
    public float p, i, d, input, output;
}

第一次运行时一切正常:

index  timestamp  p      i       d         input output
0      53.309     100    0.46    21490.59  100   0 
1      53.411     89.69  10.06  -96.27     89.69 10.30 
etc...

但是,在我注释掉 proc.Dump() 行之后,我在第二次和后续运行中开始得到意想不到的结果:

index  timestamp  p    i    d    input  output 
0      10.199     100  0    ∞    NaN    0 
1      10.299     NaN  NaN  NaN  NaN    NaN 
2      10.399     NaN  NaN  NaN  NaN    NaN 
etc...

在我的情况下,为什么第二次运行(以及后续运行)返回不同的结果?

以下任何操作都会导致下一次运行成功:

  • 修改代码(即使只是添加/删除单个空格)
  • 按 [CTRL]+[SHIFT]+[F5]

以下使代码每次都能正确运行:

  • 取消注释proc.Dump()

This answer 提到静态变量将在运行之间缓存,但我没有静态变量。我怀疑问题与 LinqPad 中的 应用程序域缓存 功能有关,但我试图了解为什么我会受到此影响。

更新

StriplingWarrior 的回答是正确的,当他们的系统运行良好时(即在 LinqPad 缓存第一次运行之后),我的一阶导数计算导致 Infinity,导致所有后续计算失败。以任何方式修改我的程序都会使这个缓存失效,并导致 deltaTime 足够大以避免在下次运行时再次出现错误。

由于派生项在第一个区间没有意义,我决定通过简单地忽略它来处理这个问题:

var p = Kp * Proportional;
var i = Ki * Integral;
var d = float.IsInfinity(Derivative) ? 0 : Kd * Derivative;

return p + i + d;

【问题讨论】:

  • 什么是PidController.ProcessValue?这不会出现在您发布的代码中。
  • @StriplingWarrior 它只是一个 POCO,用于保存我最后 Dump() 的结果列表的值 - 它的名称很差,并且在错误的位置......但只是在那里进行调试片刻。我已将其重新添加到上面的代码中。
  • 好吧,我在 VS 中运行了这个,但无法重现您的第二个场景。无论如何,如果d 是无限的,那一定是因为deltaTime 是零。这意味着pid = new PidControllerpid.GetControlValue 之间的时间几乎为零。如果你调用那些Dump 方法,你显然是在增加一些时间。即使在发布模式下构建并从 VS 外部执行,这两行之间仍然需要大约 1 毫秒。顺便说一句,该变量应该被称为CreationTime,而不是_lastTime。你在使用 CleanCode 吗?因为我没有那个List.Dump 方法。
  • 我同意@StriplingWarrior。 deltaTime 必须返回零,并且除以零会导致 Nan's。您可以尝试设置 Util.NewProcess = true; 以禁用应用程序域缓存
  • @StriplingWarrior 您正在做一些事情 - 在初始化 var proc = ... 后立即添加 Thread.Sleep(100) 会使其按预期工作......我会更仔细地查看时间计算。跨度>

标签: c# linqpad


【解决方案1】:

您可以通过更改 main 方法的第一部分来测试 Andrew 在上述 cmets 中的理论:

var sw = new Stopwatch();
sw.Start();
var pid = new PidController(1f, 0f, 0f) { SetPoint = 100f };
var proc = new SimpleProcess();

// pid.Dump();
// proc.Dump();

var values = new List<ProcessValue>();

for (int i = 0; i < 50; i++)
{
    var actual = proc.Output;
    var controlValue = pid.GetControlValue(actual);
    if(sw.IsRunning){
        sw.Stop();
        sw.ElapsedTicks.Dump();
    }

在我的机器上运行,我可以看到第一次运行需要 10,000+ 个滴答声,而第二次运行只需要 20 个滴答声。我猜这会使你的计算基于 DateTime 的差异。现在有非常小的增量值,并产生你所看到的差异。

【讨论】:

  • 你是对的-由于时间差极小,导数计算结果为NaN,导致结果总和也为NaN。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 2020-01-28
  • 2016-11-13
  • 1970-01-01
  • 2019-03-12
相关资源
最近更新 更多