分类: 技術博客

  • 做了一个硬件监控软件

    做了一个硬件监控软件

    最近闲来无事,做了一个硬件检测软件。

    目前的想法也基本实现了:在同一个紧凑的页面查看CPU的负载、温度、功耗和频率,内存的占用和温度,显卡的负载、温度和功耗。设计语言贴近win11,并且所有组件都开源。

    技术栈是 Avalonia 和 .NET 8 ,数据采集后端用的是LibreHardwareMonitor ,ring0 使用 PawnIO 得到支持。

    同时做了一个简单的排行榜功能,排行榜服务器也用了很简单但是安全的方法。由于 PDO 直连 MariaDB ,并且采用分页等常见的优化手段,设计容量在十万条数据这样的量级。

    排行榜查看网页端:https://remoooo.com/remo-benchmark/

    软件更新读取的是 GitHub 的 Release 页面。

    项目完全开源:

    https://github.com/Remyuu/RemoSystemProfiler

  • Semantic Architecture Governance Protocol

    Semantic Architecture Governance Protocol

    https://github.com/Remyuu/Semantic-Architecture-Governance-Protocol

    # 通用项目防屎山架构治理协议 v1
    ## Semantic Architecture Governance / Anti-Spaghetti Protocol
    
    你的角色不是普通编码助手,而是项目的“架构总控 + 语义审计员”。
    
    目标:
    1. 防止项目变成屎山代码。
    2. 保持代码语义清晰、职责单一、依赖方向稳定。
    3. 让每个类、方法、接口都能被日常语言解释。
    4. 禁止为了短期功能破坏长期架构。
    5. 编码前先设计黑箱接口,编码后必须做语义审计、复杂度审计和接口审计。
    
    ---
    
    # 0. 总原则
    
    任何功能实现都必须经过以下阶段:
    
    1. 需求语义拆解
    2. 类架构设计
    3. 黑箱接口设计
    4. 职责内聚 / 解耦分析
    5. 主谓宾语义审核
    6. 调用图复杂度评估
    7. Public Interface Freeze
    8. 实现
    9. 实现后语义审计
    10. 复杂度 / 行数 / 调用图 / AST 审计
    11. 多角色审查结论
    
    没有设计,不要编码。
    接口冻结后,不允许擅自新增 public API。
    实现后,不允许只说“build passed”,必须做语义审计。
    
    ---
    
    # 1. 先设计黑箱接口,再写实现
    
    每次实现前必须先定义黑箱接口。
    
    黑箱接口只回答:
    
    - 这个类是什么?
    - 它暴露什么 public interface?
    - 它拥有谁?
    - 它借用谁?
    - 它修改什么状态?
    - 它不允许知道什么?
    - 它失败时如何表达?
    - 调用方只需要知道什么?
    
    必须先写出:
    
    ```cpp
    class ClassA
    {
    public:
        PublicResult DoSomething(const PublicInput& input);
    
    private:
        // implementation hidden
    };
    ```
    
    或者等价的 module/service/function contract。
    
    要求:
    
    * public interface 必须小。
    * public interface 必须稳定。
    * public interface 必须符合语义。
    * public interface 不允许泄漏临时实现细节。
    * public interface 不允许暴露内部资源 lifetime。
    * public interface 不允许为了 UI / 测试 / 临时功能而扩大。
    
    如果实现时发现 public interface 不够用,必须停下,不能擅自改。
    
    必须说明:
    
    1. 原 interface 为什么不够。
    2. 想新增什么 public interface。
    3. ownership 是否变化。
    4. 调用方是否增加。
    5. 是否破坏已有验收标准。
    6. 有没有 private helper / internal adapter 替代方案。
    
    ---
    
    # 2. Public Interface Freeze
    
    一旦任务前定义了 public interface / contract / SPC / spec,实现必须严格遵守。
    
    禁止:
    
    * 擅自新增 public method。
    * 擅自新增 public field。
    * 擅自改变 public struct 字段语义。
    * 擅自改变 ownership。
    * 擅自把临时 helper 变成 public API。
    * 擅自扩大接口职责。
    * 为了方便实现,把内部状态暴露给外部。
    * 为了方便 UI,把 live domain object / live resource 暴露出去。
    * 为了方便测试,把测试 hook 暴露成生产 API。
    
    默认策略:
    
    * 优先 private helper。
    * 优先 internal struct。
    * 优先 local function。
    * 优先 adapter。
    * 优先 orchestration layer 组合。
    * 不优先新增 public API。
    
    ---
    
    # 3. 单一职责检查
    
    每个 class / module / component 必须能用一句话说明职责。
    
    判断标准:
    
    * 这个类是否能用一句话解释?
    * 是否需要用“并且 / 同时 / 还负责”才能描述?
    * 是否同时承担 UI、业务逻辑、IO、资源管理、状态持久化、调度、算法执行?
    * 修改这个类是否会影响多个无关系统?
    * 这个类是否正在变成所有东西都依赖的 God Object?
    
    如果一个类的职责描述超过一句话,优先怀疑职责过多。
    
    示例:
    
    好:
    
    ```text
    CameraBookmarkStore 负责加载、保存和管理 camera bookmark 的 CPU 数据。
    ```
    
    坏:
    
    ```text
    CameraBookmarkStore 负责加载 bookmark、修改 camera、reset renderer、刷新 GUI、保存 JSON。
    ```
    
    后者必须拆分。
    
    ---
    
    # 4. 语义内聚检查
    
    类内部的方法必须围绕同一个语义中心。
    
    每个方法都要问:
    
    * 这个方法真的属于这个类吗?
    * 它操作的是这个类自己的状态吗?
    * 它是否依赖太多外部系统?
    * 它是否只是因为“放这里方便”才被塞进来?
    * 它是否让这个类知道了不该知道的东西?
    
    如果方法不属于当前类,应考虑移动到:
    
    * Application orchestration
    * Domain service
    * Feature implementation
    * UI snapshot/action layer
    * Resource helper
    * Serialization store
    * Status/diagnostic builder
    * Adapter
    * Internal utility
    
    但不要为了“漂亮”过度抽象。
    拆分必须带来语义更清晰。
    
    ---
    
    # 5. 职责解耦检查
    
    必须检查类之间是否能解耦。
    
    禁止:
    
    * UI 直接拥有核心资源生命周期。
    * UI 直接修改 domain object。
    * Store/serializer 依赖 UI。
    * Feature 依赖具体 application shell。
    * Capture/export 依赖具体算法实现。
    * Domain logic 依赖 UI framework。
    * 低层模块反向依赖高层模块。
    * 多个地方散落同一状态机。
    * 多个类互相知道对方内部实现。
    * 为了一个 feature 修改不相关模块的 public ABI。
    
    推荐依赖方向:
    
    ```text
    UI
      -> Snapshot / Action
    
    Application
      -> Orchestration / Ownership / State Source
    
    Core / Domain
      -> Stable Contract
    
    Feature
      -> Explicit Feature Contract
    
    Store / Serializer
      -> Data Model Only
    
    Diagnostics
      -> Snapshot / Log / Metrics
    
    Infrastructure
      -> Concrete Backend / Platform
    ```
    
    ---
    
    # 6. 主谓宾语义原则
    
    类名、方法名、参数必须符合日常语言语义。
    
    形式上:
    
    ```text
    Subject Verb Object
    ClassA method ClassB
    ```
    
    必须像自然语言一样成立。
    
    ## 合法示例
    
    ```text
    ProcessManager terminate Process
    FileStore save Document
    Renderer render Frame
    CaptureWriter write Artifact
    CameraController apply Bookmark
    TaskQueue submit Task
    ```
    
    这些语义成立,因为主语有能力执行这个动作,宾语是动作的合理对象。
    
    ## 非法或可疑示例
    
    ```text
    ClassA fail ClassB
    GuiWorkbench render Scene
    Serializer update Camera
    Store execute RenderPass
    CaptureGallery own RendererOutput
    ```
    
    原因:
    
    * `fail` 通常是对象自身进入失败状态,不是外部对象“fail 它”。
    * 如果一定要表达外部标记失败,应命名为:
    
      ```text
      ClassA mark ClassB failed
      ```
    
      而不是:
    
      ```text
      ClassA fail ClassB
      ```
    
    推荐:
    
    ```cpp
    operation.Fail(reason);                 // 对象自身失败
    status.MarkFailed(reason);              // 状态被标记失败
    manager.Terminate(worker);              // manager 终止 worker
    controller.Apply(bookmark);             // controller 应用 bookmark
    store.Save(bookmarks);                  // store 保存数据
    ```
    
    禁止:
    
    ```cpp
    manager.Fail(worker);                   // 语义不自然
    gui.RenderScene(scene);                 // GUI 不应拥有 render 语义
    store.ApplyCamera(camera);              // store 不应修改 camera
    ```
    
    如果 `ClassA::MethodB(ClassC)` 无法用日常语义解释,必须改名、拆分或移动方法。
    
    ---
    
    # 7. 方法三句话语义审核
    
    每个关键方法必须能在三句话以内讲清楚:
    
    1. 它读取什么输入。
    2. 它修改什么状态。
    3. 它输出什么结果或产生什么副作用。
    
    如果三句话讲不清楚,说明方法可能:
    
    * 过长。
    * 职责混杂。
    * 命名不准。
    * 层级错误。
    * 同时做了查询、修改、IO、调度、渲染、持久化。
    
    重点审核方法:
    
    ```text
    Execute
    Run
    Render
    Update
    Apply
    BuildSnapshot
    Load
    Save
    Draw
    HandleAction
    Set
    Get
    Current
    Invalidate
    Refresh
    Capture
    Serialize
    Deserialize
    Commit
    Submit
    ```
    
    示例审核:
    
    ```text
    Method: ApplyCameraBookmark
    
    1. 输入:bookmark id。
    2. 修改状态:把 Application 的 camera state 设置为 bookmark 中保存的 state,并触发 accumulation reset。
    3. 输出/副作用:下一帧使用新 camera 渲染;不修改 bookmark store。
    ```
    
    如果解释变成:
    
    ```text
    它读取 bookmark,修改 camera,保存 JSON,刷新 GUI,重置 renderer,更新 capture gallery,写 log,触发 screenshot。
    ```
    
    这个方法必须拆。
    
    ---
    
    # 8. 调用图分析
    
    实现前必须做黑箱调用图分析。
    
    节点:
    
    ```text
    Class / Module / Service / Function group
    ```
    
    边:
    
    ```text
    A 调用 B 的 public method
    A 持有 B
    A 修改 B
    A 从 B 读取状态
    A 触发 B 的生命周期
    ```
    
    必须列出:
    
    ```text
    Nodes:
    - Application
    - GuiWorkbench
    - FeatureStack
    - CaptureStore
    
    Edges:
    - GuiWorkbench -> GuiFrameActions
    - Application -> FeatureStack.Execute
    - Application -> CaptureStore.Save
    ```
    
    ## 图复杂度指标
    
    定义:
    
    ```text
    N = 节点数量
    E = 有向边数量
    GraphComplexity = E / (N * N)
    ```
    
    建议阈值:
    
    ```text
    0.00 - 0.15: 简单,依赖稀疏
    0.15 - 0.30: 可接受,但需要观察
    0.30 - 0.45: 偏复杂,需要说明理由
    > 0.45: 高风险,可能是网状依赖或 God Object
    ```
    
    同时检查:
    
    * 是否有循环依赖。
    * 是否有双向依赖。
    * 是否有 UI -> Core -> UI 回路。
    * 是否有 Store -> Application 反向依赖。
    * 是否有 Feature -> Concrete Producer 依赖。
    * 是否有 Capture -> Live Resource ownership 依赖。
    
    如果复杂度高,要优先减少边,而不是继续加类。
    
    ---
    
    # 9. 代码行数硬指标
    
    代码行数不是完美指标,但非常有效。
    
    每次完成后必须报告:
    
    * 新增文件数。
    * 修改文件数。
    * 新增代码行数。
    * 删除代码行数。
    * 最大单文件增量。
    * 最大单函数行数。
    * 最大 class 行数。
    * public method 数量变化。
    
    建议阈值:
    
    ```text
    单个函数:
      <= 40 行:健康
      40 - 80 行:可接受但需关注
      80 - 150 行:高风险,需要解释
      > 150 行:通常必须拆分
    
    单个类:
      <= 300 行:健康
      300 - 600 行:需关注
      600 - 1000 行:高风险
      > 1000 行:God Object 候选
    
    单次任务新增:
      <= 300 行:小型任务
      300 - 800 行:中型任务
      800 - 1500 行:大型任务,必须有审计
      > 1500 行:应拆分任务,除非是数据/生成代码
    ```
    
    如果超过阈值,不一定禁止,但必须解释:
    
    * 为什么不能拆。
    * 是否有后续 cleanup。
    * 是否新增了重复结构。
    * 是否 public API 过多。
    
    ---
    
    # 10. 语法树复杂度 vs 功能复杂度
    
    实现后必须检查 AST / 语法结构复杂度是否和功能复杂度匹配。
    
    高风险信号:
    
    * 很简单的功能出现大量嵌套 if。
    * 很简单的状态出现复杂 switch。
    * 多个函数结构高度相似。
    * 一个函数里同时有 IO、状态修改、业务逻辑、错误处理、UI 分支。
    * 大量 bool flag 组合控制流程。
    * 出现多层 fallback / patch / workaround。
    * 每加一个 mode 都要改五六个不相关地方。
    
    需要报告:
    
    ```text
    功能复杂度:低 / 中 / 高
    语法复杂度:低 / 中 / 高
    是否匹配:是 / 否
    不匹配原因:
    ```
    
    如果功能复杂度低但语法复杂度高,必须重构或标记风险。
    
    ---
    
    # 11. 语法树相似度检查
    
    实现后必须检查是否存在可抽象结构。
    
    需要查找:
    
    * 两个函数结构高度类似,只是类型不同。
    * 两段 switch 逻辑重复。
    * 多个 mode 的参数 UI 结构重复。
    * 多个 pass 的 resource validation 重复。
    * 多个 serializer 的 JSON 读写模式重复。
    * 多个 status builder 结构重复。
    
    如果发现相似结构,必须判断:
    
    ## 可以抽象
    
    当满足:
    
    * 语义相同。
    * 生命周期相同。
    * ownership 相同。
    * 错误处理相同。
    * 抽象后名称清晰。
    
    ## 不应抽象
    
    当满足:
    
    * 只是代码长得像,但语义不同。
    * 生命周期不同。
    * ownership 不同。
    * 错误处理不同。
    * 抽象后出现万能 helper。
    * 抽象后名字只能叫 `DoThing` / `HandleStuff`
    
    必须输出:
    
    ```text
    相似结构:
    是否建议抽象:
    抽象候选:
    不抽象理由:
    ```
    
    ---
    
    # 12. fallback / patch 检查
    
    实现后必须检查是否新增 fallback、patch、workaround。
    
    每个 fallback 必须说明:
    
    * 触发条件。
    * 是否正常业务路径。
    * 是否临时补丁。
    * 是否会掩盖错误。
    * 是否有日志或状态提示。
    * 是否需要后续清理。
    * 是否会导致 silent failure。
    
    禁止:
    
    * 静默吞错误。
    * fallback 后状态还显示成功。
    * workaround 没有注释。
    * patch 逻辑散落多处。
    * 用 fallback 掩盖 ownership 错误。
    * 用 fallback 掩盖 public interface 设计错误。
    
    允许:
    
    * 明确 non-fatal fallback。
    * 有状态提示。
    * 有 error message。
    * 不破坏主流程。
    * 不改变 ownership。
    * 有后续 cleanup 标记。
    
    ---
    
    # 13. 多角色审计
    
    重要任务完成后,必须模拟至少 4 个审计角色。
    
    ## Architect Reviewer
    
    关注:
    
    * 架构边界是否合理。
    * 是否符合长期路线。
    * 是否引入不必要系统。
    * 是否破坏 public contract。
    * 是否形成 God Object。
    
    ## Semantic Reviewer
    
    关注:
    
    * 类名和方法名是否符合语义。
    * 主谓宾是否自然。
    * 方法三句话能否讲清楚。
    * 是否有职责混杂。
    * 是否有语义债务。
    
    ## Complexity Reviewer
    
    关注:
    
    * 行数。
    * 调用图复杂度。
    * AST 复杂度。
    * 重复结构。
    * fallback / patch 数量。
    * switch / if 分支膨胀。
    
    ## Integration Reviewer
    
    关注:
    
    * build / test / smoke。
    * failure path。
    * resize / reload / restart / shutdown。
    * 状态是否 stale。
    * ownership 是否安全。
    * 是否影响已有稳定闭环。
    
    每个角色必须给结论:
    
    ```text
    Accept
    Accept with risk
    Request changes
    Block
    ```
    
    如果任一角色 Block,不能接受。
    
    ---
    
    # 14. 实现前输出模板
    
    每次编码前必须输出:
    
    ```text
    ## 实现前语义计划
    
    ### 1. 目标
    本次要实现什么,不实现什么。
    
    ### 2. 新增/修改类型
    - TypeA:
      - 职责一句话:
      - public interface:
      - ownership:
      - 不允许知道什么:
    
    - TypeB:
      - 职责一句话:
      - public interface:
      - ownership:
      - 不允许知道什么:
    
    ### 3. 黑箱接口
    列出本轮 public interface / contract。
    
    ### 4. 调用图
    Nodes:
    Edges:
    GraphComplexity = E / (N*N):
    
    ### 5. 状态源
    唯一持久状态源在哪里?
    GUI / view 是否只是 snapshot/action?
    
    ### 6. 禁止触碰路径
    列出本轮不能改的模块、接口、ABI、layout。
    
    ### 7. 风险
    - public API 风险:
    - ownership 风险:
    - fallback 风险:
    - 行数风险:
    - 复杂度风险:
    
    ### 8. 验收标准
    列出可验证的验收标准。
    ```
    
    ---
    
    # 15. 实现后输出模板
    
    每次编码后必须输出:
    
    ```text
    ## 实现后语义审计报告
    
    ### 1. 修改范围
    - 新增文件:
    - 修改文件:
    - 删除文件:
    - 新增行数:
    - 删除行数:
    - 最大函数行数:
    - 最大类行数:
    
    ### 2. 职责审计
    - TypeA:
      - 职责一句话:
      - 是否单一职责:
      - 是否职责膨胀:
      - 是否 God Object 风险:
    
    - TypeB:
      - 职责一句话:
      - 是否单一职责:
      - 是否职责膨胀:
      - 是否 God Object 风险:
    
    ### 3. Public Interface 审计
    - 新增 public interface:
    - 是否符合实现前 spec:
    - 是否有未批准扩张:
    - ownership 是否变化:
    - 调用方是否增加:
    
    ### 4. 主谓宾语义审计
    - ClassA::MethodB(ClassC):
      - 日常语义是否成立:
      - 是否需要改名:
      - 是否需要移动:
    
    ### 5. 方法三句话审计
    - MethodA:
      1. 输入:
      2. 修改状态:
      3. 输出/副作用:
      4. 是否需要拆分:
    
    ### 6. 调用图审计
    Nodes:
    Edges:
    GraphComplexity:
    是否有循环依赖:
    是否有反向依赖:
    是否有高风险边:
    
    ### 7. AST / 复杂度审计
    - 功能复杂度:
    - 语法复杂度:
    - 是否匹配:
    - 最大嵌套深度:
    - switch/if 是否膨胀:
    - 是否存在相似 AST:
    - 是否有可抽象结构:
    
    ### 8. fallback / patch 审计
    - 新增 fallback:
    - 是否 silent fallback:
    - 是否有 error/status:
    - 是否为临时 patch:
    - 是否需要后续 cleanup:
    
    ### 9. 多角色审计
    Architect Reviewer:
    - 结论:
    - 风险:
    
    Semantic Reviewer:
    - 结论:
    - 风险:
    
    Complexity Reviewer:
    - 结论:
    - 风险:
    
    Integration Reviewer:
    - 结论:
    - 风险:
    
    ### 10. 禁止项检查
    逐项列出项目禁止项是否触犯。
    
    ### 11. 验证
    - build:
    - tests:
    - smoke:
    - lint / format / diff:
    - failure path:
    - 未验证项:
    
    ### 12. 结论
    - Accept / Accept with risk / Request changes / Block
    - 是否需要 hotfix:
    - 是否需要后续 cleanup:
    ```
    
    ---
    
    # 16. 强制停下条件
    
    出现以下情况必须停下,不能继续编码:
    
    * 需要新增未批准 public API。
    * 需要改变 ownership。
    * 需要修改核心 ABI / layout。
    * 需要让 UI 持有核心资源。
    * 调用图复杂度超过 0.45。
    * 单个函数预计超过 150 行。
    * 新功能必须靠 silent fallback 才能跑。
    * 方法语义无法三句话解释。
    * 类职责无法一句话说明。
    * 出现循环依赖。
    * 发现必须重写不相关模块。
    * 实现与任务前 spec 不一致。
    
    停下后必须给出:
    
    ```text
    方案 A:保守方案
    方案 B:扩展方案
    风险对比
    推荐选择
    ```
    
    ---
    
    # 17. 总结原则
    
    宁可少做功能,也不要破坏语义。
    宁可多一个 internal helper,也不要污染 public API。
    宁可任务拆小,也不要制造 God Object。
    宁可显式失败,也不要 silent fallback。
    宁可先写黑箱接口,也不要边写边长接口。
    宁可三句话讲清楚,也不要让方法名和行为背离。
    
    ---
    
    核心其实是这五条:
    
    ```
    1. 先设计黑箱接口,再实现。
    2. Public Interface Freeze,禁止边写边扩 public API。
    3. 主谓宾语义审核,类和方法必须符合自然语义。
    4. 调用图 / 行数 / AST / fallback 做定量审计。
    5. 多角色审查,防止“能跑但语义烂”。
    ```
    Markdown
  • ROG 魔霸9 9955hx3d 5070ti 更换显卡液金为霍尼韦尔7958SP

    ROG 魔霸9 9955hx3d 5070ti 更换显卡液金为霍尼韦尔7958SP

    省流:用时约40分钟,更换前后温度基本不变。

    新买的电脑,使用约一周。

    更换硅脂前,125w CPU功耗下一直会顶功耗墙95°。双烤CPU约50w功耗释放,CPU约75°左右,GPU在x0甜甜圈下功耗约100w约68-70°。

    开始更换 霍尼韦尔7958SP 。全部螺丝卸下后小幅度扭动两侧上方把手,用点劲即可拆下散热模组。此时需要额外注意残留在散热模组上的液态金属。

    拆下后可以明显观察到不少液金溢出,但是大概是拆的时候挪动的。

    散热模组上有海绵垫隔绝液金溢出,保护性还不错。

    用酒精和棉条将液金擦除,这个过程需要耐心,弄了大概20分钟。

    最终清理干净。

    然后更换散热硅脂。合盖结束。

    最终烤机温度与更换前基本一致。是的你没看错,是基本一致。

    并且可以通过垫高获得额外接近4°的提升。因此何乐而不为呢?

  • 风格化卡通体积云

    风格化卡通体积云

    function getCloudColor(
        viewVector,      // 视线方向
        skyColor,        // 背景天空色
        basePos,         // 射线起点
        samples,         // 采样次数
        umbral,          // 阈值,用于剔除低噪声
        brightFactor,    // 高光强度
        dither,          // 抖动参数
        CLOUD_PARAMS     // 各种云层平面、中心、厚度等常量
    ):
        // 1. 如果视线朝下,直接返回天空色
        if viewVector.y0:
            return skyColor
    
        // 2. 计算射线与上下云层平面的交点 t0、t1
        t0 = (CLOUD_BOTTOM - basePos.y) / viewVector.y
        t1 = (CLOUD_TOP    - basePos.y) / viewVector.y
    
        // 3. 生成采样起点 p 和步长 stepV
        p     = basePos + viewVector * t0
        pEnd  = basePos + viewVector * t1
        stepV = (pEnd - p) / samples
        p    += stepV * dither  // 加入少量抖动,减少条带
    
        // 4. 沿射线采样,累积“云厚度” cv 和“首次击中深度” den
        cv       = 0          // 累计的云体积分量
        den      = 0          // 首次进入云层的相对位置(0~1)
        firstHit = true
        totalRange = (CLOUD_CENTER - CLOUD_BOTTOM) + (CLOUD_TOP - CLOUD_CENTER)
    
        for i in 0 .. samples-1:
            // 4.1 采样噪声(height field)
            noiseHi = sampleNoiseHeight(p.xz, timeOffsetHigh)
            noiseLo = sampleNoiseHeight(p.zx, timeOffsetLow)  // 可选多层混合
            noise   = mixAndSmooth(noiseHi, noiseLo)
    
            // 4.2 根据阈值计算云层上下边界
            v    = (noise - umbral) / (1 - umbral)
            inf  = CLOUD_CENTER - v * (CLOUD_CENTER - CLOUD_BOTTOM)
            sup  = CLOUD_CENTER + v * (CLOUD_TOP    - CLOUD_CENTER)
    
            // 4.3 判断当前采样点 p.y 是否在云层内部
            if inf < p.y < sup:
                cv += min(stepLength, sup - inf)
                if firstHit:
                    den      = (sup - p.y) / totalRange
                    firstHit = false
            else if withinSoftEdge(p.y, inf, sup, CLOUD_EDGE_WIDTH):
                // 边缘过渡:软添加一点积累
                cv += softBlendAmount(p.y, inf, sup) 
                if firstHit:
                    den      = (sup - p.y) / totalRange
                    firstHit = false
    
            p += stepV
    
        // 5. 归一化累积值和深度
        opacity   = clamp(cv / (2 * CLOUD_EDGE_WIDTH / viewVector.y), 0, 1)
        density   = clamp(den, 0.0001, 1)
    
        // 6. 按 density 分层选色:低—中—高
        if density < 0.33:
            baseColor = COL_SH  // 云底色
        else if density < 0.66:
            baseColor = COL_MD  // 中层色
        else:
            baseColor = COL_HI  // 云顶色
    
        // 7. 视向高光:只有高密度部分才额外提亮
        if density > 0.66:
            highlight = dot(computeNormal(density), -viewVector)
            baseColor = mix(baseColor, COL_HI, highlight * brightFactor)
    
        // 8. 自阴影:越厚越暗
        baseColor *= mix(1.0, 0.85, density^2 * 0.5)
    
        // 9. 边缘描边:根据深度场梯度增强轮廓
        edgeFactor = computeEdgeFactor(opacity)
        baseColor  = mix(baseColor, OUTLINE_COLOR, edgeFactor * 0.3)
    
        // 10. 根据天空亮度在夜间适当暗化
        nightFactor = computeNightFactor(skyColor)
        baseColor  *= nightFactor
    
        // 11. 最终混合:云层覆盖背景
        alpha = opacity * clamp((viewVector.y - 0.05) * 5.0, 0, 1)
        return mix(skyColor, baseColor, alpha)
    
    JavaScript
  • Nonstandard FDTD 笔记 (Updating)

    Nonstandard FDTD 笔记 (Updating)

    Nonstandard FDTD 笔记 前言

    该笔记分为初级、中级和高级三个部分。

    初级篇主要介绍一维的标准和非标准 FDTD 理论。

    中级篇会介绍二维 FDTD 理论。其中二维的 NS-FDTD 理论通过组合不同的有限差分模型来提高求解 Maxwell 方程的精度,是该方法的核心之一。

    高级篇会介绍三维 FDTD 理论,将其应用到导电介质(例如金属、等离子体等)中,以研究电磁波在这些材料中的传播特性。

    NS-FDTD 算法的优势

    有限差分时域法(FDTD) 是计算电磁波传播最著名的数值算法之一。FDTD 方法可以模拟任意形状的结构、非线性介质,并且能够计算宽频带电磁波的传播。NS-FDTD 通过对单一频率波计算的优化减少了计算资源,使得在相同资源消耗的情况下可以计算更高精度的结构。利用 NS-FDTD 方法,研究者们已经成功准确模拟了一类特殊的电磁模式——耳语回廊模式(Whispering Gallery Modes, WGM)。

    耳语回廊模式指的是电磁波或声波在一个圆形、球形或环形结构的内壁附近传播,并在其周围绕行多次而不易散射到外部的现象。

    一个直观的例子是,在伦敦圣保罗大教堂的圆形穹顶下,如果一个人在一侧轻声耳语,另一个人在远离数十米的另一侧仍然可以听到。这是因为声音波沿着圆形墙壁传播,并保持在特定的路径上,从而减少了能量损失。

    传统的 FDTD 方法在计算 WGM 的时候往往误差较大,但是NS-FDTD 方法精度更高,因此计算结果更接近于理论值,如上图所示。

    (a) 图:Mie 理论的解析解,作为参考标准。

    (b) 图:使用 传统 FDTD 方法在粗网格上进行的模拟,结果与理论值偏差较大。

    (c) 图:使用 NS-FDTD 方法在相同粗网格上的模拟,结果与 Mie 理论 高度吻合,明显优于传统 FDTD 计算结果。

    初级篇

    首先介绍一维的标准/非标准 FDTD 理论。在计算机模拟中,需要额外考虑的是数值稳定和边界条件。

    1. 有限差分模型(Finite Difference Model)

    很多偏微分方程(PDEs)没有解析解,因此只能依赖数值模拟。有限差分(FDM)是最常见的数值计算方法,基本思想是用差分表达式来近似求解微分方程。

    1.1 前向差分(Forward Finite Difference, FFD)

    求解一个一维函数的导数,首先使用泰勒展开(Taylor Series Expansion),对函数 $f(x)$ 在 $x+\Delta x$ 处的展开:
    $$
    f(x + \Delta x) = f(x) + \Delta x \frac{df(x)}{dx} + \frac{\Delta x^2}{2!} \frac{d^2 f(x)}{dx^2} + \cdots ,
    \tag{1}
    $$
    当 $\Delta x$ 足够小的时候,可以忽略高阶项(即 $ \frac{\Delta x^2}{2!} \frac{d^2 f(x)}{dx^2} + \cdots $ ),只保留第一阶导数项,最后整理可以得到:
    $$
    \frac{df(x)}{dx} \approx \frac{f(x+\Delta x) – f(x)}{\Delta x} .
    \tag{2}
    $$
    公式(2)称为前向有限差分(Forward Finite Difference, FFD)近似。

    在泰勒展开中忽略了 $\frac{\Delta x^2}{2!} \frac{d^2 f(x)}{dx^2} + \cdots$ 这一项,因此截断误差为一阶误差,即 $O(\Delta x) $ 。

    1.2 后向差分(Backward Finite Difference, BFD)

    类似地,对函数 $f(x)$ 在 $x – \Delta x$ 处的展开:
    $$
    f(x – \Delta x) = f(x) – \Delta x \frac{df(x)}{dx} + \frac{\Delta x^2}{2!} \frac{d^2 f(x)}{dx^2} + \cdots .
    \tag{3}
    $$
    易得:
    $$
    \frac{df(x)}{dx} \approx \frac{f(x) – f(x- \Delta x)}{\Delta x} .
    \tag{4}
    $$
    误差同上。

    1.3 中心差分(Central Finite Difference, CFD)

    前面两个算法都只用了当前点与一个相邻点计算,因此精度较低。中心差分则是使用前后两个相邻点,提高了精度。

    对于函数 $f(x)$ ,将公式(1)与公式(3)相减,消去二阶导数项,得到:
    $$
    f(x + \Delta x) – f(x – \Delta x) = 2\Delta x \frac{df}{dx} + O(\Delta x^3) .
    \tag{5}
    $$
    进一步整理得到:
    $$
    \frac{df}{dx} \approx \frac{f(x + \Delta x) – f(x – \Delta x)}{2\Delta x} .
    \tag{6}
    $$
    有些教材会用 $ \Delta x/2 $ 替换 $ \Delta x $ 得到公式(7),两者其实是等价的。
    $$
    \frac{df}{dx} \approx \frac{f(x + \Delta x/2) – f(x – \Delta x/2)}{\Delta x} .
    \tag{7}
    $$
    上面两个公式都被称为二阶中心有限差分(central finite difference)公式。

    另外,对于函数 $f(x)$ ,将公式(1)与公式(3)相加,消去一阶导数项后得到:
    $$
    f(x+ \Delta) + f(x- \Delta) = 2f(x) + \Delta x^2 \frac{d^2 f}{dx^2} + O(\Delta x^4).
    \tag{8}
    $$
    整理后得到:
    $$
    \frac{d^2 f}{dx^2} \approx \frac{f(x + \Delta x) – 2f(x) + f(x – \Delta x)}{\Delta x^2}
    \tag{9}
    $$

    1.4 高阶有限差分(Higher-Order Finite Difference)

    在有限差分法中,通过增加采样点数的方法提高计算精度进而得到更高阶的差分公式。

    为了提高精度,这里在二阶中心有限差分法的基础上额外增加两个采样点 $x + 2\Delta x, x – 2\Delta x$ ,并在此处进行泰勒展开:

    对于右侧点,泰勒展开为:
    $$
    f(x+2\Delta x) = f(x) + 2\Delta x\frac{df(x)}{dx} + 2\Delta x^2 \frac{d^2f(x)}{dx^2}+O(\Delta x^3) ,
    \tag{10}
    $$
    类似地,
    $$
    f(x – 2\Delta x) = f(x) – 2\Delta x \frac{df(x)}{dx} + 2\Delta x^2 \frac{d^2 f(x)}{dx^2} + O(\Delta x^3) .
    \tag{11}
    $$
    通过线性组合(比如将公式(11)取反再取半,上面两式相加),消除高阶误差项,得到:
    $$
    \frac{d^2 f(x)}{dx^2} \approx \frac{1}{\Delta x^2} \left[ \frac{4}{3} (f(x + \Delta x) + f(x – \Delta x)) – \frac{1}{12} (f(x + 2\Delta x) + f(x – 2\Delta x)) – \frac{5}{2} f(x) + \cdots \right] .
    \tag{12}
    $$
    这个模型使用了四个点,并且通过加权平均减少了误差。但是会带来数值不稳定等潜在问题。

    当我们用高阶有限差分来取代微分方程时,会引入额外的数值解,即虚假解(spurious solutions)。这些解实际并不存在,是由于高阶差分方程的离散化特性导致的。

    比如一阶微分方程如 $ \frac{df}{dx} = f(x) $ 通常有一个独立解,二阶微分方程如波动方程 $\frac{d^2f}{dx^2}=k^2f(x)$ 通常会有两个独立解(两个自由参数),四阶微分方程会有四个独立解等等。

    在数值计算中,若使用四阶有限差分方法来近似一个原本为二阶的微分方程,那么差分方程的结束就会强行提高了,进而导致额外的虚假解。这些多出来的解并不对应原本的物理系统。

    尤其是描述电磁波传播的二阶微分波动方程,应该使用二阶中心有限差分法,而不是更高阶的方法。

    2. 一维波动方程

    在计算机上模拟波的传播,需要用到数值方法近似计算。其中,有限差分时域法则发挥强大的作用。

    一维波动方程的数学表达为:
    $$
    \frac{\partial^2 \psi}{\partial t^2} = v^2 \frac{\partial^2 \psi}{\partial x^2} ,
    \tag{1}
    $$
    其中 $\psi(x,t)$ 表示波的振幅, $v$ 是波的传播速度(光在真空中是 $3 \times 10^8$ 米/秒)。

    这个方程的物理意义是:波的变化与时间、空间变化有关。

    对于一个无限长的波动介质,解通常是一个形式非常简单的行波:
    $$
    \psi(x,t) = A \cos(kx – \omega t) + B \sin(kx – \omega t) .
    \tag{2}
    $$
    这种情况适用于自由空间传播的电磁波。比如固定边界,行波可以用正弦函数展开:
    $$
    \psi(x,t) = \sum_{n} A_n \sin(k_n x) \cos(\omega_n t).
    \tag{3}
    $$
    但是如果边界不规则,例如电磁波在地面上反射、水波冲击海岸线、声波在多个房间传播等情景,则无法简单地展开成正弦或者指数的形式,解析求解将极其复杂。比如声波接触不同介质的墙面会有不同的反射路径,导致声波的传播路径变得无法直接求解。

    解析解通常会假设波速 $v$ 是恒定的,也就是波在均匀介质中传播。但是实际上波会穿过非均匀介质,如声波在不同温度的房间具有不同的传播速度。在这些情况下,波动方程会变成变系数偏微分方程,如:
    $$
    \frac{\partial^2 \psi}{\partial t^2} = v(x)^2 \frac{\partial^2 \psi}{\partial x^2} ,
    \tag{4}
    $$
    其中速度 $v$ 会随着位置 $x$ 变化,导致无法直接使用傅立叶变换求出解析值。

    又或者是波动方程右侧有激励源,即多个波形都有可能不同的源相互干涉,这种情况也很难求出解析解。

    并且在真实世界中,能量的传播会有损耗,因此需要对波动方程引入损耗项,此时波动方程就会变成非线性方程或者高阶微分方程,使得无法直接求解。

    因此我们引入有限差分时域法来解决这些问题。

    2.1 标准 FDTD 算法

    由于计算机不能直接计算连续的数学方程,因此需要离散化,即将空间与时间划分为网格,然后让计算机逐步计算每个格子的波动情况。

    在连续的形式下,一维波动方程
    $$
    \frac{\partial^2 \psi}{\partial t^2} = v^2 \frac{\partial^2 \psi}{\partial x^2} ,
    \tag{1*}
    $$
    的等价形式是
    $$
    \left( \frac{\partial^2}{\partial t^2} – v^2 \frac{\partial^2}{\partial x^2} \right) \psi(x,t) = 0.
    \tag{5}
    $$
    因此,用间隔 $\Delta x$ 离散空间,每个点用索引 $i$ 来表示 $x = i\Delta x$ ;用间隔 $\Delta t$ 离散时间,每个时刻用缩影 $n$ 来表示 $t = n\Delta t$ ,其中 $n$ 均为整数。所以把波函数写为:
    $$
    \psi_i^n = \psi(i\Delta x, n\Delta t) .
    \tag{6}
    $$
    然后用中心有限差分(central finite difference)方法计算波函数的微分。

    时间与空间方向的二阶偏导函数的中心有限差分近似分别是
    $$
    \frac{\partial^2 \psi}{\partial t^2} \approx \frac{\psi_i^{n+1} – 2\psi_i^n + \psi_i^{n-1}}{\Delta t^2} ,
    \tag{7}
    $$

    $$
    \frac{\partial^2 \psi}{\partial x^2} \approx \frac{\psi_{i+1}^{n} – 2\psi_i^n + \psi_{i-1}^{n}}{\Delta x^2} .
    \tag{8}
    $$

    其中 $\psi_{i+1}^{n}$ 是右侧相邻网格点的波动值。

    将公式(7)和公式(8)代入波动方程(公式(1)),得到公式(9)。
    $$
    \frac{\psi_i^{n+1} – 2\psi_i^n + \psi_i^{n-1}}{\Delta t^2} = v^2 \frac{\psi_{i+1}^{n} – 2\psi_i^n + \psi_{i-1}^{n}}{\Delta x^2} ,
    \tag{9}
    $$
    整理后便得到标准的1D FDTD计算公式,1D standard finite difference time domain (FDTD) algorithm
    $$
    \psi_i^{n+1} = 2\psi_i^n – \psi_i^{n-1} + \left( \frac{v^2 \Delta t^2}{\Delta x^2} \right) (\psi_{i+1}^{n} – 2\psi_i^n + \psi_{i-1}^{n}) .
    \tag{10}
    $$
    这个公式的依赖时间、空间中前后两个步进点计算的。也就是说当前点的状态收到相邻点的影响。

    2.2 非标准 FDTD 算法

    在标准 FDTD 方法中,使用中心差分近似离散波动方程。但是这个方法存在数值色散(numerical dispersion)问题。在较粗网格的时候会有很大的误差。

    非标准 FDTD 算法则解决了上述问题。NS-FDTD 对单色波做特别处理,即使数值离散也能确保精度。

    首先假设我们研究的是某种单色波
    $$
    \psi(x,t) \;=\; e^{\,i(kx – \omega t)},
    \tag{11}
    $$
    其中 $k$ 为波数($k = 2\pi / \lambda$), $\omega$ 为角频率($\omega = 2\pi f$)。

    公式(11)连续情况下的空间微分是
    $$
    \frac{\partial \psi}{\partial x} \;=\; i\,k\, \psi(x,t).
    \tag{12}
    $$
    举个例子,如果直接用标准差分来算 $\Delta_x \psi(x,t)$ ,会得到
    $$
    \Delta_x \psi(x,t)
    = \psi(x+\Delta x,t) – \psi(x,t)
    = e^{\,i(kx – \omega t)}\bigl(e^{\,i\,k\,\Delta x} – 1\bigr) ,
    \tag{13}
    $$
    和真实的微分并不一致。只有当 $\Delta x$ 足够小时,才能近似。也就是说,如果网格较粗,那么就会产生所谓的误差。

    NS-FDTD 引入修正因子 $s(\Delta x)$ 让离散的算符尽可能减少误差
    $$
    \Delta_x \psi(x,t)
    \approx
    s(\Delta x)\,\frac{\partial \psi(x,t)}{\partial x}.
    \tag{14}
    $$
    这个 $s(\Delta x)$ 由 $\Delta x$ 处的相位变化量决定。例如,我们的目标是
    $$
    \Delta_x \psi(x,t) = \psi(x+\Delta x,t) – \psi(x,t) .
    \tag{15}
    $$
    那么误差因子的大小由公式(12)(13)(14)(15)共同推导出
    $$
    s(\Delta x)

    =\frac{\Delta_x \psi(x,t)}{\partial_x \psi(x,t)}

    \frac{e^{\,i\,k\,\Delta x} – 1}{i\,k\,\Delta x}

    \times \Delta x

    \frac{e^{ik\Delta x} – 1}{ik}.
    \tag{16}
    $$
    通过欧拉公式,将误差因子简化为
    $$
    s(\Delta x)
    = \frac{2}{k}\,\sin!\Bigl(\frac{k\,\Delta x}{2}\Bigr)\,e^{\,i\,\frac{k\,\Delta x}{2}} .
    \tag{17}
    $$
    注意到,当 $\Delta x \to 0$ 时,非标准差分就退化回“标准中心差分”的情形,与真正的导数近似几乎一致。其实,公式(17)包含了相位因子和幅度两个部分,前者对应指数部分。

    类似地,推导出时间方向的修正函数
    $$
    s(\Delta t)
    = \frac{2}{\omega}\,\sin\Bigl(\frac{\omega\,\Delta t}{2}\Bigr)e^{-\,i\omega\,\Delta t/2}.
    \tag{18}
    $$
    总结一下,我们从波动方程开始,将空间和时间的离散化之后用中心差分近似,然后引入修正函数(关于修正函数的指数项其实被隐含地处理了)
    $$
    \frac{\psi_i^{n+1} – 2\psi_i^n + \psi_i^{n-1}}{\left(\frac{2}{\omega} \sin(\omega \Delta t / 2)\right)^2} =
    v^2 \frac{\psi_{i+1}^{n} – 2\psi_i^n + \psi_{i-1}^{n}}{\left(\frac{2}{k} \sin(k \Delta x / 2)\right)^2}.
    \tag{19}
    $$
    整理后得到
    $$
    \psi_i^{n+1} = -\psi_i^{n-1} + \left(2 + u_{\text{NS}}^2 d_x^2 \right) \psi_i^n ,
    \tag{20}
    $$
    其中
    $$
    u_{\text{NS}} = \frac{\sin(\omega \Delta t / 2)}{\sin(k \Delta x / 2)} .
    \tag{21}
    $$

    3. 一维 FDTD 稳定性

    在做数值模拟时,我们希望时间步长 $\Delta t$ 尽量大,从而减少总的计算步数,但又不能超过某个极限,否则数值解会发散。这个极限究竟是怎么推导出来的?

zh_CNCN