From 5ef6a8d9c9ded9b33c82cb7103e7608846071cbd Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Fri, 17 Jul 2026 17:11:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EsolverType=E5=8F=82=E6=95=B0?= =?UTF-8?q?=EF=BC=8C=E5=8C=BA=E5=88=86=E4=B8=8D=E5=90=8C=E7=9B=B8=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ML/nmWTAI-ML/scripts/compare_single_case.py | 62 +++++++++++++++---- ML/nmWTAI-ML/scripts/score_pso_candidates.py | 40 ++++++++++-- .../scripts/score_pso_candidates_server.py | 3 + .../scripts/validate_autofit_local_ranking.py | 55 +++++++++++++--- .../nmCalculation/nmCalculationAutoFitPSO.cpp | 60 +++++++++++++++--- 5 files changed, 187 insertions(+), 33 deletions(-) diff --git a/ML/nmWTAI-ML/scripts/compare_single_case.py b/ML/nmWTAI-ML/scripts/compare_single_case.py index 7ad001c..f1421db 100644 --- a/ML/nmWTAI-ML/scripts/compare_single_case.py +++ b/ML/nmWTAI-ML/scripts/compare_single_case.py @@ -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, diff --git a/ML/nmWTAI-ML/scripts/score_pso_candidates.py b/ML/nmWTAI-ML/scripts/score_pso_candidates.py index 5ab2bc8..3777ec7 100644 --- a/ML/nmWTAI-ML/scripts/score_pso_candidates.py +++ b/ML/nmWTAI-ML/scripts/score_pso_candidates.py @@ -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}" diff --git a/ML/nmWTAI-ML/scripts/score_pso_candidates_server.py b/ML/nmWTAI-ML/scripts/score_pso_candidates_server.py index b70a4b5..0396a86 100644 --- a/ML/nmWTAI-ML/scripts/score_pso_candidates_server.py +++ b/ML/nmWTAI-ML/scripts/score_pso_candidates_server.py @@ -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}" diff --git a/ML/nmWTAI-ML/scripts/validate_autofit_local_ranking.py b/ML/nmWTAI-ML/scripts/validate_autofit_local_ranking.py index ae8ee7a..3bb80a7 100644 --- a/ML/nmWTAI-ML/scripts/validate_autofit_local_ranking.py +++ b/ML/nmWTAI-ML/scripts/validate_autofit_local_ranking.py @@ -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) diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index 831fc93..727b7ef 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -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__gen.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(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 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(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/h,Cf 作为固定背景参数传入。 + // 参数 gate:变化 PVT 的 T2/T4 允许 Cf 参与拟合;常数 PVT 的 T1/T3 仍固定 Cf。 // 用户勾选其它参数时,代理无法可靠反映这些参数变化,直接禁用代理筛选。 QVector 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 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;