新增solverType参数,区分不同相态

feature/Model-20260625
lvjunjie 2 weeks ago
parent 6d3b577579
commit 5ef6a8d9c9

@ -42,7 +42,11 @@ from src.common.experiment_paths import (
processed_path_for_tag,
)
from src.data.curve_processing import clean_curve_for_dataset, is_valid_curve, resample_curve_to_model_features
from src.data.param_features import param_feature_transform_from_meta, transform_param_features
from src.data.param_features import (
build_raw_param_vector,
param_feature_transform_from_meta,
transform_param_features,
)
from src.data.params import Params, Schedule
from src.data.runner_client import CppRunner, read_result_bin
from src.data.schedule_features import build_schedule_model_vector
@ -103,6 +107,13 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--h", type=float, default=DEFAULT_SINGLE_CASE["params"]["h"])
parser.add_argument("--Ct", type=float, default=DEFAULT_SINGLE_CASE["params"]["Ct"])
parser.add_argument("--Cf", type=float, default=DEFAULT_SINGLE_CASE["params"]["Cf"])
parser.add_argument("--solver-type", type=int, choices=[1, 2, 3, 4], default=None)
parser.add_argument(
"--dataset-case",
type=str,
default=None,
help="Dataset case name used to select the matching scene/PVT files",
)
parser.add_argument(
"--well-index",
type=int,
@ -277,12 +288,26 @@ def resolve_paths(args: argparse.Namespace) -> tuple[Config, Path, Path, Path]:
if config_path is None:
config_path = str(config_for_stage(args.stage) or Path("configs/data_gen_family_random.yaml"))
processed_path = Path(args.processed) if args.processed is not None else processed_path_for_tag(tag)
model_path = (
Path(args.model)
if args.model is not None
else model_checkpoint_for_tag(tag, use_schedule=not args.no_schedule)
)
cfg = Config(config_path, dataset_case=args.dataset_case)
if args.processed is not None:
processed_path = Path(args.processed)
else:
processed_candidates = [
processed_path_for_tag(tag),
cfg.paths.processed_dir / f"dataset_{tag}.pkl",
cfg.paths.processed_dir / f"processed_{tag}.pkl",
]
processed_path = next((path for path in processed_candidates if path.exists()), processed_candidates[0])
if args.model is not None:
model_path = Path(args.model)
else:
model_candidates = [
model_checkpoint_for_tag(tag, use_schedule=not args.no_schedule),
cfg.paths.models_dir / str(tag) / "forward_surrogate_best.pt",
cfg.paths.models_dir / f"forward_surrogate_{tag}" / "forward_surrogate_best.pt",
]
model_path = next((path for path in model_candidates if path.exists()), model_candidates[0])
if args.output_dir is not None:
output_dir = Path(args.output_dir)
@ -290,7 +315,19 @@ def resolve_paths(args: argparse.Namespace) -> tuple[Config, Path, Path, Path]:
suffix = "" if not args.no_schedule else "_no_schedule"
output_dir = Path("results") / (f"single_compare_{tag}{suffix}" if tag else f"single_compare{suffix}")
return Config(config_path), processed_path, model_path, output_dir
return cfg, processed_path, model_path, output_dir
def resolve_solver_type(args: argparse.Namespace, cfg: Config) -> int | None:
"""Resolve solverType from an explicit CLI value or the selected dataset case."""
configured = cfg.get("solver_type", default=None)
if args.solver_type is not None and configured is not None:
if int(args.solver_type) != int(configured):
raise ValueError(
f"--solver-type={args.solver_type} conflicts with dataset case solver_type={configured}"
)
value = args.solver_type if args.solver_type is not None else configured
return None if value is None else int(value)
def build_params_from_args(args: argparse.Namespace, cfg: Config, schedule: Schedule) -> Params:
@ -405,6 +442,7 @@ def predict_surrogate_curve(
params: Params,
schedule: Schedule,
cfg: Config,
solver_type: int | None = None,
) -> np.ndarray:
"""使用正演代理模型预测单个参数和流量制度对应的完整曲线。"""
scaler_params = processed["scaler_params"]
@ -414,10 +452,7 @@ def predict_surrogate_curve(
# 构建参数特征向量
# 顺序必须与训练阶段保持一致
params_vec = np.asarray(
[params.k, params.skin, params.wellboreC, params.phi, params.h, params.Cf],
dtype=np.float32,
).reshape(1, -1)
params_vec = build_raw_param_vector(params, param_transform, solver_type=solver_type)
# 将流量制度 schedule 编码成模型输入向量
schedule_vec = build_schedule_vector(cfg, schedule).reshape(1, -1)
@ -534,6 +569,7 @@ def main() -> None:
# 先用同一组参数和制度跑真实求解器,再用代理模型预测同一输入,保证对比公平。
schedule = resolve_case_schedule(cfg, args)
params = build_params_from_args(args, cfg, schedule)
solver_type = resolve_solver_type(args, cfg)
print("Running solver...")
# 运行真实数值求解器
@ -553,6 +589,7 @@ def main() -> None:
params=params,
schedule=schedule,
cfg=cfg,
solver_type=solver_type,
)
plot_path = output_dir / "single_case_comparison.png"
@ -583,6 +620,7 @@ def main() -> None:
"h": params.h,
"Ct": params.Ct,
"Cf": params.Cf,
"solverType": solver_type,
},
"schedule": {
"sectionIndex": schedule.sectionIndex,

@ -34,7 +34,7 @@ from src.data.params import Params, Schedule
from src.evaluation.autofit_objective import dual_log_objective
PARAM_COLUMNS = ["k", "skin", "wellboreC", "phi", "h", "Cf"]
PARAM_COLUMNS = ["k", "skin", "wellboreC", "phi", "h", "Cf", "solverType"]
def parse_args() -> argparse.Namespace:
@ -44,7 +44,7 @@ def parse_args() -> argparse.Namespace:
"--candidates",
required=True,
type=str,
help="候选粒子 CSV包含 particle_id,k,skin,wellboreC,phi,h,Cf",
help="候选粒子 CSV包含 particle_id,k,skin,wellboreC,phi,h,Cf,solverType",
)
parser.add_argument(
"--trace-meta",
@ -67,8 +67,27 @@ def resolve_paths(args: argparse.Namespace) -> tuple[Path, Path, Path]:
config_path = Path(args.config) if args.config else config_for_stage(args.stage)
if config_path is None:
raise ValueError(f"Cannot resolve config for stage={args.stage!r}; pass --config explicitly")
processed_path = Path(args.processed) if args.processed else processed_path_for_tag(tag)
model_path = Path(args.model) if args.model else model_checkpoint_for_tag(tag, use_schedule=True)
cfg = Config(config_path)
if args.processed:
processed_path = Path(args.processed)
else:
processed_candidates = [
processed_path_for_tag(tag),
cfg.paths.processed_dir / f"dataset_{tag}.pkl",
cfg.paths.processed_dir / f"processed_{tag}.pkl",
]
processed_path = next((path for path in processed_candidates if path.exists()), processed_candidates[0])
if args.model:
model_path = Path(args.model)
else:
model_candidates = [
model_checkpoint_for_tag(tag, use_schedule=True),
cfg.paths.models_dir / str(tag) / "forward_surrogate_best.pt",
cfg.paths.models_dir / f"forward_surrogate_{tag}" / "forward_surrogate_best.pt",
]
model_path = next((path for path in model_candidates if path.exists()), model_candidates[0])
return config_path.resolve(), processed_path.resolve(), model_path.resolve()
@ -132,6 +151,17 @@ def params_from_row(row: dict) -> Params:
)
def solver_type_from_row(row: dict) -> int | None:
"""Read solverType while keeping legacy candidate CSV files usable with legacy models."""
raw = row.get("solverType")
if raw is None or str(raw).strip() == "":
return None
value = float(raw)
if not value.is_integer():
raise ValueError(f"solverType must be an integer, got {raw!r}")
return int(value)
def main() -> None:
"""用训练好的正演代理模型快速评价一批 PSO 候选粒子的目标函数。"""
args = parse_args()
@ -177,6 +207,7 @@ def main() -> None:
}
try:
params = params_from_row(row)
solver_type = solver_type_from_row(row)
params.schedule = schedule
# 代理预测的曲线与 trace 中的目标曲线使用同一套双对数目标函数比较。
pred_curve = predict_surrogate_curve(
@ -187,6 +218,7 @@ def main() -> None:
params=params,
schedule=schedule,
cfg=cfg,
solver_type=solver_type,
)
obj = dual_log_objective(target_curve, pred_curve, curve_layout)
out["surrogate_objective"] = f"{obj['dual_log_objective']:.17g}"

@ -27,6 +27,7 @@ from scripts.score_pso_candidates import (
params_from_row,
read_candidates,
resolve_paths,
solver_type_from_row,
)
from scripts.validate_autofit_local_ranking import infer_curve_layout, predict_surrogate_curve
from src.common.config import Config
@ -108,6 +109,7 @@ class PsoScoringServer:
}
try:
params = params_from_row(row)
solver_type = solver_type_from_row(row)
params.schedule = self.schedule
# 预测曲线后直接与固定目标曲线比较;这里不再启动数值求解器。
pred_curve = predict_surrogate_curve(
@ -118,6 +120,7 @@ class PsoScoringServer:
params=params,
schedule=self.schedule,
cfg=self.cfg,
solver_type=solver_type,
)
obj = dual_log_objective(self.target_curve, pred_curve, self.curve_layout)
out["surrogate_objective"] = f"{obj['dual_log_objective']:.17g}"

@ -27,7 +27,11 @@ import torch
from src.common.config import Config
from src.common.experiment_paths import config_for_stage, model_checkpoint_for_tag, normalize_tag, processed_path_for_tag
from src.data.curve_processing import clean_curve_for_dataset, is_valid_curve, resample_curve_to_model_features
from src.data.param_features import param_feature_transform_from_meta, transform_param_features
from src.data.param_features import (
build_raw_param_vector,
param_feature_transform_from_meta,
transform_param_features,
)
from src.data.params import Params, Schedule
from src.data.runner_client import CppRunner, read_result_bin
from src.data.schedule_features import build_schedule_model_vector
@ -85,6 +89,8 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--h", type=float, default=DEFAULT_CASE["params"]["h"])
parser.add_argument("--Ct", type=float, default=DEFAULT_CASE["params"]["Ct"])
parser.add_argument("--Cf", type=float, default=DEFAULT_CASE["params"]["Cf"])
parser.add_argument("--solver-type", type=int, choices=[1, 2, 3, 4], default=None)
parser.add_argument("--dataset-case", type=str, default=None)
return parser.parse_args()
@ -135,14 +141,47 @@ def resolve_paths(args: argparse.Namespace) -> tuple[Config, Path, Path, Path]:
if config_path is None:
config_path = str(config_for_stage(args.stage) or Path(DEFAULT_CASE["config"]))
processed_path = (Path(args.processed) if args.processed is not None else processed_path_for_tag(tag)).resolve()
model_path = (Path(args.model) if args.model is not None else model_checkpoint_for_tag(tag, use_schedule=True)).resolve()
cfg = Config(config_path, dataset_case=args.dataset_case)
if args.processed is not None:
processed_path = Path(args.processed)
else:
processed_candidates = [
processed_path_for_tag(tag),
cfg.paths.processed_dir / f"dataset_{tag}.pkl",
cfg.paths.processed_dir / f"processed_{tag}.pkl",
]
processed_path = next((path for path in processed_candidates if path.exists()), processed_candidates[0])
if args.model is not None:
model_path = Path(args.model)
else:
model_candidates = [
model_checkpoint_for_tag(tag, use_schedule=True),
cfg.paths.models_dir / str(tag) / "forward_surrogate_best.pt",
cfg.paths.models_dir / f"forward_surrogate_{tag}" / "forward_surrogate_best.pt",
]
model_path = next((path for path in model_candidates if path.exists()), model_candidates[0])
processed_path = processed_path.resolve()
model_path = model_path.resolve()
if args.output_dir is not None:
output_dir = Path(args.output_dir).resolve()
else:
output_dir = (Path("results") / f"autofit_local_validation_{tag}").resolve()
return Config(config_path), processed_path, model_path, output_dir
return cfg, processed_path, model_path, output_dir
def resolve_solver_type(args: argparse.Namespace, cfg: Config) -> int | None:
"""Resolve solverType from an explicit CLI value or the selected dataset case."""
configured = cfg.get("solver_type", default=None)
if args.solver_type is not None and configured is not None:
if int(args.solver_type) != int(configured):
raise ValueError(
f"--solver-type={args.solver_type} conflicts with dataset case solver_type={configured}"
)
value = args.solver_type if args.solver_type is not None else configured
return None if value is None else int(value)
def build_params_from_args(args: argparse.Namespace, schedule: Schedule) -> Params:
@ -192,6 +231,7 @@ def predict_surrogate_curve(
params: Params,
schedule: Schedule,
cfg: Config,
solver_type: int | None = None,
) -> np.ndarray:
"""使用正演代理模型预测单个参数和流量制度对应的完整曲线。"""
scaler_params = processed["scaler_params"]
@ -199,10 +239,7 @@ def predict_surrogate_curve(
scaler_curve = processed["scaler_curve"]
param_transform = param_feature_transform_from_meta(processed.get("meta", {}))
params_vec = np.asarray(
[params.k, params.skin, params.wellboreC, params.phi, params.h, params.Cf],
dtype=np.float32,
).reshape(1, -1)
params_vec = build_raw_param_vector(params, param_transform, solver_type=solver_type)
schedule_vec = build_schedule_vector(cfg, schedule).reshape(1, -1)
params_x = scaler_params.transform(transform_param_features(params_vec, param_transform)).astype(np.float32)
@ -412,6 +449,7 @@ def main() -> None:
curve_layout = infer_curve_layout(processed["meta"], int(processed["meta"]["curve_dim"]))
schedule = resolve_case_schedule(cfg)
target_params = build_params_from_args(args, schedule)
solver_type = resolve_solver_type(args, cfg)
search_names = _search_param_names(cfg)
model, use_schedule, device = load_model(model_path)
@ -479,6 +517,7 @@ def main() -> None:
params=cand,
schedule=schedule,
cfg=cfg,
solver_type=solver_type,
)
solver_obj = dual_log_objective(target_curve, solver_curve, curve_layout)

@ -75,6 +75,8 @@ static const double kSurrogateHMin = 2.0; // 代理模型训练
static const double kSurrogateHMax = 50.0; // 代理模型训练域:储层厚度上界。
static const double kSurrogateCfFixed = 4.315e-4; // 当前代理模型按近似固定 Cf 训练。
static const double kSurrogateCfRelTol = 0.25; // Cf 允许相对偏差,超过则不使用代理筛选。
static const double kSurrogateCfMin = 1.0e-6; // 变化 PVT 模型的 Cf 训练域下界。
static const double kSurrogateCfMax = 5.0e-3; // 变化 PVT 模型的 Cf 训练域上界。
static const int kSurrogateMinProdSections = 3; // 代理训练流量制度:生产段数量下界。
static const int kSurrogateMaxProdSections = 5; // 代理训练流量制度:生产段数量上界。
static const double kSurrogateProdTimeMin = 24.0; // 代理训练流量制度:生产总时长下界。
@ -1224,7 +1226,7 @@ QString nmCalculationAutoFitPSO::getSurrogateTag() const
{
// 当前 C++ 绑定的代理模型实验标识,对应 ML 侧训练输出目录/checkpoint 命名。
// 如果以后换模型,需要同步更新这里和 ML 脚本可识别的 tag。
return "family_random_v2_q_50k_noslope";
return "T1_T4_merged_40000";
}
QString nmCalculationAutoFitPSO::getSurrogateStage() const
@ -1277,7 +1279,7 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat
//
// 文件位置通常是 ML/nmWTAI-ML/data/temp/pso_screen_candidates_<run_id>_gen<N>.csv。
// 字段顺序必须与 ML 脚本 score_pso_candidates*.py 保持一致:
// particle_id,k,skin,wellboreC,phi,h,Cf
// particle_id,k,skin,wellboreC,phi,h,Cf,solverType
//
// 这里写的是“完整参数体系”中的关键字段,不是粒子内部紧凑向量。
// 例如用户没有勾选 Cf 时Cf 会从当前 DataManager 取值写入 CSV。
@ -1289,8 +1291,16 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat
}
QTextStream out(&file);
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
if(!dataManager) {
DEBUG_OUT("Failed to write surrogate candidates: data manager is unavailable");
return false;
}
int solverType = static_cast<int>(dataManager->getSolverModelType());
// 代理模型只需要这些输入字段;其它参数当前不在训练输入集中。
out << "particle_id,k,skin,wellboreC,phi,h,Cf\n";
out << "particle_id,k,skin,wellboreC,phi,h,Cf,solverType\n";
for(int i = 0; i < m_swarm.size(); ++i) {
QVector<double> params = buildTraceParameterVector(m_swarm[i].position);
@ -1301,7 +1311,8 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat
<< traceParamAt(params, 2)
<< traceParamAt(params, 3)
<< traceParamAt(params, 5)
<< traceParamAt(params, 7);
<< traceParamAt(params, 7)
<< QString::number(solverType);
out << cols.join(",") << "\n";
}
@ -1851,6 +1862,22 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co
return false;
}
NM_SOLVER_MODEL_TYPE solverModelType = dataManager->getSolverModelType();
bool isVariablePvt = solverModelType == SMT_Oil_VariablePvt ||
solverModelType == SMT_Water_VariablePvt;
bool isSupportedSolverType = solverModelType == SMT_Oil_ConstPvt ||
solverModelType == SMT_Oil_VariablePvt ||
solverModelType == SMT_Water_ConstPvt ||
solverModelType == SMT_Water_VariablePvt;
if(!isSupportedSolverType) {
if(reason) {
*reason = QString("solverType=%1 is outside the T1-T4 surrogate model")
.arg(static_cast<int>(solverModelType));
}
return false;
}
// 网格类型 gate当前代理模型只按 PEBI 网格样本训练。
if(dataManager->getGridType() != NM_Grid_PEBI) {
if(reason) *reason = "surrogate model is trained only for PEBI grid cases";
@ -1898,11 +1925,15 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co
return false;
}
// 参数 gate代理输入目前只覆盖 k/skin/wellboreC/phi/hCf 作为固定背景参数传入
// 参数 gate变化 PVT 的 T2/T4 允许 Cf 参与拟合;常数 PVT 的 T1/T3 仍固定 Cf
// 用户勾选其它参数时,代理无法可靠反映这些参数变化,直接禁用代理筛选。
QVector<int> allowedParamIndices;
allowedParamIndices << 0 << 1 << 2 << 3 << 5;
if(isVariablePvt) {
allowedParamIndices << 7;
}
for(int i = 0; i < m_enabledParamIndices.size(); ++i) {
if(!allowedParamIndices.contains(m_enabledParamIndices[i])) {
if(reason) {
@ -1919,10 +1950,12 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co
nmDataReservoir reservoirData = dataManager->getReservoirDataCopy();
double cf = reservoirData.getCf().getValue().toDouble();
// Cf gate当前模型按近似固定 Cf 训练,允许小范围相对偏差。
if(!isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol)) {
bool cfSupported = isVariablePvt
? isInClosedRange(cf, kSurrogateCfMin, kSurrogateCfMax)
: isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol);
if(!cfSupported) {
if(reason) {
*reason = QString("Cf=%1 is outside surrogate fixed-Cf domain").arg(cf, 0, 'g', 10);
*reason = QString("Cf=%1 is outside surrogate domain").arg(cf, 0, 'g', 10);
}
return false;
@ -2075,7 +2108,16 @@ bool nmCalculationAutoFitPSO::isSurrogateCandidateInDomain(const QVector<double>
return false;
}
if(!isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol)) {
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
NM_SOLVER_MODEL_TYPE solverModelType = dataManager
? dataManager->getSolverModelType()
: SMT_Oil_ConstPvt;
bool isVariablePvt = solverModelType == SMT_Oil_VariablePvt ||
solverModelType == SMT_Water_VariablePvt;
bool cfSupported = isVariablePvt
? isInClosedRange(cf, kSurrogateCfMin, kSurrogateCfMax)
: isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol);
if(!cfSupported) {
if(reason) *reason = QString("Cf=%1").arg(cf, 0, 'g', 10);
return false;

Loading…
Cancel
Save