You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/ML/nmWTAI-ML/src/data/schedule_encoding.py

161 lines
5.8 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Tuple
import numpy as np
from src.common.config import Config
@dataclass
class EncodedSchedule:
"""正演代理模型使用的固定长度流量制度表示。"""
x_sched: np.ndarray
x_sec: np.ndarray
def canonicalize_schedule(cfg: Config, timeQ: List[float], q: List[float]) -> Tuple[np.ndarray, np.ndarray, bool]:
"""清洗原始流量制度,同时保留求解器看到的主要流量变化形态。"""
max_points = int(cfg.get("schedule", "max_points", default=512))
dt = np.asarray(list(map(float, timeQ)), dtype=np.float64).reshape(-1)[:max_points]
qq = np.asarray(list(map(float, q)), dtype=np.float64).reshape(-1)[:max_points]
dt = np.maximum(dt, 1e-12)
qq = np.maximum(qq, 0.0)
cf = cfg.raw["schedule"].get("canonicalize_for_model", {}) or {}
q_thr = float(cf.get("q_thr", 1e-6))
merge_same_q = bool(cf.get("merge_same_q", True))
merge_rel_tol = float(cf.get("merge_rel_tol", 1e-4))
remove_shutin = bool(cf.get("remove_shutin", False))
has_shutin = bool(np.any(qq <= q_thr))
if merge_same_q and dt.size >= 2:
# 合并相邻且流量几乎相同的流动段,让编码器关注真正有意义的流量变化。
new_dt, new_q = [], []
cur_dt, cur_q = float(dt[0]), float(qq[0])
def close(a: float, b: float) -> bool:
denom = max(abs(a), abs(b), 1.0)
return abs(a - b) / denom <= merge_rel_tol
for i in range(1, dt.size):
if close(float(qq[i]), cur_q):
cur_dt += float(dt[i])
else:
new_dt.append(cur_dt)
new_q.append(cur_q)
cur_dt = float(dt[i])
cur_q = float(qq[i])
new_dt.append(cur_dt)
new_q.append(cur_q)
dt = np.asarray(new_dt, dtype=np.float64)
qq = np.asarray(new_q, dtype=np.float64)
if remove_shutin and dt.size >= 2:
m = qq > q_thr
if int(np.sum(m)) >= 2:
dt = dt[m]
qq = qq[m]
return dt, qq, has_shutin
def _make_u_grid(cfg: Config, T_total: float, Nu: int, mode: str) -> np.ndarray:
t_min = float((cfg.raw["schedule"].get("obs_window", {}) or {}).get("t_min", 1e-6))
T_total = max(float(T_total), t_min * 2.0)
if mode == "linear":
return np.linspace(t_min, T_total, Nu, dtype=np.float64)
return np.geomspace(t_min, T_total, Nu, dtype=np.float64)
def encode_schedule_to_timegrid(
cfg: Config,
sectionIndex: int,
timeQ: List[float],
q: List[float],
n_sections: Optional[int] = None,
) -> EncodedSchedule:
"""将变长流量制度编码为固定长度的神经网络输入。"""
dt, qq, has_shutin = canonicalize_schedule(cfg, timeQ, q)
if dt.size < 2 or qq.size != dt.size:
raise ValueError("invalid schedule after canonicalize")
sec = int(max(1, sectionIndex))
N = int(n_sections) if n_sections is not None else int(dt.size)
N = max(N, int(dt.size))
T_total = float(np.sum(dt))
enc = cfg.raw["timegrid_encoding"]
Nu = int(enc.get("n_u_points", 256))
grid_mode = str(enc.get("grid", "log")).lower()
include_cum = bool(enc.get("include_cum", True))
include_dq = bool(enc.get("include_dq", True))
include_shutin = bool(enc.get("include_shutin", False))
q_eps = float(enc.get("q_eps", 1e-12))
t_edges = np.concatenate([[0.0], np.cumsum(dt)], axis=0)
u = _make_u_grid(cfg, T_total=T_total, Nu=Nu, mode=("linear" if grid_mode == "linear" else "log"))
idx = np.searchsorted(t_edges[1:], u, side="right")
idx = np.clip(idx, 0, dt.size - 1)
q_u = qq[idx].astype(np.float64)
# q(t) 是主通道cum(t)、dq(t) 和关井标志是可选辅助通道,
# 用于帮助代理模型区分不同流量制度形态。
cum_u = None
if include_cum:
prefix = np.concatenate([[0.0], np.cumsum(dt * qq)], axis=0)
cum_u = prefix[idx] + (u - t_edges[:-1][idx]) * qq[idx]
dq_u = None
if include_dq:
dq_u = np.zeros_like(u, dtype=np.float64)
dq_u[1:] = np.diff(q_u)
dq_u[0] = dq_u[1] if dq_u.size > 1 else 0.0
shut_u = None
if include_shutin:
thr = float(enc.get("shutin_thr", 1e-6))
shut_u = (q_u <= thr).astype(np.float64)
norm_mode = str(enc.get("normalize_mode", "global")).lower()
if norm_mode == "per_sample":
q_scale = max(float(np.max(q_u)), q_eps)
cum_scale = max(float(cum_u.max()) if cum_u is not None else 1.0, q_eps)
else:
q_scale = float(enc.get("q_global_max", 1.0))
cum_scale = float(enc.get("cum_global_max", 1.0))
q_u = q_u / max(q_scale, q_eps)
if cum_u is not None:
cum_u = cum_u / max(cum_scale, q_eps)
if dq_u is not None:
dq_u = dq_u / max(q_scale, q_eps)
chans = [q_u]
if include_cum:
chans.append(cum_u if cum_u is not None else np.zeros_like(q_u))
if include_dq:
chans.append(dq_u if dq_u is not None else np.zeros_like(q_u))
if include_shutin:
chans.append(shut_u if shut_u is not None else np.zeros_like(q_u))
X = np.stack(chans, axis=1)
x_sched = X.reshape(-1).astype(np.float32)
# 时间网格之外额外拼接 5 个流动段级别特征,
# 用于告诉模型当前预测的是哪个流动段对应的双对数曲线。
n_sec = int(max(1, N))
sec_clamped = int(np.clip(sec, 1, n_sec))
section_pos = float((sec_clamped - 1) / max(n_sec - 1, 1))
sectionIndex_norm = float(sec_clamped / max(n_sec, 1))
n_sections_norm = float(n_sec / max(12, 1))
log_T_total = float(np.log(max(T_total, 1e-12)))
x_sec = np.array(
[sectionIndex_norm, section_pos, n_sections_norm, float(has_shutin), log_T_total],
dtype=np.float32,
)
return EncodedSchedule(x_sched=x_sched, x_sec=x_sec)