|
|
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)
|