Skill: wangjiaocheng/cda-code-lab (version 1.0.0)

--- name: cda-code-lab author: 王教成 Wang Jiaocheng (波动几何) description: | CDA 架构代码工坊——按 Causal Dynamics Architecture 规范生成可执行的 Python 仿真代码。 覆盖核心组件：CDABlock 管线、PINN 机制函数、哈密顿投影模块、辛积分器、NOTEARS 因果发现、 贝叶斯在线更新、CER 因果封装递归。 生成的代码可直接运行，依赖 PyTorch，接受 CDA Data Synthesizer 产出的 JSON 数据作为输入。 当用户需要实现 CDA 架构组件、生成仿真原型代码、构建因果推理实验时触发。 触发词：CDA代码、因果动力学实现、CDABlock、哈密顿投影、PINN机制函数、辛积分器、NOTEARS因果发现、CER实现、因果仿真代码。 --- # CDA 架构代码工坊（Code Lab） 将 Causal Dynamics Architecture 的理论设计转化为可执行的 Python 仿真代码。 ## 核心定位 CDA 主技能定义了架构蓝图，Data Synthesizer 生成数据，本技能负责**让架构跑起来**。 **数据格式依赖**：本技能生成的代码读取 CDA Data Synthesizer 定义的 JSON 格式（见 `references/data-format-spec.md`）。 ## 可生成的组件 按 CDA 五层栈和路线图 Phase 1-2 组织： ### Phase 1 组件（热力学仿真引擎） | 组件 | CDA 章节 | 输入 | 输出 | |------|---------|------|------| | **CDABlock** | §3.2 | EntityState + CausalEdges | 更新后的 EntityState | | **PINN 机制函数** | §3.3 | 实体状态对 (s_i, s_j) | 状态变化量 Δs | | **哈密顿投影** | §4.1 | 状态 (q, p) | 约束后状态 | | **辛积分器** | §4.2 | 状态 + 力函数 | 下一时刻状态 | | **贝叶斯状态更新** | §6.2 L1 | 旧信念 + 观测 | 新信念 | | **JSON 数据加载器** | — | *.graph.json / *.trajectory.json | Python 对象 | ### Phase 2 组件（多域因果耦合） | 组件 | CDA 章节 | 说明 | |------|---------|------| | **机制类型注册表** | §3.3.1 | 力学/热力学/流体/化学反应类型的 W 矩阵约束 | | **因果计算路由** | §3.4 | 稀疏边选择（路由分数 → Top-K → 预算） | | **NOTEARS 因果发现** | §6.1.2 | 从时序数据中学习因果结构 | | **在线 Laplace 学习** | §6.2 L2 | 机制函数参数的在线贝叶斯更新 | | **粗粒化 + 重正化** | §7 | 信息瓶颈驱动的实体聚合 | ### 高级组件（Phase 3-4） > 以下组件对应 CDA 主参考文档的深层章节（§5 do-演算、§7 多尺度、§8 CER）， > 属于路线图 Phase 3（反事实推理与决策，2-4 年）和 Phase 4（通用因果世界模型，4-8 年）。 > 当前 SKILL.md 和 references 中的 Phase 1-2 组件已覆盖核心计算原语。 > 当用户请求实现以下组件时，应先加载 CDA 主技能的参考文档对应章节获取完整规格。 | 组件 | CDA 章节 | Phase | 说明 | |------|---------|-------|------| | **do-演算图手术** | §五 | 3 | 干预推理：切断因果边 + 传播 | | **反事实推理** | §五 | 3 | 溯因→干预→预测三步 | | **CER 因果封装递归** | §八 | 4 | 异构嵌套因果子系统（CDA 最原创贡献） | | **多尺度聚合** | §七 | 3 | 自适应分辨率切换（粗粒化 + 重正化） | | **量纲一致性模块** | §3.3.2 / §4.4 | 2 | 禁止跨量纲的参数耦合（物理量纲通道） | | **感知校准层** | §2.1-2.2 | 4 | 统一感知接口 + 贝叶斯校准 | ## 参考文件 | 文件 | 内容 | |------|------| | `references/implementation-guide.md` | **核心指南**：代码架构设计、依赖管理、运行约定 | | `references/component-specs.md` | 每个组件的接口签名、输入输出规格、实现要点 | | `references/pinn-templates.md` | PINN 机制函数的 PyTorch 模板（热力学/力学/流体） | | `references/data-format-spec.md` | 数据格式协议（从 Data Synthesizer 共享，本文件为副本） | ## 代码生成原则 1. **PyTorch 优先**：所有可微分组件使用 PyTorch 实现，确保自动微分可用 2. **JSON 兼容**：生成的代码必须能直接读取 Data Synthesizer 的 JSON 输出 3. **可验证**：每个组件附带 self-test 函数，使用合成数据验证正确性 4. **渐进复杂度**：先生成最小可运行版本（单组件），再组合完整管线 5. **物理硬约束**：哈密顿投影、辛积分器等不可用 soft constraint 替代 ## 典型生成流程 ``` 用户请求："实现热力学仿真引擎" ↓ 1. 读取 implementation-guide.md → 确定项目结构和依赖 ↓ 2. 读取 component-specs.md → 确定需要哪些组件 ↓ 3. 读取 pinn-templates.md → 获取机制函数模板 ↓ 4. 生成代码文件： ├── data_loader.py （JSON → EntityState/CausalEdge） ├── entities.py （EntityState, CausalEdge, WorldState 类） ├── mechanisms.py （PINN 机制函数） ├── hamiltonian.py （哈密顿投影） ├── integrator.py （辛积分器） ├── cdablock.py （完整管线） └── run_simulation.py （主入口 + 示例） ↓ 5. 生成 self_test.py → 验证每个组件 ``` ## 与 CDA 主技能的关系 ``` CDA（主技能） ├── 理论参考：架构设计、公式、路线图 ├── CDA Data Synthesizer │ └── 生成 JSON 格式的因果数据集 └── CDA Code Lab（本技能） └── 生成代码：读取 JSON → 运行仿真 → 输出结果 ``` ## 使用指南 1. 用户指定要实现的组件或场景 2. 读取 `references/implementation-guide.md` 获取代码架构 3. 读取 `references/component-specs.md` 获取组件接口规格 4. 根据域类型读取 `references/pinn-templates.md` 获取机制函数模板 5. 生成完整的 Python 代码文件，附带注释和 self-test

path: references/component-specs.md
content: # CDA 组件接口规格（Component Specifications）

> **配合**: `implementation-guide.md`
> **作用**: 每个可生成组件的完整接口签名、输入输出规格、实现要点

---

## 一、核心数据类

### 1.1 EntityState

**文件**: `src/entities.py`

```python
@dataclass
class EntityState:
    id: int
    name: str
    type: str                          # 实体类型标识
    domain: str                        # 物理域
    q: torch.Tensor                    # 广义坐标 (dim_q,)
    p: torch.Tensor                    # 共轭动量 (dim_p,)
    attributes: dict                   # 固有属性（数值用 Tensor）
    belief_mean: torch.Tensor          # 信念均值 (dim_q + dim_p,)
    belief_cov: torch.Tensor           # 协方差矩阵 (dim, dim) 正定

    @property
    def state_dim(self) -> int:
        """总状态维度 = dim_q + dim_p"""
        return self.q.shape[0] + self.p.shape[0]

    def full_state(self) -> torch.Tensor:
        """拼接 q 和 p 为完整状态向量"""
        return torch.cat([self.q, self.p])

    def clone(self, **overrides) -> 'EntityState':
        """深拷贝，支持部分字段覆盖"""
        ...
```

**实现要点**：
- `belief_cov` 必须保证正定：初始化时使用 `torch.eye(dim) * initial_variance`
- `attributes` 中的数值参数在加载时转为 Tensor（便于自动微分）
- `clone()` 用于仿真步进中创建新状态（避免原地修改）

### 1.2 CausalEdge

**文件**: `src/entities.py`

```python
@dataclass
class CausalEdge:
    source_id: int
    target_id: int
    mechanism: 'MechanismFunction'
    mechanism_type: str
    strength: float                    # α_ij ∈ [0, 1]
    delay: float                       # τ_ij（秒）
    confidence: float                  # ∈ [0, 1]

    def is_active(self, state: WorldState, threshold: float = 0.1) -> bool:
        """因果路由判断：该边在当前状态下是否激活"""
        # 路由分数 = strength × |Δs_source| × uncertainty_target × relevance
        ...
```

### 1.3 WorldState

**文件**: `src/entities.py`

```python
@dataclass
class WorldState:
    timestamp: float
    entities: Dict[int, EntityState]
    edges: List[CausalEdge]
    hamiltonian_value: float = None    # 当前总能量

    def get_entity(self, entity_id: int) -> EntityState:
        ...

    def entity_ids(self) -> List[int]:
        return list(self.entities.keys())

    def to_snapshot(self) -> dict:
        """序列化为 JSON 快照（兼容 data-format-spec.md）"""
        ...
    
    @classmethod
    def from_snapshot(cls, data: dict) -> 'WorldState':
        """从 JSON 快照反序列化"""
        ...
```

---

## 二、机制函数

### 2.1 MechanismFunction 基类

**文件**: `src/mechanisms/base.py`

```python
class MechanismFunction(nn.Module):
    """因果机制函数抽象基类。
    
    继承 nn.Module 以支持参数化（PINN）和自动微分。
    """
    
    def __init__(self, mechanism_type: str):
        super().__init__()
        self.mechanism_type = mechanism_type
    
    def delta_q(self, s_src: EntityState, s_tgt: EntityState) -> torch.Tensor:
        """对 target 广义坐标 q 的影响量"""
        return torch.zeros_like(s_tgt.q)
    
    def delta_p(self, s_src: EntityState, s_tgt: EntityState) -> torch.Tensor:
        """对 target 共轭动量 p 的影响量"""
        return torch.zeros_like(s_tgt.p)
    
    def forward(self, s_src: EntityState, s_tgt: EntityState) -> Tuple[torch.Tensor, torch.Tensor]:
        """完整前向：返回 (delta_q, delta_p)"""
        return self.delta_q(s_src, s_tgt), self.delta_p(s_src, s_tgt)
    
    def force_on(self, state: WorldState, target_id: int) -> torch.Tensor:
        """计算作用在 target_id 上的总广义力 F = -∂V/∂q"""
        f_total = torch.zeros_like(state.get_entity(target_id).q)
        for edge in state.edges:
            if edge.target_id == target_id:
                src = state.get_entity(edge.source_id)
                tgt = state.get_entity(edge.target_id)
                dq, dp = edge.mechanism(src, tgt)
                f_total += dq  # q 方向的力
        return f_total
```

### 2.2 ThermalConduction（热传导）

**文件**: `src/mechanisms/thermal.py`

```python
class ThermalConduction(MechanismFunction):
    """傅里叶定律：Q = k·A/d · (T_src - T_tgt)"""
    
    def __init__(self, conductivity: float, area: float, distance: float):
        super().__init__("thermal_conduction")
        self.k = torch.tensor(conductivity, dtype=torch.float64)
        self.A = torch.tensor(area, dtype=torch.float64)
        self.d = torch.tensor(distance, dtype=torch.float64)
        self.eff = self.k * self.A / self.d  # 有效传热系数
    
    def delta_p(self, s_src: EntityState, s_tgt: EntityState) -> torch.Tensor:
        """热流变化：J = kA/d · (T_src - T_tgt)"""
        dT = s_src.q[0] - s_tgt.q[0]  # q[0] = 温度
        heat_flux = self.eff * dT       # W
        return torch.tensor([heat_flux])
    
    def delta_q(self, s_src: EntityState, s_tgt: EntityState) -> torch.Tensor:
        """温度变化：dT = J·dt / (m·C)"""
        # 注意：delta_q 这里返回的是 dT/dt 的系数
        # 实际的 ΔT = delta_q · dt
        heat_flux = self.delta_p(s_src, s_tgt)
        C_total = s_tgt.attributes['mass'] * s_tgt.attributes['specific_heat']
        dT_dt = heat_flux / C_total
        return dT_dt
```

**实现要点**：
- `q[0]` 始终是温度（热力学实体的第一个广义坐标）
- `p[0]` 始终是热流（热力学实体的第一个共轭动量）
- 能量守恒：source 失去的热量 = target 获得的热量

### 2.3 SpringForce（弹簧力）

**文件**: `src/mechanisms/mechanical.py`

```python
class SpringForce(MechanismFunction):
    """胡克定律：F = -k·(x_tgt - x_src - L₀)"""
    
    def __init__(self, stiffness: float, rest_length: float = 0.0):
        super().__init__("spring_force")
        self.k = torch.tensor(stiffness, dtype=torch.float64)
        self.L0 = torch.tensor(rest_length, dtype=torch.float64)
    
    def delta_p(self, s_src: EntityState, s_tgt: EntityState) -> torch.Tensor:
        """弹簧力：F = -k·Δx（作用在 target 上）"""
        delta_x = s_tgt.q[0] - s_src.q[0] - self.L0
        force = -self.k * delta_x
        return torch.tensor([force])
    
    def delta_q(self, s_src: EntityState, s_tgt: EntityState) -> torch.Tensor:
        """速度变化：dv/dt = F/m"""
        force = self.delta_p(s_src, s_tgt)
        m = s_tgt.attributes['mass']
        return force / m
```

### 2.4 PINNMechanism（数据驱动）

**文件**: `src/mechanisms/pinn.py`

```python
class PINNMechanism(MechanismFunction):
    """PINN 参数化的通用因果机制函数。
    
    当物理定律不明确或需要从数据中学习时使用。
    网络输入：concat(s_src.full_state, s_tgt.full_state)
    网络输出：(delta_q, delta_p)
    """
    
    def __init__(self, state_dim: int, hidden_dims: List[int] = [64, 64, 32]):
        super().__init__("pinn_learned")
        in_dim = state_dim * 2  # source + target
        out_dim = state_dim     # delta_q + delta_p
        
        layers = []
        prev_dim = in_dim
        for h_dim in hidden_dims:
            layers.extend([nn.Linear(prev_dim, h_dim), nn.Tanh()])
            prev_dim = h_dim
        layers.append(nn.Linear(prev_dim, out_dim))
        self.net = nn.Sequential(*layers)
        
        # 初始化：接近零输出（不破坏已有物理行为）
        for layer in self.net:
            if isinstance(layer, nn.Linear):
                nn.init.xavier_uniform_(layer.weight, gain=0.01)
                nn.init.zeros_(layer.bias)
    
    def forward(self, s_src: EntityState, s_tgt: EntityState) -> Tuple[torch.Tensor, torch.Tensor]:
        x = torch.cat([s_src.full_state(), s_tgt.full_state()])
        out = self.net(x)
        dim_q = s_tgt.q.shape[0]
        return out[:dim_q], out[dim_q:]
```

**实现要点**：
- 初始化增益设为 0.01（小输出），避免 PINN 初期破坏已有因果结构
- 物理约束通过 loss 函数注入（非硬约束）
- 支持 partial mechanism：部分边用物理公式，部分用 PINN

---

## 三、辛积分器

**文件**: `src/integrator.py`

### 3.1 StörmerVerletIntegrator

```python
class StörmerVerletIntegrator:
    """速度 Verlet 辛积分器。
    
    算法：
      p_{1/2} = p_t + (dt/2) · F(q_t)
      q_{t+1} = q_t + dt · M^{-1} · p_{1/2}
      F_{t+1} = compute_forces(q_{t+1})
      p_{t+1} = p_{1/2} + (dt/2) · F_{t+1}
    
    性质：辛性、时间可逆、二阶精度 O(dt²)
    """
    
    def __init__(self, force_fn: Callable[[WorldState, int], torch.Tensor]):
        """
        Args:
            force_fn: force_fn(state, entity_id) → 广义力 F
        """
        self.force_fn = force_fn
    
    def step(self, state: WorldState, dt: float) -> WorldState:
        """执行一步 Verlet 积分。返回新 WorldState。"""
        new_entities = {}
        for eid, entity in state.entities.items():
            m = entity.attributes.get('mass', torch.tensor(1.0))
            F = self.force_fn(state, eid)
            
            # 半步动量
            p_half = entity.p + 0.5 * dt * F
            # 全步位置
            q_new = entity.q + dt * p_half / m
            
            # 创建临时状态计算新力
            temp_entity = entity.clone(q=q_new, p=p_half)
            temp_state = state.clone_with_entity(eid, temp_entity)
            F_new = self.force_fn(temp_state, eid)
            
            # 半步动量
            p_new = p_half + 0.5 * dt * F_new
            
            new_entities[eid] = entity.clone(q=q_new, p=p_new)
        
        return state.clone_with_entities(new_entities, timestamp=state.timestamp + dt)
```

### 3.2 Yoshida4Integrator

```python
class Yoshida4Integrator:
    """Yoshida 四阶辛积分器。
    
    S_4(dt) = S_2(x₁·dt) · S_2(x₀·dt) · S_2(x₁·dt)
    x₀ = 1 - 2·x₁, x₁ = 1/(2 - 2^{1/3}) ≈ 1.3512
    
    性质：辛性、时间可逆、四阶精度 O(dt⁴)
    """
    
    X1 = 1.0 / (2.0 - 2.0 ** (1.0 / 3.0))  # ≈ 1.3512
    X0 = 1.0 - 2.0 * X1                      # ≈ -1.7024
    
    def __init__(self, force_fn: Callable):
        self.force_fn = force_fn
        self.verlet = StörmerVerletIntegrator(force_fn)
    
    def step(self, state: WorldState, dt: float) -> WorldState:
        """三段 Verlet 组合。"""
        state = self.verlet.step(state, self.X1 * dt)
        state = self.verlet.step(state, self.X0 * dt)
        state = self.verlet.step(state, self.X1 * dt)
        return state
```

---

## 四、哈密顿投影

**文件**: `src/hamiltonian.py`

```python
class HamiltonianProjector:
    """将状态投影到能量守恒流形上。
    
    方法：拉格朗日乘子法
    目标：min ‖(q',p') - (q,p)‖²  s.t. H(q',p') = E_target
    """
    
    def __init__(self, energy_fn: Callable[[WorldState], float],
                 tolerance: float = 1e-6):
        """
        Args:
            energy_fn: energy_fn(state) → 系统总能量
            tolerance: 能量偏差容忍度
        """
        self.energy_fn = energy_fn
        self.E_target = None  # 在首次调用时设定
        self.tolerance = tolerance
    
    def initialize(self, initial_state: WorldState):
        """设定目标能量为初始状态的总能量"""
        self.E_target = self.energy_fn(initial_state)
    
    def project(self, state: WorldState) -> WorldState:
        """投影到能量流形。
        
        通过梯度下降法迭代修正：
          q' = q - λ · ∂H/∂q
          p' = p - λ · ∂H/∂p
        直到 |H(q',p') - E_target| < tolerance
        """
        if self.E_target is None:
            self.initialize(state)
        
        # 数值梯度
        E_current = self.energy_fn(state)
        if abs(E_current - self.E_target) < self.tolerance:
            return state  # 已满足约束
        
        # 简单投影：按动能/势能比例缩放
        # （完整实现需要自动微分计算 ∂H/∂q 和 ∂H/∂p）
        error = E_current - self.E_target
        scale = 1.0 - error / (2.0 * E_current + 1e-10)
        
        new_entities = {}
        for eid, entity in state.entities.items():
            # 对 p 进行缩放（动能 ∝ p²）
            new_entities[eid] = entity.clone(
                q=entity.q * (2.0 - scale),  # 互补缩放
                p=entity.p * scale
            )
        
        return state.clone_with_entities(new_entities)
```

**实现要点**：
- 投影应该在辛积分之后执行（管线 Step C）
- 投影不应过大：如果单步投影修正 > 1% 总能量，说明积分器步长过大
- 生产级实现应使用自动微分（`torch.autograd.grad`）而非数值梯度

---

## 五、CDABlock 完整管线

**文件**: `src/cdablock.py`

```python
class CDABlock:
    """CDA 核心计算块。
    
    管线：路由 → 机制计算 → 辛积分 → 哈密顿投影 → 贝叶斯更新
    """
    
    def __init__(self, integrator: 'SymplecticIntegrator',
                 projector: 'HamiltonianProjector',
                 bayesian_updater: Optional['BayesianUpdater'] = None):
        self.integrator = integrator
        self.projector = projector
        self.bayesian_updater = bayesian_updater
    
    def compute_forces(self, state: WorldState, entity_id: int) -> torch.Tensor:
        """汇总所有因果边对该实体的力"""
        force = torch.zeros_like(state.get_entity(entity_id).p)
        for edge in state.edges:
            if edge.target_id == entity_id and edge.is_active(state):
                src = state.get_entity(edge.source_id)
                tgt = state.get_entity(edge.target_id)
                _, dp = edge.mechanism(src, tgt)
                force += dp
        return force
    
    def step(self, state: WorldState, dt: float) -> WorldState:
        """一步完整的因果动力学步进"""
        # Step 1: 因果路由 + 力计算（在 compute_forces 中隐式完成）
        # Step 2: 辛积分（更新 q, p）
        self.integrator.force_fn = self.compute_forces
        new_state = self.integrator.step(state, dt)
        # Step 3: 哈密顿投影
        new_state = self.projector.project(new_state)
        # Step 4: 贝叶斯更新（可选）
        if self.bayesian_updater is not None:
            new_state = self.bayesian_updater.update(new_state)
        return new_state
    
    def simulate(self, initial_state: WorldState,
                 dt: float, n_steps: int,
                 observer: Optional[Callable] = None) -> List[WorldState]:
        """运行完整仿真，返回轨迹"""
        self.projector.initialize(initial_state)
        trajectory = [initial_state]
        state = initial_state
        
        for i in range(n_steps):
            state = self.step(state, dt)
            trajectory.append(state)
            if observer is not None:
                observer(state, i)
        
        return trajectory
```

---

## 六、学习组件

### 6.1 BayesianUpdater（EKF 贝叶斯更新）

**文件**: `src/learning/bayesian_update.py`

```python
class BayesianUpdater:
    """扩展卡尔曼滤波（EKF）贝叶斯状态更新。
    
    对应 CDA §6.2 Level 1。
    """
    
    def __init__(self, process_noise: float = 1e-4, 
                 observation_noise: float = 1e-2):
        self.Q = process_noise   # 过程噪声
        self.R = observation_noise  # 观测噪声
    
    def predict(self, entity: EntityState, dt: float) -> EntityState:
        """预测步：不确定性随时间增长"""
        dim = entity.state_dim
        # 状态不变，协方差增大
        new_cov = entity.belief_cov + self.Q * torch.eye(dim) * dt
        return entity.clone(belief_cov=new_cov)
    
    def update(self, entity: EntityState, 
               observation: torch.Tensor) -> EntityState:
        """更新步：用观测修正信念"""
        # K = P·H^T / (H·P·H^T + R)
        P = entity.belief_cov
        H = torch.eye(entity.state_dim)  # 观测矩阵（直接观测状态）
        S = H @ P @ H.T + self.R * torch.eye(entity.state_dim)
        K = P @ H.T @ torch.inverse(S)
        
        innovation = observation - entity.belief_mean
        new_mean = entity.belief_mean + K @ innovation
        new_cov = (torch.eye(entity.state_dim) - K @ H) @ P
        
        return entity.clone(belief_mean=new_mean, belief_cov=new_cov)
```

### 6.2 NOTEARS 因果发现

**文件**: `src/learning/notears.py`

```python
class NOTEARSDiscovery:
    """NOTEARS 因果结构学习。
    
    对应 CDA §6.1.2（骨架引导版本）。
    
    输入：时序观测矩阵 X ∈ R^{T×N}
    输出：邻接矩阵 W ∈ R^{N×N}（DAG）
    """
    
    def __init__(self, skeleton: Optional[np.ndarray] = None,
                 lambda1: float = 0.01, max_iter: int = 100):
        self.skeleton = skeleton      # 先验骨架 (N, N)
        self.lambda1 = lambda1        # L1 正则化系数
        self.max_iter = max_iter
    
    def discover(self, X: np.ndarray) -> np.ndarray:
        """从观测数据学习因果结构。
        
        Args:
            X: (T, N) 时序观测矩阵
            
        Returns:
            W: (N, N) 邻接矩阵，W[i,j] > 0 表示 i → j 的因果边
        """
        T, N = X.shape
        W = np.zeros((N, N))
        # 初始化：骨架边给予较大初始值
        if self.skeleton is not None:
            W = self.skeleton.copy()
        
        # 增广拉格朗日法求解
        # ...（完整实现见 CDA §6.1.2 参考代码）
        return W
```

---

## 七、数据加载器

**文件**: `src/data_loader.py`

```python
def load_graph(path: str) -> Tuple[Dict[int, EntityState], List[CausalEdge]]:
    """从 .graph.json 加载因果图。
    
    Returns:
        entities: {id: EntityState}
        edges: List[CausalEdge]
    """
    with open(path) as f:
        data = json.load(f)
    
    entities = {}
    for e_data in data['entities']:
        entity = EntityState(
            id=e_data['id'],
            name=e_data['name'],
            type=e_data['type'],
            domain=e_data['domain'],
            q=torch.tensor(e_data['position']['values'], dtype=torch.float64),
            p=torch.tensor(e_data['momentum']['values'], dtype=torch.float64),
            attributes=_convert_attrs(e_data['attributes']),
            belief_mean=torch.tensor(e_data['belief']['mean'], dtype=torch.float64),
            belief_cov=torch.tensor(e_data['belief']['covariance'], dtype=torch.float64),
        )
        entities[entity.id] = entity
    
    edges = []
    for e_data in data['edges']:
        mechanism = _create_mechanism(e_data)
        edge = CausalEdge(
            source_id=e_data['source_id'],
            target_id=e_data['target_id'],
            mechanism=mechanism,
            mechanism_type=e_data['mechanism_type'],
            strength=e_data['strength'],
            delay=e_data['delay'],
            confidence=e_data['confidence'],
        )
        edges.append(edge)
    
    return entities, edges


def load_trajectory(path: str) -> List[WorldState]:
    """从 .trajectory.json 加载轨迹。"""
    with open(path) as f:
        data = json.load(f)
    
    trajectory = []
    for snap in data['snapshots']:
        state = WorldState.from_snapshot(snap)
        trajectory.append(state)
    
    return trajectory


def _create_mechanism(edge_data: dict) -> MechanismFunction:
    """根据 edge 的 mechanism_type 和 function_form 创建对应的机制函数。
    
    分发逻辑（对应 data-format-spec §4.2 机制类型注册表）：
    
    Phase 1（确定性物理公式）：
      thermal_conduction + fourier_law → ThermalConduction
      thermal_conduction + radiation    → ThermalRadiation
      thermal_convection                  → ThermalConvection
      spring_force + hooke_law           → SpringForce
      damping_force                       → DampingForce
      pinn_learned                        → PINNMechanism
    
    Phase 2（可学习参数化，fallback）：
      * → CausalMechanismFunction（通用 W_type·Φ·σ 框架）
    """
    m_type = edge_data['mechanism_type']
    params = edge_data.get('mechanism_params', {})
    form = params.get('function_form', '')

    from src.mechanisms.thermal import ThermalConduction, ThermalConvection
    from src.mechanisms.mechanical import SpringForce, DampingForce
    from src.mechanisms.pinn import PINNMechanism

    if m_type == 'thermal_conduction' and form == 'fourier_law':
        return ThermalConduction(
            conductivity=params['conductivity'],
            area=params['cross_section_area'],
            distance=params['distance'],
        )
    elif m_type == 'thermal_conduction' and form == 'radiation':
        from src.mechanisms.thermal import ThermalRadiation
        return ThermalRadiation(
            emissivity=params.get('emissivity', 0.9),
            area=params.get('cross_section_area', 1.0),
        )
    elif m_type == 'thermal_convection':
        return ThermalConvection(
            h_conv=params.get('h_convection', 10.0),
            area=params.get('area', 1.0),
        )
    elif m_type == 'spring_force' and form == 'hooke_law':
        return SpringForce(
            stiffness=params['stiffness'],
            rest_length=params.get('rest_length', 0.0),
        )
    elif m_type == 'damping_force':
        return DampingForce(
            damping_coeff=params.get('damping_coeff', 1.0),
        )
    elif m_type == 'pinn_learned':
        pinn_cfg = params.get('pinn_params', {})
        return PINNMechanism(
            state_dim=pinn_cfg.get('input_dim', 6),
            hidden_dims=pinn_cfg.get('network_arch', [64, 64, 32]),
        )
    else:
        from src.mechanisms.causal_mechanism import CausalMechanismFunction
        return CausalMechanismFunction(
            causal_type=m_type,
            state_dim=params.get('state_dim', 6),
        )
```

---

## 八、Phase 2：通用机制函数框架（CDA §3.3）

> **Phase 分层说明**：§二~§七中的机制函数为 Phase 1 实现（确定性物理公式，直接计算）。
> §八~§九为 Phase 2 框架规格（可学习参数化，从 CDA §3.3 完整迁移）。
> 两层通过 `PhysicsPlusResidualPINN`（见 pinn-templates.md §六）桥接。

### 8.1 MechanismTypeRegistry（机制类型约束注册表）

**文件**: `src/mechanisms/registry.py`

对应 CDA §3.3.1。每种因果类型定义 W 矩阵的结构约束，保证梯度下降不可能违反物理定律。

```python
class MechanismTypeRegistry:
    """因果机制类型注册表。
    
    约束在参数化层面保证，而非损失函数层面。
    无论梯度如何更新，W_type 始终满足物理定律。
    """

    @staticmethod
    def mechanical_W(raw_A: torch.Tensor) -> torch.Tensor:
        """力学型：牛顿第三定律（作用力 = 反作用力）。
        W = A - A^T → W^T = -W（反对称）。
        自由度：d(d-1)/2。
        """
        return raw_A - raw_A.T

    @staticmethod
    def thermal_W(raw_A: torch.Tensor, beta: float = 5.0) -> torch.Tensor:
        """热力学型：热力学第二定律（热量从高温流向低温）。
        W = exp(-β·sigmoid(A))，所有元素 ∈ (0,1]，保证热流方向正确。
        """
        return torch.exp(-beta * torch.sigmoid(raw_A))

    @staticmethod
    def information_W(raw_diag: torch.Tensor, gamma: float = 1.0) -> torch.Tensor:
        """信息型：信号衰减（强度随距离递减）。
        W = diag(exp(-γ·|a_i|))，对角衰减矩阵。
        """
        return torch.diag(torch.exp(-gamma * torch.abs(raw_diag)))

    @staticmethod
    def chemical_W(raw_A: torch.Tensor) -> torch.Tensor:
        """化学型：质量守恒（所有反应物的质量转移总和为零）。
        W 行和为零（流入 = 流出）。
        W = A - diag(A·1)。
        """
        row_sum = raw_A.sum(dim=1, keepdim=True)
        return raw_A - row_sum / raw_A.shape[1]
```

### 8.2 InteractionBasis（状态交互基函数 Φ）

**文件**: `src/mechanisms/interaction_basis.py`

对应 CDA §3.3.2。可学习部分，捕获"实体 i 在实体 j 影响下的状态变化"。

```python
class InteractionBasis(nn.Module):
    """状态交互基函数 Φ(s_i, s_j)。
    
    设计原则：
    - 分离变量：Φ = Σ_k φ_k(s_i) · ψ_k(s_j)
    - 物理量纲一致性：每个基函数对应一个物理量纲通道
    - 多尺度交互：低阶捕获线性效应，高阶捕获非线性耦合
    """

    def __init__(self, state_dim: int, hidden_dim: int = 64, num_basis: int = 16):
        super().__init__()
        self.state_dim = state_dim
        self.num_basis = num_basis

        self.source_encoder = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, num_basis),
        )
        self.target_encoder = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.Tanh(),
            nn.Linear(hidden_dim, num_basis),
        )
        self.basis_weights = nn.Parameter(torch.randn(num_basis, state_dim) * 0.01)

    def forward(self, s_i: torch.Tensor, s_j: torch.Tensor) -> torch.Tensor:
        """计算 Φ(s_i, s_j) ∈ R^state_dim。
        
        Args:
            s_i: 目标实体状态 (batch, state_dim)
            s_j: 源实体状态 (batch, state_dim)
        Returns:
            delta_s: 状态变化量 (batch, state_dim)
        """
        h_i = self.target_encoder(s_i)
        h_j = self.source_encoder(s_j)
        interaction = h_i * h_j
        return interaction @ self.basis_weights
```

### 8.3 CausalDecay（因果强度衰减 σ）

**文件**: `src/mechanisms/decay.py`

对应 CDA §3.3.3。

```python
class CausalDecay(nn.Module):
    """因果强度衰减函数。
    
    σ(r) = exp(-α·r)              指数衰减（默认，中程如热传导）
    σ(r) = 1/(1+α·r²)             逆二次衰减（长程如引力）
    σ(r) = exp(-(α·r)²)           高斯衰减（短程如化学反应）
    
    衰减类型由因果类型自动决定（DECAY_RULES）。
    """

    DECAY_RULES = {
        'mechanical': 'inverse_quadratic',
        'thermal':    'exponential',
        'chemical':   'gaussian',
        'information':'exponential',
    }

    def __init__(self, causal_type: str):
        super().__init__()
        self.decay_type = self.DECAY_RULES.get(causal_type, 'exponential')
        self.alpha = nn.Parameter(torch.tensor(1.0))

    def forward(self, distance: torch.Tensor) -> torch.Tensor:
        r = distance + 1e-8
        if self.decay_type == 'exponential':
            return torch.exp(-self.alpha * r)
        elif self.decay_type == 'inverse_quadratic':
            return 1.0 / (1.0 + self.alpha * r ** 2)
        elif self.decay_type == 'gaussian':
            return torch.exp(-(self.alpha * r) ** 2)
```

### 8.4 CausalMechanismFunction（完整参数化组装）

**文件**: `src/mechanisms/causal_mechanism.py`

对应 CDA §3.3.4。`f_ij = W_type · Φ(s_i, s_j) · σ(α·distance)`

```python
class CausalMechanismFunction(nn.Module):
    """因果机制函数 f_ij(s_i, s_j; θ) 的完整实现。
    
    f_ij = strength · (Φ(s_i, s_j) · W_type^T) · σ(distance)
    
    这是 CDA 的通用机制函数框架，Phase 1 的确定性物理公式是其特例。
    """

    def __init__(self, causal_type: str, state_dim: int,
                 hidden_dim: int = 64, num_basis: int = 16):
        super().__init__()
        self.causal_type = causal_type
        raw_dim = state_dim
        self.raw_W = nn.Parameter(torch.randn(raw_dim, raw_dim) * 0.01)
        self.phi = InteractionBasis(state_dim, hidden_dim, num_basis)
        self.decay = CausalDecay(causal_type)
        self.strength = nn.Parameter(torch.tensor(0.5))
        self.registry = MechanismTypeRegistry()

    def get_W(self) -> torch.Tensor:
        """获取受物理约束的 W_type 矩阵"""
        if self.causal_type == 'mechanical':
            return self.registry.mechanical_W(self.raw_W)
        elif self.causal_type == 'thermal':
            return self.registry.thermal_W(self.raw_W)
        elif self.causal_type == 'information':
            diag = F.softplus(self.raw_W.diagonal())
            return self.registry.information_W(diag)
        elif self.causal_type == 'chemical':
            return self.registry.chemical_W(self.raw_W)
        else:
            return self.raw_W

    def forward(self, s_i: torch.Tensor, s_j: torch.Tensor,
                distance: torch.Tensor) -> torch.Tensor:
        """计算实体 i 在实体 j 影响下的状态变化 Δs_i。
        
        Args:
            s_i: 目标实体状态 (batch, state_dim)
            s_j: 源实体状态 (batch, state_dim)
            distance: 因果距离 (batch, 1)
        Returns:
            delta_s: 状态变化量 (batch, state_dim)
        """
        W = self.get_W()
        phi_val = self.phi(s_i, s_j)
        decay_val = self.decay(distance)
        strength = torch.sigmoid(self.strength)
        return strength * (phi_val @ W.T) * decay_val
```

---

## 九、Phase 2：因果计算路由（CDA §3.4）

> **Phase 分层说明**：Phase 1 的 CDABlock 使用简单 `edge.is_active()` 判断。
> Phase 2 引入完整 CausalRouter，支持 N > 1000 实体的大规模系统。

**文件**: `src/routing/causal_router.py`

对应 CDA §3.4。物理语义驱动的稀疏边选择。

```python
class CausalRouter:
    """因果计算路由器。
    
    路由分数 r_ij = α_ij × S_change(i) × S_unc(j) × Q(j)
    
    - α_ij: 因果强度（边的固有属性）
    - S_change(i): 源实体最近的状态变化显著性（tanh 归一化）
    - S_unc(j): 目标实体的当前不确定性（sigmoid 归一化）
    - Q(j): 查询相关性（可选，无查询时恒为 1）
    
    三级筛选策略：
      Level 1: 硬阈值（分数 < 0.01 的边直接跳过）
      Level 2: Top-K（每个目标实体保留分数最高的 k 条入边）
      Level 3: 全局预算（总边数不超过 max_edges）
    
    复杂度：O(N²k) → O(NKk)，K ≪ N 时 640× 加速。
    """

    def __init__(self, num_entities: int, top_k: int = 16,
                 max_edges: int = 2048, hard_threshold: float = 0.01):
        self.num_entities = num_entities
        self.top_k = top_k
        self.max_edges = max_edges
        self.hard_threshold = hard_threshold

    def route(self, world: WorldState,
              query_entities: Optional[Set[int]] = None) -> Set[Tuple[int, int]]:
        """计算本步需要激活的因果边集合。
        
        Args:
            world: 当前世界状态
            query_entities: 查询关注的实体集合（可选）
            
        Returns:
            active_edges: Set[(source_id, target_id)] 本步需要计算的边
        """
        scores = self._compute_scores(world, query_entities)

        # Level 1: 硬阈值过滤
        mask = scores > self.hard_threshold

        # Level 2: 每个目标实体保留 top-k 入边
        selected = torch.zeros_like(mask)
        for j in range(self.num_entities):
            col = scores[:, j] * mask[:, j]
            nonzero = (col > 0).sum().item()
            k = min(self.top_k, nonzero)
            if k > 0:
                top_idx = col.topk(k).indices
                selected[top_idx, j] = True

        # Level 3: 全局预算上限
        total = selected.sum().item()
        if total > self.max_edges:
            flat = (scores * selected).flatten()
            cutoff = flat.topk(self.max_edges).values[-1]
            selected &= (scores >= cutoff)

        # 转换为边集合
        active = set()
        idx = selected.nonzero()
        for row in idx:
            active.add((row[0].item(), row[1].item()))
        return active

    def _compute_scores(self, world: WorldState,
                        query_entities: Optional[Set[int]]) -> torch.Tensor:
        """计算 N×N 路由分数矩阵"""
        scores = torch.zeros(self.num_entities, self.num_entities)

        for edge in world.edges:
            i, j = edge.source_id, edge.target_id

            alpha = edge.strength

            # 源实体状态变化显著性
            delta = self._recent_state_change(world, i)
            s_change = torch.tanh(delta / (self._state_scale(world, i) + 1e-8))

            # 目标实体不确定性
            sigma_trace = world.get_entity(j).belief_cov.trace().item()
            s_unc = torch.sigmoid((sigma_trace - self._unc_thresh) / self._unc_scale)

            # 查询相关性
            q_rel = 1.0
            if query_entities is not None:
                q_rel = float(j in query_entities)

            scores[i, j] = alpha * (1.0 + s_change) * (1.0 + s_unc) * q_rel

        return scores

    @staticmethod
    def _recent_state_change(world: WorldState, entity_id: int) -> float:
        """获取实体最近的状态变化量（需要 WorldState 保存历史快照）"""
        # Phase 1 简化：返回 q 的绝对值作为代理
        return world.get_entity(entity_id).q.abs().sum().item()

    @staticmethod
    def _state_scale(world: WorldState, entity_id: int) -> float:
        return world.get_entity(entity_id).q.abs().mean().item() + 1e-8

    _unc_thresh: float = 0.1
    _unc_scale: float = 1.0
```

### 9.1 CDABlock 路由集成

Phase 2 的 CDABlock 应集成 CausalRouter，替代简单的 `edge.is_active()` 判断：

```python
class CDABlockV2(CDABlock):
    """Phase 2 CDABlock：集成因果路由器。
    
    与 Phase 1 的区别：
    - Step 1 前增加路由决策（CausalRouter.route）
    - 仅对 active_edges 计算机制函数
    - 支持大规模实体系统（N > 1000）
    """

    def __init__(self, integrator, projector, router: CausalRouter,
                 bayesian_updater=None):
        super().__init__(integrator, projector, bayesian_updater)
        self.router = router

    def compute_forces(self, state: WorldState, entity_id: int) -> torch.Tensor:
        """汇总路由选中边对该实体的力"""
        force = torch.zeros_like(state.get_entity(entity_id).p)
        for edge in state.edges:
            if (edge.source_id, edge.target_id) in self._active_edges:
                if edge.target_id == entity_id:
                    src = state.get_entity(edge.source_id)
                    tgt = state.get_entity(edge.target_id)
                    _, dp = edge.mechanism(src, tgt)
                    force += dp
        return force

    def step(self, state: WorldState, dt: float) -> WorldState:
        """带路由的因果动力学步进"""
        # Step 0: 路由决策
        self._active_edges = self.router.route(state)
        # Step 1~4: 同 Phase 1
        self.integrator.force_fn = self.compute_forces
        new_state = self.integrator.step(state, dt)
        new_state = self.projector.project(new_state)
        if self.bayesian_updater is not None:
            new_state = self.bayesian_updater.update(new_state)
        return new_state
```


path: references/data-format-spec.md
content: # CDA 数据格式规范（Data Format Specification）

> **版本**: v0.1.0
> **状态**: 初版提案
> **所有权**: CDA Data Synthesizer 技能
> 
> **本文件为 `cda-data-synth` 技能权威副本，供 Code Lab 技能直接引用。**
> **更新时须同步两个技能中的此文件。**

本文件定义 Causal Dynamics Architecture 的核心数据序列化协议。所有 CDA 组件（仿真引擎、训练管线、评估框架）必须遵守此格式。

---

## 一、总体设计原则

1. **JSON-first**：以 JSON 为主要序列化格式，人类可读、工具链友好
2. **NumPy 兼容**：所有张量/矩阵字段设计为可直接 `np.array()` 加载
3. **float64 精度**：所有数值字段在加载时必须转为 `torch.float64` / `np.float64`。物理仿真对数值精度高度敏感，JSON 本身不区分 float32/float64，但 consuming 端（Code Lab）应始终以双精度处理。`belief.covariance` 尤其需要高精度以保证正定性
4. **因果图 = 拓扑 + 动力学**：静态结构（实体+边）和动态行为（机制函数参数）分离
5. **时间序列友好**：轨迹数据按 snapshot 组织，支持稀疏/密集采样

---

## 二、核心数据结构

### 2.1 EntityState（实体状态）

对应 CDA §3.1 的 `EntityState` 类。

```json
{
  "id": 0,
  "name": "compressor_1",
  "type": "thermal_node",
  "domain": "thermodynamics",
  "position": {
    "values": [350.0, 0.0, 0.0],
    "coordinates": ["temperature_K", "x_m", "y_m"],
    "units": ["K", "m", "m"]
  },
  "momentum": {
    "values": [1500.0, 0.0, 0.0],
    "coordinates": ["heat_flux_W", "velocity_x", "velocity_y"],
    "units": ["W", "m/s", "m/s"]
  },
  "attributes": {
    "mass": 25.0,
    "specific_heat": 500.0,
    "thermal_conductivity": 50.0,
    "volume": 0.05,
    "material_type": "steel"
  },
  "belief": {
    "distribution_type": "gaussian",
    "mean": [350.0, 0.0, 0.0],
    "covariance": [
      [1.0, 0.0, 0.0],
      [0.0, 0.01, 0.0],
      [0.0, 0.0, 0.01]
    ]
  }
}
```

**字段说明**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | int | ✅ | 实体唯一标识符（全局） |
| `name` | string | ✅ | 人类可读名称 |
| `type` | string | ✅ | 实体类型（见 §2.5 类型注册表） |
| `domain` | string | ✅ | 所属物理域（thermodynamics/mechanics/fluid/circuit） |
| `position` | object | ✅ | 广义坐标 q |
| `position.values` | float[] | ✅ | 坐标值数组 |
| `position.coordinates` | string[] | ✅ | 每个维度的物理含义 |
| `position.units` | string[] | ✅ | 每个维度的单位 |
| `momentum` | object | ✅ | 共轭动量 p（结构同 position） |
| `attributes` | object | ✅ | 固有属性（质量、材料参数等，域相关） |
| `belief` | object | ✅ | 贝叶斯信念分布 |

**belief 子结构**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `distribution_type` | string | 目前仅支持 `gaussian`，预留 `mixture`, `laplace` |
| `mean` | float[] | 信念均值（维度 = position + momentum） |
| `covariance` | float[][] | 协方差矩阵（方阵，维度同 mean） |

---

### 2.2 CausalEdge（因果边）

对应 CDA §3.1 的 `CausalEdge` 类。

```json
{
  "source_id": 0,
  "target_id": 1,
  "mechanism_type": "thermal_conduction",
  "mechanism_params": {
    "conductivity": 50.0,
    "cross_section_area": 0.01,
    "distance": 2.0,
    "function_form": "fourier_law"
  },
  "strength": 0.85,
  "delay": 0.0,
  "confidence": 0.95,
  "source_knowledge": "physics_equation"
}
```

**字段说明**：

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `source_id` | int | ✅ | 因端实体 ID |
| `target_id` | int | ✅ | 果端实体 ID |
| `mechanism_type` | string | ✅ | 机制类型（见 §2.6 机制类型注册表） |
| `mechanism_params` | object | ✅ | 机制函数参数（域相关，见 §2.7） |
| `strength` | float | ✅ | 因果强度 α_ij ∈ [0, 1] |
| `delay` | float | ✅ | 传播延迟 τ_ij（秒），0 表示即时 |
| `confidence` | float | ✅ | 置信度 ∈ [0, 1] |
| `source_knowledge` | string | ✅ | 知识来源（physics_equation/topology_diagram/equipment_manual/expert/data_inferred） |

**自环边语义（source_id == target_id）**：

当 `source_id == target_id` 时，表示**外部干预或边界条件**作用在该实体自身，而非实体间的因果传播。典型场景：

| 场景 | mechanism_type | 语义 |
|------|---------------|------|
| 空调制冷 | `thermal_convection` | 外部冷源对房间的热注入/提取 |
| 散热器加热 | `thermal_convection` | 外部热源对房间的热注入 |
| 外力施加 | `spring_force` | 外部边界条件/载荷 |
| 控制器反馈 | `pinn_learned` | 自适应控制器对自身状态的修正 |

自环边的机制函数 `f(s_self, s_self)` 只接收一个实体的状态作为输入，输出该实体的状态变化量 Δs。在因果传播计算中，自环边应在普通因果边**之后**处理（先计算实体间传播，再叠加外部干预）。

---

### 2.3 WorldState（世界状态快照）

一个时刻的完整系统状态。

```json
{
  "timestamp": 0.0,
  "time_unit": "s",
  "entities": [
    { "..." : "EntityState 对象（同 §2.1）" }
  ],
  "edges": [
    { "..." : "CausalEdge 对象（同 §2.2）" }
  ],
  "hamiltonian": {
    "total_energy": 45000.0,
    "kinetic_energy": 0.0,
    "potential_energy": 45000.0,
    "energy_error": 1.2e-6,
    "conservation_violated": false
  }
}
```

**字段说明**：

| 字段 | 类型 | 说明 |
|------|------|------|
| `timestamp` | float | 时刻值 |
| `time_unit` | string | 时间单位（s/min/h） |
| `entities` | EntityState[] | 当前时刻所有实体状态 |
| `edges` | CausalEdge[] | 当前时刻所有因果边（通常不变，但在线学习可变） |
| `hamiltonian` | object | 哈密顿量守恒检查（可选，用于验证） |

---

## 三、文件类型规范

### 3.1 Graph 文件（*.graph.json）

静态因果图定义。包含初始时刻的完整系统状态。

```json
{
  "version": "0.1.0",
  "format": "cda-graph",
  "metadata": {
    "name": "building_thermal_system",
    "domain": "thermodynamics",
    "description": "三房间建筑热力学系统",
    "entity_count": 5,
    "edge_count": 6,
    "created_by": "cda-data-synth",
    "creation_time": "2026-04-27T03:00:00Z"
  },
  "entities": [ "..." ],
  "edges": [ "..." ],
  "initial_state": {
    "timestamp": 0.0,
    "hamiltonian": {
      "initial_total_energy": 45000.0
    }
  },
  "hamiltonian_definition": {
    "type": "custom",
    "potential_function": {
      "type": "pairwise",
      "form": "V(q) = sum_{i<j} (alpha_ij * (q_i - q_j)^2)",
      "params": {
        "note": "alpha_ij stored in edge mechanism_params"
      }
    },
    "conservation_quantity": "total_energy",
    "tolerance": 1.0e-4
  }
}
```

### 3.2 Trajectory 文件（*.trajectory.json）

时序轨迹。由多个 WorldState 快照组成。

```json
{
  "version": "0.1.0",
  "format": "cda-trajectory",
  "metadata": {
    "graph_ref": "building_thermal_system.graph.json",
    "snapshot_count": 1000,
    "time_range": {
      "start": 0.0,
      "end": 3600.0,
      "unit": "s"
    },
    "sampling": {
      "method": "uniform",
      "dt": 3.6,
      "steps": 1000
    },
    "generator": {
      "method": "störmer-verlet",
      "noise_level": 0.01
    }
  },
  "snapshots": [
    {
      "timestamp": 0.0,
      "entities": [ "..." ],
      "hamiltonian": { "..." }
    },
    {
      "timestamp": 3.6,
      "entities": [ "..." ],
      "hamiltonian": { "..." }
    }
  ]
}
```

**优化建议**：对于大型轨迹（>100 实体 × >10000 时间步），考虑分片存储：
```
trajectory/
  ├── meta.json          （metadata）
  ├── snapshot_000.json   （t=0 ~ t=99）
  ├── snapshot_001.json   （t=100 ~ t=199）
  └── ...
```

### 3.3 Hamiltonian 文件（*.hamiltonian.json）

哈密顿量的完整定义。用于训练时的能量约束。

```json
{
  "version": "0.1.0",
  "format": "cda-hamiltonian",
  "metadata": {
    "domain": "thermodynamics",
    "entity_count": 5
  },
  "kinetic_energy": {
    "formula": "T(p) = sum_i (p_i^2 / (2 * C_i * m_i))",
    "parameters": {
      "C": [500.0, 500.0, 500.0, 500.0, 500.0],
      "m": [25.0, 25.0, 25.0, 25.0, 25.0]
    },
    "note": "C_i = specific_heat, p_i = heat_flux analogy"
  },
  "potential_energy": {
    "formula": "V(q) = sum_{edges} alpha_ij * k_ij * A_ij / d_ij * (T_i - T_j)^2 / 2",
    "parameters": {
      "note": "Edge-dependent, see mechanism_params in edges"
    },
    "note": "Thermal analogy: potential = stored thermal energy"
  },
  "total_energy": {
    "E_total": 45000.0,
    "tolerance": 1.0e-4,
    "enforcement": "hard_constraint"
  }
}
```

### 3.4 Meta 文件（*.meta.json）

数据集元信息，用于索引和管理。

```json
{
  "version": "0.1.0",
  "dataset_name": "thermal_building_3room",
  "description": "三房间建筑热力学仿真：室外→外墙→房间→内墙→房间，含空调和散热器",
  "files": {
    "graph": "thermal_building_3room.graph.json",
    "trajectory": "thermal_building_3room.trajectory.json",
    "hamiltonian": "thermal_building_3room.hamiltonian.json"
  },
  "statistics": {
    "entity_count": 5,
    "edge_count": 6,
    "trajectory_steps": 1000,
    "time_range_s": [0.0, 3600.0],
    "state_dimension": 6
  },
  "tags": ["thermodynamics", "building", "phase1", "multi-entity"]
}
```

---

## 四、类型注册表

### 4.1 实体类型（entity type）

| type | domain | 广义坐标 q | 共轭动量 p | 典型属性 |
|------|--------|-----------|-----------|---------|
| `thermal_node` | thermodynamics | 温度 T [K] | 热流 J [W] | mass, specific_heat, thermal_conductivity |
| `mechanical_body` | mechanics | 位移 x [m] | 动量 p [kg·m/s] | mass, stiffness, damping |
| `fluid_cell` | fluid | 压力 P [Pa] | 质量流 J_m [kg/s] | volume, viscosity, density |
| `circuit_node` | circuit | 电压 V [V] | 电流 I [A] | capacitance, resistance |
| `chemical_species` | chemistry | 浓度 c [mol/L] | 反应流 J_r [mol/s] | molecular_weight, activation_energy |
| `abstract_entity` | general | 任意 | 任意 | 无约束 |

### 4.2 机制类型（mechanism type）

| mechanism_type | 因果关系 | 公式 | 对应物理定律 |
|---------------|---------|------|-------------|
| `thermal_conduction` | T_hot → T_cold | Q = k·A·(T_i - T_j)/d | 傅里叶定律 |
| `thermal_convection` | T_surface → T_fluid | Q = h·A·(T_s - T_f) | 牛顿冷却定律 |
| `thermal_radiation` | T_body → T_absorber | Q = ε·σ·A·(T_i⁴ - T_j⁴) | Stefan-Boltzmann |
| `spring_force` | x_displacement → F | F = -k·(x_i - x_j) | 胡克定律 |
| `damping_force` | v_velocity → F | F = -c·v | 粘性阻尼 |
| `gravitational` | mass → mass | F = -G·m_i·m_j/r² | 万有引力 |
| `pressure_driven_flow` | P_high → P_low | Q = ΔP/R | 压力驱动流动 |
| `ohmic_resistance` | V → I | I = (V_i - V_j)/R | 欧姆定律 |
| `chemical_reaction` | c_reactant → c_product | J = k·c^n | 质量作用定律 |
| `pinn_learned` | general → general | f(q, p; θ) | 数据驱动 PINN |

---

## 五、机制参数格式（§2.7 详解）

不同 mechanism_type 的 `mechanism_params` 结构：

### 5.1 thermal_conduction

```json
{
  "conductivity": 50.0,
  "cross_section_area": 0.01,
  "distance": 2.0,
  "function_form": "fourier_law",
  "pinn_params": null
}
```

### 5.2 spring_force

```json
{
  "stiffness": 1000.0,
  "rest_length": 1.0,
  "damping_coeff": 5.0,
  "function_form": "hooke_law",
  "pinn_params": null
}
```

### 5.3 pinn_learned（数据驱动）

```json
{
  "function_form": "pinn",
  "pinn_params": {
    "network_arch": [64, 64, 32],
    "activation": "tanh",
    "trained": false,
    "param_count": 8000
  }
}
```

---

## 六、Python 加载接口

推荐的数据加载代码骨架（Code Lab 技能会基于此生成完整实现）：

```python
@dataclass
class EntityState:
    id: int
    name: str
    type: str
    domain: str
    position: np.ndarray      # (dim_q,)
    momentum: np.ndarray      # (dim_p,)
    attributes: dict
    belief_mean: np.ndarray   # (dim_q + dim_p,)
    belief_cov: np.ndarray    # (dim, dim)

@dataclass
class CausalEdge:
    source_id: int
    target_id: int
    mechanism_type: str
    mechanism_params: dict
    strength: float
    delay: float
    confidence: float

@dataclass
class WorldState:
    timestamp: float
    entities: Dict[int, EntityState]
    edges: List[CausalEdge]

def load_graph(path: str) -> Tuple[Dict[int, EntityState], List[CausalEdge]]:
    """从 .graph.json 加载静态因果图"""
    ...

def load_trajectory(path: str) -> List[WorldState]:
    """从 .trajectory.json 加载时序轨迹"""
    ...
```

---

## 七、版本策略

- **主版本号**（X.0.0）：破坏性格式变更，不兼容旧版
- **次版本号**（0.X.0）：新增可选字段，向后兼容
- **补丁号**（0.0.X）：文档修正、示例更新

当前版本 `0.1.0`：定义了热力学域的完整格式，力学和流体域的属性参数待补充。


path: references/implementation-guide.md
content: # CDA 实现指南（Implementation Guide）

> **配合**: CDA Code Lab 技能
> **作用**: 定义代码项目的结构、依赖、运行约定、质量标准

---

## 一、技术栈

| 组件 | 选型 | 版本要求 | 原因 |
|------|------|---------|------|
| 语言 | Python | ≥ 3.10 | 科学计算生态 |
| 深度学习框架 | PyTorch | ≥ 2.0 | 自动微分 + GPU |
| 数值计算 | NumPy | ≥ 1.24 | 张量基础操作 |
| 科学计算 | SciPy | ≥ 1.10 | ODE 求解器、稀疏矩阵 |
| 可视化 | Matplotlib | ≥ 3.7 | 轨迹绘图 |
| 数据处理 | Pandas | ≥ 2.0 | CSV/JSON 辅助 |
| 配置管理 | dataclasses | 标准库 | 无额外依赖 |

**不引入的依赖**（保持精简）：
- ❌ 不使用 TensorFlow/JAX（避免框架分散）
- ❌ 不使用 causal-learn NOTEARS 包（自行实现以控制细节）
- ❌ 不使用 gym/pygame（纯仿真，不涉及环境交互）

---

## 二、项目结构

Phase 1（热力学仿真引擎）的标准项目布局：

```
cda_thermal_sim/
├── README.md                  # 项目说明
├── requirements.txt           # pip 依赖
├── config.py                  # 全局配置（dataclass）
├── data/
│   ├── input/
│   │   └── *.graph.json       # Data Synthesizer 生成的输入数据
│   └── output/
│       └── *.trajectory.json  # 仿真输出轨迹
├── src/
│   ├── __init__.py
│   ├── entities.py            # EntityState, CausalEdge, WorldState 数据类
│   ├── data_loader.py         # JSON → Python 对象加载器
│   ├── mechanisms/
│   │   ├── __init__.py
│   │   ├── base.py            # MechanismFunction 抽象基类
│   │   ├── thermal.py         # 热力学机制函数（傅里叶/牛顿冷却）
│   │   ├── mechanical.py      # 力学机制函数（胡克/阻尼）
│   │   └── pinn.py            # PINN 通用机制函数
│   ├── integrator.py          # 辛积分器（Störmer-Verlet / Yoshida 4阶）
│   ├── hamiltonian.py         # 哈密顿投影模块
│   ├── cdablock.py            # CDABlock 完整管线
│   ├── learning/
│   │   ├── __init__.py
│   │   ├── bayesian_update.py # 贝叶斯状态更新（EKF）
│   │   ├── online_laplace.py  # 机制函数在线学习
│   │   └── notears.py         # NOTEARS 因果发现
│   └── utils.py               # 工具函数
├── tests/
│   ├── __init__.py
│   ├── test_entities.py       # 数据结构测试
│   ├── test_mechanisms.py     # 机制函数测试
│   ├── test_integrator.py     # 积分器测试
│   ├── test_hamiltonian.py    # 哈密顿投影测试
│   ├── test_cdablock.py       # 完整管线测试
│   └── test_energy.py         # 能量守恒验证
└── run_simulation.py          # 主入口
```

---

## 三、核心接口约定

### 3.1 EntityState 接口

```python
@dataclass
class EntityState:
    """CDA 实体状态。与 data-format-spec.md §2.1 一一对应。"""
    id: int
    name: str
    type: str               # thermal_node / mechanical_body / ...
    domain: str             # thermodynamics / mechanics / ...
    q: torch.Tensor          # 广义坐标 (dim_q,)
    p: torch.Tensor          # 共轭动量 (dim_p,)
    attributes: dict         # 固有属性
    belief_mean: torch.Tensor # 信念均值 (dim_q + dim_p,)
    belief_cov: torch.Tensor  # 信念协方差 (dim, dim)
```

**关键约定**：
- `q` 和 `p` 始终为 **1D Tensor**（即使单维度也是 shape `(1,)`，不是标量）
- 所有状态变量使用 **float64**（双精度），物理仿真对精度敏感
- `attributes` 中的数值参数也尽量使用 Tensor（便于自动微分）

### 3.2 CausalEdge 接口

```python
@dataclass
class CausalEdge:
    """CDA 因果边。与 data-format-spec.md §2.2 一一对应。"""
    source_id: int
    target_id: int
    mechanism: MechanismFunction  # 可微分因果机制函数
    mechanism_type: str
    strength: float           # α_ij ∈ [0, 1]
    delay: float              # τ_ij（秒）
    confidence: float         # 置信度 ∈ [0, 1]
```

### 3.3 MechanismFunction 接口

```python
class MechanismFunction(ABC):
    """因果机制函数抽象基类。
    
    每条因果边持有一个 MechanismFunction 实例。
    输入：source 实体状态 + target 实体状态
    输出：target 实体的状态变化量 Δs
    """

    @abstractmethod
    def forward(self, s_source: EntityState, s_target: EntityState) -> torch.Tensor:
        """计算因果影响。
        
        Args:
            s_source: 因端实体状态
            s_target: 果端实体状态
            
        Returns:
            delta_s: 状态变化量，shape 与 s_target.q + s_target.p 拼接一致
        """
        ...
    
    @abstractmethod
    def force(self, state: WorldState, entity_id: int) -> torch.Tensor:
        """计算作用在指定实体上的广义力（供辛积分器调用）。
        
        Args:
            state: 当前世界状态
            entity_id: 目标实体 ID
            
        Returns:
            F: 广义力，shape 与 entity.p 一致
        """
        ...
```

### 3.4 CDABlock 接口

```python
class CDABlock:
    """CDA 核心计算块。一次因果动力学步进的完整管线。
    
    管线流程（对应 CDA §3.2）：
    1. 因果路由（稀疏边选择）
    2. 机制计算（每条边计算 Δs）
    3. 辛积分（更新 q, p）
    4. 哈密顿投影（拉回能量流形）
    5. 贝叶斯更新（传播不确定性）
    """
    
    def step(self, state: WorldState, dt: float) -> WorldState:
        """执行一步因果动力学步进。
        
        Args:
            state: 当前世界状态 S_t
            dt: 时间步长
            
        Returns:
            next_state: 下一时刻世界状态 S_{t+1}
        """
        ...
```

---

## 四、运行约定

### 4.1 设备管理

```python
# config.py
@dataclass
class SimConfig:
    device: str = "cpu"          # 默认 CPU（物理仿真通常不需要 GPU）
    dtype: torch.dtype = torch.float64
    seed: int = 42
```

**GPU 使用原则**：
- 仿真本身通常在 CPU 上运行（实体数 < 10000 时）
- PINN 训练可使用 GPU
- 批量轨迹生成（蒙特卡洛采样）可使用 GPU 并行

### 4.2 数值精度要求

| 组件 | 精度要求 | 验证方法 |
|------|---------|---------|
| 辛积分器 | 局部误差 < 1e-6 | 与解析解对比 |
| 哈密顿投影 | 能量偏差 < tolerance | 每步检查 E(t) - E(0) |
| 贝叶斯更新 | 协方差矩阵正定 | Cholesky 分解检查 |
| NOTEARS | DAG 约束 h(W) < 1e-6 | 指数矩阵迹检查 |

### 4.3 随机种子与可复现性

```python
def set_seed(seed: int):
    torch.manual_seed(seed)
    np.random.seed(seed)
    random.seed(seed)
```

所有仿真必须支持种子固定，确保结果可复现。

---

## 五、代码生成顺序

生成代码时，严格按以下顺序逐文件创建，每个文件完成后立即生成对应的测试：

```
1. config.py           →  无测试
2. src/entities.py     →  tests/test_entities.py
3. src/data_loader.py  →  tests/test_entities.py（扩展）
4. src/mechanisms/base.py      →  无测试（抽象类）
5. src/mechanisms/thermal.py   →  tests/test_mechanisms.py
6. src/integrator.py   →  tests/test_integrator.py
7. src/hamiltonian.py  →  tests/test_hamiltonian.py
8. src/cdablock.py     →  tests/test_cdablock.py + tests/test_energy.py
9. run_simulation.py   →  手动验证
```

**前置依赖规则**：后序文件可以导入前序文件，但不可反向依赖。

---

## 六、self-test 规范

每个组件的测试函数必须覆盖：

```python
def test_thermal_mechanism():
    """热传导机制函数测试。"""
    # 1. 正向传播：高温 → 低温，热流方向正确
    # 2. 平衡态：T_source == T_target → 热流为零
    # 3. 对称性：交换 source/target 改变热流符号
    # 4. 梯度检查：torch.autograd.gradcheck
    # 5. 数值精度：与解析傅里叶定律对比误差 < 1e-10
```

**必须通过的测试**：

| 测试 | 验证内容 | 通过条件 |
|------|---------|---------|
| energy_conservation | 长时间仿真的能量漂移 | \|E(t) - E(0)\| / E(0) < 0.001 |
| reversibility | 时间反演 (dt → -dt) 回到初态 | \|q(t) - q(0)\| < 1e-6 |
| causality | 因果边方向正确性 | 热量只从高温流向低温 |
| gradient_check | 自动微分一致性 | gradcheck error < 1e-5 |

---

## 七、依赖版本锁定

```
# requirements.txt
torch>=2.0.0
numpy>=1.24.0
scipy>=1.10.0
matplotlib>=3.7.0
pandas>=2.0.0
```

不锁定具体小版本号，仅规定最低版本。通过 `set_seed()` 保证可复现性，而非依赖版本锁定。


path: references/pinn-templates.md
content: # PINN 机制函数模板（PINN Mechanism Templates）

> **配合**: `component-specs.md` §2.4
> **作用**: 按物理域提供 PINN 机制函数的网络架构、物理约束 loss、训练循环模板

---

## 一、PINN 在 CDA 中的角色

CDA 的机制函数有两种实现方式：

| 方式 | 适用场景 | 优势 | 劣势 |
|------|---------|------|------|
| **物理公式** | 已知物理定律 | 精确、可解释、零训练 | 需要领域知识 |
| **PINN 学习** | 未知或复杂机制 | 通用、数据驱动 | 需要训练、黑箱 |

**CDA 的设计偏好**：优先使用物理公式，PINN 作为补充（用于物理公式无法覆盖的子过程）。

```
典型混合方案：
  已知边 → ThermalConduction（傅里叶定律）
  未知边 → PINNMechanism（从数据学习）
  在线精化 → 物理公式 + PINN 残差修正
```

---

## 二、热力学 PINN 模板

### 2.1 纯热传导学习

**场景**：已知存在热传导关系，但等效传热系数 k_eff 未知（材料老化、接触热阻等）。

```python
class ThermalPINN(nn.Module):
    """学习热传导的等效机制函数。
    
    输入：[T_src, T_tgt, dT/dt_src, dT/dt_tgt, distance, area]
    输出：Q_dot（热流率 W）
    
    物理约束：
    1. 因果方向：T_src > T_tgt → Q > 0
    2. 线性近似：Q ≈ k_eff · A/d · ΔT（PINN 应在大范围内逼近此形式）
    """
    
    def __init__(self, hidden_dims=[64, 64, 32]):
        super().__init__()
        # 输入：6 维
        in_dim = 6
        layers = []
        prev = in_dim
        for h in hidden_dims:
            layers.extend([nn.Linear(prev, h), nn.Tanh()])
            prev = h
        layers.append(nn.Linear(prev, 1))  # 输出：热流率标量
        self.net = nn.Sequential(*layers)
        
        # 初始化为小输出
        self._init_small()
    
    def _init_small(self):
        for m in self.net:
            if isinstance(m, nn.Linear):
                nn.init.xavier_uniform_(m.weight, gain=0.01)
                nn.init.zeros_(m.bias)
    
    def forward(self, T_src, T_tgt, dT_src, dT_tgt, dist, area):
        x = torch.stack([T_src, T_tgt, dT_src, dT_tgt, dist, area], dim=-1)
        return self.net(x).squeeze(-1)


class ThermalPINNLoss(nn.Module):
    """热力学 PINN 的物理约束 loss。
    
    Loss = L_data + λ₁·L_physics + λ₂·L_monotonicity + λ₃·L_energy
    """
    
    def __init__(self, lambda_physics=1.0, lambda_mono=10.0, lambda_energy=5.0):
        super().__init__()
        self.lam_phy = lambda_physics
        self.lam_mono = lambda_mono
        self.lam_ene = lambda_energy
    
    def forward(self, model, batch):
        """计算总 loss。
        
        batch 含：
          T_src, T_tgt: 温度
          Q_obs: 观测热流（用于 L_data）
          T_tgt_next: 下一时刻 target 温度（用于物理约束）
          mass, specific_heat: target 属性（用于能量平衡）
          dt: 时间步长
        """
        T_src, T_tgt = batch['T_src'], batch['T_tgt']
        Q_pred = model(T_src, T_tgt, 
                       batch.get('dT_src', torch.zeros_like(T_src)),
                       batch.get('dT_tgt', torch.zeros_like(T_tgt)),
                       batch['distance'], batch['area'])
        
        # L_data: MSE with observations
        L_data = F.mse_loss(Q_pred, batch['Q_obs'])
        
        # L_physics: 能量平衡
        # Q_in = m·C·dT/dt（流入 target 的热量 = 温升所需热量）
        Q_in = Q_pred * batch['dt']
        dT = batch['T_tgt_next'] - T_tgt
        Q_expected = batch['mass'] * batch['specific_heat'] * dT
        L_physics = F.mse_loss(Q_in, Q_expected)
        
        # L_monotonicity: 热从高温流向低温
        dT = T_src - T_tgt
        # Q 和 dT 应该同号
        violation = torch.relu(-Q_pred * dT)  # 违反时 > 0
        L_monotonicity = violation.mean()
        
        # L_energy: 对称性（交换 src/tgt 改变符号）
        if 'T_src_swap' in batch:
            Q_swap = model(batch['T_src_swap'], batch['T_tgt_swap'],
                          batch.get('dT_tgt', torch.zeros_like(T_tgt)),
                          batch.get('dT_src', torch.zeros_like(T_src)),
                          batch['distance'], batch['area'])
            L_energy = F.mse_loss(Q_pred, -Q_swap)
        else:
            L_energy = torch.tensor(0.0)
        
        total = (L_data 
                 + self.lam_phy * L_physics 
                 + self.lam_mono * L_monotonicity 
                 + self.lam_ene * L_energy)
        
        return total, {
            'data': L_data.item(),
            'physics': L_physics.item(),
            'monotonicity': L_monotonicity.item(),
            'energy': L_energy.item(),
        }
```

### 2.2 训练循环模板

```python
def train_thermal_pinn(model, loss_fn, train_loader, 
                       n_epochs=1000, lr=1e-3):
    """标准 PINN 训练循环。"""
    optimizer = torch.optim.Adam(model.parameters(), lr=lr)
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, n_epochs)
    
    history = []
    for epoch in range(n_epochs):
        model.train()
        epoch_losses = {'data': 0, 'physics': 0, 'monotonicity': 0, 'energy': 0}
        
        for batch in train_loader:
            total_loss, loss_dict = loss_fn(model, batch)
            
            optimizer.zero_grad()
            total_loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
            optimizer.step()
            
            for k, v in loss_dict.items():
                epoch_losses[k] += v
        
        scheduler.step()
        
        # 记录
        n = len(train_loader)
        history.append({k: v/n for k, v in epoch_losses.items()})
        
        if epoch % 100 == 0:
            print(f"Epoch {epoch}: "
                  f"data={epoch_losses['data']/n:.6f} "
                  f"phy={epoch_losses['physics']/n:.6f} "
                  f"mono={epoch_losses['monotonicity']/n:.6f}")
    
    return history
```

---

## 三、力学 PINN 模板

### 3.1 弹簧-阻尼学习

**场景**：已知存在弹性连接，但刚度和阻尼系数未知。

```python
class MechanicalPINN(nn.Module):
    """学习力学系统的力-位移关系。
    
    输入：[x_src, x_tgt, v_src, v_tgt, relative_distance]
    输出：F（作用在 target 上的力）
    
    物理约束：
    1. 牛顿第三定律：F_src→tgt = -F_tgt→src
    2. 能量守恒：弹性势能可积
    3. 因果方向：力趋向于减小偏差
    """
    
    def __init__(self, hidden_dims=[64, 32]):
        super().__init__()
        in_dim = 5
        layers = []
        prev = in_dim
        for h in hidden_dims:
            layers.extend([nn.Linear(prev, h), nn.Tanh()])
            prev = h
        layers.append(nn.Linear(prev, 1))
        self.net = nn.Sequential(*layers)
        self._init_small()


class MechanicalPINNLoss(nn.Module):
    """力学 PINN 物理约束 loss。
    
    Loss = L_data + λ₁·L_newton3 + λ₂·L_energy_conservation + λ₃·L_dissipation
    """
    
    def __init__(self, lambda_newton3=10.0, lambda_energy=5.0, lambda_dissipation=1.0):
        super().__init__()
        self.lam_n3 = lambda_newton3
        self.lam_ene = lambda_energy
        self.lam_diss = lambda_dissipation
    
    def forward(self, model, batch):
        """
        batch 含：
          x_src, x_tgt: 位移
          v_src, v_tgt: 速度
          F_obs: 观测力
          mass: target 质量
          dt: 时间步长
          x_tgt_next, v_tgt_next: 下一时刻状态
        """
        x_src, x_tgt = batch['x_src'], batch['x_tgt']
        F_pred = model(x_src, x_tgt, batch['v_src'], batch['v_tgt'],
                       torch.abs(x_tgt - x_src))
        
        # L_data
        L_data = F.mse_loss(F_pred, batch['F_obs'])
        
        # L_newton3: 作用力 = 反作用力
        if 'x_src_swap' in batch:
            F_swap = model(batch['x_tgt'], batch['x_src'], 
                          batch['v_tgt'], batch['v_src'],
                          torch.abs(x_tgt - x_src))
            L_newton3 = F.mse_loss(F_pred, -F_swap)
        else:
            L_newton3 = torch.tensor(0.0)
        
        # L_energy: F = m·a → a = F/m → v_next = v + a·dt
        a = F_pred / batch['mass']
        v_pred = batch['v_tgt'] + a * batch['dt']
        L_energy = F.mse_loss(v_pred, batch['v_tgt_next'])
        
        # L_dissipation: 阻尼力方向与速度相反
        violation = torch.relu(F_pred * batch['v_tgt'])
        L_dissipation = violation.mean() * 0.1  # 弱约束（不是所有力学系统都有阻尼）
        
        total = (L_data + self.lam_n3 * L_newton3 
                 + self.lam_ene * L_energy + self.lam_diss * L_dissipation)
        
        return total, {
            'data': L_data.item(),
            'newton3': L_newton3.item(),
            'energy': L_energy.item(),
        }
```

---

## 四、通用 PINN 模板

### 4.1 无领域知识时的通用机制

**场景**：物理域未知、因果机制完全从数据学习。

```python
class GeneralPINN(nn.Module):
    """通用因果机制函数——无物理假设。
    
    输入：concat(full_state_source, full_state_target)
    输出：delta_state (dim_state,)
    
    物理约束：仅数据拟合 + 因果方向软约束
    """
    
    def __init__(self, state_dim: int, hidden_dims=[128, 64, 32],
                 residual: bool = False):
        """
        Args:
            state_dim: 单个实体的状态维度
            hidden_dims: 隐藏层尺寸
            residual: 是否使用残差连接（推荐 True）
        """
        super().__init__()
        in_dim = state_dim * 2
        out_dim = state_dim
        
        layers = []
        prev = in_dim
        for h in hidden_dims:
            layers.extend([nn.Linear(prev, h), nn.GELU()])
            prev = h
        layers.append(nn.Linear(prev, out_dim))
        self.net = nn.Sequential(*layers)
        
        self.residual = residual
        self._init_small()
    
    def forward(self, s_src: torch.Tensor, s_tgt: torch.Tensor) -> torch.Tensor:
        x = torch.cat([s_src, s_tgt], dim=-1)
        out = self.net(x)
        if self.residual:
            out = out + s_tgt  # 残差：机制函数是状态的微扰
        return out
```

---

## 五、物理约束 Loss 设计指南

### 5.1 约束优先级矩阵

| 约束 | 优先级 | 权重范围 | 说明 |
|------|--------|---------|------|
| 数据拟合 L_data | 基础 | 1.0（基准） | 必须有，否则无法学习 |
| 守恒律（能量/动量） | 高 | 5.0 ~ 20.0 | 架构核心，不可违反 |
| 因果方向 | 高 | 10.0 ~ 50.0 | 热从高温到低温，力恢复平衡 |
| 对称性（牛顿第三定律） | 中 | 1.0 ~ 10.0 | 取决于物理域 |
| 因果延迟 | 低 | 0.1 ~ 1.0 | 弱约束，因为延迟可能由架构处理 |

### 5.2 权重调度策略

```python
class LossWeightScheduler:
    """训练过程中动态调整物理约束权重。
    
    策略：前期侧重数据拟合，后期加强物理约束。
    """
    
    def __init__(self, total_epochs, warmup_ratio=0.3):
        self.total_epochs = total_epochs
        self.warmup = int(total_epochs * warmup_ratio)
    
    def get_weights(self, epoch):
        if epoch < self.warmup:
            # 预热阶段：以数据拟合为主
            return {'data': 1.0, 'physics': 0.1, 'mono': 0.5}
        else:
            # 正式阶段：加强物理约束
            progress = (epoch - self.warmup) / (self.total_epochs - self.warmup)
            return {
                'data': 1.0,
                'physics': 0.1 + 9.9 * progress,      # 0.1 → 10.0
                'mono': 0.5 + 19.5 * progress,         # 0.5 → 20.0
            }
```

---

## 六、PINN 与物理公式的混合策略

### 6.1 残差修正

```python
class PhysicsPlusResidualPINN(MechanismFunction):
    """物理公式 + PINN 残差修正。
    
    f_total = f_physics + f_pinn_residual
    
    适用场景：物理公式大致正确但有系统性偏差。
    """
    
    def __init__(self, physics_fn: MechanismFunction, 
                 residual_dim: int, hidden_dims=[32, 16]):
        super().__init__("physics_plus_residual")
        self.physics = physics_fn
        self.residual_net = GeneralPINN(residual_dim, hidden_dims, residual=True)
    
    def forward(self, s_src, s_tgt):
        dq_phys, dp_phys = self.physics(s_src, s_tgt)
        dq_res = self.residual_net(s_src.full_state(), s_tgt.full_state())
        dim_q = s_tgt.q.shape[0]
        return dq_phys + dq_res[:dim_q], dp_phys + dq_res[dim_q:]
```

### 6.2 门控混合

```python
class GatedPhysicsPINN(MechanismFunction):
    """门控混合：物理公式和 PINN 通过学习到的权重混合。
    
    f_total = gate · f_physics + (1 - gate) · f_pinn
    
    gate 接近 1 → 信任物理公式
    gate 接近 0 → 信任 PINN
    """
    
    def __init__(self, physics_fn, pinn_fn, state_dim):
        super().__init__("gated_mixed")
        self.physics = physics_fn
        self.pinn = pinn_fn
        self.gate_net = nn.Sequential(
            nn.Linear(state_dim * 2, 16),
            nn.Sigmoid(),  # 输出 [0, 1]
        )
        # 初始化 gate ≈ 0.9（默认信任物理）
        nn.init.constant_(self.gate_net[0].weight, 0.0)
        nn.init.constant_(self.gate_net[0].bias, 2.0)  # sigmoid(2.0) ≈ 0.88
    
    def forward(self, s_src, s_tgt):
        x = torch.cat([s_src.full_state(), s_tgt.full_state()])
        gate = self.gate_net(x)
        
        dq_phys, dp_phys = self.physics(s_src, s_tgt)
        dq_pinn, dp_pinn = self.pinn(s_src, s_tgt)
        
        return (gate * dq_phys + (1 - gate) * dq_pinn,
                gate * dp_phys + (1 - gate) * dp_pinn)
```

---

## 七、训练数据准备

### 7.1 从 JSON 加载训练数据

```python
def prepare_training_data(graph_path: str, trajectory_path: str,
                          target_edge: Tuple[int, int],
                          window_size: int = 5):
    """从 CDA 合成数据准备 PINN 训练集。
    
    Args:
        graph_path: *.graph.json 路径
        trajectory_path: *.trajectory.json 路径
        target_edge: (source_id, target_id) 要学习的因果边
        window_size: 时间窗口大小（用于计算导数）
    
    Returns:
        dataset: 含 (features, targets) 的字典列表
    """
    trajectory = load_trajectory(trajectory_path)
    src_id, tgt_id = target_edge
    
    samples = []
    for i in range(len(trajectory) - window_size):
        src_states = [trajectory[i+j].get_entity(src_id) for j in range(window_size)]
        tgt_states = [trajectory[i+j].get_entity(tgt_id) for j in range(window_size)]
        
        # 数值微分
        dT_src = (src_states[-1].q[0] - src_states[0].q[0]) / (window_size * dt)
        
        samples.append({
            'T_src': src_states[0].q[0],
            'T_tgt': tgt_states[0].q[0],
            'dT_src': dT_src,
            'Q_obs': tgt_states[0].p[0],  # 热流作为目标
            'T_tgt_next': tgt_states[-1].q[0],
            'distance': ...,
            'area': ...,
            'mass': tgt_states[0].attributes['mass'],
            'specific_heat': tgt_states[0].attributes['specific_heat'],
            'dt': dt,
        })
    
    return samples
```


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)