<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>数值模拟 on Keqi的博客</title><link>https://yekq.top/tags/%E6%95%B0%E5%80%BC%E6%A8%A1%E6%8B%9F/</link><description>Recent content in 数值模拟 on Keqi的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><managingEditor>plloningye@gmail.com (Keqi Ye)</managingEditor><webMaster>plloningye@gmail.com (Keqi Ye)</webMaster><copyright>Keqi Ye</copyright><lastBuildDate>Tue, 07 Apr 2026 14:00:00 +0800</lastBuildDate><atom:link href="https://yekq.top/tags/%E6%95%B0%E5%80%BC%E6%A8%A1%E6%8B%9F/index.xml" rel="self" type="application/rss+xml"/><item><title>GASPHiA中的自引力树代码效率与优化分析</title><link>https://yekq.top/posts/gasphia/treecode/</link><pubDate>Tue, 07 Apr 2026 14:00:00 +0800</pubDate><author>plloningye@gmail.com (Keqi Ye)</author><guid>https://yekq.top/posts/gasphia/treecode/</guid><description>&lt;iframe src="//player.bilibili.com/player.html?isOutside=true&amp;aid=116482834958240&amp;bvid=BV1u69yBGEwJ&amp;cid=37919327464&amp;p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"
style="width: 100%; aspect-ratio: 16/9; border-radius: 8px; margin-bottom: 20px;">
&lt;/iframe>
&lt;p align="center" style="font-size: 0.9em; color: gray; margin-top: -10px; margin-bottom: 20px;">
&lt;em> 500万粒子纯引力 N-Body 模拟：星系碰撞产生的潮汐尾结构
本模拟采用 GASPHiA 的纯 N 体模块实现。初始模型通过开源工具 DICE 构建，包含了完整的暗物质晕（DM Halo）与恒星盘（Stellar Disk）组分。重点展示了在引力相互作用下，星系盘部粒子受扰动演化出典型潮汐尾（Tidal Tails）的过程。&lt;/em>
&lt;/p>
&lt;hr>
&lt;h2 id="为什么需要考虑引力">为什么需要考虑引力？
&lt;/h2>&lt;p>在天体物理的 SPH 模拟中，除了撞击过程本身，星体自身的引力对最终结果的影响同样至关重要。这也解释了为什么在模拟固态行星的撞击时，我们往往不引入物质强度模型，而是将其近似为纯粹的流体来处理。对于某些巨型撞击模拟而言，引力的作用不仅体现在模拟过程中需要实时计算引力，甚至在预处理阶段就必须求解泊松方程，以构建一个处于引力平衡状态的初始天体，再将其投入撞击计算。&lt;/p>
&lt;p>这里所说的引力，即自引力，指的是 SPH 粒子之间由于质量分布而产生的相互吸引力。这与模拟水动力学现象（如溃坝）时所采用的引力模型截然不同——后者仅需为每个粒子统一施加一个指向地面的恒定加速度即可。&lt;/p>
&lt;h2 id="如何计算自引力为什么需要树代码">如何计算自引力，为什么需要树代码？
&lt;/h2>&lt;p>在 SPH 模拟中，自引力的计算本质上是对每一个粒子求解其所受的引力合力。根据万有引力定律，任意两个粒子之间都存在引力作用，这意味着对于包含 $N$ 个粒子的系统，直接计算所有粒子对的相互作用需要 $O(N^2)$ 次运算。当天体物理模拟的粒子数量达到百万甚至千万级别时，这种直接求和的方式在计算量上将变得完全不可接受。&lt;/p>
&lt;p>这正是引入 &lt;strong>Tree Code（树代码）&lt;/strong> 的核心动机。树代码的核心思想源于一个朴素的物理直觉：当一个远方的粒子团簇距离我们足够远时，我们无需逐一计算团簇内每个粒子的贡献，而是可以将整个团簇近似视为一个位于其质心的等效质点。通过这种近似，计算复杂度可以从 $O(N^2)$ 大幅降低至 $O(N\log N)$。&lt;/p>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/treecode/concept.png"
width="297"
height="320"
srcset="https://yekq.top/posts/gasphia/treecode/concept_huc14661fad8e86edd86be0c107bea60bf_14013_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/treecode/concept_huc14661fad8e86edd86be0c107bea60bf_14013_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="基于树的引力计算概念图，来源于参考文献[2]"
class="gallery-image"
data-flex-grow="92"
data-flex-basis="222px"
>&lt;/p>
&lt;p>Tree Code（树代码）最初由 Barnes 和 Hut 提出[1]，因此基于该文献实现的树结构通常被称为 &lt;strong>Barnes-Hut Tree&lt;/strong>。GASPHiA 在面向 CUDA 架构实现这一数据结构时，借鉴了文献[2]中的并行实现方案。&lt;/p>
&lt;p>然而，文献[2]的方法直接服务于纯粹的 N 体模拟，其数据结构仅需满足自引力计算的需求；而 SPH 方法除了计算自引力外，还需依赖树结构进行高效的邻近粒子搜索。这一根本性的需求差异，导致 GASPHiA 最终的树代码结构与文献[2]存在显著不同。具体差异主要体现在两个方面：一是树节点中子节点（Child）数量的管理策略；二是在并行遍历树结构时的线程束投票机制——文献[2]仅需判定引力相互作用的条件，而 GASPHiA 作为SPH代码，必须额外融合邻近搜索的判定逻辑。&lt;/p>
&lt;p>尽管在实现细节上存在上述分歧，二者在核心的空间递归划分思路上仍保持高度一致。因此，读者仍可将文献[2]作为理解底层空间划分逻辑的重要参考。&lt;/p>
&lt;h2 id="效率对比与优化">效率对比与优化
&lt;/h2>&lt;h3 id="背景">背景
&lt;/h3>&lt;p>目前 GASPHiA 虽然已经实现了基于 Barnes-Hut Tree 的自引力计算。但是与文献[2]相比，在遍历树的时候，由于没有对粒子进行空间排序，因此效率肯定没有经过空间排序的效率高。为了直观展示 Barnes-Hut Tree 的威力，我们实现了暴力双循环计算自引力作为一个参考，同时以不使用空间排序的效率作为基准，探讨最终包含排序过程后的自引力计算的效率提升。&lt;/p>
&lt;h3 id="计算核函数">计算核函数
&lt;/h3>&lt;p>基于树的自引力计算核包含以下几个：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>重置树结构&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">SPHOctree&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">resetOctree&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">resetOctreeKernel&lt;/span>&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="n">numBlocks&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ThreadsPerBlock&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_child&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_count&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_start&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_sorted&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_com_mass&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_hmax&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_mutex&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">num_particles&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">max_nodes&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaGetLastError&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>计算粒子边界&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">SPHOctree&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">computeBoundingBox&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RealType4&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_particles&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">computeMin&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">d_particles&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">d_reduceTmp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">num_particles&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">d_bounding_box_min&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">computeMax&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">d_particles&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">d_reduceTmp&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">num_particles&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">d_bounding_box_max&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaDeviceSynchronize&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaGetLastError&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>自顶向下建立树&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">SPHOctree&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">buildTree&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">RealType4&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="n">d_positions&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">buildTreeKernel&lt;/span>&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="n">numBlocks&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ThreadsPerBlock&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">d_positions&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_com_mass&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_count&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_start&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_child&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_bounding_box_min&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_bounding_box_max&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">num_particles&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">max_nodes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaGetLastError&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaDeviceSynchronize&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>计算节点质心&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">void&lt;/span> &lt;span class="n">SPHOctree&lt;/span>&lt;span class="o">::&lt;/span>&lt;span class="n">computeCenterOfMass&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">computeCenterOfMassKernel&lt;/span>&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="n">numBlocks&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ThreadsPerBlock&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_com_mass&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_index&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">num_particles&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaGetLastError&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">CUDA_CHECK&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">cudaDeviceSynchronize&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>计算自引力&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="n">computeGravityKernel&lt;/span>&lt;span class="o">&amp;lt;&amp;lt;&amp;lt;&lt;/span>&lt;span class="n">numBlocks&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">ThreadsPerBlock&lt;/span>&lt;span class="o">&amp;gt;&amp;gt;&amp;gt;&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">d_positions&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_node_com_mass&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_child&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">d_accelerations&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_bounding_box_min&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">d_bounding_box_max&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">num_particles&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">theta&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">theta&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">this&lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="n">constG&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;/ol>
&lt;h3 id="性能瓶颈分析">性能瓶颈分析
&lt;/h3>&lt;p>根据文献[2]的实现，整个树代码流程中最耗时的部分在于最后一步——遍历树结构以计算自引力。其根本原因在于线程访问模式与数据空间分布的错配：若未对粒子数据进行显式排序，处于同一 Warp 中的粒子在物理空间上可能相距甚远。这种空间分布的离散性会直接导致 Warp 内部的线程在执行剪枝判定时产生严重分歧：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="kt">bool&lt;/span> &lt;span class="n">mac_satisfied&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">child&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">n&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">is_active&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">w_sq&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="n">theta_sq&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">r_sq&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">__all_sync&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mh">0xffffffff&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">mac_satisfied&lt;/span>&lt;span class="p">))&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 剪枝
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>当同一 Warp 内的不同粒子面对各自不同的目标节点时，部分线程可能满足剪枝条件（&lt;code>mac_satisfied&lt;/code> 为 true），而其余线程则仍需继续深入遍历（false）。由于 CUDA 的 Warp 执行遵循单指令多线程模型，所有线程必须同步执行同一指令路径。因此，只要 Warp 中存在任意一个线程不满足剪枝条件——即便多数线程本可以提前终止——整个 Warp 都必须进入后续的节点下钻流程。这种由线程间数据依赖差异导致的执行路径分叉，大幅削弱了剪枝机制的有效性，使得大量计算资源被耗费在不必要的深层节点访问上。&lt;/p>
&lt;h3 id="性能测试与对比">性能测试与对比
&lt;/h3>&lt;p>为验证上述分析，我先做了一个测试：通过离散一个正方体获得规则排列的粒子，采用单精度计算，离散得到 100 万个粒子进行测试。测试结果显示，遍历树计算引力确实是最耗时的步骤：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 规则排列（相当于粗略排序）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Calculating Gravity on GPU &lt;span class="o">(&lt;/span>Barnes-Hut, &lt;span class="nv">theta&lt;/span>&lt;span class="o">=&lt;/span>0.5&lt;span class="o">)&lt;/span>...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">--- Gravity Computation Profiling ---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> resetOctree: 11.6206 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> computeBoundingBox: 0.4268 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> buildTree: 45.4690 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> computeCoM: 0.1025 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> computeGravity: 2955.5117 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-------------------------------------
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 打乱后（无排序）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">--- Gravity Computation Profiling ---
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> resetOctree: 10.7203 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> computeBoundingBox: 0.4361 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> buildTree: 44.1606 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> computeCoM: 0.1303 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> computeGravity: 202.4100 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">-------------------------------------
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>关键发现：&lt;strong>时间差距达到 15 倍&lt;/strong>，区别仅仅在于是否把粒子打乱。如果不打乱，就相当于我们做了排序（因为初始的粒子排布就是规则的）；如果打乱，时间激增。&lt;/p>
&lt;p>详细的 Profile 数据如下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">指标名称&lt;/th>
&lt;th style="text-align:left">指标含义&lt;/th>
&lt;th style="text-align:center">规则排布&lt;/th>
&lt;th style="text-align:center">打乱排布&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Elapsed Cycles&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Kernel 执行消耗的总 GPU 时钟周期数&lt;/td>
&lt;td style="text-align:center">~0.29 Billion&lt;/td>
&lt;td style="text-align:center">~2.58 Billion&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Duration&lt;/strong>&lt;/td>
&lt;td style="text-align:left">Kernel 实际运行时间&lt;/td>
&lt;td style="text-align:center">~0.19 秒&lt;/td>
&lt;td style="text-align:center">~3.10 秒&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>SM Frequency&lt;/strong>&lt;/td>
&lt;td style="text-align:left">流式多处理器平均运行频率&lt;/td>
&lt;td style="text-align:center">~1.55 GHz&lt;/td>
&lt;td style="text-align:center">~830 MHz&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Compute (SM) Throughput&lt;/strong>&lt;/td>
&lt;td style="text-align:left">计算单元繁忙程度&lt;/td>
&lt;td style="text-align:center">~80.4%&lt;/td>
&lt;td style="text-align:center">~79.2%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Memory Throughput&lt;/strong>&lt;/td>
&lt;td style="text-align:left">内存子系统整体繁忙程度&lt;/td>
&lt;td style="text-align:center">~61.5%&lt;/td>
&lt;td style="text-align:center">~59.0%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;span style="color: red;">&lt;strong>L2 Cache Throughput&lt;/strong>&lt;/span>&lt;/td>
&lt;td style="text-align:left">&lt;span style="color: red;">L2 缓存访问吞吐率&lt;/span>&lt;/td>
&lt;td style="text-align:center">&lt;span style="color: red;">~23.9%&lt;/span>&lt;/td>
&lt;td style="text-align:center">&lt;span style="color: red;">~14.1%&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;span style="color: red;">&lt;strong>DRAM Throughput&lt;/strong>&lt;/span>&lt;/td>
&lt;td style="text-align:left">&lt;span style="color: red;">显存直接访问吞吐率&lt;/span>&lt;/td>
&lt;td style="text-align:center">&lt;span style="color: red;">~0.35%&lt;/span>&lt;/td>
&lt;td style="text-align:center">&lt;span style="color: red;">~15.7%&lt;/span>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>Achieved Occupancy&lt;/strong>&lt;/td>
&lt;td style="text-align:left">实际活跃 Warp 占比&lt;/td>
&lt;td style="text-align:center">~98.6%&lt;/td>
&lt;td style="text-align:center">~95.2%&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>从表中可以清晰看出，打乱后 &lt;strong>L2 Cache Throughput&lt;/strong> 从 23.9% 降至 14.1%，而 &lt;strong>DRAM Throughput&lt;/strong> 从 0.35% 增至 15.7%。这表明粒子排序对于提升数据局部性、充分利用 L2 缓存、减少显存直接访问至关重要，直接导致了 15 倍的性能差距。&lt;/p>
&lt;h3 id="排序算法">排序算法
&lt;/h3>&lt;p>上一小节通过对输入粒子进行 shuffle 发现：在 CUDA 上实现树算法时，粒子的**空间局部性（Spatial Locality）**起着决定性作用。局部性越好，同一个 Warp 内的 32 个线程在遍历八叉树时，就越容易访问相同的树节点，从而大幅减少 Warp 发散（Warp Divergence），并将 L1/L2 缓存的命中率推向极限，避免对极慢的 DRAM 产生大量直接读写。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>注意&lt;/strong>：这里我虽然用了“减少 Warp 发散（Warp Divergence）”的表达，但其实线程并没有发散，只是打开了过多的节点，某种意义上，相当于warp投票&lt;code>mac_satisfied&lt;/code>的不一致性较强，所以我还是借用了 Warp 发散的说法。&lt;/p>
&lt;/blockquote>
&lt;p>然而，在实际的 SPH 或 N-body 模拟中，随着时间的推移，粒子在空间中剧烈碰撞、混合，其在显存数组中的物理索引会与它们真实的几何位置彻底脱节。因此，我们需要在每一帧建树完成后，进行一次高效的空间排序（Spatial Sorting）。&lt;/p>
&lt;h4 id="1-核心思想利用树拓扑天然排序">1. 核心思想：利用树拓扑天然排序
&lt;/h4>&lt;p>既然八叉树本身就是对三维空间的完美网格化划分，那么按照遍历树的顺序（例如深度优先 DFS 或广度优先 BFS）依次读取叶子节点，得到的就是在三维空间中聚拢的粒子序列。&lt;/p>
&lt;h4 id="2-gather-寻址策略只排索引不搬数据">2. Gather 寻址策略：只排索引，不搬数据
&lt;/h4>&lt;p>在拥有数百万粒子的系统中，如果每次都在全局显存中来回拷贝几十 MB 的粒子坐标（float4 数据），不仅极其耗时，还会额外占用大量显存。采用 Gather 寻址策略:&lt;/p>
&lt;ul>
&lt;li>不移动粒子数据在显存中的排布位置&lt;/li>
&lt;li>生成一个一维的映射数组 &lt;code>sort&lt;/code>&lt;/li>
&lt;li>&lt;code>sort[i]&lt;/code> 存放的是&amp;quot;第 i 个线程应该去处理的真实粒子编号&amp;quot;&lt;/li>
&lt;/ul>
&lt;p>在后续的引力计算中，同一个 Warp 内的相邻线程将读取 &lt;code>sort&lt;/code> 数组中相邻的值，这样同一个warp处理的粒子在空间上都相近了，他们遍历树的路径也大概率会一致。&lt;/p>
&lt;h4 id="3-自顶向下排序">3. 自顶向下排序
&lt;/h4>&lt;p>为了在 GPU 上极速完成排序，我们利用建树阶段已经统计好的 &lt;code>count&lt;/code>（子树粒子数），采用自顶向下的并行分配机制。父节点会根据各个子节点的 &lt;code>count&lt;/code>，为它们在 &lt;code>sort&lt;/code> 数组中划分好专属的&amp;quot;内存区间&amp;quot;。&lt;/p>
&lt;h4 id="4-profile-验证">4. Profile 验证
&lt;/h4>&lt;p>理论上，经过树排序后遍历，路径分化会进一步降低，因为同一个 Warp 的粒子的父节点几乎都在一起。为了量化这种提升，我将粒子数降到 100 万，对以下三种计算模式进行 Profile 分析：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">计算模式&lt;/th>
&lt;th style="text-align:left">描述&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">1. 完全随机&lt;/td>
&lt;td style="text-align:left">不使用树排序，粒子顺序在空间上随机分布&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">2. 初始规则&lt;/td>
&lt;td style="text-align:left">不使用树排序，粒子顺序在空间上规则排列&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">3. 树排序&lt;/td>
&lt;td style="text-align:left">树排序，粒子顺序在空间上随机分布&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Profile 命令：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">ncu --set full -f -o profile/no_shuffle_no_sort_profile -k computeGravityKernel -s &lt;span class="m">2&lt;/span> -c &lt;span class="m">2&lt;/span> ./sph_simulator
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>命令参数说明：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">参数&lt;/th>
&lt;th style="text-align:left">含义&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;code>--set full&lt;/code>&lt;/td>
&lt;td style="text-align:left">收集所有可用的性能指标&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>-f&lt;/code>&lt;/td>
&lt;td style="text-align:left">强制覆盖已存在的输出文件&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>-o profile/xxx&lt;/code>&lt;/td>
&lt;td style="text-align:left">指定输出文件的路径和名称&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>-k computeGravityKernel&lt;/code>&lt;/td>
&lt;td style="text-align:left">指定要 Profile 的 Kernel 名称&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>-s 2&lt;/code>&lt;/td>
&lt;td style="text-align:left">在程序启动后跳过前 2 次迭代再开始 Profile（避免冷启动影响）&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;code>-c 2&lt;/code>&lt;/td>
&lt;td style="text-align:left">重复执行 2 次取平均（减少测量误差）&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>结果对比如下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">计算模式&lt;/th>
&lt;th style="text-align:center">核心耗时&lt;/th>
&lt;th style="text-align:center">执行指令数&lt;/th>
&lt;th style="text-align:center">L1/TEX 命中率&lt;/th>
&lt;th style="text-align:center">内存吞吐量&lt;/th>
&lt;th style="text-align:center">Executed IPC&lt;/th>
&lt;th style="text-align:center">Warp 均指令周期 (CPI)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">1. 完全随机&lt;/td>
&lt;td style="text-align:center">33.13 ms&lt;/td>
&lt;td style="text-align:center">246.2 亿&lt;/td>
&lt;td style="text-align:center">48.23%&lt;/td>
&lt;td style="text-align:center">2.37 GB/s&lt;/td>
&lt;td style="text-align:center">2.97&lt;/td>
&lt;td style="text-align:center">13.75 周期&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">2. 初始规则&lt;/td>
&lt;td style="text-align:center">5.13 ms&lt;/td>
&lt;td style="text-align:center">41.9 亿&lt;/td>
&lt;td style="text-align:center">57.52%&lt;/td>
&lt;td style="text-align:center">8.14 GB/s&lt;/td>
&lt;td style="text-align:center">2.99&lt;/td>
&lt;td style="text-align:center">13.44 周期&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">3. 树排序&lt;/td>
&lt;td style="text-align:center">3.11 ms&lt;/td>
&lt;td style="text-align:center">22.1 亿&lt;/td>
&lt;td style="text-align:center">58.66%&lt;/td>
&lt;td style="text-align:center">12.25 GB/s&lt;/td>
&lt;td style="text-align:center">2.74&lt;/td>
&lt;td style="text-align:center">13.28 周期&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>从结果可以看出，&lt;strong>树排序模式（3.11 ms）比完全随机模式（33.13 ms）快了约 10 倍&lt;/strong>，且优于初始规则模式（5.13 ms）。这证明了基于树拓扑的空间排序策略能够显著提升自引力计算的效率。&lt;/p>
&lt;p>现在，GASPHiA使用了基于树排序模式优化后的代码。&lt;/p>
&lt;h3 id="性能曲线">性能曲线
&lt;/h3>&lt;p>下面两图展示了在不同粒子数量下，各计算模式的性能对比（线性坐标与对数坐标）：&lt;/p>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/treecode/performance_plot_linear.png"
width="1000"
height="700"
srcset="https://yekq.top/posts/gasphia/treecode/performance_plot_linear_huafae19ad656c60aef7f71be576aca324_80463_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/treecode/performance_plot_linear_huafae19ad656c60aef7f71be576aca324_80463_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="效率验证线性坐标系"
class="gallery-image"
data-flex-grow="142"
data-flex-basis="342px"
>&lt;/p>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/treecode/performance_plot.png"
width="1000"
height="700"
srcset="https://yekq.top/posts/gasphia/treecode/performance_plot_hu64e62595c1a297bc5695bbbe5a2bbf85_98245_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/treecode/performance_plot_hu64e62595c1a297bc5695bbbe5a2bbf85_98245_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="效率验证对数坐标系"
class="gallery-image"
data-flex-grow="142"
data-flex-basis="342px"
>&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>注意&lt;/strong>：上图中，&amp;ldquo;构建树&amp;quot;包含了计算包围盒、排序等所有流程，相较于计算引力或者邻居搜索，建立树的耗时可以忽略。&lt;/p>
&lt;/blockquote>
&lt;h3 id="精度与加速比验证">精度与加速比验证
&lt;/h3>&lt;p>我还测试了 100 万粒子情况下，Barnes-Hut 算法相对于暴力计算的加速比与误差。注意这里的误差都是相对误差。&lt;/p>
&lt;p>&lt;strong>不同 $\theta$ 参数下的性能与精度对比&lt;/strong>：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:center">$\theta$&lt;/th>
&lt;th style="text-align:center">计算时间 (ms)&lt;/th>
&lt;th style="text-align:center">最大相对误差&lt;/th>
&lt;th style="text-align:center">平均相对误差&lt;/th>
&lt;th style="text-align:center">加速比&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:center">0.1&lt;/td>
&lt;td style="text-align:center">181.877&lt;/td>
&lt;td style="text-align:center">0.00219153&lt;/td>
&lt;td style="text-align:center">3.27768e-05&lt;/td>
&lt;td style="text-align:center">7.42231x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.2&lt;/td>
&lt;td style="text-align:center">39.5222&lt;/td>
&lt;td style="text-align:center">0.00502609&lt;/td>
&lt;td style="text-align:center">6.86399e-05&lt;/td>
&lt;td style="text-align:center">34.1566x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.3&lt;/td>
&lt;td style="text-align:center">14.9356&lt;/td>
&lt;td style="text-align:center">0.0214612&lt;/td>
&lt;td style="text-align:center">0.00015996&lt;/td>
&lt;td style="text-align:center">90.3847x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.4&lt;/td>
&lt;td style="text-align:center">8.75622&lt;/td>
&lt;td style="text-align:center">0.0335255&lt;/td>
&lt;td style="text-align:center">0.00028779&lt;/td>
&lt;td style="text-align:center">154.17x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.5&lt;/td>
&lt;td style="text-align:center">5.88032&lt;/td>
&lt;td style="text-align:center">0.0711218&lt;/td>
&lt;td style="text-align:center">0.00053247&lt;/td>
&lt;td style="text-align:center">229.57x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.6&lt;/td>
&lt;td style="text-align:center">4.20147&lt;/td>
&lt;td style="text-align:center">0.0565245&lt;/td>
&lt;td style="text-align:center">0.000783245&lt;/td>
&lt;td style="text-align:center">321.303x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.7&lt;/td>
&lt;td style="text-align:center">3.52717&lt;/td>
&lt;td style="text-align:center">0.106591&lt;/td>
&lt;td style="text-align:center">0.00153542&lt;/td>
&lt;td style="text-align:center">382.728x&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:center">0.8&lt;/td>
&lt;td style="text-align:center">3.21018&lt;/td>
&lt;td style="text-align:center">0.170889&lt;/td>
&lt;td style="text-align:center">0.002965&lt;/td>
&lt;td style="text-align:center">420.521x&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>精度验证图：&lt;/p>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/treecode/Accuracy.png"
width="1000"
height="600"
srcset="https://yekq.top/posts/gasphia/treecode/Accuracy_hu2514c82add25ddfeced948d86ddae09b_66968_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/treecode/Accuracy_hu2514c82add25ddfeced948d86ddae09b_66968_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="精度验证"
class="gallery-image"
data-flex-grow="166"
data-flex-basis="400px"
>&lt;/p>
&lt;p>&lt;strong>结论&lt;/strong>：随着 $\theta$ 增大，计算精度下降（误差增大），但计算速度显著提升。在 $\theta = 0.5$ 时，可以在 200x 加速的同时保持约 7% 的最大相对误差，是较为理想的平衡点。&lt;/p>
&lt;h2 id="参考资料">参考资料
&lt;/h2>&lt;p>[1] Barnes J, Hut P. A hierarchical O (N log N) force-calculation algorithm. nature. 1986 Dec 4;324(6096):446-9.&lt;/p>
&lt;p>[2] Burtscher M, Pingali K. An efficient CUDA implementation of the tree-based barnes hut n-body algorithm. In GPU computing Gems Emerald edition 2011 Jan 1 (pp. 75-92). Morgan Kaufmann.&lt;/p></description></item><item><title>P-alpha 孔隙度模型的实现与踩坑记录</title><link>https://yekq.top/posts/gasphia/p-alpha-porosity-model/</link><pubDate>Tue, 07 Apr 2026 14:00:00 +0800</pubDate><author>plloningye@gmail.com (Keqi Ye)</author><guid>https://yekq.top/posts/gasphia/p-alpha-porosity-model/</guid><description>&lt;h1 id="p-alpha-孔隙度模型的实现与踩的一些坑">P-alpha 孔隙度模型的实现与踩的一些坑
&lt;/h1>&lt;iframe src="//player.bilibili.com/player.html?isOutside=true&amp;bvid=BV1t7DeBiEEv&amp;p=1&amp;high_quality=1&amp;danmaku=0"
scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"
style="width: 100%; aspect-ratio: 16/9; border-radius: 8px; margin-bottom: 20px;">
&lt;/iframe>
&lt;p align="center" style="font-size: 0.9em; color: gray; margin-top: -10px; margin-bottom: 20px;">
&lt;em> 高孔隙度浮石遭受撞击模拟（2.58 km/s）动画中完全损伤的粒子被移除，模拟只启用了核矫正&lt;/em>
&lt;/p>
&lt;hr>
&lt;p>GASPHiA 实现了以下两篇论文描述的 P-alpha 模型：&lt;/p>
&lt;ul>
&lt;li>Numerical simulations of impacts involving porous bodies I. Implementing sub-resolution porosity in a 3D SPH hydrocode&lt;/li>
&lt;li>Numerical simulations of impacts involving porous bodies: II. Comparison with laboratory experiments&lt;/li>
&lt;/ul>
&lt;p>下面就实现过程遇到的问题做一个笔记。&lt;/p>
&lt;hr>
&lt;h2 id="1-物理问题定义p-alpha-模型">1. 物理问题定义：P-alpha 模型
&lt;/h2>&lt;p>在多孔材料（如rubble pile、浮石）的冲击动力学中，宏观压力 $P$ 与固相压力 $P_{\text{eos}}$ 以及孔隙度 $\alpha$（也称&lt;strong>膨胀度&lt;/strong>）满足关系：&lt;/p>
$$P = \frac{P_{\text{eos}}(\rho_s, e)}{\alpha}$$
&lt;p>其中 $\rho_s = \alpha \rho$ 是&lt;strong>固相密度&lt;/strong>，$\rho$ 是&lt;strong>宏观密度&lt;/strong>，$e$ 是&lt;strong>内能&lt;/strong>。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>注意&lt;/strong>：$\alpha$ 必须大于 1，等于 1 代表这个粒子代表的空间没有空隙。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h3 id="压实曲线-compaction-curve">压实曲线 (Compaction Curve)
&lt;/h3>&lt;p>材料的压缩过程遵循压实曲线 $\alpha_{\text{curve}}(P)$，定义如下：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">阶段&lt;/th>
&lt;th style="text-align:center">压力范围&lt;/th>
&lt;th style="text-align:left">公式&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>弹性阶段&lt;/strong>&lt;/td>
&lt;td style="text-align:center">$P \le P_e$&lt;/td>
&lt;td style="text-align:left">$\alpha = \alpha_0$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>塑性压溃阶段&lt;/strong>&lt;/td>
&lt;td style="text-align:center">$P_e &amp;lt; P &amp;lt; P_s$&lt;/td>
&lt;td style="text-align:left">$\displaystyle \alpha = 1.0 + (\alpha_0 - 1.0) \left( \frac{P_s - P}{P_s - P_e} \right)^2$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">&lt;strong>完全压实阶段&lt;/strong>&lt;/td>
&lt;td style="text-align:center">$P \ge P_s$&lt;/td>
&lt;td style="text-align:left">$\alpha = 1.0$&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>另外需要注意，孔隙压实是塑性不可逆的。若当前压力导致的理论 $\alpha$ 大于历史最小孔隙度 $\alpha_{\text{old}}$，则取 $\alpha = \alpha_{\text{old}}$。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="2-非线性方程">2. 非线性方程
&lt;/h2>&lt;p>我们需要寻找一个 $\alpha$，使得它既满足状态方程（EOS）产生的压力，又落在压实曲线上。定义目标函数：&lt;/p>
$$F(\alpha) = \alpha - \min\left( \alpha_{\text{curve}}\left( \frac{P_{\text{eos}}(\alpha \rho, e)}{\alpha} \right), \alpha_{\text{old}} \right) = 0$$
&lt;blockquote>
&lt;p>式中，$\alpha_{\text{curve}}$ 指的就是压实曲线。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="3-二分法-bisection-method">3. 二分法 (Bisection Method)
&lt;/h2>&lt;p>最简单直观想到的方案就是二分，只是二分法的运行时间可能会稍长一点。&lt;/p>
&lt;h3 id="31-迭代次数分析">3.1 迭代次数分析
&lt;/h3>&lt;p>假设对于一个高孔隙材料，初始孔隙度 $\alpha = 4$，对应的物理孔隙率为 $\phi = 1 - 1/\alpha$，即 &lt;strong>75%&lt;/strong>。如果要求迭代最后的收敛精度：&lt;/p>
$$\alpha_{n+1} - \alpha_n &lt; \text{tol} = 10^{-12}$$
&lt;p>那么最坏的情况下需要迭代：&lt;/p>
$$n \ge \log_2 \left( \frac{L_0}{\text{tol}} \right) = \log_2 \left( \frac{4-1}{10^{-12}} \right)=42$$
&lt;h3 id="32-精度要求">3.2 精度要求
&lt;/h3>&lt;p>值得注意的是：EOS 对密度极度敏感，实际测试过 &lt;strong>tol 必须小于 1e-7&lt;/strong>，这样损伤场才不会产生虚假震荡。这对单精度计算来说是很难达到的，因此GASPH iA的孔隙度模型强制运行在双精度上，但是写回的时候会进行精度调整，适配整体的计算流程。&lt;/p>
&lt;h3 id="33-性能测试结果">3.3 性能测试结果
&lt;/h3>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ grep &lt;span class="s2">&amp;#34;calPressureSoundSpeed execution time:&amp;#34;&lt;/span> run.log &lt;span class="p">|&lt;/span> tail -n &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.882 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.807 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.775 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.766 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.822 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.873 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.766 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.765 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.753 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.774 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.776 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.756 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 3.197 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.786 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.854 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.885 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.816 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.799 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.787 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.891 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;span class="lnt">9
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ tail -n &lt;span class="m">10&lt;/span> run.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Sub2: 0.050 ms &lt;span class="o">(&lt;/span> 2.3%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed execution time: 2.891 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>Step 2273&lt;span class="o">]&lt;/span> &lt;span class="nv">t&lt;/span>&lt;span class="o">=&lt;/span>3.200656e-05 &lt;span class="p">|&lt;/span> &lt;span class="nv">dt&lt;/span>&lt;span class="o">=&lt;/span>1.761e-08 &lt;span class="p">|&lt;/span> &lt;span class="nv">Tree&lt;/span>&lt;span class="o">=&lt;/span>5.467 ms &lt;span class="o">(&lt;/span>B: 0.536, S: 4.931, G: 0.000&lt;span class="o">)&lt;/span> &lt;span class="p">|&lt;/span> &lt;span class="nv">Step&lt;/span>&lt;span class="o">=&lt;/span>15.816 ms &lt;span class="p">|&lt;/span> &lt;span class="nv">Outputs&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">7&lt;/span> &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>computeRHS&lt;span class="o">]&lt;/span> Total: 2.263 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ Corr: 0.956 ms &lt;span class="o">(&lt;/span>42.3%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ Sub1: 1.256 ms &lt;span class="o">(&lt;/span>55.5%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Sub2: 0.050 ms &lt;span class="o">(&lt;/span> 2.2%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>computeRHS&lt;span class="o">]&lt;/span> Total: 2.227 ms
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>二分法求解 EOS 的运行时间是不可接受的，已经要和计算右端项（单精度）的时间持平了，我们需要收敛更快的算法。&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="4-牛顿-拉夫逊法-newton-raphson">4. 牛顿-拉夫逊法 (Newton-Raphson)
&lt;/h2>&lt;p>牛顿-拉夫逊法是一种高效的非线性方程求根近似算法。对于一般方程 $f(x) = 0$，假设已知其近似根 $x_n$ 且导数 $f&amp;rsquo;(x_n) \neq 0$，该方法通过在 $(x_n, f(x_n))$ 处作曲线的切线，用切线与 x 轴的交点作为下一个近似根。其标准的迭代格式为：&lt;/p>
$$x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)}$$
&lt;p>牛顿法在根的附近具有&lt;strong>二阶收敛&lt;/strong>的优良特性，即每次迭代的有效数字大约会翻倍，收敛速度远快于二分法。&lt;/p>
&lt;p>回到我们的孔隙度模型中，我们要求解的目标方程是 $F(\alpha) = 0$，因此对应的牛顿迭代格式即为：&lt;/p>
$$\alpha_{k+1} = \alpha_k - \frac{F(\alpha_k)}{F'(\alpha_k)}$$
&lt;p>为了实现这一迭代过程，核心在于计算目标方程对孔隙度的非线性导数 $F&amp;rsquo;(\alpha) = \frac{dF}{d\alpha}$。利用链式法则将其展开：&lt;/p>
$$\frac{dF}{d\alpha} = 1 - \frac{d\alpha_{\text{curve}}}{dP} \cdot \frac{dP}{d\alpha}$$
&lt;hr>
&lt;h3 id="41-压实曲线导数-displaystyle-fracdalpha_textcurvedp">4.1 压实曲线导数 $\displaystyle \frac{d\alpha_{\text{curve}}}{dP}$
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">阶段&lt;/th>
&lt;th style="text-align:left">导数&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">弹性或完全压实阶段，或处于&lt;strong>卸载状态&lt;/strong>（$\alpha_{\text{curve}} &amp;gt; \alpha_{\text{old}}$）&lt;/td>
&lt;td style="text-align:left">$\displaystyle \frac{d\alpha_{\text{curve}}}{dP} = 0$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">塑性压溃阶段且处于&lt;strong>加载状态&lt;/strong>时&lt;/td>
&lt;td style="text-align:left">$\displaystyle \frac{d\alpha_{\text{curve}}}{dP} = -2 \frac{(\alpha_0 - 1.0)(P_s - P)}{(P_s - P_e)^2}$&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h3 id="42-压力对孔隙度导数-displaystyle-fracdpdalpha">4.2 压力对孔隙度导数 $\displaystyle \frac{dP}{d\alpha}$
&lt;/h3>&lt;p>已知 $P = \frac{P_{\text{eos}}(\alpha \rho, e)}{\alpha}$，对 $\alpha$ 求导：&lt;/p>
$$\frac{dP}{d\alpha} = \frac{\alpha \cdot \frac{d P_{\text{eos}}}{d\alpha} - P_{\text{eos}}}{\alpha^2}$$
&lt;p>根据链式法则，$\frac{d P_{\text{eos}}}{d\alpha} = \frac{\partial P_{\text{eos}}}{\partial \rho_s} \cdot \frac{d \rho_s}{d\alpha} = \frac{\partial P_{\text{eos}}}{\partial \rho_s} \cdot \rho$。&lt;/p>
&lt;p>定义 $\frac{\partial P_{\text{eos}}}{\partial \rho_s}$ 为 &lt;code>dpdrho&lt;/code>（由 Tillotson EOS 直接提供），则：&lt;/p>
$$\frac{dP}{d\alpha} = \frac{\text{dpdrho} \cdot \rho}{\alpha} - \frac{P}{\alpha}$$
&lt;hr>
&lt;h2 id="5-遇到的挑战震荡">5. 遇到的挑战：震荡
&lt;/h2>&lt;p>&lt;strong>在撞击瞬间，粒子可能处于物理分界线（如弹性极限 $P_e$）附近。&lt;/strong>&lt;/p>
&lt;h3 id="51-问题描述">5.1 问题描述
&lt;/h3>&lt;ol>
&lt;li>&lt;strong>加载步&lt;/strong>：压力大 $\to$ 导数大 $\to$ 牛顿步过大 $\to$ 跨过分界线进入卸载区。&lt;/li>
&lt;li>&lt;strong>卸载步&lt;/strong>：进入卸载区 $\to$ 导数突变为 $0$ $\to$ 修正步直接弹回加载区。&lt;/li>
&lt;/ol>
&lt;blockquote>
&lt;p>这种由于导数不连续导致的变化造成了牛顿法在两个点之间&lt;strong>无限循环&lt;/strong>，无法收敛。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/p-alpha-porosity-model/ns_pingpong.png"
width="1398"
height="921"
srcset="https://yekq.top/posts/gasphia/p-alpha-porosity-model/ns_pingpong_hu91d1ca08882b7d836d9e5640233cbf66_66110_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/p-alpha-porosity-model/ns_pingpong_hu91d1ca08882b7d836d9e5640233cbf66_66110_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="牛顿迭代震荡"
class="gallery-image"
data-flex-grow="151"
data-flex-basis="364px"
>&lt;/p>
&lt;p>图中展示的就是模拟初期，某些粒子受到一点点压力之后，牛顿迭代一直震荡无法收敛的情况。&lt;/p>
&lt;hr>
&lt;h2 id="6-解决方案安全牛顿法">6. 解决方案：安全牛顿法
&lt;/h2>&lt;p>为了兼顾牛顿法的速度与二分法的稳定性，引入了&lt;strong>动态区间收缩&lt;/strong>的混合算法。&lt;/p>
&lt;h3 id="61-算法逻辑">6.1 算法逻辑
&lt;/h3>&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:left">步骤&lt;/th>
&lt;th style="text-align:left">操作&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:left">1&lt;/td>
&lt;td style="text-align:left">&lt;strong>维护区间&lt;/strong>：初始化安全区间 $[a, b] = [1.0, \alpha_{\text{old}}]$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">2&lt;/td>
&lt;td style="text-align:left">&lt;strong>区间收紧&lt;/strong>：若 $F(\alpha_k) &amp;gt; 0$，则令 $b = \alpha_k$；若 $F(\alpha_k) &amp;lt; 0$，则令 $a = \alpha_k$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">3&lt;/td>
&lt;td style="text-align:left">&lt;strong>决策拦截&lt;/strong>：计算牛顿步 $\alpha_{\text{new}} = \alpha_k - \frac{F(\alpha_k)}{F&amp;rsquo;(\alpha_k)}$，若 $\alpha_{\text{new}}$ 落在开区间 $(a, b)$ 之外，说明牛顿法失效&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">4&lt;/td>
&lt;td style="text-align:left">&lt;strong>降级处理&lt;/strong>：此时强行执行二分法 $\alpha_{\text{new}} = \frac{a + b}{2}$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:left">5&lt;/td>
&lt;td style="text-align:left">&lt;strong>保底策略&lt;/strong>：要是牛顿法在规定的迭代步中没有找到解，那么求解器会调用二分法求解&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/p-alpha-porosity-model/ns_final.png"
width="1400"
height="921"
srcset="https://yekq.top/posts/gasphia/p-alpha-porosity-model/ns_final_hu058d968be20c8fd3b031241422de0152_83434_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/p-alpha-porosity-model/ns_final_hu058d968be20c8fd3b031241422de0152_83434_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="安全牛顿迭代"
class="gallery-image"
data-flex-grow="152"
data-flex-basis="364px"
>&lt;/p>
&lt;blockquote>
&lt;p>在相同的初始条件下，相比于震荡情况，现在求解器可以准确跳出震荡区间找到解了。&lt;/p>
&lt;/blockquote>
&lt;h3 id="62-性能对比">6.2 性能对比
&lt;/h3>&lt;p>效率方面比二分法的 2.8ms 左右，&lt;strong>快了十倍&lt;/strong>。相较于求解 SPH 右端项函数的运行时间 2.4ms 左右，EOS 只需要花费其十分之一。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ grep &lt;span class="s2">&amp;#34;calPressureSoundSpeed&amp;#34;&lt;/span> run.log &lt;span class="p">|&lt;/span> tail -n &lt;span class="m">20&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.237 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.237 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.236 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.236 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.228 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.237 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.242 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.234 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.240 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.231 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.234 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.234 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.233 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.238 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.229 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.236 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.238 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.236 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.239 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.231 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ tail -n &lt;span class="m">10&lt;/span> run.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Sub2: 0.050 ms &lt;span class="o">(&lt;/span> 2.3%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="o">[&lt;/span>computeRHS&lt;span class="o">]&lt;/span> Total: 2.420 ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ Press: 0.000 ms &lt;span class="o">(&lt;/span> 0.0%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ Corr: 1.026 ms &lt;span class="o">(&lt;/span>42.4%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ Sub1: 1.352 ms &lt;span class="o">(&lt;/span>55.9%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ Sub2: 0.042 ms &lt;span class="o">(&lt;/span> 1.7%&lt;span class="o">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">calPressureSoundSpeed_newton execution time: 0.207 ms
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;hr>
&lt;h2 id="7-最终成果与物理验证">7. 最终成果与物理验证
&lt;/h2>&lt;h3 id="71-压实曲线验证">7.1 压实曲线验证
&lt;/h3>&lt;p>&lt;img src="https://yekq.top/posts/gasphia/p-alpha-porosity-model/crushcurvepng.png"
width="1425"
height="895"
srcset="https://yekq.top/posts/gasphia/p-alpha-porosity-model/crushcurvepng_hu50877c013649b28d691f95bdd30bec23_52726_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/p-alpha-porosity-model/crushcurvepng_hu50877c013649b28d691f95bdd30bec23_52726_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="模拟成功复现了压实曲线"
class="gallery-image"
data-flex-grow="159"
data-flex-basis="382px"
>&lt;/p>
&lt;p>图中红色虚线为理论压实曲线，散点为 GASPHiA 的模拟输出。随时间推进，压力导致孔隙正确压实；在压力卸载后，孔隙度严格保持不变，未出现非物理的回弹现象。这从底层印证了 P-alpha 模型的不可逆逻辑实现完全准确。&lt;/p>
&lt;hr>
&lt;h3 id="72-宏观实验对比">7.2 宏观实验对比
&lt;/h3>&lt;p>&lt;img src="https://yekq.top/posts/gasphia/p-alpha-porosity-model/compare.png"
width="3204"
height="1090"
srcset="https://yekq.top/posts/gasphia/p-alpha-porosity-model/compare_hu7a518e0164ca7d0f345b86db39d3ade9_1853758_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/p-alpha-porosity-model/compare_hu7a518e0164ca7d0f345b86db39d3ade9_1853758_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="对论文中撞击结果的复现和比较"
class="gallery-image"
data-flex-grow="293"
data-flex-basis="705px"
>&lt;/p>
&lt;ul>
&lt;li>图 a, b：Jutzi et al. 2009 论文中的实验结果和基准模拟结果。&lt;/li>
&lt;li>图 c：GASPHiA 的模拟结果。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>可以看到，GASPHiA 输出的物理形态与原始实验及基准论文高度吻合。&lt;/p>
&lt;/blockquote>
&lt;p>&lt;img src="https://yekq.top/posts/gasphia/p-alpha-porosity-model/porous_cm.png"
width="472"
height="372"
srcset="https://yekq.top/posts/gasphia/p-alpha-porosity-model/porous_cm_hu390938432d188224f48d787e440b4964_41680_480x0_resize_box_3.png 480w, https://yekq.top/posts/gasphia/p-alpha-porosity-model/porous_cm_hu390938432d188224f48d787e440b4964_41680_1024x0_resize_box_3.png 1024w"
loading="lazy"
alt="碎片质量累积分布曲线对比"
class="gallery-image"
data-flex-grow="126"
data-flex-basis="304px"
>&lt;/p>
&lt;p>对撞击结果进行后处理分析，提取碎片累积质量分布数据：&lt;/p>
&lt;blockquote>
&lt;p>GASPHiA 计算得到的最大残余碎片质量占比为 8.35%，与实验室给出的真实实验数据 9.96% 误差极小，进一步验证了整个多孔材料求解器核心计算逻辑的可靠性。&lt;/p>
&lt;/blockquote></description></item><item><title>SPH基础(四): 状态方程与压力计算</title><link>https://yekq.top/posts/sphseries/4-equation-of-state/</link><pubDate>Sun, 25 May 2025 00:00:00 +0000</pubDate><author>plloningye@gmail.com (Keqi Ye)</author><guid>https://yekq.top/posts/sphseries/4-equation-of-state/</guid><description>&lt;p>在 SPH 方法中，状态方程（Equation of State, EOS）用于将粒子的密度与压强建立联系，进而计算作用于粒子间的力。&lt;/p>
&lt;p>最常用的状态方程是 Tait 方程，其形式为：&lt;/p>
$$
P = B\left[\left(\frac{\rho}{\rho_0}\right)^\gamma - 1\right]
$$
&lt;p>其中：&lt;/p>
&lt;ul>
&lt;li>\( \rho \) 为当前密度，&lt;/li>
&lt;li>\( \rho_0 \) 为参考密度，&lt;/li>
&lt;li>\( \gamma \) 为多项式指数（通常为 7），&lt;/li>
&lt;li>\( B \) 为常数，决定压缩性。&lt;/li>
&lt;/ul>
&lt;p>合理选取 EOS 参数对于模拟结果的稳定性和准确性至关重要。&lt;/p></description></item><item><title>SPH基础(五): Runge-Kutta自适应时间积分</title><link>https://yekq.top/posts/sphseries/5-adaptive-timestepping/</link><pubDate>Fri, 24 May 2024 11:00:00 +0800</pubDate><author>plloningye@gmail.com (Keqi Ye)</author><guid>https://yekq.top/posts/sphseries/5-adaptive-timestepping/</guid><description>&lt;h2 id="1-引言">1. 引言
&lt;/h2>&lt;p>在流体动力学乃至更广泛的科学计算领域中，光滑粒子流体动力学（Smoothed Particle Hydrodynamics, SPH）是一种强大的无网格拉格朗日方法。SPH模拟的核心之一是对描述系统演化的常微分方程组（ODEs）进行时间积分，以更新每个粒子的物理状态（如位置、速度、内能等）。&lt;/p>
&lt;p>传统的时间积分方案（如简单的欧拉法或固定步长的龙格-库塔法）虽然实现简单，但在处理复杂动态过程时面临效率与稳定性的两难困境：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>步长过大&lt;/strong>：可能导致数值不稳定，模拟结果迅速发散，功亏一篑。&lt;/li>
&lt;li>&lt;strong>步长过小&lt;/strong>：虽然能保证稳定性，但会极大地增加计算成本，尤其是在模拟过程相对平稳、变化缓慢的阶段，造成了不必要的资源浪费。&lt;/li>
&lt;/ul>
&lt;p>为了在保证计算精度的同时最大化效率，&lt;strong>自适应步长（Adaptive Time-Stepping）&lt;/strong> 的积分方法应运而生。本文将详细介绍自适应步长的核心思想，重点讲解龙格-库塔（Runge-Kutta）方法家族中的 &lt;strong>RK23&lt;/strong> 和 &lt;strong>RK45&lt;/strong> 算法，并阐述如何将其与 SPH 的物理计算流程相结合。&lt;/p>
&lt;h2 id="2-龙格-库塔runge-kutta方法简介">2. 龙格-库塔（Runge-Kutta）方法简介
&lt;/h2>&lt;h3 id="21-广义龙格-库塔方法">2.1 广义龙格-库塔方法
&lt;/h3>&lt;p>龙格-库塔方法是一类用于求解常微分方程的、应用广泛的显式和隐式迭代法。其核心思想是通过在当前时间步内评估多个“中间”斜率，并用这些斜率的加权平均值来更新解，从而获得比简单欧拉法更高的精度。&lt;/p>
&lt;p>对于一个形如 $\frac{dy}{dt} = f(t, y)$ 的初值问题，一个 $s$ 级的显式RK方法可以表示为：
&lt;/p>
$$
\begin{aligned}
k_1 &amp;= f(t_n, y_n) \\
k_2 &amp;= f(t_n + c_2 h, y_n + h a_{21} k_1) \\
k_3 &amp;= f(t_n + c_3 h, y_n + h (a_{31} k_1 + a_{32} k_2)) \\
&amp;\vdots \\
k_s &amp;= f(t_n + c_s h, y_n + h \sum_{j=1}^{s-1} a_{sj} k_j)
\end{aligned}
$$
&lt;p>
最终的解通过这些中间斜率的加权和来计算：
&lt;/p>
$$
y_{n+1} = y_n + h \sum_{i=1}^{s} b_i k_i
$$
&lt;p>
其中 $h$ 是时间步长，系数 $a_{ij}$, $c_i$, 和 $b_i$ 是预先确定的常数，它们的选择决定了方法的精度阶数和稳定性。例如，经典的四阶RK方法（RK4）就是这个家族的一个特例。&lt;/p>
&lt;h3 id="22-嵌入式rk方法自适应步长的关键">2.2 嵌入式RK方法：自适应步长的关键
&lt;/h3>&lt;p>固定步长的RK方法虽然精度高，但无法感知模拟过程的动态变化。自适应步长的精髓在于“在积分的同时估计误差”。&lt;strong>嵌入式龙格-库塔方法（Embedded Runge-Kutta Methods）&lt;/strong> 正是为此而生。&lt;/p>
&lt;p>这类方法（也称 RKF 或 Fehlberg 方法）通过一组精心设计的系数，在一次计算中同时得到两个不同阶数的解：&lt;/p>
&lt;ul>
&lt;li>一个 $p$ 阶精度的解 $y_{n+1}$。&lt;/li>
&lt;li>一个 $p-1$ 阶精度的嵌入解 $\hat{y}_{n+1}$。&lt;/li>
&lt;/ul>
&lt;p>这两个解共享大部分（甚至全部）的 $k_i$ 计算，因此额外开销很小。它们的差值则可以作为局部截断误差 $E_{n+1}$ 的一个可靠估计：
&lt;/p>
$$
E_{n+1} = \| y_{n+1} - \hat{y}_{n+1} \|
$$
&lt;p>
通过将这个误差估计 $E_{n+1}$ 与用户设定的容忍度 &lt;code>tol&lt;/code> 进行比较，我们就可以动态地调整时间步长 $h$：&lt;/p>
&lt;ul>
&lt;li>若 $E_{n+1} \le \text{tol}$，则接受当前步（通常使用更高阶的解 $y_{n+1}$），并可尝试在下一步增大大步长。&lt;/li>
&lt;li>若 $E_{n+1} &amp;gt; \text{tol}$，则拒绝当前步，缩小步长并重新计算。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>RK23&lt;/strong> 和 &lt;strong>RK45&lt;/strong> 都是这个家族中的杰出代表。&lt;/p>
&lt;h2 id="3-两种经典的自适应rk算法">3. 两种经典的自适应RK算法
&lt;/h2>&lt;h3 id="31-rk23-bogacki-shampine-方法">3.1 RK23 (Bogacki-Shampine) 方法
&lt;/h3>&lt;p>RK23 方法是一种广泛应用的低阶嵌入式方法，它同时计算一个三阶解和一个二阶嵌入解。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>特点&lt;/strong>：
&lt;ul>
&lt;li>它需要进行&lt;strong>3次&lt;/strong>函数求值（计算 $k_1, k_2, k_3$）来得到一个三阶精度的解。&lt;/li>
&lt;li>一个二阶精度的解可以通过这些 $k_i$ 的不同线性组合得到，用于误差估计。&lt;/li>
&lt;li>它具有 &lt;strong>FSAL (First Same As Last)&lt;/strong> 特性：一个步长计算结束时所用的 $k_3$（在新的 $y_{n+1}$ 处的值），可以作为下一个步长的 $k_1$，从而每步实际只需要额外计算2次函数求值，非常高效。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>适用场景&lt;/strong>：
&lt;ul>
&lt;li>对精度要求不是特别苛刻，但希望有自适应步长能力的场景。&lt;/li>
&lt;li>当函数 $f(t, y)$ 的计算成本较高时，其较少的函数求值次数是一个优势。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>其步长调整和误差控制逻辑与高阶方法完全相同，只是系数和阶数不同。&lt;/p>
&lt;h3 id="32-rk45-dormand-prince-54-方法">3.2 RK45 (Dormand-Prince 5(4)) 方法
&lt;/h3>&lt;p>RK45 是自适应积分方法中的“黄金标准”，也是 MATLAB &lt;code>ode45&lt;/code> 的默认选择。它通过一次计算得到一个五阶解和一个四阶嵌入解。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>核心思想&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>它需要进行 &lt;strong>7 次&lt;/strong>函数求值（计算 $k_1$ 到 $k_7$）。&lt;/li>
&lt;li>使用这些 $k_i$ 的线性组合，分别构造出五阶解 $y_{n+1}^{(5)}$ (用于更新状态) 和四阶解 $y_{n+1}^{(4)}$ (用于误差估计)。&lt;/li>
&lt;li>&lt;strong>Dormand-Prince 系数&lt;/strong>经过特别优化，使得误差估计 $| y_{n+1}^{(5)} - y_{n+1}^{(4)} |$ 相对于步长 $h$ 更加平滑和精确。&lt;/li>
&lt;li>同样具备 &lt;strong>FSAL&lt;/strong> 特性，计算 $k_7$ 的函数值可以复用于下一步的 $k_1$，使得每个成功步长的平均函数求值次数约为6次。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>误差控制策略 (模仿 &lt;code>ode45&lt;/code>)&lt;/strong>：
为了使误差控制更具鲁棒性，我们不使用固定的绝对误差，而是结合&lt;strong>相对容忍度 (&lt;code>RelTol&lt;/code>)&lt;/strong> 和&lt;strong>绝对容忍度 (&lt;code>AbsTol&lt;/code>)&lt;/strong>。对于状态向量 &lt;code>y&lt;/code> 的每个分量 &lt;code>y_i&lt;/code>，容忍度阈值 &lt;code>Tol_i&lt;/code> 定义为：
&lt;/p>
$$
\text{Tol}_i = \text{RelTol} \times |y_i| + \text{AbsTol}
$$
&lt;p>
这个策略的优点是：&lt;/p>
&lt;ul>
&lt;li>当解的数值很大时，误差控制主要由相对容忍度决定。&lt;/li>
&lt;li>当解趋近于零时，由绝对容忍度托底，防止步长被无限压缩。&lt;/li>
&lt;/ul>
&lt;p>最终，我们计算一个归一化的误差率 &lt;code>err_rate&lt;/code>：
&lt;/p>
$$
\text{err\_rate} = \sqrt{ \frac{1}{N} \sum_{i=1}^{N} \left( \frac{E_{n+1, i}}{\text{Tol}_i} \right)^2 }
$$
&lt;p>
其中 $E_{n+1, i}$ 是第 $i$ 个分量的误差估计。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>步长调整决策&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>如果 &lt;code>err_rate &amp;lt;= 1.0&lt;/code>&lt;/strong>: &lt;strong>接受当前步&lt;/strong>。使用更高阶的解 $y_{n+1}^{(5)}$ 更新状态，并计算下一个建议步长 $h_{\text{new}}$。&lt;/li>
&lt;li>&lt;strong>如果 &lt;code>err_rate &amp;gt; 1.0&lt;/code>&lt;/strong>: &lt;strong>拒绝当前步&lt;/strong>。状态回退，使用一个更小的步长 $h_{\text{new}}$ 重新计算。&lt;/li>
&lt;/ul>
&lt;p>步长调整的经典公式为：
&lt;/p>
$$
h_{\text{new}} = h_{\text{old}} \times \text{safe} \times \left( \frac{1.0}{\text{err\_rate}} \right)^{p}
$$
&lt;ul>
&lt;li>&lt;code>safe&lt;/code>: 安全因子，通常取 0.9。&lt;/li>
&lt;li>指数 &lt;code>p&lt;/code>: 对于RK45，通常取 &lt;code>0.2&lt;/code> (即 $1/5$)。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="4-sph右端项rhs的计算">4. SPH右端项（RHS）的计算
&lt;/h2>&lt;p>在前面的讨论中，我们反复提到函数 $f(t, y)$，它代表了系统状态量的时间导数，即常微分方程的&lt;strong>右端项（Right-Hand Side, RHS）&lt;/strong>。在 SPH 模拟中，这个函数 &lt;code>compute_derivatives&lt;/code> 的任务就是根据当前所有粒子的状态，计算出它们各自的时间导数。&lt;/p>
&lt;p>对于复杂的物理过程，尤其是涉及固体力学的弹塑性、损伤和断裂时，RHS 的计算远不止一个简单的压力梯度。下面是构成 SPH 中 RHS 的核心部分：&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>速度导数（加速度 $\mathbf{a}$）&lt;/strong>:
这是动量方程的右端项。加速度由作用在粒子上的所有力的总和除以质量得到。
&lt;/p>
$$
\frac{d\mathbf{v}_i}{dt} = \mathbf{a}_i = \frac{1}{m_i} \sum_j \mathbf{F}_{ij}
$$
&lt;p>
力 $\mathbf{F}_{ij}$ 包括：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>压力梯度力&lt;/strong>: 由压强 $P$ 产生。&lt;/li>
&lt;li>&lt;strong>粘性力&lt;/strong>: 人工粘性，用于处理冲击波。&lt;/li>
&lt;li>&lt;strong>应力散度力&lt;/strong>: 这是固体力学中的关键项。总应力张量 $\boldsymbol{\sigma}$ 可以分解为各向同性的压力 $P$ 和&lt;strong>偏应力张量 $\mathbf{S}$&lt;/strong>。由偏应力引起的力代表了材料的抗剪切和形变能力。
$$ \boldsymbol{\sigma} = -P\mathbf{I} + \mathbf{S} $$
因此，加速度的计算需要准确的应力信息。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>应力导数（应力率 $\dot{\mathbf{S}}$）&lt;/strong>:
应力本身不是一个守恒量，它会随着材料的变形而演化。为了计算应力的变化，我们需要&lt;strong>本构模型（Constitutive Model）&lt;/strong>。对于弹塑性材料，通常使用客观应力率（如 Jaumann 率）来描述偏应力张量的时间导数：
&lt;/p>
$$
\frac{d\mathbf{S}_i}{dt} = \text{JaumannRate}(\mathbf{S}_i, \dot{\boldsymbol{\epsilon}}_i, \boldsymbol{\Omega}_i)
$$
&lt;p>
其中 $\dot{\boldsymbol{\epsilon}}$ 是应变率张量，$\boldsymbol{\Omega}$ 是自旋张量，均由速度梯度计算得到。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>屈服与塑性&lt;/strong>:
当应力达到材料的**屈服强度（Yield Strength）**时，材料进入塑性流动状态。&lt;strong>屈服模型（Yield Model）&lt;/strong>，如 von Mises 或 Drucker-Prager，定义了这个边界。对于岩石等材料，屈服强度还依赖于压力。一旦屈服，就需要通过“径向返回算法”将应力拉回到屈服面上，这个过程是非线性的，并决定了塑性变形的能量耗散。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>损伤导数（损伤率 $\dot{D}$）&lt;/strong>:
为了模拟材料的开裂和失效，我们引入一个内部状态变量——损伤 $D$ (从0到1)。&lt;strong>损伤模型（Damage Model）&lt;/strong>，如 Grady-Kipp 模型，描述了损伤如何随着拉伸或应变而累积。
&lt;/p>
$$
\frac{dD_i}{dt} = g(\boldsymbol{\sigma}_i, \dot{\boldsymbol{\epsilon}}_i, D_i, \dots)
$$
&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>总结&lt;/strong>：在每一个时间积分步中，&lt;code>compute_derivatives&lt;/code> 函数的计算流程大致如下：&lt;/p>
&lt;ol>
&lt;li>根据当前密度 $\rho$ 和内能 $u$，通过状态方程计算&lt;strong>压力&lt;/strong> $P$。&lt;/li>
&lt;li>利用&lt;strong>屈服模型&lt;/strong>判断当前应力状态。&lt;/li>
&lt;li>通过&lt;strong>本构模型&lt;/strong>计算弹性试探应力，如果屈服则进行塑性修正，得到最终的&lt;strong>偏应力&lt;/strong> $\mathbf{S}$。&lt;/li>
&lt;li>利用&lt;strong>损伤模型&lt;/strong>更新损伤变量 $D$。&lt;/li>
&lt;li>最后，将压力 $P$ 和偏应力 $\mathbf{S}$ 代入动量方程，计算出最终的&lt;strong>加速度&lt;/strong> $\mathbf{a}$。&lt;/li>
&lt;li>同时，计算内能变化率 $\dot{u}$、密度变化率 $\dot{\rho}$ 等其他变量的导数。&lt;/li>
&lt;/ol>
&lt;p>这些导数共同构成了时间积分器所需要的 RHS 向量。&lt;/p>
&lt;h2 id="5-应用算例简述">5. 应用算例简述
&lt;/h2>&lt;p>在实现复杂的自适应积分器后，用一些已知解或具有守恒律的简单问题进行验证是至关重要的一步。&lt;/p>
&lt;h3 id="51-太阳系模拟">5.1 太阳系模拟
&lt;/h3>&lt;p>这是一个经典的 N 体问题。每个行星（粒子）的 RHS 就是其他所有天体对其施加的万有引力之和。
&lt;/p>
$$
\frac{d\mathbf{v}_i}{dt} = \mathbf{a}_i = \sum_{j \ne i} \frac{G m_j (\mathbf{r}_j - \mathbf{r}_i)}{\| \mathbf{r}_j - \mathbf{r}_i \|^3}
$$
&lt;ul>
&lt;li>&lt;strong>验证重点&lt;/strong>：
&lt;ul>
&lt;li>&lt;strong>长期能量守恒和角动量守恒&lt;/strong>：是衡量积分器好坏的关键指标。&lt;/li>
&lt;li>&lt;strong>轨道精度&lt;/strong>：能否准确再现行星的椭圆轨道。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>自适应步长优势&lt;/strong>：对于具有高偏心率轨道的天体（如彗星），它在靠近太阳时（速度快，引力变化剧烈）会自动采用小步长，而在远离时（速度慢，引力平缓）则采用大步长，兼顾了精度和效率。&lt;/li>
&lt;/ul>
&lt;h3 id="52-单摆模拟">5.2 单摆模拟
&lt;/h3>&lt;p>一个简单的单摆系统由以下一阶方程组描述：
&lt;/p>
$$
\begin{cases}
\frac{d\theta}{dt} = \omega \\
\frac{d\omega}{dt} = -\frac{g}{L} \sin(\theta)
\end{cases}
$$
&lt;ul>
&lt;li>&lt;strong>验证重点&lt;/strong>：
&lt;ul>
&lt;li>&lt;strong>周期稳定性&lt;/strong>：对于小角度摆动，周期应接近 $2\pi\sqrt{L/g}$。&lt;/li>
&lt;li>&lt;strong>能量守恒&lt;/strong>：在无阻尼情况下，总能量（动能+势能）应保持不变。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>自适应步长优势&lt;/strong>：当摆锤经过最低点（速度最快）时，步长会自然减小；在最高点（速度为零）时，步长会增大。这展示了积分器对系统动态的灵敏响应。&lt;/li>
&lt;/ul>
&lt;h2 id="6-结合sph的计算伪代码">6. 结合SPH的计算伪代码
&lt;/h2>&lt;p>现在，我们将上述 RK45 误差控制逻辑整合到 SPH 的主循环中。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-cpp" data-lang="cpp">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// --- 全局参数 ---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">double&lt;/span> &lt;span class="n">RelTol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">1e-6&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 相对容忍度
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">double&lt;/span> &lt;span class="n">AbsTol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">1e-9&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 绝对容忍度
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">double&lt;/span> &lt;span class="n">h_min&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">1e-8&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">h_max&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">1e-2&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 步长上下限
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">double&lt;/span> &lt;span class="n">safe_factor&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.9&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">double&lt;/span> &lt;span class="n">max_increase&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">5.0&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">min_decrease&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.2&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// --- 主循环 ---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="kt">double&lt;/span> &lt;span class="n">t&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mf">0.0&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kt">double&lt;/span> &lt;span class="n">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">initial_dt&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 初始步长
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// y_n 包含了所有粒子的位置、速度、内能、应力等状态量
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">StateVector&lt;/span> &lt;span class="n">y_n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">get_initial_conditions&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// 预计算下一步的 k1 (利用 FSAL 特性)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">RHSVector&lt;/span> &lt;span class="n">k1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">compute_derivatives&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y_n&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">T_max&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="kt">bool&lt;/span> &lt;span class="n">step_accepted&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">false&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">while&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">step_accepted&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">h&lt;/span> &lt;span class="o">&amp;lt;&lt;/span> &lt;span class="n">h_min&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Timestep smaller than h_min&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">break&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 1. RK45 核心计算：利用已有的 k1 计算 k2, ..., k7
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// 并得到 y_next_4 (四阶解) 和 y_next_5 (五阶解)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// (此处省略繁杂的系数计算，但会返回 k_next 用于 FSAL)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">auto&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="n">y_next_4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y_next_5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">k_next&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">perform_rk45_step&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y_n&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">h&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">k1&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 2. 计算误差率 err_rate (模仿 MATLAB)
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">double&lt;/span> &lt;span class="n">err_rate&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">calculate_error_rate&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">y_n&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y_next_4&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">y_next_5&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">RelTol&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">AbsTol&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 3. 决策与步长调整
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kt">double&lt;/span> &lt;span class="n">h_new&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">h&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">safe_factor&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">pow&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">err_rate&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="o">-&lt;/span>&lt;span class="mf">0.2&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">err_rate&lt;/span> &lt;span class="o">&amp;lt;=&lt;/span> &lt;span class="mf">1.0&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// --- 接受步长 ---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">step_accepted&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">true&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">t&lt;/span> &lt;span class="o">+=&lt;/span> &lt;span class="n">h&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">y_n&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">y_next_5&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 更新状态为更高阶的解
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">k1&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">k_next&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// FSAL: 下一步的 k1 已经算好
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 限制步长增幅
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">min&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="n">h&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">max_increase&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">h_new&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">h_max&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// --- 拒绝步长 ---
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="c1">// 状态 y_n, t, k1 保持不变
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// 限制步长降幅
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">h&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">max&lt;/span>&lt;span class="p">({&lt;/span>&lt;span class="n">h&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="n">min_decrease&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">h_new&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">h_min&lt;/span>&lt;span class="p">});&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="c1">// (可选) 在每个成功的时间步后，更新邻域、输出数据等
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">UpdateAndSaveData&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">t&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// `compute_derivatives` 函数实现了第4节描述的RHS计算逻辑。
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// `perform_rk45_step` 和 `calculate_error_rate` 分别实现了第3节的算法核心和误差控制。
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div></description></item></channel></rss>