|
|
# -*- coding: utf-8 -*-
|
|
|
"""原始 HDF5 样本预处理。
|
|
|
|
|
|
数据生成阶段写出的 HDF5 文件仍处在“原始样本”状态:参数尚未做特征变换,输入和
|
|
|
监督目标尚未标准化,也没有训练/验证/测试划分。本模块完成这些准备工作,并把
|
|
|
结果保存为 joblib 文件供训练脚本直接读取。
|
|
|
|
|
|
预处理只在训练集上拟合 scaler,随后复用到验证集和测试集,避免数据泄漏。输出
|
|
|
payload 中保留 curve_layout、参数变换配置、流量制度元数据和附加字段,使后续
|
|
|
训练、评估和误差分析可以追溯每个样本的来源。
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import copy
|
|
|
from pathlib import Path
|
|
|
|
|
|
import h5py
|
|
|
import joblib
|
|
|
import numpy as np
|
|
|
from sklearn.model_selection import GroupShuffleSplit, train_test_split
|
|
|
from sklearn.preprocessing import StandardScaler
|
|
|
|
|
|
from src.data.param_features import build_param_feature_transform, transform_param_features
|
|
|
|
|
|
|
|
|
class SelectiveStandardScaler:
|
|
|
"""只标准化指定连续列,同时保持 one-hot 等类别列原值不变。"""
|
|
|
|
|
|
def __init__(self, scaled_indices: np.ndarray | list[int]):
|
|
|
self.scaled_indices = np.asarray(scaled_indices, dtype=np.int64)
|
|
|
|
|
|
def fit(self, x: np.ndarray) -> "SelectiveStandardScaler":
|
|
|
"""在训练集的连续特征列上拟合均值和标准差。"""
|
|
|
values = _ensure_2d("params", x).astype(np.float64)
|
|
|
indices = np.unique(self.scaled_indices)
|
|
|
if np.any(indices < 0) or np.any(indices >= values.shape[1]):
|
|
|
raise ValueError(f"scaled parameter indices out of range: {indices.tolist()}")
|
|
|
|
|
|
self.scaled_indices_ = indices
|
|
|
self.n_features_in_ = int(values.shape[1])
|
|
|
self.n_samples_seen_ = int(values.shape[0])
|
|
|
self.mean_ = np.zeros(self.n_features_in_, dtype=np.float64)
|
|
|
self.var_ = np.ones(self.n_features_in_, dtype=np.float64)
|
|
|
self.scale_ = np.ones(self.n_features_in_, dtype=np.float64)
|
|
|
|
|
|
if indices.size:
|
|
|
inner = StandardScaler().fit(values[:, indices])
|
|
|
self.mean_[indices] = inner.mean_
|
|
|
self.var_[indices] = inner.var_
|
|
|
self.scale_[indices] = inner.scale_
|
|
|
return self
|
|
|
|
|
|
def _validate_transform_input(self, x: np.ndarray) -> np.ndarray:
|
|
|
"""检查待变换数组维度是否与拟合时一致。"""
|
|
|
if not hasattr(self, "n_features_in_"):
|
|
|
raise RuntimeError("SelectiveStandardScaler is not fitted")
|
|
|
values = _ensure_2d("params", x).astype(np.float64)
|
|
|
if values.shape[1] != self.n_features_in_:
|
|
|
raise ValueError(
|
|
|
f"parameter feature dimension mismatch: {values.shape[1]} != {self.n_features_in_}"
|
|
|
)
|
|
|
return values
|
|
|
|
|
|
def transform(self, x: np.ndarray) -> np.ndarray:
|
|
|
"""标准化连续列;未选择的类别列保持0/1。"""
|
|
|
values = self._validate_transform_input(x)
|
|
|
return (values - self.mean_) / self.scale_
|
|
|
|
|
|
def inverse_transform(self, x: np.ndarray) -> np.ndarray:
|
|
|
"""恢复标准化前的连续特征,同时保持类别列不变。"""
|
|
|
values = self._validate_transform_input(x)
|
|
|
return values * self.scale_ + self.mean_
|
|
|
|
|
|
def fit_transform(self, x: np.ndarray) -> np.ndarray:
|
|
|
"""拟合后立即变换训练集。"""
|
|
|
return self.fit(x).transform(x)
|
|
|
|
|
|
|
|
|
class MaskedStandardScaler:
|
|
|
"""每个曲线列只使用实际物理时间覆盖范围内的有效值进行标准化。"""
|
|
|
|
|
|
def fit(self, x: np.ndarray, valid_mask: np.ndarray) -> "MaskedStandardScaler":
|
|
|
values = _ensure_2d("curve", x).astype(np.float64)
|
|
|
mask = np.asarray(valid_mask, dtype=bool)
|
|
|
if mask.shape != values.shape:
|
|
|
raise ValueError(f"curve mask shape mismatch: {mask.shape} != {values.shape}")
|
|
|
if not np.isfinite(values[mask]).all():
|
|
|
raise ValueError("valid curve values contain non-finite values")
|
|
|
|
|
|
counts = mask.sum(axis=0).astype(np.int64)
|
|
|
sums = np.where(mask, values, 0.0).sum(axis=0)
|
|
|
means = np.divide(sums, counts, out=np.zeros_like(sums), where=counts > 0)
|
|
|
centered = np.where(mask, values - means, 0.0)
|
|
|
variances = np.divide(
|
|
|
(centered * centered).sum(axis=0),
|
|
|
counts,
|
|
|
out=np.ones_like(sums),
|
|
|
where=counts > 0,
|
|
|
)
|
|
|
scales = np.sqrt(np.maximum(variances, 0.0))
|
|
|
scales[(counts == 0) | (scales < 1.0e-12)] = 1.0
|
|
|
|
|
|
self.n_features_in_ = int(values.shape[1])
|
|
|
self.n_samples_seen_ = counts
|
|
|
self.mean_ = means
|
|
|
self.var_ = variances
|
|
|
self.scale_ = scales
|
|
|
return self
|
|
|
|
|
|
def _validate(self, x: np.ndarray) -> np.ndarray:
|
|
|
if not hasattr(self, "n_features_in_"):
|
|
|
raise RuntimeError("MaskedStandardScaler is not fitted")
|
|
|
values = _ensure_2d("curve", x).astype(np.float64)
|
|
|
if values.shape[1] != self.n_features_in_:
|
|
|
raise ValueError(
|
|
|
f"curve feature dimension mismatch: {values.shape[1]} != {self.n_features_in_}"
|
|
|
)
|
|
|
return values
|
|
|
|
|
|
def transform(self, x: np.ndarray) -> np.ndarray:
|
|
|
values = self._validate(x)
|
|
|
return (values - self.mean_) / self.scale_
|
|
|
|
|
|
def inverse_transform(self, x: np.ndarray) -> np.ndarray:
|
|
|
values = self._validate(x)
|
|
|
return values * self.scale_ + self.mean_
|
|
|
|
|
|
|
|
|
def _value_counts(values: np.ndarray | None, indices: np.ndarray) -> dict[str, int]:
|
|
|
"""按字符串键记录某个划分中的类别样本数。"""
|
|
|
if values is None:
|
|
|
return {}
|
|
|
unique, counts = np.unique(np.asarray(values)[indices], return_counts=True)
|
|
|
result = {}
|
|
|
for value, count in zip(unique.tolist(), counts.tolist()):
|
|
|
key = str(int(value)) if float(value).is_integer() else str(value)
|
|
|
result[key] = int(count)
|
|
|
return result
|
|
|
|
|
|
|
|
|
def _split_sample_indices(
|
|
|
n_samples: int,
|
|
|
test_size: float,
|
|
|
val_size: float,
|
|
|
random_seed: int,
|
|
|
group_id: np.ndarray | None,
|
|
|
solver_type: np.ndarray | None,
|
|
|
) -> tuple[np.ndarray, np.ndarray, np.ndarray, str]:
|
|
|
"""优先按group整体划分;没有可靠group时按solverType分层随机划分。"""
|
|
|
if test_size <= 0.0 or val_size <= 0.0 or test_size + val_size >= 1.0:
|
|
|
raise ValueError("test_size and val_size must be positive and sum to less than 1")
|
|
|
|
|
|
idx = np.arange(n_samples)
|
|
|
val_ratio_in_train_val = val_size / (1.0 - test_size)
|
|
|
groups = None if group_id is None else np.asarray(group_id).reshape(-1)
|
|
|
use_groups = bool(
|
|
|
groups is not None
|
|
|
and len(groups) == n_samples
|
|
|
and np.all(np.isfinite(groups))
|
|
|
and np.all(groups >= 0)
|
|
|
and np.unique(groups).size >= 3
|
|
|
)
|
|
|
|
|
|
if use_groups:
|
|
|
first = GroupShuffleSplit(n_splits=1, test_size=test_size, random_state=random_seed)
|
|
|
train_val_pos, test_pos = next(first.split(idx, groups=groups))
|
|
|
idx_train_val = idx[train_val_pos]
|
|
|
idx_test = idx[test_pos]
|
|
|
|
|
|
second = GroupShuffleSplit(
|
|
|
n_splits=1,
|
|
|
test_size=val_ratio_in_train_val,
|
|
|
random_state=random_seed + 1,
|
|
|
)
|
|
|
train_pos, val_pos = next(
|
|
|
second.split(idx_train_val, groups=groups[idx_train_val])
|
|
|
)
|
|
|
idx_train = idx_train_val[train_pos]
|
|
|
idx_val = idx_train_val[val_pos]
|
|
|
return idx_train, idx_val, idx_test, "group_id"
|
|
|
|
|
|
stratify = None if solver_type is None else np.asarray(solver_type).reshape(-1)
|
|
|
idx_train_val, idx_test = train_test_split(
|
|
|
idx,
|
|
|
test_size=test_size,
|
|
|
random_state=random_seed,
|
|
|
shuffle=True,
|
|
|
stratify=stratify,
|
|
|
)
|
|
|
train_val_stratify = None if stratify is None else stratify[idx_train_val]
|
|
|
idx_train, idx_val = train_test_split(
|
|
|
idx_train_val,
|
|
|
test_size=val_ratio_in_train_val,
|
|
|
random_state=random_seed,
|
|
|
shuffle=True,
|
|
|
stratify=train_val_stratify,
|
|
|
)
|
|
|
return idx_train, idx_val, idx_test, "solverType" if stratify is not None else "random"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_2d(name: str, arr: np.ndarray) -> np.ndarray:
|
|
|
"""保证数组至少为二维,避免单样本情况下维度被压扁。"""
|
|
|
arr = np.asarray(arr, dtype=np.float32)
|
|
|
if arr.ndim != 2:
|
|
|
raise ValueError(f"{name} must be a 2D array, got shape={arr.shape}")
|
|
|
return arr
|
|
|
|
|
|
|
|
|
def _decode_attr_list(raw_names) -> list[str] | None:
|
|
|
"""解码 HDF5 属性中保存的 JSON 字符串或字节串列表。"""
|
|
|
if raw_names is None:
|
|
|
return None
|
|
|
return [
|
|
|
item.decode("utf-8") if isinstance(item, (bytes, np.bytes_)) else str(item)
|
|
|
for item in raw_names
|
|
|
]
|
|
|
|
|
|
|
|
|
def _read_optional_string_dataset(f: h5py.File, name: str) -> np.ndarray | None:
|
|
|
"""读取可选字符串数据集;不存在时返回 None。"""
|
|
|
if name not in f:
|
|
|
return None
|
|
|
return np.asarray(f[name][:]).astype(str)
|
|
|
|
|
|
|
|
|
def _read_optional_numeric_dataset(f: h5py.File, name: str) -> np.ndarray | None:
|
|
|
"""读取可选数值数据集;不存在时返回 None。"""
|
|
|
if name not in f:
|
|
|
return None
|
|
|
return np.asarray(f[name][:])
|
|
|
|
|
|
|
|
|
def _validate_optional_length(name: str, arr: np.ndarray | None, expected_n: int) -> None:
|
|
|
"""检查可选数据集长度是否与主样本数量一致。"""
|
|
|
if arr is not None and len(arr) != expected_n:
|
|
|
raise ValueError(f"{name} row count does not match main arrays: {len(arr)} != {expected_n}")
|
|
|
|
|
|
|
|
|
def _validate_curve_time(
|
|
|
curve_time: np.ndarray,
|
|
|
expected_n: int,
|
|
|
expected_n_time_points: int,
|
|
|
) -> np.ndarray:
|
|
|
"""校验每条曲线对应的物理时间坐标。"""
|
|
|
values = np.asarray(curve_time, dtype=np.float64)
|
|
|
expected_shape = (expected_n, expected_n_time_points)
|
|
|
if values.ndim != 2 or values.shape != expected_shape:
|
|
|
raise ValueError(
|
|
|
f"curve_time must have shape {expected_shape}, got shape={values.shape}"
|
|
|
)
|
|
|
if not np.all(np.isfinite(values)):
|
|
|
raise ValueError("curve_time contains non-finite values")
|
|
|
if np.any(values <= 0.0):
|
|
|
raise ValueError("curve_time values must be positive")
|
|
|
if np.any(np.diff(values, axis=1) <= 0.0):
|
|
|
raise ValueError("each curve_time row must be strictly increasing")
|
|
|
return values
|
|
|
|
|
|
|
|
|
def _validate_curve_valid_mask(
|
|
|
curve_valid_mask: np.ndarray,
|
|
|
expected_n: int,
|
|
|
expected_n_time_points: int,
|
|
|
) -> np.ndarray:
|
|
|
"""校验逐时间点记录物理覆盖范围的有效掩码。"""
|
|
|
values = np.asarray(curve_valid_mask)
|
|
|
expected_shape = (expected_n, expected_n_time_points)
|
|
|
if values.ndim != 2 or values.shape != expected_shape:
|
|
|
raise ValueError(
|
|
|
f"curve_valid_mask must have shape {expected_shape}, got shape={values.shape}"
|
|
|
)
|
|
|
if not np.all(np.isfinite(values)) or not np.all((values == 0) | (values == 1)):
|
|
|
raise ValueError("curve_valid_mask must contain only 0/1 values")
|
|
|
values = values.astype(bool)
|
|
|
if np.any(values.sum(axis=1) < 2):
|
|
|
raise ValueError("each curve_valid_mask row must contain at least two valid points")
|
|
|
return values
|
|
|
|
|
|
|
|
|
def _curve_keep_indices(meta: dict, curve_dim: int) -> np.ndarray:
|
|
|
"""返回旧 processed 曲线中 pressure/derivative 两段的列索引。"""
|
|
|
layout = meta.get("curve_layout") or {}
|
|
|
parts = {str(part["name"]): part for part in layout.get("parts", [])}
|
|
|
if "log_pressure" in parts and "log_derivative" in parts:
|
|
|
indices = []
|
|
|
for name in ("log_pressure", "log_derivative"):
|
|
|
part = parts[name]
|
|
|
indices.extend(range(int(part["start"]), int(part["end"])))
|
|
|
return np.asarray(indices, dtype=np.int64)
|
|
|
|
|
|
if curve_dim % 3 != 0:
|
|
|
raise ValueError(
|
|
|
"processed 数据没有 curve_layout,且曲线维度不能按旧版三通道布局识别"
|
|
|
)
|
|
|
n_time_points = curve_dim // 3
|
|
|
return np.arange(2 * n_time_points, dtype=np.int64)
|
|
|
|
|
|
|
|
|
def _slice_standard_scaler(scaler: StandardScaler, keep_indices: np.ndarray) -> StandardScaler:
|
|
|
"""裁剪按列独立拟合的 StandardScaler,同时保留其拟合状态。"""
|
|
|
result = copy.deepcopy(scaler)
|
|
|
for name in ("mean_", "scale_", "var_"):
|
|
|
value = getattr(result, name, None)
|
|
|
if value is not None:
|
|
|
setattr(result, name, np.asarray(value)[keep_indices].copy())
|
|
|
result.n_features_in_ = int(keep_indices.size)
|
|
|
return result
|
|
|
|
|
|
|
|
|
def migrate_processed_dataset_without_slope(input_path: Path, output_path: Path) -> None:
|
|
|
"""把旧三通道 processed pkl 精确迁移为 pressure/derivative 双通道数据。"""
|
|
|
if not input_path.exists():
|
|
|
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
|
|
|
|
source = joblib.load(input_path)
|
|
|
required = ["Y_curve_train", "Y_curve_val", "Y_curve_test", "scaler_curve", "meta"]
|
|
|
missing = [name for name in required if name not in source]
|
|
|
if missing:
|
|
|
raise KeyError(f"processed 数据缺少字段: {missing}")
|
|
|
|
|
|
curve_dim = int(source["Y_curve_train"].shape[1])
|
|
|
keep_indices = _curve_keep_indices(source.get("meta", {}), curve_dim)
|
|
|
if keep_indices.size == curve_dim:
|
|
|
raise ValueError("输入 processed 数据已经不包含 slope,无需迁移")
|
|
|
if keep_indices.size % 2 != 0:
|
|
|
raise ValueError(f"保留后的曲线维度必须能被 2 整除,当前为 {keep_indices.size}")
|
|
|
|
|
|
migration_n_time_points = int(keep_indices.size // 2)
|
|
|
migration_meta = dict(source.get("meta", {}))
|
|
|
if migration_meta.get("curve_time_mode") != "fixed":
|
|
|
raise ValueError(
|
|
|
"processed migration requires meta.curve_time_mode='fixed'; "
|
|
|
"regenerate the H5 dataset on a fixed time grid and preprocess it again"
|
|
|
)
|
|
|
|
|
|
prediction_curve_time = np.asarray(
|
|
|
migration_meta.get("prediction_curve_time"),
|
|
|
dtype=np.float64,
|
|
|
)
|
|
|
if prediction_curve_time.shape != (migration_n_time_points,):
|
|
|
raise ValueError(
|
|
|
"processed migration requires meta.prediction_curve_time with shape "
|
|
|
f"({migration_n_time_points},); regenerate the H5 dataset and preprocess it again"
|
|
|
)
|
|
|
if (
|
|
|
not np.all(np.isfinite(prediction_curve_time))
|
|
|
or np.any(prediction_curve_time <= 0.0)
|
|
|
or np.any(np.diff(prediction_curve_time) <= 0.0)
|
|
|
):
|
|
|
raise ValueError(
|
|
|
"processed migration requires a finite, positive, strictly increasing "
|
|
|
"meta.prediction_curve_time"
|
|
|
)
|
|
|
|
|
|
for split in ("train", "val", "test"):
|
|
|
mask_key = f"curve_valid_mask_{split}"
|
|
|
if mask_key not in source:
|
|
|
raise ValueError(
|
|
|
f"processed migration requires {mask_key}; regenerate the H5 dataset "
|
|
|
"on a fixed time grid and preprocess it again"
|
|
|
)
|
|
|
_validate_curve_valid_mask(
|
|
|
source[mask_key],
|
|
|
len(source[f"Y_curve_{split}"]),
|
|
|
migration_n_time_points,
|
|
|
)
|
|
|
|
|
|
payload = dict(source)
|
|
|
for split in ("train", "val", "test"):
|
|
|
key = f"Y_curve_{split}"
|
|
|
payload[key] = np.asarray(source[key][:, keep_indices], dtype=np.float32)
|
|
|
payload["scaler_curve"] = _slice_standard_scaler(source["scaler_curve"], keep_indices)
|
|
|
|
|
|
# 这些字段属于已停用的 time-conditioned 路径,不进入旧正演模型训练包。
|
|
|
for key in (
|
|
|
"T_curve_train", "T_curve_val", "T_curve_test",
|
|
|
"X_time_train", "X_time_val", "X_time_test", "scaler_time",
|
|
|
):
|
|
|
payload.pop(key, None)
|
|
|
|
|
|
n_time_points = int(keep_indices.size // 2)
|
|
|
meta = dict(source.get("meta", {}))
|
|
|
meta.update(
|
|
|
{
|
|
|
"curve_dim": int(keep_indices.size),
|
|
|
"raw_curve_dim": curve_dim,
|
|
|
"dropped_legacy_slope": True,
|
|
|
"migrated_from_processed": str(input_path),
|
|
|
"curve_layout": {
|
|
|
"n_time_points": n_time_points,
|
|
|
"parts": [
|
|
|
{"name": "log_pressure", "start": 0, "end": n_time_points},
|
|
|
{"name": "log_derivative", "start": n_time_points, "end": 2 * n_time_points},
|
|
|
],
|
|
|
},
|
|
|
}
|
|
|
)
|
|
|
meta.pop("time_feature_dim", None)
|
|
|
meta.pop("curve_time_source", None)
|
|
|
payload["meta"] = meta
|
|
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
joblib.dump(payload, output_path)
|
|
|
print(f"Processed migration complete, saved to: {output_path}")
|
|
|
print(
|
|
|
f"train={len(payload['Y_curve_train'])}, val={len(payload['Y_curve_val'])}, "
|
|
|
f"test={len(payload['Y_curve_test'])}, curve_dim={curve_dim}->{keep_indices.size}"
|
|
|
)
|
|
|
|
|
|
|
|
|
def preprocess_dataset(
|
|
|
input_path: Path,
|
|
|
output_path: Path,
|
|
|
test_size: float = 0.15,
|
|
|
val_size: float = 0.15,
|
|
|
random_seed: int = 42,
|
|
|
use_param_feature_transform: bool = True,
|
|
|
) -> None:
|
|
|
"""将原始 HDF5 样本整理为训练脚本可直接加载的 joblib 数据包。
|
|
|
|
|
|
处理步骤包括:读取 params/schedule/curve 等核心数组,保留可选元数据,构造参数
|
|
|
特征变换,划分 train/val/test,并只在训练集上拟合 StandardScaler。输出 payload
|
|
|
同时保存 scaler 和 meta,确保后续评估可以恢复原始尺度并正确拆分压力和导数。
|
|
|
"""
|
|
|
if not input_path.exists():
|
|
|
raise FileNotFoundError(f"Input file not found: {input_path}")
|
|
|
|
|
|
with h5py.File(input_path, "r") as f:
|
|
|
required_keys = ["params", "schedule", "curve"]
|
|
|
for key in required_keys:
|
|
|
if key not in f:
|
|
|
raise KeyError(f"HDF5 dataset is missing required key: {key}")
|
|
|
|
|
|
x_params = _ensure_2d("params", f["params"][:])
|
|
|
x_schedule = _ensure_2d("schedule", f["schedule"][:])
|
|
|
y_curve = _ensure_2d("curve", f["curve"][:])
|
|
|
curve_time = _read_optional_numeric_dataset(f, "curve_time")
|
|
|
curve_valid_mask = _read_optional_numeric_dataset(f, "curve_valid_mask")
|
|
|
# 可选元数据会保留下来,供后续分析和排序验证使用;
|
|
|
# 基础正演训练循环不依赖这些字段。
|
|
|
param_names = _decode_attr_list(f.attrs.get("param_names"))
|
|
|
schedule_meta_names = _decode_attr_list(f.attrs.get("schedule_meta_names"))
|
|
|
source_name_vocab = _decode_attr_list(f.attrs.get("source_name_vocab"))
|
|
|
if param_names is None:
|
|
|
param_names = ["k", "skin", "wellboreC", "phi", "h", "Ct", "Cf"]
|
|
|
|
|
|
solver_type = None
|
|
|
categorical_values: dict[str, list[float]] = {}
|
|
|
if "solverType" in param_names:
|
|
|
solver_col = param_names.index("solverType")
|
|
|
solver_type = np.asarray(x_params[:, solver_col], dtype=np.float64)
|
|
|
categories = np.unique(solver_type)
|
|
|
if not np.all(np.isfinite(categories)):
|
|
|
raise ValueError("solverType contains non-finite values")
|
|
|
categorical_values["solverType"] = categories.tolist()
|
|
|
|
|
|
schedule_meta = _ensure_2d("schedule_meta", f["schedule_meta"][:]) if "schedule_meta" in f else None
|
|
|
family_name = _read_optional_string_dataset(f, "family_name")
|
|
|
source_id = _read_optional_numeric_dataset(f, "source_id")
|
|
|
source_name = _read_optional_string_dataset(f, "source_name")
|
|
|
source_row = _read_optional_numeric_dataset(f, "source_row")
|
|
|
extra_numeric_names = [
|
|
|
"group_id",
|
|
|
"is_anchor",
|
|
|
"neighbor_objective",
|
|
|
"neighbor_objective_p",
|
|
|
"neighbor_objective_d",
|
|
|
"neighbor_span_frac",
|
|
|
"section_index",
|
|
|
]
|
|
|
extra_string_names = ["timeQ_json", "q_json"]
|
|
|
extra_numeric = {name: _read_optional_numeric_dataset(f, name) for name in extra_numeric_names}
|
|
|
extra_string = {name: _read_optional_string_dataset(f, name) for name in extra_string_names}
|
|
|
|
|
|
n = len(x_params)
|
|
|
if len(x_schedule) != n or len(y_curve) != n:
|
|
|
raise ValueError("params / schedule / curve row counts do not match")
|
|
|
|
|
|
raw_curve_dim = int(y_curve.shape[1])
|
|
|
if curve_time is not None:
|
|
|
curve_time_shape = np.asarray(curve_time).shape
|
|
|
if len(curve_time_shape) != 2 or curve_time_shape[0] != n:
|
|
|
raise ValueError(
|
|
|
f"curve_time must be a 2D array with {n} rows, got shape={curve_time_shape}"
|
|
|
)
|
|
|
n_time_points = int(curve_time_shape[1])
|
|
|
if raw_curve_dim == 3 * n_time_points:
|
|
|
# 兼容已有的压力/导数/斜率三通道 HDF5;训练时丢弃斜率通道。
|
|
|
y_curve = y_curve[:, : 2 * n_time_points]
|
|
|
elif raw_curve_dim != 2 * n_time_points:
|
|
|
raise ValueError(
|
|
|
"curve dimension does not match curve_time: "
|
|
|
f"curve_dim={raw_curve_dim}, n_time_points={n_time_points}; "
|
|
|
"expected 2*N or legacy 3*N"
|
|
|
)
|
|
|
elif raw_curve_dim % 3 == 0:
|
|
|
n_time_points = raw_curve_dim // 3
|
|
|
y_curve = y_curve[:, : 2 * n_time_points]
|
|
|
elif raw_curve_dim % 2 == 0:
|
|
|
n_time_points = raw_curve_dim // 2
|
|
|
else:
|
|
|
raise ValueError(
|
|
|
f"curve dimension {raw_curve_dim} cannot be interpreted as 2*N or legacy 3*N"
|
|
|
)
|
|
|
curve_dim = int(y_curve.shape[1])
|
|
|
|
|
|
curve_time_mode = "missing"
|
|
|
prediction_curve_time = None
|
|
|
if curve_time is not None:
|
|
|
curve_time = _validate_curve_time(curve_time, n, n_time_points)
|
|
|
if np.allclose(curve_time, curve_time[0:1], rtol=1e-6, atol=1e-10):
|
|
|
curve_time_mode = "fixed"
|
|
|
prediction_curve_time = curve_time[0].copy()
|
|
|
else:
|
|
|
curve_time_mode = "per_sample"
|
|
|
|
|
|
if curve_time_mode != "fixed":
|
|
|
raise ValueError(
|
|
|
"forward-surrogate preprocessing requires one fixed curve_time grid; "
|
|
|
f"got curve_time_mode={curve_time_mode!r}"
|
|
|
)
|
|
|
if curve_valid_mask is None:
|
|
|
raise ValueError(
|
|
|
"fixed-time preprocessing requires curve_valid_mask; regenerate the H5 dataset"
|
|
|
)
|
|
|
|
|
|
curve_valid_mask = _validate_curve_valid_mask(
|
|
|
curve_valid_mask,
|
|
|
n,
|
|
|
n_time_points,
|
|
|
)
|
|
|
curve_valid_mask_mode = "explicit"
|
|
|
curve_mask = np.concatenate([curve_valid_mask, curve_valid_mask], axis=1)
|
|
|
|
|
|
param_feature_transform = build_param_feature_transform(
|
|
|
param_names=param_names,
|
|
|
categorical_values=categorical_values,
|
|
|
enabled=bool(use_param_feature_transform),
|
|
|
)
|
|
|
# 物理参数在 StandardScaler 前先做特征变换:
|
|
|
# 例如 k、wellboreC、h 使用 log10,skin 使用 asinh。
|
|
|
x_params_features = transform_param_features(x_params, param_feature_transform)
|
|
|
|
|
|
_validate_optional_length("schedule_meta", schedule_meta, n)
|
|
|
_validate_optional_length("family_name", family_name, n)
|
|
|
_validate_optional_length("source_id", source_id, n)
|
|
|
_validate_optional_length("source_name", source_name, n)
|
|
|
_validate_optional_length("source_row", source_row, n)
|
|
|
for name, arr in extra_numeric.items():
|
|
|
_validate_optional_length(name, arr, n)
|
|
|
for name, arr in extra_string.items():
|
|
|
_validate_optional_length(name, arr, n)
|
|
|
|
|
|
# 同一group通常对应相同流量制度在不同solverType下的样本,必须整体进入同一划分。
|
|
|
group_id = extra_numeric.get("group_id")
|
|
|
idx_train, idx_val, idx_test, split_strategy = _split_sample_indices(
|
|
|
n_samples=n,
|
|
|
test_size=test_size,
|
|
|
val_size=val_size,
|
|
|
random_seed=random_seed,
|
|
|
group_id=group_id,
|
|
|
solver_type=solver_type,
|
|
|
)
|
|
|
|
|
|
x_params_train = x_params_features[idx_train]
|
|
|
x_schedule_train = x_schedule[idx_train]
|
|
|
y_curve_train = y_curve[idx_train]
|
|
|
curve_valid_mask_train = curve_valid_mask[idx_train]
|
|
|
curve_mask_train = curve_mask[idx_train]
|
|
|
|
|
|
x_params_val = x_params_features[idx_val]
|
|
|
x_schedule_val = x_schedule[idx_val]
|
|
|
y_curve_val = y_curve[idx_val]
|
|
|
curve_valid_mask_val = curve_valid_mask[idx_val]
|
|
|
curve_mask_val = curve_mask[idx_val]
|
|
|
|
|
|
x_params_test = x_params_features[idx_test]
|
|
|
x_schedule_test = x_schedule[idx_test]
|
|
|
y_curve_test = y_curve[idx_test]
|
|
|
curve_valid_mask_test = curve_valid_mask[idx_test]
|
|
|
curve_mask_test = curve_mask[idx_test]
|
|
|
|
|
|
schedule_meta_train = schedule_meta[idx_train] if schedule_meta is not None else None
|
|
|
schedule_meta_val = schedule_meta[idx_val] if schedule_meta is not None else None
|
|
|
schedule_meta_test = schedule_meta[idx_test] if schedule_meta is not None else None
|
|
|
|
|
|
family_name_train = family_name[idx_train] if family_name is not None else None
|
|
|
family_name_val = family_name[idx_val] if family_name is not None else None
|
|
|
family_name_test = family_name[idx_test] if family_name is not None else None
|
|
|
|
|
|
source_id_train = source_id[idx_train] if source_id is not None else None
|
|
|
source_id_val = source_id[idx_val] if source_id is not None else None
|
|
|
source_id_test = source_id[idx_test] if source_id is not None else None
|
|
|
|
|
|
source_name_train = source_name[idx_train] if source_name is not None else None
|
|
|
source_name_val = source_name[idx_val] if source_name is not None else None
|
|
|
source_name_test = source_name[idx_test] if source_name is not None else None
|
|
|
|
|
|
source_row_train = source_row[idx_train] if source_row is not None else None
|
|
|
source_row_val = source_row[idx_val] if source_row is not None else None
|
|
|
source_row_test = source_row[idx_test] if source_row is not None else None
|
|
|
|
|
|
extra_numeric_split = {
|
|
|
name: (
|
|
|
arr[idx_train] if arr is not None else None,
|
|
|
arr[idx_val] if arr is not None else None,
|
|
|
arr[idx_test] if arr is not None else None,
|
|
|
)
|
|
|
for name, arr in extra_numeric.items()
|
|
|
}
|
|
|
extra_string_split = {
|
|
|
name: (
|
|
|
arr[idx_train] if arr is not None else None,
|
|
|
arr[idx_val] if arr is not None else None,
|
|
|
arr[idx_test] if arr is not None else None,
|
|
|
)
|
|
|
for name, arr in extra_string.items()
|
|
|
}
|
|
|
|
|
|
categorical_indices = np.asarray(
|
|
|
param_feature_transform.get("categorical_feature_indices", []),
|
|
|
dtype=np.int64,
|
|
|
)
|
|
|
scaled_param_indices = np.setdiff1d(
|
|
|
np.arange(x_params_features.shape[1], dtype=np.int64),
|
|
|
categorical_indices,
|
|
|
)
|
|
|
scaler_params = SelectiveStandardScaler(scaled_param_indices)
|
|
|
scaler_schedule = StandardScaler()
|
|
|
scaler_curve = MaskedStandardScaler()
|
|
|
|
|
|
valid_count_train = curve_valid_mask_train.sum(axis=0).astype(np.int64)
|
|
|
if prediction_curve_time is not None and np.any(valid_count_train == 0):
|
|
|
missing_columns = np.flatnonzero(valid_count_train == 0).tolist()
|
|
|
raise ValueError(
|
|
|
"fixed curve_time has training columns without physical coverage: "
|
|
|
f"{missing_columns}"
|
|
|
)
|
|
|
|
|
|
# 只在训练集上拟合 scaler,避免验证/测试信息泄漏。
|
|
|
x_params_train_s = scaler_params.fit_transform(x_params_train)
|
|
|
x_schedule_train_s = scaler_schedule.fit_transform(x_schedule_train)
|
|
|
scaler_curve.fit(y_curve_train, curve_mask_train)
|
|
|
y_curve_train_s = scaler_curve.transform(y_curve_train)
|
|
|
y_curve_train_s[~curve_mask_train] = 0.0
|
|
|
|
|
|
x_params_val_s = scaler_params.transform(x_params_val)
|
|
|
x_schedule_val_s = scaler_schedule.transform(x_schedule_val)
|
|
|
y_curve_val_s = scaler_curve.transform(y_curve_val)
|
|
|
y_curve_val_s[~curve_mask_val] = 0.0
|
|
|
|
|
|
x_params_test_s = scaler_params.transform(x_params_test)
|
|
|
x_schedule_test_s = scaler_schedule.transform(x_schedule_test)
|
|
|
y_curve_test_s = scaler_curve.transform(y_curve_test)
|
|
|
y_curve_test_s[~curve_mask_test] = 0.0
|
|
|
|
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
# 保存 curve_layout,训练和评估脚本就能按元数据切分拼接曲线,
|
|
|
# 避免在各处硬编码索引范围。
|
|
|
meta = {
|
|
|
"n_total": int(n),
|
|
|
"n_train": int(len(idx_train)),
|
|
|
"n_val": int(len(idx_val)),
|
|
|
"n_test": int(len(idx_test)),
|
|
|
"param_dim": int(x_params_features.shape[1]),
|
|
|
"raw_param_dim": int(x_params.shape[1]),
|
|
|
"schedule_dim": int(x_schedule.shape[1]),
|
|
|
"curve_dim": curve_dim,
|
|
|
"raw_curve_dim": raw_curve_dim,
|
|
|
"dropped_legacy_slope": bool(raw_curve_dim != curve_dim),
|
|
|
"curve_time_mode": curve_time_mode,
|
|
|
"curve_valid_mask_mode": curve_valid_mask_mode,
|
|
|
"curve_valid_fraction": {
|
|
|
"train": float(np.mean(curve_valid_mask_train)),
|
|
|
"val": float(np.mean(curve_valid_mask_val)),
|
|
|
"test": float(np.mean(curve_valid_mask_test)),
|
|
|
},
|
|
|
"curve_valid_count_train": valid_count_train.astype(int).tolist(),
|
|
|
"input_h5": str(input_path),
|
|
|
"param_names": param_names,
|
|
|
"param_feature_transform": param_feature_transform,
|
|
|
"param_feature_names": list(param_feature_transform.get("feature_names", [])),
|
|
|
"param_scaled_indices": scaled_param_indices.tolist(),
|
|
|
"param_unscaled_indices": categorical_indices.tolist(),
|
|
|
"split_strategy": split_strategy,
|
|
|
"solver_type_counts": {
|
|
|
"train": _value_counts(solver_type, idx_train),
|
|
|
"val": _value_counts(solver_type, idx_val),
|
|
|
"test": _value_counts(solver_type, idx_test),
|
|
|
},
|
|
|
"group_counts": {
|
|
|
"train": int(np.unique(group_id[idx_train]).size) if group_id is not None else 0,
|
|
|
"val": int(np.unique(group_id[idx_val]).size) if group_id is not None else 0,
|
|
|
"test": int(np.unique(group_id[idx_test]).size) if group_id is not None else 0,
|
|
|
},
|
|
|
"schedule_meta_names": schedule_meta_names,
|
|
|
"source_name_vocab": source_name_vocab,
|
|
|
"extra_numeric_fields": [name for name, arr in extra_numeric.items() if arr is not None],
|
|
|
"extra_string_fields": [name for name, arr in extra_string.items() if arr is not None],
|
|
|
"curve_layout": {
|
|
|
"n_time_points": int(n_time_points),
|
|
|
"parts": [
|
|
|
{"name": "log_pressure", "start": 0, "end": int(n_time_points)},
|
|
|
{"name": "log_derivative", "start": int(n_time_points), "end": int(2 * n_time_points)},
|
|
|
],
|
|
|
},
|
|
|
}
|
|
|
if prediction_curve_time is not None:
|
|
|
meta["prediction_curve_time"] = prediction_curve_time.tolist()
|
|
|
|
|
|
payload = {
|
|
|
"X_params_train": x_params_train_s.astype(np.float32),
|
|
|
"X_schedule_train": x_schedule_train_s.astype(np.float32),
|
|
|
"Y_curve_train": y_curve_train_s.astype(np.float32),
|
|
|
"X_params_val": x_params_val_s.astype(np.float32),
|
|
|
"X_schedule_val": x_schedule_val_s.astype(np.float32),
|
|
|
"Y_curve_val": y_curve_val_s.astype(np.float32),
|
|
|
"X_params_test": x_params_test_s.astype(np.float32),
|
|
|
"X_schedule_test": x_schedule_test_s.astype(np.float32),
|
|
|
"Y_curve_test": y_curve_test_s.astype(np.float32),
|
|
|
"curve_valid_mask_train": curve_valid_mask_train.astype(np.uint8),
|
|
|
"curve_valid_mask_val": curve_valid_mask_val.astype(np.uint8),
|
|
|
"curve_valid_mask_test": curve_valid_mask_test.astype(np.uint8),
|
|
|
"scaler_params": scaler_params,
|
|
|
"scaler_schedule": scaler_schedule,
|
|
|
"scaler_curve": scaler_curve,
|
|
|
"meta": meta,
|
|
|
}
|
|
|
|
|
|
if schedule_meta is not None:
|
|
|
payload["schedule_meta_train"] = np.asarray(schedule_meta_train, dtype=np.float32)
|
|
|
payload["schedule_meta_val"] = np.asarray(schedule_meta_val, dtype=np.float32)
|
|
|
payload["schedule_meta_test"] = np.asarray(schedule_meta_test, dtype=np.float32)
|
|
|
if family_name is not None:
|
|
|
payload["family_name_train"] = np.asarray(family_name_train, dtype=object)
|
|
|
payload["family_name_val"] = np.asarray(family_name_val, dtype=object)
|
|
|
payload["family_name_test"] = np.asarray(family_name_test, dtype=object)
|
|
|
if source_id is not None:
|
|
|
payload["source_id_train"] = np.asarray(source_id_train)
|
|
|
payload["source_id_val"] = np.asarray(source_id_val)
|
|
|
payload["source_id_test"] = np.asarray(source_id_test)
|
|
|
if source_name is not None:
|
|
|
payload["source_name_train"] = np.asarray(source_name_train, dtype=object)
|
|
|
payload["source_name_val"] = np.asarray(source_name_val, dtype=object)
|
|
|
payload["source_name_test"] = np.asarray(source_name_test, dtype=object)
|
|
|
if source_row is not None:
|
|
|
payload["source_row_train"] = np.asarray(source_row_train)
|
|
|
payload["source_row_val"] = np.asarray(source_row_val)
|
|
|
payload["source_row_test"] = np.asarray(source_row_test)
|
|
|
for name, (train_arr, val_arr, test_arr) in extra_numeric_split.items():
|
|
|
if train_arr is None:
|
|
|
continue
|
|
|
payload[f"{name}_train"] = np.asarray(train_arr)
|
|
|
payload[f"{name}_val"] = np.asarray(val_arr)
|
|
|
payload[f"{name}_test"] = np.asarray(test_arr)
|
|
|
for name, (train_arr, val_arr, test_arr) in extra_string_split.items():
|
|
|
if train_arr is None:
|
|
|
continue
|
|
|
payload[f"{name}_train"] = np.asarray(train_arr, dtype=object)
|
|
|
payload[f"{name}_val"] = np.asarray(val_arr, dtype=object)
|
|
|
payload[f"{name}_test"] = np.asarray(test_arr, dtype=object)
|
|
|
|
|
|
joblib.dump(payload, output_path)
|
|
|
|
|
|
print(f"Preprocess complete, saved to: {output_path}")
|
|
|
print(
|
|
|
f"train={len(idx_train)}, val={len(idx_val)}, test={len(idx_test)}, "
|
|
|
f"param_dim={x_params_features.shape[1]}, schedule_dim={x_schedule.shape[1]}, "
|
|
|
f"curve_dim={y_curve.shape[1]}"
|
|
|
)
|
|
|
print(
|
|
|
f"split_strategy={split_strategy}, "
|
|
|
f"solver_type_counts={meta['solver_type_counts']}, "
|
|
|
f"group_counts={meta['group_counts']}, "
|
|
|
f"curve_time_mode={curve_time_mode}"
|
|
|
)
|
|
|
if schedule_meta is not None:
|
|
|
print(f"schedule_meta_dim={schedule_meta.shape[1]}, family_name_saved={family_name is not None}")
|
|
|
if source_name is not None:
|
|
|
print(f"source_name_saved=True, source_vocab={source_name_vocab}")
|
|
|
if meta["extra_numeric_fields"] or meta["extra_string_fields"]:
|
|
|
print(
|
|
|
"extra_fields_saved="
|
|
|
f"{meta['extra_numeric_fields'] + meta['extra_string_fields']}"
|
|
|
)
|