【问题标题】:An efficient data structure or method to manage plotting data that grow with time一种有效的数据结构或方法来管理随时间增长的绘图数据
【发布时间】:2011-09-05 15:07:40
【问题描述】:

我想问一下我管理模拟绘制结果的以下方式是否是对 Mathematica 的有效使用,以及是否有更“实用”的方式来做到这一点。 (可能正在使用 Sow、Reap 等)。

问题是基本问题。假设您想模拟一个物理过程,比如一个钟摆,并想绘制解在运行(或任何其他类型的结果)时的时间序列(即时间与角度)。

为了能够显示图表,需要在运行时保留数据点。

以下是一个简单的示例,它绘制了解决方案,但仅绘制了当前点,而不是完整的时间序列:

Manipulate[
 sol = First@NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4, y'[0] == 0}, 
                      y, {t, time, time + 1}];

 With[{angle = y /. sol},
  (
   ListPlot[{{time, angle[time]}}, AxesLabel -> {"time", "angle"}, 
            PlotRange -> {{0, max}, {-Pi, Pi}}]
   )
  ],

 {{time, 0, "run"}, 0, max, Dynamic@delT, ControlType -> Trigger},
 {{delT, 0.1, "delT"}, 0.1, 1, 0.1, Appearance -> "Labeled"},

 TrackedSymbols :> {time},
 Initialization :> (max = 10)
 ]

以上内容并不有趣,因为人们只看到一个点移动,而不是完整的解决方案路径。

我目前处理这个问题的方法是使用Table[] 分配一个足够大的缓冲区,以容纳可以生成的最大可能时间序列大小。

问题是时间步长可以改变,越小,生成的数据就越多。

但是由于我知道可能的最小时间步长(在本例中为 0.1 秒),并且我知道运行的总时间(在此为 10 秒),所以我知道要分配多少。

我还需要一个“索引”来跟踪缓冲区。使用这种方法,这里有一个方法:

Manipulate[

 If[time == 0, index = 0];

 sol = First@NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4,y'[0] == 0}, 
                      y, {t, time, time + 1}];

 With[{angle = y /. sol},
  (
   index += 1;
   buffer[[index]] = {time, angle[time]};

   ListPlot[buffer[[1 ;; index]], Joined -> True, AxesLabel -> {"time", "angle"}, 
            PlotRange -> {{0, 10}, {-Pi, Pi}}]
   )
  ],

 {{time, 0, "run"}, 0, 10, Dynamic@delT, AnimationRate -> 1, ControlType -> Trigger},
 {{delT, 0.1, "delT"}, 0.1, 1, 0.1, Appearance -> "Labeled"},

 {{buffer, Table[{0, 0}, {(max + 1)*10}]}, None},
 {{index, 0}, None},

 TrackedSymbols :> {time},
 Initialization :> (max = 10)
 ]

作为参考,当我在 Matlab 中执行上述操作时,它有一个很好的绘图工具,称为“hold on”。这样人们就可以绘制一个点,然后说“坚持”,这意味着下一个情节不会删除情节上已经存在的内容,而是会添加它。

我在 Mathematica 中没有找到类似的东西,即即时更新当前绘图。

我也不想在运行时使用 Append[] 和 AppendTo[] 来构建缓冲区,因为那样会很慢而且效率不高。

我的问题:除了我正在做的事情之外,是否有更高效的 Mathematica 方式(可以更快、更优雅)来完成上述典型任务?

谢谢,

更新:

关于为什么不一次性解决 ODE 的问题。 是的,这是可能的,但是出于性能原因,它简化了很多事情来做这件事。 这是一个带有初始条件的 ode 示例:

Manipulate[
 If[time == 0, index = 0];
 sol = First@
   NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == y0, 
     y'[0] == yder0}, y, {t, time, time + 1}];

 With[{angle = (y /. sol)[time]},
  (
   index += 1;
   buffer[[index]] = {time, angle};

   ListPlot[buffer[[1 ;; index]], Joined -> True, 
    AxesLabel -> {"time", "angle"}, 
    PlotRange -> {{0, 10}, {-Pi, Pi}}])],

 {{time, 0, "run"}, 0, 10, Dynamic@delT, AnimationRate -> 1, 
  ControlType -> Trigger}, {{delT, 0.1, "delT"}, 0.1, 1, 0.1, 
  Appearance -> "Labeled"},
 {{y0, Pi/4, "y(0)"}, -Pi, Pi, Pi/100, Appearance -> "Labeled"},
 {{yder0, 0, "y'(0)"}, -1, 1, .1, Appearance -> "Labeled"},

 {{buffer, Table[{0, 0}, {(max + 1)*10}]}, None},
 {{index, 0}, None},

 TrackedSymbols :> {time},
 Initialization :> (max = 10)
 ]

现在,如果以前解决过一次系统,那么他们需要注意IC是否发生变化。这可以做到,但需要额外的逻辑,我以前做过很多次,但它确实使事情复杂了一点。我在这个here.写了一个小笔记

另外,我注意到,随着时间的推移,我可以通过解决系统更小的时间段来获得更快的速度,而不是一次解决整个问题。 NDSolve 调用开销非常小。但是当 NDsolve 的持续时间很长时,当人们要求 NDSolve 提供更高的精度时,可能会出现问题,例如选项AccuracyGoal ->, PrecisionGoal ->,当时间间隔非常大时我无法做到这一点。

总体而言,与它在简化逻辑和速度方面的优势相比,为较小的段调用 NDSolve 的开销似乎要少得多(可能更准确,但我没有对此进行更多检查)。我知道继续调用 NDSolve 似乎有点奇怪,但是在尝试了这两种方法(一次全部,但添加逻辑以检查其他控制变量)与此方法之后,我现在倾向于此方法。

更新 2

我针对 2 个测试用例比较了以下 4 种方法:

tangle[j][j] 方法(贝利撒留) AppendTo(由 Sjoerd 建议) 动态链表(Leonid)(带和不带SetAttributes[linkedList, HoldAllComplete]) 预分配缓冲区(Nasser)

我这样做的方法是运行 2 个案例,一个为 10,000 分,第二个为 20,000 分。我确实将 Plot[[] 命令留在那里,但不要在屏幕上显示它,这是为了消除实际渲染的任何开销。

我在 Do 循环周围使用了 Timing[],该循环遍历称为 NDSolve 的核心逻辑,并使用上面的 delT 增量遍历时间跨度。没有使用 Manipulate。

我在每次运行前都使用了 Quit[]。

对于 Leonid 方法,我通过 Do 循环更改了他的 Column[]。我最后验证了,但是使用他的 getData[] 方法绘制数据,结果是好的。

我使用的所有代码都在下面。我做了一个表格,显示了 10,000 点和 20,000 点的结果。计时是每秒:

 result = Grid[{
   {Text[Style["method", Bold]], 
    Text[Style["number of elements", Bold]], SpanFromLeft},
   {"", 10000, 20000},
   {"", SpanFromLeft},
   {"buffer", 129, 571},
   {"AppendTo", 128, 574},
   {"tangle[j][j]", 612, 2459},
   {"linkedList with SetAttribute", 25, 81},
   {"linkedList w/o SetAttribute", 27, 90}}
  ]

显然,除非我做错了什么,但下面的代码供任何人验证,Leonid 方法在这里很容易获胜。我也很惊讶 AppendTo 和预分配数据的 buffer 方法一样好。

这是我用来生成上述结果的稍微修改的代码。

缓冲方法

delT = 0.01; max = 100; index = 0;
buffer = Table[{0, 0}, {(max + 1)*1/delT}];

Timing[
 Do[
  sol = First@
    NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4, 
      y'[0] == 0}, y, {t, time, time + 1}];

  With[{angle = y /. sol},
   (index += 1;
    buffer[[index]] = {time, angle[time]};
    foo = 
     ListPlot[buffer[[1 ;; index]], Joined -> True, 
      AxesLabel -> {"time", "angle"}, 
      PlotRange -> {{0, 10}, {-Pi, Pi}}]
    )
   ], {time, 0, max, delT}
  ]
 ]

AppendTo 方法

Clear[y, t];
delT = 0.01; max = 200;
buffer = {{0, 0}};  (*just a hack to get ball rolling, would not do this in real code*)

Timing[
 Do[
  sol = First@
    NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4, 
      y'[0] == 0}, y, {t, time, time + 1}];

  With[{angle = y /. sol},
   (AppendTo[buffer, {time, angle[time]}];
    foo = 
     ListPlot[buffer, Joined -> True, AxesLabel -> {"time", "angle"}, 
      PlotRange -> {{0, 10}, {-Pi, Pi}}]
    )
   ], {time, 0, max, delT}
  ]
 ]

缠结[j][j]方法

Clear[y, t];
delT = 0.01; max = 200;
Timing[
 Do[
  sol = First@
    NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4, 
      y'[0] == 0}, y, {t, time, time + 1}];
  tangle[time] = y /. sol;
  foo = ListPlot[
    Table[{j, tangle[j][j]}, {j, .1, max, delT}],
    AxesLabel -> {"time", "angle"},
    PlotRange -> {{0, max}, {-Pi, Pi}}
    ]
  , {time, 0, max, delT}
  ]
 ]

动态链表法

Timing[
 max = 200;

 ClearAll[linkedList, toLinkedList, fromLinkedList, addToList, pop, 
  emptyList];
 SetAttributes[linkedList, HoldAllComplete];
 toLinkedList[data_List] := Fold[linkedList, linkedList[], data];
 fromLinkedList[ll_linkedList] := 
  List @@ Flatten[ll, Infinity, linkedList];
 addToList[ll_, value_] := linkedList[ll, value];
 pop[ll_] := Last@ll;
 emptyList[] := linkedList[];

 Clear[getData];

 Module[{ll = emptyList[], time = 0, restart, plot, y},

  getData[] := fromLinkedList[ll];

  plot[] := Graphics[
    {
     Hue[0.67`, 0.6`, 0.6`],
     Line[fromLinkedList[ll]]
     },
    AspectRatio -> 1/GoldenRatio,
    Axes -> True,
    AxesLabel -> {"time", "angle"},
    PlotRange -> {{0, 10}, {-Pi, Pi}},
    PlotRangeClipping -> True
    ];

  DynamicModule[{sol, angle, llaux, delT = 0.01},

   restart[] := (time = 0; llaux = emptyList[]);
   llaux = ll;

   sol := 
    First@NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4, 
       y'[0] == 0}, y, {t, time, time + 1}];
   angle := y /. sol;

   ll := With[{res = 
       If[llaux === emptyList[] || pop[llaux][[1]] != time,
        addToList[llaux, {time, angle[time]}],
        (*else*)llaux]
      },
     llaux = res
     ];

   Do[
    time += delT;
    plot[]
    , {i, 0, max, delT}
    ]
   ]
  ]
 ]

感谢大家的帮助。

【问题讨论】:

  • 你有什么理由不只保留整个系列的数据吗?它真的不会占用那么多空间。您可以求解整个 t 值范围,它会生成一个插值函数,该函数存储最少量的数据来生成该曲线。
  • 我现在正在比较这里讨论的方法对于相同测试用例的性能,稍后会发布结果。

标签: wolfram-mathematica


【解决方案1】:

我不知道如何使用Manipulate 获得您想要的东西,但我似乎已经设法通过自定义Dynamic 获得了一些接近的东西。以下代码将:使用链表来提高效率,通过按钮停止/恢复您的绘图,并在任何给定时间按需提供迄今为止收集的数据:

ClearAll[linkedList, toLinkedList, fromLinkedList, addToList, pop, emptyList];
SetAttributes[linkedList, HoldAllComplete]; 
toLinkedList[data_List] := Fold[linkedList, linkedList[], data];
fromLinkedList[ll_linkedList] := List @@ Flatten[ll, Infinity, linkedList];
addToList[ll_, value_] := linkedList[ll, value];
pop[ll_] := Last@ll;
emptyList[] := linkedList[];


Clear[getData];
Module[{ll = emptyList[], time = 0, restart, plot, y},
   getData[] := fromLinkedList[ll];
   plot[] := 
      Graphics[{Hue[0.67`, 0.6`, 0.6`], Line[fromLinkedList[ll]]}, 
        AspectRatio -> 1/GoldenRatio, Axes -> True, 
        AxesLabel -> {"time", "angle"}, PlotRange -> {{0, 10}, {-Pi, Pi}}, 
        PlotRangeClipping -> True];
   DynamicModule[{sol, angle, llaux, delT = 0.1},
     restart[] := (time = 0; llaux = emptyList[]);
     llaux = ll;
     sol := First@
        NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, y[0] == Pi/4, y'[0] == 0}, 
             y, {t, time, time + 1}];
     angle := y /. sol;
     ll := With[{res =
              If[llaux === emptyList[] || pop[llaux][[1]] != time, 
                 addToList[llaux, {time, angle[time]}],
                 (* else  *)
                 llaux]},
              llaux = res];
     Column[{
        Row[{Dynamic@delT, Slider[Dynamic[delT], {0.1, 1., 0.1}]}],
        Dynamic[time, {None, Automatic, None}],
        Row[{
          Trigger[Dynamic[time], {0, 10, Dynamic@delT}, 
               AppearanceElements -> { "PlayPauseButton"}], 
          Button[Style["Restart", Small], restart[]]
        }],
        Dynamic[plot[]]
      }, Frame -> True]
  ]
]

此处的链接列表取代了您的buffer,您无需预先分配并提前知道您将拥有多少数据点。 plot[] 是一个自定义的低级绘图函数,尽管我们可能也可以使用 ListPlot。您可以使用“播放”按钮来停止和恢复绘图,并使用自定义的“重新启动”按钮来重置参数。

您可以在任何给定时间致电getData[] 以获取迄今为止累积的数据列表,如下所示:

In[218]:= getData[]
Out[218]= {{0,0.785398},{0.2,0.771383},{0.3,0.754062},{0.4,0.730105},{0.5,0.699755},
{0.6,0.663304},{0.7,0.621093},{0.8,0.573517},{0.9,0.521021},{1.,0.464099},
{1.1,0.403294},{1.2,0.339193},{1.3,0.272424}}

【讨论】:

  • 我同意其他人的观点,这里的效率问题主要是学术兴趣,至少在问题的具体设置中是这样。但是我不能排除具有数千甚至数万个点的类似场景(例如,当使用 Plot 和非常大的 PlotPoints 设置时,偶尔需要这样做),对此这种担忧可能是有道理的。
  • 谢谢,我需要一段时间才能理解这一点:) 但我想提一下,在演示中不允许使用“SetAttribute”,不会生成 CDF 并会导致错误如果我尝试上传笔记本以转换为带有此功能的演示 CDF。出于安全原因,不允许使用某些功能和符号。
  • 我想知道是否可以在不使用 SetAttributes 的情况下实现上述方法?如果您同意,我想在演示中使用它,但不允许使用 SetAttributes 符号。谢谢。
  • @me,没关系。我在没有 SetAttribute 的情况下重新运行了测试,性能仅比以前稍微低了一点。所以我可以使用这种方法而不使用 SetAttribute,它仍然比我以前做的要快得多。我更新了上面的性能表。谢谢
  • @Nasser 确实,HoldAllComplete 属性加快了速度,但效果并不显着。您也可以使用HoldComplete head 而不是linkedListFlatten,这样您就不需要使用SetAttributes - 这适用于这种特殊情况。关于演示 - 当然,继续,期待看到它!
【解决方案2】:

我只是想知道您为什么要分段解决 DE。它可以一次解决整个区间。也没有必要将NDSolve 放在 Manipulate 中。当Manipulate的body被触发时,不需要一次又一次的解决。 Plot 本身足够快,可以在每个时间步绘制增长图。以下代码无需任何存储即可满足您的需求。

sol = First@
   NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]]==0,y[0] == Pi/4,y'[0] == 0}, y, {t, 0, 10}];
eps = 0.000001;
Manipulate[
 With[{angle = y /. sol}, 
   Plot[angle[t], {t, 0, time + eps}, 
    AxesLabel -> {"time", "angle"}, 
    PlotRange -> {{0, max}, {-Pi, Pi}}
   ]
 ], 
 {{time, 0, "run"}, 0, max,Dynamic@delT, ControlType -> Trigger}, 
 {{delT, 0.1, "delT"}, 0.1, 1, 0.1, Appearance -> "Labeled"}, TrackedSymbols :> {time}, 
 Initialization :> (max = 10)
]

顺便说一句:AppendTo 可能会被指责为慢,但它并没有那么慢。在适合绘制的典型列表上,它需要不到一毫秒的时间,所以它根本不应该减慢绘制速度。

【讨论】:

  • 是的,我知道它可以一次全部解决,但实际上更常见的是如图所示“分段”完成。这样做有很好的理由:1.当间隔更短时获得解决方案的速度更快,2.如果用户随时更改初始条件,那么没问题,因为每次都解决了ode,因此无需添加额外的逻辑检测初始条件的变化(我没有在示例中显示 IC,但在实际演示中,也会有控制变量)。调用 NDSolve 的速度非常快,因此对于小段一次调用它实际上不是问题。谢谢。
【解决方案3】:

内存根本没有效率,但它的优点是它只需要对你的第一个代码稍作修改:

Clear[tangle]; 
Manipulate[
 sol = First@NDSolve[{y''[t] + 0.1 y'[t] + Sin[y[t]] == 0, 
                    y[0]  == Pi/4, 
                    y'[0] == 0}, 
             y, {t, time, time + 1}];

 (tangle[time] = y /. sol; 
  ListPlot[Table[{j, tangle[j][j]}, {j, .1, max, delT}], 
    AxesLabel -> {"time", "angle"}, 
    PlotRange -> {{0, max}, {-Pi, Pi}}]), 
 {{time, 0, "run"}, 0, max, Dynamic@delT, ControlType -> Trigger}, 
 {{delT, 0.1, "delT"}, 0.1, 1, 0.1, Appearance -> "Labeled"}, 
 TrackedSymbols :> {time}, 
 Initialization :> {(max = 10); i = 0}]

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-10-12
    • 2015-06-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-18
    相关资源
    最近更新 更多