Skip to content

Thinking...

> Clarifying R's meaning > > If R stands for Runner or Runtime, then it acts as a numerical interpreter. That fits nicely! But I don't necessarily need to start with the raw "R" as an unknown. I could express it differently, like writing “如果你说的 R 是 Reference(参考/任务指令),” to clarify further. So, it's good to have that in mind as I proceed. Let’s keep going!

你的类比非常准,而且它可以变成一个相当普遍的程序设计框架。

一句话概括:

> 微分方程给出“局部演化律”,边界/初始条件给出“问题实例”;函数式程序中的纯函数给出演化律,Reader/State/Writer 这类 monad 给出背景、演化状态与观测记录。数值积分器则是把连续演化律解释为离散计算过程的“解释器”。


1. 数学物理里的结构,和程序结构是一一对应的

可以先建立这张对应表:

数学物理对象程序设计对象RWS / pipeline 解释
状态 (x(t))stateState
时间 (t)仿真推进变量State 的一部分
微分方程 (\dot{x}=f(t,x))rhs(ctx, t, state)纯函数
环境场:重力场、风场、大气ctx.env.gravity(...)Reader
物体自身参数:气动表、推力曲线、质量惯量ctx.assetsReader
控制器controller(obs, ctrl_state)State + Reader
初始条件initial_state初始 State
边界条件 / 终止条件event, residual, objective外层 solver / event system
数值积分方法rk4, euler, dop853连续方程的离散解释器
观测量、日志、诊断量log, trace, df_rowsWriter

所以 RWS monad 的核心并不神秘。

Haskell 里可以把它想成:

haskell
RWS r w s a  r -> s -> (a, s, w)

翻译成仿真语境就是:

text
给定背景环境 r,
给定当前状态 s,
执行一次计算,
得到结果 a、更新后的状态 s、以及日志 w。

这几乎就是数值仿真的自然形式。


2. 火箭问题可以抽象成一个受控混合动力系统

一枚火箭在世界中运动,它不只是一个 ODE,而是一个 controlled hybrid dynamical system

可以写成:

[ \dot{x} = f_m(t, x, u; R) ]

其中:

  • (x):火箭连续状态,例如位置、速度、姿态、角速度、质量等;
  • (m):离散模式,例如一级飞行、二级飞行、滑翔段、再入段等;
  • (u):控制输入,例如舵偏、推力矢量、节流量;
  • (R):Reader 环境,包括风场、重力场、大气模型、气动系数表、发动机参数、任务配置等。

控制器可以写成:

[ y = h(t, x; R) ]

[ (u, c^+) = \pi(t, y, c; R) ]

其中:

  • (y):观测量;
  • (c):控制器内部状态;
  • (\pi):控制律;
  • (c^+):更新后的控制器状态。

事件条件可以写成:

[ e_i(t, x, c, m; R) = 0 ]

一旦事件触发,例如燃尽、级间分离、落地、达到最大动压点,就执行重置映射:

[ (x, c, m)^+ = \Gamma_i(t, x, c, m; R) ]

所以完整系统不是单纯的:

[ \dot{x}=f(t,x) ]

而是:

[ \begin{cases} \dot{x} = f_m(t,x,u;R) \ (u,c^+) = \pi_m(t,h(t,x;R),c;R) \ e_i(t,x,c,m;R)=0 \Rightarrow (x,c,m)^+=\Gamma_i(t,x,c,m;R) \end{cases} ]

这就是你说的:

> 火箭自身特性、环境、控制器、积分方法、边界条件之间的解耦。


3. 从数学物理基础可以推出一种程序架构

可以把仿真程序分成四层。

第一层:组成律 / constitutive laws

这对应物理模型中的材料性质、气动特性、发动机特性、环境场。

例如:

text
wind(t, position)
gravity(t, position)
atmosphere(t, altitude)
aero_coeff(mach, alpha, beta)
thrust(t_burn, p_ambient)
mass_properties(t_burn)

这些大多应该是纯函数。

它们属于 Reader,也就是背景配置:

text
ctx = {
    env,
    assets,
    mission,
    numeric_options
}

这里的 assets 不只是“配置文件”,而是方程中的 组成关系

例如气动系数表本质上是:

[ C_D = C_D(Ma, \alpha, \beta) ]

推力修正函数本质上是:

[ T = T(t_{\text{burn}}, p_{\text{ambient}}) ]

所以资产化配置并不是工程上的小技巧,而是数学物理里的“组成律外置”。


第二层:模型装配 / vector field assembly

这一层把所有力、力矩、运动学关系装配成右端项:

[ \dot{x}=f(t,x,u;R) ]

例如:

[ F_{\text{total}}

F_{\text{gravity}} + F_{\text{aero}} + F_{\text{thrust}} + F_{\text{control}} ]

程序上可以写成类似:

python
force_terms = [
    gravity_force,
    aero_force,
    thrust_force,
    control_force,
]

total_force = sum(term(ctx, t, state, command) for term in force_terms)

这其实很函数式。

每个力项都是一个纯函数:

text
term : Context -> Time -> PlantState -> Command -> ForceMoment

然后用 foldsum 合成。

这里很像 monoid:

text
多个 ForceMoment 可以相加;
多个 log 可以拼接;
多个 residual 可以组合;
多个 event condition 可以并列检查。

所以你的“pipeline monad”直觉是对的:很多物理计算天然就是组合代数。


第三层:数值积分器 / numerical interpreter

积分器不应该知道“火箭”是什么。

它只应该知道:

text
rhs(t, x) -> dxdt

因此 Euler、RK4、RK45、DOP853、Verlet 等方法都可以抽象为:

[ \Phi_h^{\text{method}}(f): (t,x) \mapsto (t+h,x') ]

程序上是:

text
integrator_step(method, rhs, t, x, dt) -> x_next

也就是说:

> 微分方程是模型,积分器是解释器。

这类似编程语言里:

text
AST + Interpreter

在仿真里是:

text
continuous model + numerical integrator

所以你想解耦:

  • 微分方程;
  • 积分方法;
  • 被计算对象自身特性;
  • 环境;
  • 控制器;

这在数学上完全自然。


第四层:问题实例 / boundary conditions / scenario

数学物理里,方程本身还不是“问题”。

真正的问题是:

[ \dot{x}=f(t,x) ]

加上:

[ x(t_0)=x_0 ]

或者:

[ B(x(t_0), x(t_f))=0 ]

或者:

[ J[x,u] \to \min ]

程序上就是:

text
scenario = {
    initial_state,
    start_time,
    stop_condition,
    target_condition,
    mission_reference,
    optimization_variables
}

例如:

  • 给定发射角,算落点;
  • 给定目标落点,反求发射角;
  • 给定最大动压约束,优化俯仰程序;
  • 给定目标轨道,优化控制律参数。

如果是初值问题:

text
trajectory = simulate(ctx, initial_state)

如果是边值问题:

text
residual(params) = terminal_error(simulate(ctx.with_params(params), initial_state))
params_star = root_find(residual)

这就是 shooting method。

如果是最优控制:

text
cost(params) = mission_cost(simulate(ctx.with_control_params(params), initial_state))
params_star = optimizer(cost)

所以边界条件在程序中通常体现为:

  1. event;
  2. terminal residual;
  3. objective function;
  4. outer solver。

4. RWS monad 在这里的真正作用

RWS 不是为了让代码“看起来函数式”。

它真正解决的是:把仿真中的隐式上下文显式化

典型仿真代码里经常有这些隐藏东西:

  • 全局配置;
  • 全局环境模型;
  • 当前状态;
  • 当前时间;
  • 控制器内部状态;
  • 日志数组;
  • 诊断量;
  • 误差估计;
  • 事件记录。

RWS 把它们分开:

text
Reader: 不随时间改变的背景
State : 随计算推进而改变的状态
Writer: 只记录、不反过来影响动力学的输出

对应到火箭:

text
Reader:
    - 风场
    - 重力模型
    - 大气模型
    - 气动系数表
    - 发动机曲线
    - 任务参数
    - 数值积分参数

State:
    - t
    - position
    - velocity
    - attitude
    - angular velocity
    - mass / propellant state
    - controller_state
    - flight_mode

Writer:
    - 每步采样结果
    - 动压
    - 迎角
    - Mach
    - Cd / Cl
    - 推力
    - 事件记录
    - 数值误差估计

所以一个仿真 step 可以抽象成:

text
sim_step : Context -> SimState -> (StepReport, SimState, Trace)

也就是:

text
RWS Context Trace SimState StepReport

这就是 RWS 和数值积分 pipeline 的深层对应。


5. 一个函数式 pipeline 骨架

不用 OOP,可以写成这种结构:

python
def sim_step(ctx, state, dt):
    state, log1 = controller_tick(ctx, state)

    rhs = make_rhs(ctx, state)

    state, log2 = integrate_plant(ctx, state, rhs, dt)

    state, log3 = handle_events(ctx, state)

    log4 = sample_observables(ctx, state)

    return state, log1 + log2 + log3 + [log4]


def run_simulation(ctx, initial_state, time_steps):
    state = initial_state
    logs = []

    for dt in time_steps:
        state, step_logs = sim_step(ctx, state, dt)
        logs.extend(step_logs)

        if should_stop(ctx, state):
            break

    return state, logs

这里的关键是:

text
ctx      = Reader
state    = State
logs     = Writer
sim_step = RWS computation

如果继续拆:

python
def make_rhs(ctx, state):
    mode = state["mode"]
    command = state["command"]

    def rhs(t, plant_state):
        force_moment, aux = assemble_force_moment(
            ctx=ctx,
            mode=mode,
            t=t,
            plant_state=plant_state,
            command=command,
        )
        return plant_derivative(ctx, mode, t, plant_state, force_moment)

    return rhs

注意这里 rhs 最好保持纯净,不要在里面直接写日志。

因为 RK4 这类方法会多次调用 rhs,如果 rhs 每次调用都写 Writer,就会把中间 stage 也记成真实轨迹点,容易污染日志。

更好的做法是:

text
rhs 只返回导数和必要 aux;
真正 accepted step 后,再统一 sample/log。

6. 控制器和积分器的一个重要坑

你提到 Plant-Controller-R,这里有一个关键点。

控制器有两种性质:

情况一:连续控制器

例如:

[ \dot{c}=g(t,c,y) ]

此时控制器状态 (c) 应该并入总状态:

[ z = (x,c) ]

然后整体积分:

[ \dot{z} = \begin{bmatrix} \dot{x} \ \dot{c} \end{bmatrix} ]

这时 RK4 调用中间 stage 是合理的。


情况二:采样控制器

例如飞控每 (10\text{ms}) 更新一次,控制量在两次更新之间零阶保持。

此时不应该在 RK4 的每个 substage 更新控制器内部状态。

正确结构是:

text
控制器 tick 一次
生成 command
积分器在 dt 内使用保持的 command
到下一个采样点再更新 controller_state

也就是:

python
state, control_log = controller_tick(ctx, state)
state, integ_log = integrate_plant_with_held_command(ctx, state, dt)

这是很多仿真程序中最容易耦合出错的地方。


7. “数据和函数”与“微分方程和边界条件”的深层对应

你说编程中面对的是数据和函数。

在这个语境中可以更精确地说:

text
数据 = 坐标、参数、采样值、状态、边界条件;
函数 = 物理律、组成律、控制律、积分算子、观测算子。

数学物理里也不是只有方程。

它至少包括:

text
状态空间

算子
组成律
约束
边界条件
观测量
泛函

对应程序就是:

text
State schema
Reader context
Pure model functions
Numerical operators
Constraints / events
Scenario
Logs / outputs
Objective functions

所以你可以把程序框架理解为:

> 把数学物理问题中的对象,分别落到 Reader、State、Writer、纯函数、积分器、事件系统和外层 solver 上。


8. 一个可复用的“方程程序设计框架”

我会把它总结成下面这个通用模板。

Step 1:定义状态空间

明确哪些量是状态:

text
plant_state:
    position
    velocity
    attitude
    angular_velocity
    mass_or_propellant

不要把所有中间量都放进 state。

例如 Mach、动压、迎角通常是 derived quantity,应该由状态和环境算出来,不一定作为状态保存。


Step 2:定义 Reader 上下文

text
ctx:
    env:
        wind
        gravity
        atmosphere

    assets:
        aero_table
        engine_table
        mass_property_table
        geometry

    mission:
        reference
        target
        constraints

    numeric:
        integrator
        tolerances
        event_options

Step 3:定义组成律

text
aero_coeff(ctx, mach, alpha, beta)
thrust(ctx, burn_time, ambient_pressure)
mass_properties(ctx, burn_time)
gravity(ctx, t, position)
wind(ctx, t, position)

这些函数最好是纯函数。


Step 4:定义力和力矩项

text
gravity_term(ctx, t, state, command)
aero_term(ctx, t, state, command)
thrust_term(ctx, t, state, command)
control_term(ctx, t, state, command)

然后合成:

text
total_force_moment = fold_sum(force_terms)

Step 5:定义 RHS

text
rhs(ctx, mode, command, t, plant_state) -> plant_state_derivative

RHS 不关心积分器。


Step 6:定义积分器

text
integrator_step(rhs, t, x, dt) -> x_next

积分器不关心火箭。


Step 7:定义事件系统

text
events:
    burnout
    stage_separation
    ground_impact
    max_q
    target_altitude

事件触发后执行:

text
reset_map(ctx, state, event) -> new_state

Step 8:定义 Writer 输出

text
sample_observables(ctx, state):
    t
    position
    velocity
    mach
    q
    alpha
    beta
    thrust
    cd
    cl
    event_flags

Step 9:定义外层问题

初值问题:

text
run(ctx, initial_state) -> trajectory

边值问题:

text
residual(params) = terminal_error(run(ctx.with_params(params), initial_state))
params_star = solve(residual)

优化问题:

text
objective(params) = cost(run(ctx.with_control_params(params), initial_state))
params_star = minimize(objective)

9. 对 Plant-Controller-R 的解释

如果这里的 R 指的是 Reference,那么它自然对应控制问题中的参考输入:

[ r(t) ]

控制器可以写成:

[ u = \pi(y, r, c) ]

其中:

  • Plant:被控对象,也就是火箭动力学;
  • Controller:控制律;
  • Reference:目标轨迹、目标姿态、目标速度、制导指令。

程序结构就是:

text
reference(ctx, t) -> ref
observe(ctx, state) -> y
controller(ctx, y, ref, ctrl_state) -> command, ctrl_state_next
plant_rhs(ctx, state, command) -> state_derivative

如果 R 指的是 Runner / Runtime,那么它就更接近本文里的数值解释器:

text
Plant + Controller + Runner

即:

text
模型 + 控制律 + 仿真执行器

这两种解释都可以放进同一个框架里。


10. 这个框架的核心原则

我建议你把它记成几条原则。

原则一:区分模型和解释器

text
rhs 是模型;
rk4/dop853 是解释器。

不要让气动力模型知道自己正在被 RK4 调用。


原则二:区分状态和背景

text
会随仿真推进改变,并且影响未来演化的,是 State;
固定不变或外生给定的,是 Reader;
只用于记录的,是 Writer。

例如:

  • 气动表:Reader;
  • 当前燃料质量:State;
  • 由燃烧时间查出来的推力曲线:Reader;
  • 当前动压:通常是 Writer / derived output。

原则三:区分物理量和数值量

物理层:

text
force
moment
mass
inertia
velocity
attitude

数值层:

text
dt
tolerance
interpolation
error_estimate
substep

两者不要混在一起。


原则四:事件是离散动力学,不是普通日志

燃尽、分离、落地不是普通输出,而是会改变未来演化规律的事件。

所以它们属于:

text
State transition / mode transition

而不只是 Writer。


原则五:坐标系变换必须显式

对火箭这种问题,坐标系是最容易出错的地方。

建议所有力项都约定:

text
输入状态在哪个坐标系;
输出力/力矩在哪个坐标系;
最后在哪个坐标系统一求和。

例如:

text
aero_force_body
thrust_force_body
gravity_force_inertial
body_to_inertial
inertial_to_body

不要让不同模块偷偷返回不同坐标系的向量。


11. 最后的抽象图景

你可以把整个过程看成:

text
数学物理问题
    =
状态空间
+ 演化律
+ 组成律
+ 环境场
+ 控制律
+ 初始/边界条件
+ 观测量
+ 数值解释器

对应程序:

text
simulation program
    =
State
+ pure rhs functions
+ assets / context
+ environment functions
+ controller pipeline
+ scenario / events
+ writer logs
+ integrator

所以,是的:

> 你完全可以从数学物理基础推出一种普遍的方程程序设计框架。

它不是某个具体设计模式,而是一种分层原则:

text
Reader 负责背景;
State 负责演化;
Writer 负责观测;
纯函数负责局部物理律;
积分器负责连续到离散;
事件系统负责模式切换;
外层 solver 负责边界条件和优化。

这就是 RWS monad pipeline 在数值物理仿真中的“真意”。