diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index c234a7d0..ee2939dc 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 4aee9b73..45d2ab6c 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -677,22 +677,16 @@ Reason: %1 【初始参数】 - [Stage 1: Height alignment] -Adjust permeability only; accept by height error. - 【第一阶段:高度预调】 -只调整渗透率,按高度误差判断是否接受。 + [Stage 1: Height alignment] + 【第一阶段:高度预调】 - [Stage 2: Joint LM - Shape] -Adjust selected parameters; accept by shape error. - 【第二阶段:联合 LM · 形状】 -调整勾选参数,按形状误差判断是否接受。 + [Stage 2: Joint LM - Shape] + 【第二阶段:联合 LM · 形状】 - [Stage 2: Joint LM - Total error] -Adjust selected parameters; accept by total error. - 【第二阶段:联合 LM · 总误差】 -调整勾选参数,按总误差判断是否接受。 + [Stage 2: Joint LM - Total error] + 【第二阶段:联合 LM · 总误差】 %1 | %2 | %3 | Improvement: %4% @@ -759,12 +753,8 @@ No valid error was returned; keep the last accepted result and reduce the step.< mD·m - Trial %1 | %2 | Height error: %3 -> %4 - 试算 %1|%2|高度误差:%3 → %4 - - - Trial %1 | Solver failed; keep the last accepted result and reduce the step. - 试算 %1|求解失败,保留上次已接受结果并缩小步长。 + Height error: %1 -> %2 + 高度误差:%1 → %2 Height error did not decrease; keep the last accepted result and reduce the step. @@ -816,7 +806,7 @@ No valid error was returned; keep the last accepted result and reduce the step.< Iteration or evaluation budget reached - 达到本阶段迭代次数或试算次数上限 + 达到总迭代上限或本段试算次数上限 Final total error did not improve; restoring the initial result. @@ -1091,8 +1081,8 @@ No valid error was returned; keep the last accepted result and reduce the step.< === LM 自动拟合 - 局部最优 === - Total-stage budget reached. Best error: %1, Cumulative iterations: %2 - 整体阶段预算已用尽。最佳误差:%1,累计迭代次数:%2 + Iteration or evaluation budget reached. Best error: %1, Cumulative iterations: %2 + 达到总迭代上限或本段试算次数上限。最佳误差:%1,累计迭代次数:%2 === LM AUTOMATIC FITTING - MAX ITERATIONS === @@ -4467,6 +4457,30 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We Gas pseudo-pressure data is unavailable or invalid. 气体拟压力数据不可用或无效。 + + Iteration limit: + 迭代上限: + + + Maximum iterations: + 最大迭代次数: + + + Height, shape and total-error adjustments share this iteration limit; one iteration may call the solver multiple times. + 高度、形状和总误差调整共用此迭代上限;一次迭代可能调用多次求解器。 + + + Start fitting + 开始拟合 + + + Target error: + 目标误差: + + + Fitting parameters + 拟合参数 + nmWxAutomaticfitting @@ -4562,8 +4576,24 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We nmWxAutomaticfittingStart - Total time: %1 min %2 s - 总耗时:%1 分 %2 秒 + Iterations: + 迭代数: + + + Fitting process + 拟合过程 + + + Total time: %1 + 总耗时:%1 + + + %1 s + %1 秒 + + + %1 min %2 s + %1 分 %2 秒 Auto Fitting Progress @@ -4757,6 +4787,86 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We Pre-adjustment 预调整中 + + %1 | Iteration %2 / limit %3 + %1 · 迭代 %2 / 上限 %3 + + + Adjustment log + 调整记录 + + + Calculating initial error... + 正在计算初始误差…… + + + Current parameters + 当前参数 + + + Current value + 当前值 + + + Fitting ended + 拟合结束 + + + Fitting failed + 拟合失败 + + + Generating final result... + 正在生成最终结果…… + + + Height alignment + 高度预调 + + + Shape adjustment + 形状调整 + + + Total-error adjustment + 总误差调整 + + + Collapse + 收起 + + + Expand + 展开 + + + Fitted + 拟合 + + + Fitting in progress + 拟合中 + + + Observed + 实测 + + + Solver calls: %1 | Total time: %2 + 求解器调用 %1 次 · 总耗时 %2 + + + Target error: + 目标误差: + + + Total error: %1 -> %2 + 总误差:%1 → %2 + + + Current error: %1 + 当前误差:%1 + nmWxChangeAnal diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h index d7ff03aa..06350bc9 100644 --- a/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h +++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h @@ -118,6 +118,8 @@ public: void setTargetWellName(const QString& wellName); signals: + // 阶段仅用于界面显示:0 初始求解,1 高度,2 形状,3 总误差。 + void fittingStageChanged(int stage); void progressUpdated(int iteration, double bestFitness); void fittingFinished(bool success, const QString& message); void bestCurveUpdated(QVector > targetData, diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h index 9482bdd4..4620b8f6 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h @@ -4,6 +4,7 @@ #include "iDlgBase.h" #include #include +#include #include #include #include @@ -33,6 +34,7 @@ class NM_SUB_WXS_EXPORT nmWxAutomaticFitting : public iDlgBase public: nmWxAutomaticFitting(QWidget *parent = 0); ~nmWxAutomaticFitting(); + virtual QSize sizeHint() const; private slots: void onCellSelectionChanged(int currentRow, int currentColumn, int previousRow, int previousColumn); @@ -73,16 +75,15 @@ private: private: // UI组件 QVBoxLayout* m_mainLayout; - QVBoxLayout* m_controlLayout; + QGridLayout* m_controlLayout; QTableWidget* m_parameterTable; // 控制面板组件 QLineEdit* m_iterationEdit; + QLabel* m_iterationLabel; QLineEdit* m_errorLimitEdit; QComboBox* m_targetWellCombo; QComboBox* m_algorithmCombo; - QLabel* m_surrogateLabel; - QComboBox* m_surrogateCombo; // 参数复选框 QCheckBox* m_kCheckBox; // 渗透率 diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h index 0697b6f1..e09b291f 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h @@ -7,13 +7,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -75,6 +73,7 @@ class nmWxAutomaticfittingStart : public iDlgBase public: explicit nmWxAutomaticfittingStart(QWidget *parent = 0); ~nmWxAutomaticfittingStart(); + virtual QSize sizeHint() const; // 算法接口 void setAutoFitter(nmCalculationAutoFitPSO* autoFitter); @@ -83,7 +82,7 @@ public: // 通用设置接口 void setFittingParameters(int maxIterations, double targetError, const QString& wellName); - void setSelectedParameters(const QStringList& parameterNames); + void setSelectedParameters(const QStringList& parameterNames, const QStringList& parameterUnits); void setTargetLogLogData(const QVector >& targetData); void setPseudoPressureMode(bool enabled); @@ -92,6 +91,8 @@ public: public slots: void onFittingProgress(int iteration, double fitness); + void onLMStageChanged(int stage); + void onToggleLog(); void onFittingFinished(bool success, const QString& message); void onFinalizingStarted(); void onStopButtonClicked(); @@ -107,24 +108,22 @@ protected: private: void setupUI(); void setupParameterArea(); - void setupTableArea(); + void setupParameterStrip(); void setupChartArea(); void setupLogArea(); void setupControlArea(); - void updateParameterTable(); + void updateAlgorithmDisplay(); + void updateParameterValues(); + void updateCurrentError(double fitness); void addLogMessage(const QString& message); QString formatScientific(double value); // UI组件 - QHBoxLayout* mainLayout; - QVBoxLayout* leftLayout; - QVBoxLayout* rightLayout; QSplitter* rightSplitter; // 参数显示区域 QGroupBox* parameterGroup; QLabel* currentIterationLabel; - QLabel* maxIterationLabel; QLabel* currentComfortLabel; QLabel* bestComfortLabel; QLabel* targetPrecisionLabel; @@ -132,25 +131,31 @@ private: QLabel* algorithmTypeLabel; QLabel* currentIterationValue; - QLabel* maxIterationValue; QLabel* currentComfortValue; QLabel* bestComfortValue; QLabel* targetPrecisionValue; QLabel* targetWellValue; QLabel* algorithmTypeValue; - // 参数表格区域 - QGroupBox* tableGroup; - QTableWidget* tableWidget; + // 曲线和日志下方的整行参数,名称和单位在上、数值在下,用竖线分隔。 + QWidget* parameterStrip; + QGridLayout* parameterLayout; + QVector parameterValues; // 日志区域 QGroupBox* chartGroup; nmAutomaticFittingCurveChart* curveChart; QGroupBox* logGroup; QTextEdit* logTextEdit; + QPushButton* logToggle; + QList expandedLogSizes; // 控制区域 QProgressBar* progressBar; + QLabel* stageStatus; + QLabel* stateBadge; + QLabel* resultSummary; + QLabel* runSummary; QPushButton* stopButton; // 算法实例 @@ -162,7 +167,6 @@ private: int m_maxIterations; double m_targetError; QString m_wellName; - QStringList m_selectedParameters; bool m_isFinished; bool m_isFinalizing; double m_bestFitnessEver; diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp index 58fadf72..afcb16b1 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp @@ -688,7 +688,7 @@ nmCalculationAutoFitLM::nmCalculationAutoFitLM(QObject* parent) , m_isRunning(false) , m_shouldStop(false) , m_isFinalizing(false) - , m_currentIteration(0) + , m_currentIteration(-1) , m_globalBestFitness(1e10) , m_comparisonTimeMin(0.0) , m_comparisonTimeMax(0.0) @@ -884,7 +884,7 @@ void nmCalculationAutoFitLM::resetOptimizer() m_userInitialObjectiveBreakdown = AutoFitObjectiveBreakdownLM(); m_comparisonTimeMin = 0.0; m_comparisonTimeMax = 0.0; - m_currentIteration = 0; + m_currentIteration = -1; // 尚未执行迭代;对外日志使用索引加一。 m_totalEvaluations = 0; m_solverCallCount = 0; m_successfulEvaluations = 0; @@ -1042,17 +1042,17 @@ void nmCalculationAutoFitLM::writeTraceMetaFile() QTextStream out(&metaFile); out << "{\n"; - out << " \"schema_version\": 52,\n"; + out << " \"schema_version\": 53,\n"; out << " \"strategy\": \"permeability_height_then_joint_lm_shape_then_total\",\n"; out << " \"height_acceptance\": \"reliable_vertical_loss_decrease; no_shape_or_total_loss_constraint\",\n"; out << " \"shape_metric\": \"pressure_and_derivative_log_slopes_shared_sampling_grid_lag_round_10_percent_min_1\",\n"; out << " \"shape_parameter_selection\": \"all_valid_free_columns_joint_LM; no_single_parameter_sweep_or_wellbore_recheck\",\n"; out << " \"shape_acceptance\": \"strict_full_shape_loss_decrease; no_total_loss_constraint\",\n"; out << " \"lm_step_policy\": \"same_joint_LM_damping_trust_radius_secant_updates_and_sensitivity_rebuilds_in_both_phases\",\n"; - out << " \"lm_phase_switch\": \"shape_target_reached_or_confirmed_stagnation_or_phase_budget_exhausted_then_total; user_stop_and_consecutive_solver_failure_abort\",\n"; - out << " \"lm_phase_budget\": \"each_phase_has_max_iterations_and_max(dimensions+2,20,3*max_iterations)_evaluations; independent_of_height_prealignment\",\n"; - out << " \"lm_effective_improvement\": \"max(0.00001,0.002*baseline_stage_error); 3_ineffective_steps_trigger_stagnation_confirmation\",\n"; - out << " \"iteration_count_scope\": \"max_iterations_applies_independently_to_each_LM_phase; trace_iteration_is_cumulative\",\n"; + out << " \"lm_phase_switch\": \"shape_target_reached_or_confirmed_stagnation_or_phase_evaluation_budget_exhausted_then_total_if_iterations_remain; shared_iteration_limit_or_user_stop_or_consecutive_solver_failure_abort\",\n"; + out << " \"lm_phase_budget\": \"shared_max_iterations_including_height; each_LM_phase_retains_max(dimensions+2,20,3*max_iterations)_evaluations\",\n"; + out << " \"lm_effective_improvement\": \"max(0.01,0.02*baseline_stage_error); 3_ineffective_steps_trigger_stagnation_confirmation\",\n"; + out << " \"iteration_count_scope\": \"height_shape_and_total_share_max_iterations; rejected_and_failed_candidates_count; sensitivity_evaluations_are_not_iterations; trace_iteration_is_zero_based_cumulative\",\n"; out << " \"total_stage_shape_constraint\": false,\n"; out << " \"total_parameter_selection\": \"all_valid_free_columns\",\n"; out << " \"skin_difference_policy\": \"local_scale_fraction_0.02_in_both_LM_phases\",\n"; @@ -1193,6 +1193,14 @@ static QString lmLogError(double value) ? QString::number(value, 'f', 6) : QString("--"); } +// 改善率统一相对上次已接受误差计算;起点为零时不作除法。 +static QString lmLogImprovement(double beforeError, double afterError) +{ + return beforeError > 0.0 + ? QString::number((beforeError - afterError) / beforeError * 100.0, 'f', 2) + : QString("--"); +} + void nmCalculationAutoFitLM::emitRunSummary(bool success, StopReasonLM finalReason) { // 最终只汇总总误差,耗时由进程窗口在收到结束信号后追加。 @@ -1220,12 +1228,8 @@ void nmCalculationAutoFitLM::emitTrialLog(const QString& title, const QString objective = (shapeStage ? tr("Shape error: %1 -> %2") : tr("Total error: %1 -> %2")) .arg(lmLogError(beforeError)).arg(lmLogError(afterError)); - // 起点为零时相对改善率无定义,避免除零。 - const QString improvement = beforeError > 0.0 - ? QString::number((beforeError - afterError) / beforeError * 100.0, 'f', 2) - : QString("--"); QString message = tr("%1 | %2 | %3 | Improvement: %4%") - .arg(title).arg(status).arg(objective).arg(improvement); + .arg(title).arg(status).arg(objective).arg(lmLogImprovement(beforeError, afterError)); if(!accepted) message += tr("\n Objective error did not decrease; keep the last accepted result and reduce the step."); emit logMessageGenerated(message); } @@ -1435,7 +1439,7 @@ bool nmCalculationAutoFitLM::startAutoFitting() m_isRunning = true; m_shouldStop = false; m_isFinalizing = false; - m_currentIteration = 0; + m_currentIteration = -1; m_consecutiveFailures = 0; m_initialValues = savedInitialValues; initializeTraceFile(); @@ -1443,6 +1447,7 @@ bool nmCalculationAutoFitLM::startAutoFitting() emit logMessageGenerated(tr("========== LM fitting started ==========")); emit logMessageGenerated(tr("Target well: %1").arg(m_targetWellName)); emit logMessageGenerated(tr("Target error: %1").arg(lmLogError(m_targetError))); + emit fittingStageChanged(0); // 先用真实求解器评价用户当前模型,供最终精英保护使用。 if(!savedInitialValues.isEmpty()) { @@ -1653,7 +1658,7 @@ bool nmCalculationAutoFitLM::startAutoFitting() DEBUG_OUT(tr("=== LM AUTOMATIC FITTING - LOCAL OPTIMUM ===")); } else if(finalReason == LM_MAX_ITERATIONS) { success = true; - message = QString(tr("Total-stage budget reached. Best error: %1, Cumulative iterations: %2")) + message = QString(tr("Iteration or evaluation budget reached. Best error: %1, Cumulative iterations: %2")) .arg(m_globalBestFitness, 0, 'e', 4) .arg(m_currentIteration + 1); DEBUG_OUT(tr("=== LM AUTOMATIC FITTING - MAX ITERATIONS ===")); @@ -1825,22 +1830,23 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() return LM_OPTIMIZATION_FAILED; } - // 形状与总误差共用原联合 LM,每段独立使用原迭代和求解额度。 + // 高度、形状、总误差共用迭代上限;灵敏度求解仍使用原来的各段评价额度。 const int phaseEvaluationBudget = qMax(dimensions + 2, qMax(20, m_maxIterations * 3)); int maximumEvaluations = 0; int phaseIterations = 0; int shapeIterations = 0; int totalIterations = 0; + int completedIterations = 0; // 下列步长均位于归一化内部坐标:0.04 表示参数范围的 4%,信赖半径 // 限制一次联合移动的二范数,相关性门槛用于排除响应近乎共线的参数。 const double sensitivityStep = 0.04; const double minimumCoordinateStep = 1.0e-5; const double minimumTrustRadius = 2.0e-3; const double maximumTrustRadius = 0.30; - // 误差下降至少达到绝对 1e-5 且相对当前有效基准 0.2% 才算有效改善。 + // 误差下降至少达到绝对 0.01 且相对当前有效基准 2% 才算有效改善。 // 更小的下降仍保留为最佳解,但不能反复清除停滞状态、延长拟合时间。 - const double effectiveRelativeImprovement = 2.0e-3; - const double effectiveAbsoluteImprovement = 1.0e-5; + const double effectiveRelativeImprovement = 2.0e-2; + const double effectiveAbsoluteImprovement = 1.0e-2; const int maximumIneffectiveSteps = 3; // damping 是 LM 阻尼;拒绝或预测失准时增大,真实下降与预测一致时减小。 @@ -1962,7 +1968,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() } restoreEvaluationState(current); - emit progressUpdated(-1, current.fitness); + emit progressUpdated(0, current.fitness); DEBUG_OUT(tr("=== Starting LM Main Loop ===")); DEBUG_OUT( tr("LM starting point error: %1; evaluation budget per LM phase: %2") @@ -1978,9 +1984,10 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() double heightSlope = -1.0; int heightTrials = 0; if(permeabilityColumn >= 0) { - emit logMessageGenerated(tr("[Stage 1: Height alignment]\nAdjust permeability only; accept by height error.")); - // 不限制试算次数,由高度对齐、改善停滞及可行步长决定何时结束。 - while(processPauseAndStop()) { + emit fittingStageChanged(1); + emit logMessageGenerated(tr("[Stage 1: Height alignment]")); + // 高度调整也占用总迭代额度,仍保留原来的对齐、停滞和可行步长判断。 + while(completedIterations < m_maxIterations && processPauseAndStop()) { const double bias = current.breakdown.verticalCommonBias; if(!current.breakdown.verticalReliable || qAbs(bias) <= 0.01 || heightStagnation >= 2) break; const double k = current.parameters[permeabilityColumn]; @@ -1988,13 +1995,15 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() const double logRange = qLn(m_parameterUpper[0]) - qLn(m_parameterLower[0]); const double change = qBound(-0.30 * logRange, -bias / heightSlope * heightScale, 0.30 * logRange); const AutoFitObjectiveBreakdownLM previous = current.breakdown; - ++heightTrials; TrustRegionEvaluation candidate; candidate.parameters = current.parameters; candidate.parameters[permeabilityColumn] = qBound(m_parameterLower[0], k * qExp(change), m_parameterUpper[0]); candidate.coordinates = coordinatesFromParameters(candidate.parameters); const double actualStep = qLn(candidate.parameters[permeabilityColumn] / k); if(qAbs(actualStep) < 1.0e-5) break; + m_currentIteration = completedIterations++; + ++heightTrials; + emit progressUpdated(completedIterations, current.fitness); candidate.valid = evaluateTrustRegionPoint(candidate.parameters, &candidate.fitness, &candidate.breakdown, &candidate.curve, &candidate.elapsedMs); // 第一阶段只按上下偏移改善验收,不限制形状;仍防止两条曲线一高一低互相抵消。 @@ -2004,7 +2013,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() const double slope = (candidate.breakdown.verticalCommonBias - bias) / actualStep; if(slope < -0.05 && isFiniteNumber(slope)) heightSlope = slope; } - writeTraceRow(-1, 0, "permeability_height", candidate.parameters, candidate.fitness, + writeTraceRow(m_currentIteration, 0, "permeability_height", candidate.parameters, candidate.fitness, candidate.valid, candidate.elapsedMs, accepted ? "accepted_height" : "rejected_height", candidate.valid ? &candidate.breakdown : nullptr); if(accepted) { @@ -2019,18 +2028,24 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() heightStagnation = 0; } else ++heightStagnation; if(candidate.valid) { - emit logMessageGenerated(tr("Trial %1 | %2 | Height error: %3 -> %4") - .arg(heightTrials).arg(accepted ? tr("accepted") : tr("rejected")) - .arg(lmLogError(previous.verticalLoss)).arg(lmLogError(candidate.breakdown.verticalLoss))); + const QString objective = tr("Height error: %1 -> %2") + .arg(lmLogError(previous.verticalLoss)).arg(lmLogError(candidate.breakdown.verticalLoss)); + emit logMessageGenerated(tr("%1 | %2 | %3 | Improvement: %4%") + .arg(tr("Iteration %1").arg(completedIterations)) + .arg(accepted ? tr("accepted") : tr("rejected")).arg(objective) + .arg(lmLogImprovement(previous.verticalLoss, candidate.breakdown.verticalLoss))); if(!accepted) emit logMessageGenerated(candidate.breakdown.verticalReliable ? tr("Height error did not decrease; keep the last accepted result and reduce the step.") : tr("Pressure and derivative height directions conflict; keep the last accepted result and reduce the step.")); } else { - emit logMessageGenerated(tr("Trial %1 | Solver failed; keep the last accepted result and reduce the step.").arg(heightTrials)); + emit logMessageGenerated(tr("%1 | Solver failed\nNo valid error was returned; keep the last accepted result and reduce the step.") + .arg(tr("Iteration %1").arg(completedIterations))); } + emit progressUpdated(completedIterations, current.fitness); } QString heightReason = tr("no feasible permeability step or step too small"); if(m_shouldStop) heightReason = tr("stopped by user"); + else if(completedIterations >= m_maxIterations) heightReason = getStopReasonDescription(LM_MAX_ITERATIONS); else if(!current.breakdown.verticalReliable) heightReason = tr("pressure and derivative height directions conflict"); else if(current.breakdown.verticalLoss <= 0.01) heightReason = tr("curve height is approximately aligned"); else if(heightStagnation >= 2) heightReason = tr("2 consecutive steps without effective height improvement"); @@ -2041,6 +2056,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() if(permeabilityColumn < 0) emit logMessageGenerated(tr("Height alignment skipped: permeability is not selected.")); if(m_shouldStop) return LM_USER_STOPPED; + if(completedIterations >= m_maxIterations) return LM_MAX_ITERATIONS; // 原逐参数第二阶段已移除;同一个联合 LM 先匹配整体形状,再匹配总误差。 bool shapeStage = true; @@ -2288,8 +2304,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() return true; }; - int completedIterations = 0; - for(int phase = 0; phase < 2 && !m_shouldStop; ++phase) { + for(int phase = 0; phase < 2 && !m_shouldStop && completedIterations < m_maxIterations; ++phase) { shapeStage = phase == 0; phaseIterations = 0; maximumEvaluations = m_totalEvaluations + phaseEvaluationBudget; @@ -2309,20 +2324,18 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() rebuildRequested = true; rebuildReason = shapeStage ? QT_TR_NOOP("full sensitivity at shape LM entry") : QT_TR_NOOP("full sensitivity at total-stage entry"); - emit progressUpdated(0, current.fitness); + emit fittingStageChanged(shapeStage ? 2 : 3); + emit progressUpdated(completedIterations, current.fitness); emit logMessageGenerated(shapeStage - ? tr("[Stage 2: Joint LM - Shape]\nAdjust selected parameters; accept by shape error.") - : tr("[Stage 2: Joint LM - Total error]\nAdjust selected parameters; accept by total error.")); - DEBUG_OUT(tr("LM phase budget: %1 iterations, %2 evaluations.") - .arg(m_maxIterations).arg(phaseEvaluationBudget)); + ? tr("[Stage 2: Joint LM - Shape]") + : tr("[Stage 2: Joint LM - Total error]")); + DEBUG_OUT(tr("LM phase budget: %1 remaining shared iterations, %2 evaluations.") + .arg(m_maxIterations - completedIterations).arg(phaseEvaluationBudget)); writeTraceRow(completedIterations, -1, "stage_switch", current.parameters, current.fitness, true, 0, shapeStage ? "height_to_shape_lm" : "shape_to_total", ¤t.breakdown); - for(int iteration = completedIterations; - !m_shouldStop && phaseIterations < m_maxIterations && m_totalEvaluations < maximumEvaluations; - ++iteration) { - m_currentIteration = iteration; - completedIterations = iteration + 1; + while(!m_shouldStop && completedIterations < m_maxIterations && m_totalEvaluations < maximumEvaluations) { + m_currentIteration = completedIterations; if(!processPauseAndStop()) break; if(stageError(current) < m_targetError) { stopReason = LM_TARGET_ACHIEVED; @@ -2350,8 +2363,10 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() registerEffectiveImprovement(stageError(current)); } - // 两段分别计数;无可行方向也消耗一次尝试,避免无限缩步重建。 + // 全程累计迭代;无可行方向、拒绝和求解失败也占用一次额度。 ++phaseIterations; + ++completedIterations; + emit progressUpdated(completedIterations, current.fitness); const QVector objectiveResidual = shapeStage ? current.breakdown.shapeResiduals : current.breakdown.residualVector; const int rowOffset = shapeStage ? current.breakdown.residualVector.size() : 0; @@ -2386,7 +2401,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() // 当前阶段没有可行下降步时,收缩半径并进入原有重建/收敛处理。 if(selectedColumns.isEmpty()) { - emit logMessageGenerated(tr("Iteration %1 | No feasible descent step; keep the last accepted result.").arg(phaseIterations)); + emit logMessageGenerated(tr("Iteration %1 | No feasible descent step; keep the last accepted result.").arg(completedIterations)); if(trustRadius <= minimumTrustRadius * 1.01 && modelRebuiltAtMinimumRadius) { stopReason = LM_LOCAL_OPTIMUM; @@ -2433,7 +2448,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() } if(!candidate.valid) { - emitTrialLog(tr("Iteration %1").arg(phaseIterations), current.breakdown, candidate.breakdown, + emitTrialLog(tr("Iteration %1").arg(completedIterations), current.breakdown, candidate.breakdown, false, false, shapeStage); // 求解失败的候选不能改变 current。先完整恢复上一个已接受参数和 // 对应误差快照,再缩小信赖域;连续失败达到上限才终止整个拟合。 @@ -2549,9 +2564,9 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() : "rejected_" + selectionName, &candidate.breakdown); - emitTrialLog(tr("Iteration %1").arg(phaseIterations), oldBreakdown, candidate.breakdown, + emitTrialLog(tr("Iteration %1").arg(completedIterations), oldBreakdown, candidate.breakdown, true, accepted, shapeStage); - emit progressUpdated(phaseIterations, m_globalBestFitness); + emit progressUpdated(completedIterations, m_globalBestFitness); const bool effectiveImprovement = registerEffectiveImprovement(stageError(current)); if(!effectiveImprovement && recordIneffectiveStep()) stopReason = LM_LOCAL_OPTIMUM; @@ -2576,7 +2591,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() else totalIterations = phaseIterations; if(m_shouldStop || stopReason == LM_CONSECUTIVE_FAILURES || stopReason == LM_OPTIMIZATION_FAILED) break; - // 形状达标、确认停滞或用完额度后,保留当前参数进入总误差段。 + // 形状部分结束后仅在总迭代额度有剩余时进入总误差部分。 if(shapeStage) { emit logMessageGenerated(tr("Shape phase ended: %1").arg(getStopReasonDescription(stopReason))); writeTraceRow(m_currentIteration, -1, "lm_phase_end", current.parameters, current.fitness, @@ -2586,12 +2601,11 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() } } - if(completedIterations > 0) { - m_currentIteration = completedIterations - 1; - } + // 灵敏度建模或初始误差已达标时可能没有候选迭代,不多计一次。 + m_currentIteration = completedIterations - 1; restoreEvaluationState(current); - DEBUG_OUT(tr("Adaptive fitting counts: %1 shape LM iterations, %2 total LM iterations, %3 total evaluations.") - .arg(shapeIterations).arg(totalIterations).arg(m_totalEvaluations)); + DEBUG_OUT(tr("Adaptive fitting counts: %1 height iterations, %2 shape LM iterations, %3 total LM iterations, %4 total evaluations.") + .arg(heightTrials).arg(shapeIterations).arg(totalIterations).arg(m_totalEvaluations)); if(m_shouldStop) { return LM_USER_STOPPED; @@ -2599,6 +2613,9 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() if(!shapeStage && current.fitness < m_targetError) { return LM_TARGET_ACHIEVED; } + if(completedIterations >= m_maxIterations && stopReason != LM_CONSECUTIVE_FAILURES && stopReason != LM_OPTIMIZATION_FAILED) { + return LM_MAX_ITERATIONS; + } if(stopReason == LM_CONSECUTIVE_FAILURES || stopReason == LM_LOCAL_OPTIMUM || stopReason == LM_OPTIMIZATION_FAILED) { diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index 899254a4..3ffa3c27 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -15,6 +15,7 @@ #pragma comment(lib, "mGuiAnal.lib") #include +#include #include #include #include @@ -66,13 +67,14 @@ bool nmAutoFitReadPhysicalRange(const char* parameterName, } -// 设置某一行参数是否显示。隐藏时同步取消勾选并禁用,避免隐藏参数参与自动拟合。 +// 隐藏参数取消勾选并禁用;新显示的参数默认勾选,保留已显示参数的手工选择。 void nmWxAutomaticFitting::setParameterRowVisible(QTableWidget* table, int row, bool visible) { if(!table || row < 0 || row >= table->rowCount()) { return; } + const bool wasHidden = table->isRowHidden(row); table->setRowHidden(row, !visible); QCheckBox* checkBox = qobject_cast(table->cellWidget(row, 1)); @@ -80,6 +82,8 @@ void nmWxAutomaticFitting::setParameterRowVisible(QTableWidget* table, int row, checkBox->setEnabled(visible); if(!visible) { checkBox->setChecked(false); + } else if(wasHidden) { + checkBox->setChecked(true); } } } @@ -115,7 +119,8 @@ void nmWxAutomaticFitting::updateParameterVisibility(QTableWidget* table, NM_SOL return; } - for(int row = 0; row < table->rowCount(); ++row) { + // 裂缝参数直接按井型设置,避免先显示再隐藏造成选择状态重置。 + for(int row = 0; row < 4; ++row) { setParameterRowVisible(table, row, true); } @@ -426,7 +431,8 @@ nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent) setupUI(); setWindowTitle(tr("Automatic fitting")); setModal(true); - resize(800, 480); + // 初始尺寸随所在屏幕变化,最小尺寸交给实际控件布局决定。 + resize(sizeHint()); if(m_targetWellCombo->count() > 0) { onWellSelected(0); // 默认选中第一口井 @@ -438,6 +444,15 @@ nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent) DEBUG_UI("AutoFitting Constructor completed"); } +QSize nmWxAutomaticFitting::sizeHint() const +{ + // 沿用项目的可用屏幕尺寸计算方式,优先取父窗口所在屏幕,支持多显示器。 + QDesktopWidget* desktop = QApplication::desktop(); + if(desktop == nullptr) return iDlgBase::sizeHint(); + const QRect available = desktop->availableGeometry(parentWidget() ? parentWidget()->window() : this); + return QSize(available.width() * 45 / 100, available.height() * 55 / 100); +} + nmWxAutomaticFitting::~nmWxAutomaticFitting() { DEBUG_UI(QString("AutoFitting Destructor: this=0x%1").arg((quintptr)this, 0, 16)); @@ -461,35 +476,56 @@ nmWxAutomaticFitting::~nmWxAutomaticFitting() void nmWxAutomaticFitting::setupUI() { + // 用浅底色区分白色设置区和参数区,避免所有内容铺在同一平面。 + setObjectName("autoFittingSettings"); + setFont(QFont("Microsoft YaHei", 9)); + setStyleSheet( + "#autoFittingSettings { background: #f3f5f8; color: #273449; }" + "#autoFittingSettings QFrame#fittingControls, #autoFittingSettings QFrame#fittingParameters { background: #ffffff; border: 1px solid #dce2ea; border-radius: 7px; }" + "#autoFittingSettings QLabel { color: #667487; background: transparent; border: none; }" + "#autoFittingSettings QLabel#parameterHeading { color: #273449; font-weight: bold; }" + "#autoFittingSettings QLineEdit, #autoFittingSettings QComboBox { background: #ffffff; color: #273449; border: 1px solid #cfd7e2; border-radius: 4px; padding: 5px 8px; min-height: 20px; selection-background-color: #dceafb; selection-color: #273449; }" + "#autoFittingSettings QLineEdit:focus, #autoFittingSettings QComboBox:focus { border-color: #3978c6; }" + "#autoFittingSettings QComboBox::drop-down { border: none; width: 24px; }" + "#autoFittingSettings QComboBox QAbstractItemView { background: #ffffff; color: #273449; selection-background-color: #eaf1fb; selection-color: #273449; }" + "#autoFittingSettings QTableWidget { border: none; background: #ffffff; color: #273449; selection-background-color: #eaf1fb; selection-color: #273449; }" + "#autoFittingSettings QHeaderView::section { background: #f5f7fa; color: #667487; border: none; border-bottom: 1px solid #e5eaf1; padding: 7px; }" + "#autoFittingSettings QTableWidget::item { border-bottom: 1px solid #eef1f5; padding: 5px 8px; }" + "#autoFittingSettings QCheckBox { color: #273449; background: transparent; padding-left: 10px; spacing: 8px; }" + "#autoFittingSettings QPushButton { background: #ffffff; color: #46556b; border: 1px solid #cfd7e2; border-radius: 4px; padding: 6px 18px; min-width: 64px; min-height: 20px; }" + "#autoFittingSettings QPushButton:hover { background: #edf3fc; border-color: #9bb7db; }" + "#autoFittingSettings QPushButton:pressed { background: #dce8f8; }" + "#autoFittingSettings QPushButton#startFitting { background: #3978c6; color: #ffffff; border-color: #3978c6; }" + "#autoFittingSettings QPushButton#startFitting:hover { background: #306bb5; }" + "#autoFittingSettings QPushButton#startFitting:pressed { background: #285b9c; }"); m_mainLayout = new QVBoxLayout(this); - - // 创建主要内容的水平布局 - QHBoxLayout* contentLayout = new QHBoxLayout(); - - // 添加左侧间距 - contentLayout->addSpacing(20); - - setupControlPanel(); // 左侧控制面板 - contentLayout->addLayout(m_controlLayout); - - // 添加控制面板和表格之间的间距 - contentLayout->addSpacing(30); - - setupParameterTable(); // 右侧参数表格 - contentLayout->addWidget(m_parameterTable, 1); // 设置拉伸因子为1,使表格可以扩展 - - // 添加右侧间距 - contentLayout->addSpacing(20); - - m_mainLayout->addLayout(contentLayout); - setupButtons(); // 底部按钮 + m_mainLayout->setContentsMargins(16, 16, 16, 14); + m_mainLayout->setSpacing(12); + setupControlPanel(); + QFrame* controlsPanel = new QFrame(this); + controlsPanel->setObjectName("fittingControls"); + m_controlLayout->setContentsMargins(16, 14, 16, 14); + controlsPanel->setLayout(m_controlLayout); + m_mainLayout->addWidget(controlsPanel); + setupParameterTable(); + QFrame* tablePanel = new QFrame(this); + tablePanel->setObjectName("fittingParameters"); + QVBoxLayout* tableLayout = new QVBoxLayout(tablePanel); + tableLayout->setContentsMargins(12, 12, 12, 8); + tableLayout->setSpacing(10); + QLabel* tableHeading = new QLabel(tr("Fitting parameters"), tablePanel); + tableHeading->setObjectName("parameterHeading"); + tableLayout->addWidget(tableHeading); + tableLayout->addWidget(m_parameterTable, 1); + m_mainLayout->addWidget(tablePanel, 1); + setupButtons(); setLayout(m_mainLayout); } void nmWxAutomaticFitting::setupParameterTable() { - // 创建表格 + // 每次打开时默认勾选全部参数,再按井型隐藏不可用参数。 m_parameterTable = new QTableWidget(7, 6, this); // 设置表头 @@ -499,7 +535,10 @@ void nmWxAutomaticFitting::setupParameterTable() // 设置表格属性 m_parameterTable->setSelectionBehavior(QAbstractItemView::SelectItems); - m_parameterTable->setAlternatingRowColors(true); + m_parameterTable->setAlternatingRowColors(false); + m_parameterTable->setShowGrid(false); + m_parameterTable->verticalHeader()->setDefaultSectionSize(36); + m_parameterTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_parameterTable->verticalHeader()->setVisible(false); // 设置列宽 @@ -513,8 +552,11 @@ void nmWxAutomaticFitting::setupParameterTable() // 设置固定列的宽度 m_parameterTable->setColumnWidth(0, 40); // 序号列宽度 - m_parameterTable->setColumnWidth(1, 120); // 参数列宽度 + m_parameterTable->setColumnWidth(1, 180); // 参数名称与复选框保留足够空间 m_parameterTable->setColumnWidth(5, 80); // 单位列宽度 + // 只改变视觉顺序,保留数据读写使用的原列索引。 + m_parameterTable->setColumnHidden(0, true); + header->moveSection(header->visualIndex(3), header->visualIndex(2)); // 设置表格的大小策略 m_parameterTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); @@ -522,7 +564,7 @@ void nmWxAutomaticFitting::setupParameterTable() // 渗透率 (Permeability) m_parameterTable->setItem(0, 0, new QTableWidgetItem("1")); m_kCheckBox = new QCheckBox(tr("Permeability")); - m_kCheckBox->setChecked(automaticFittingData.getPermeabilitySelected()); + m_kCheckBox->setChecked(true); m_parameterTable->setCellWidget(0, 1, m_kCheckBox); m_parameterTable->setItem(0, 2, new QTableWidgetItem(QString::number(automaticFittingData.getPermeabilityMin().getValue().toDouble()))); m_parameterTable->setItem(0, 3, new QTableWidgetItem(QString::number(reservoirData.getPermeability().getValue().toDouble()))); @@ -532,7 +574,7 @@ void nmWxAutomaticFitting::setupParameterTable() // 表皮系数 (Skin) m_parameterTable->setItem(1, 0, new QTableWidgetItem("2")); m_sCheckBox = new QCheckBox(tr("Skin")); - m_sCheckBox->setChecked(automaticFittingData.getSkinSelected()); + m_sCheckBox->setChecked(true); m_parameterTable->setCellWidget(1, 1, m_sCheckBox); m_parameterTable->setItem(1, 2, new QTableWidgetItem(QString::number(automaticFittingData.getSkinMin().getValue().toDouble()))); m_parameterTable->setItem(1, 3, new QTableWidgetItem()); @@ -542,7 +584,7 @@ void nmWxAutomaticFitting::setupParameterTable() // 井筒储集系数 (Wellbore storage) m_parameterTable->setItem(2, 0, new QTableWidgetItem("3")); m_cCheckBox = new QCheckBox(tr("Wellbore storage")); - m_cCheckBox->setChecked(automaticFittingData.getWellboreStorageSelected()); + m_cCheckBox->setChecked(true); m_parameterTable->setCellWidget(2, 1, m_cCheckBox); m_parameterTable->setItem(2, 2, new QTableWidgetItem(QString::number(automaticFittingData.getWellboreStorageMin().getValue().toDouble()))); m_parameterTable->setItem(2, 3, new QTableWidgetItem()); @@ -552,7 +594,7 @@ void nmWxAutomaticFitting::setupParameterTable() // 孔隙度 (Porosity) m_parameterTable->setItem(3, 0, new QTableWidgetItem("4")); m_phiCheckBox = new QCheckBox(tr("Porosity")); - m_phiCheckBox->setChecked(automaticFittingData.getPorositySelected()); + m_phiCheckBox->setChecked(true); m_parameterTable->setCellWidget(3, 1, m_phiCheckBox); m_parameterTable->setItem(3, 2, new QTableWidgetItem(QString::number(automaticFittingData.getPorosityMin().getValue().toDouble()))); m_parameterTable->setItem(3, 3, new QTableWidgetItem(QString::number(reservoirData.getPorosity().getValue().toDouble()))); @@ -565,7 +607,7 @@ void nmWxAutomaticFitting::setupParameterTable() // 从当前目标井读取,其他裂缝几何参数保持固定,不进入自动拟合。 m_parameterTable->setItem(5, 0, new QTableWidgetItem("6")); m_dfcCheckBox = new QCheckBox(tr("Fracture conductivity")); - m_dfcCheckBox->setChecked(automaticFittingData.getFractureConductivitySelected()); + m_dfcCheckBox->setChecked(true); m_parameterTable->setCellWidget(5, 1, m_dfcCheckBox); m_parameterTable->setItem(5, 2, new QTableWidgetItem(QString::number(automaticFittingData.getFractureConductivityMin().getValue().toDouble()))); m_parameterTable->setItem(5, 3, new QTableWidgetItem()); @@ -575,7 +617,7 @@ void nmWxAutomaticFitting::setupParameterTable() // 裂缝半长。该行与 Dfc 一样只对压裂井显示,初值从当前目标井读取。 m_parameterTable->setItem(6, 0, new QTableWidgetItem("7")); m_fractureHalfLengthCheckBox = new QCheckBox(tr("Fracture half length")); - m_fractureHalfLengthCheckBox->setChecked(automaticFittingData.getFractureHalfLengthSelected()); + m_fractureHalfLengthCheckBox->setChecked(true); m_parameterTable->setCellWidget(6, 1, m_fractureHalfLengthCheckBox); m_parameterTable->setItem(6, 2, new QTableWidgetItem(QString::number(automaticFittingData.getFractureHalfLengthMin().getValue().toDouble()))); m_parameterTable->setItem(6, 3, new QTableWidgetItem()); @@ -622,11 +664,10 @@ void nmWxAutomaticFitting::setupParameterTable() void nmWxAutomaticFitting::setupControlPanel() { - m_controlLayout = new QVBoxLayout(); - m_controlLayout->setSizeConstraint(QLayout::SetFixedSize); - - // 上方弹簧 - m_controlLayout->addStretch(1); + // 四列设置:目标井、算法、目标误差、迭代上限。 + m_controlLayout = new QGridLayout(); + m_controlLayout->setHorizontalSpacing(16); + m_controlLayout->setVerticalSpacing(6); // 算法选择 QLabel* algorithmLabel = new QLabel(tr("Algorithm:")); @@ -634,30 +675,16 @@ void nmWxAutomaticFitting::setupControlPanel() m_algorithmCombo->addItem(tr("PSO (Particle Swarm)")); m_algorithmCombo->addItem(tr("LM")); m_algorithmCombo->setCurrentIndex(0); - m_algorithmCombo->setMaximumWidth(160); - m_algorithmCombo->setMinimumWidth(160); - - m_surrogateLabel = new QLabel(tr("PSO acceleration:")); - m_surrogateCombo = new QComboBox(); - m_surrogateCombo->addItem(tr("Off")); - m_surrogateCombo->addItem(tr("On")); - m_surrogateCombo->setCurrentIndex(automaticFittingData.getSurrogateScreeningEnabled() ? 1 : 0); - m_surrogateCombo->setMaximumWidth(160); - m_surrogateCombo->setMinimumWidth(160); + connect(m_algorithmCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onAlgorithmChanged(int))); - onAlgorithmChanged(m_algorithmCombo->currentIndex()); // 迭代次数 - QLabel* iterationLabel = new QLabel(tr("Number of iterations:")); + m_iterationLabel = new QLabel(); m_iterationEdit = new QLineEdit(QString::number(automaticFittingData.getIterationCount().getValue().toInt())); - m_iterationEdit->setMaximumWidth(100); - m_iterationEdit->setMinimumWidth(100); // 误差上限 - QLabel* errorLabel = new QLabel(tr("Error tolerance:")); + QLabel* errorLabel = new QLabel(tr("Target error:")); m_errorLimitEdit = new QLineEdit(QString::number(automaticFittingData.getErrorTolerance().getValue().toDouble())); - m_errorLimitEdit->setMaximumWidth(100); - m_errorLimitEdit->setMinimumWidth(100); // 目标井选择 QLabel* wellLabel = new QLabel(tr("Target Well:")); @@ -689,39 +716,26 @@ void nmWxAutomaticFitting::setupControlPanel() } connect(m_targetWellCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onWellSelected(int))); - m_targetWellCombo->setMaximumWidth(100); - m_targetWellCombo->setMinimumWidth(100); - - // 添加到垂直布局 - m_controlLayout->addWidget(algorithmLabel); - m_controlLayout->addWidget(m_algorithmCombo); - m_controlLayout->addSpacing(15); - - m_controlLayout->addWidget(m_surrogateLabel); - m_controlLayout->addWidget(m_surrogateCombo); - m_controlLayout->addSpacing(15); - - m_controlLayout->addWidget(iterationLabel); - m_controlLayout->addWidget(m_iterationEdit); - m_controlLayout->addSpacing(15); - - m_controlLayout->addWidget(errorLabel); - m_controlLayout->addWidget(m_errorLimitEdit); - m_controlLayout->addSpacing(15); - - m_controlLayout->addWidget(wellLabel); - m_controlLayout->addWidget(m_targetWellCombo); - - // 下方弹簧 - m_controlLayout->addStretch(1); + m_targetWellCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); + m_targetWellCombo->setMinimumContentsLength(10); + m_controlLayout->addWidget(wellLabel, 0, 0); + m_controlLayout->addWidget(m_targetWellCombo, 1, 0); + m_controlLayout->addWidget(algorithmLabel, 0, 1); + m_controlLayout->addWidget(m_algorithmCombo, 1, 1); + m_controlLayout->addWidget(errorLabel, 0, 2); + m_controlLayout->addWidget(m_errorLimitEdit, 1, 2); + m_controlLayout->addWidget(m_iterationLabel, 0, 3); + m_controlLayout->addWidget(m_iterationEdit, 1, 3); + for(int column = 0; column < 4; ++column) m_controlLayout->setColumnStretch(column, 1); + onAlgorithmChanged(m_algorithmCombo->currentIndex()); } void nmWxAutomaticFitting::onAlgorithmChanged(int index) { - // LM 不使用 PSO 加速;只切换可用状态,保留切回 PSO 时的原选择。 + // 两种算法统一显示最大迭代次数,提示仍说明 LM 共用次数的含义。 const bool usePSO = (index == 0); - m_surrogateLabel->setEnabled(usePSO); - m_surrogateCombo->setEnabled(usePSO); + m_iterationLabel->setText(tr("Maximum iterations:")); + m_iterationEdit->setToolTip(usePSO ? QString() : tr("Height, shape and total-error adjustments share this iteration limit; one iteration may call the solver multiple times.")); } void nmWxAutomaticFitting::setupButtons() @@ -729,7 +743,9 @@ void nmWxAutomaticFitting::setupButtons() QHBoxLayout* buttonLayout = new QHBoxLayout(); m_reverseBtn = new QPushButton(tr("Reverse Selection")); - m_okBtn = new QPushButton(tr("OK")); + m_okBtn = new QPushButton(tr("Start fitting")); + m_okBtn->setObjectName("startFitting"); + m_okBtn->setDefault(true); m_cancelBtn = new QPushButton(tr("Cancel")); // 设置按钮大小 @@ -737,10 +753,10 @@ void nmWxAutomaticFitting::setupButtons() m_okBtn->setMinimumWidth(80); m_cancelBtn->setMinimumWidth(80); - buttonLayout->addStretch(); buttonLayout->addWidget(m_reverseBtn); - buttonLayout->addWidget(m_okBtn); + buttonLayout->addStretch(); buttonLayout->addWidget(m_cancelBtn); + buttonLayout->addWidget(m_okBtn); m_mainLayout->addLayout(buttonLayout); @@ -989,7 +1005,8 @@ void nmWxAutomaticFitting::setAutomaticFittingValue() automaticFittingData.setPorositySelected(m_phiCheckBox->isChecked()); automaticFittingData.setFractureConductivitySelected(m_dfcCheckBox->isChecked()); automaticFittingData.setFractureHalfLengthSelected(m_fractureHalfLengthCheckBox->isChecked()); - automaticFittingData.setSurrogateScreeningEnabled(m_surrogateCombo && m_surrogateCombo->currentIndex() == 1); + // 界面不再提供 PSO 加速选项,同时关闭旧配置中可能保留的加速开关。 + automaticFittingData.setSurrogateScreeningEnabled(false); // 保存渗透率的最小值和最大值 automaticFittingData.getPermeabilityMin().setValue(m_parameterTable->item(0, 2)->text().toDouble()); @@ -1163,7 +1180,15 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ double targetError = m_errorLimitEdit->text().toDouble(); QString wellName = m_targetWellCombo->currentText(); m_progressMonitor->setFittingParameters(maxIterations, targetError, wellName); - m_progressMonitor->setSelectedParameters(selectedParams); + // 单位和勾选参数保持同一顺序,供横排参数区显示。 + QStringList selectedUnits; + if(m_kCheckBox->isChecked()) selectedUnits << tr("mD"); + if(m_sCheckBox->isChecked()) selectedUnits << QString(); + if(m_cCheckBox->isChecked()) selectedUnits << tr("m^3/MPa"); + if(m_phiCheckBox->isChecked()) selectedUnits << QString(); + if(m_dfcCheckBox->isChecked()) selectedUnits << tr("md.m"); + if(m_fractureHalfLengthCheckBox->isChecked()) selectedUnits << tr("m"); + m_progressMonitor->setSelectedParameters(selectedParams, selectedUnits); m_progressMonitor->show(); QTimer::singleShot(100, this, SLOT(runAutoFitting())); diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp index de0d367c..f220d974 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp @@ -1,9 +1,11 @@ #include "nmWxAutomaticfittingStart.h" #include +#include #include #include #include #include +#include #include #include #include @@ -33,7 +35,8 @@ nmAutomaticFittingCurveChart::nmAutomaticFittingCurveChart(QWidget* parent) , m_bestFitness(0.0) , m_pseudoPressureMode(false) { - setMinimumHeight(220); + // 保留绘图空间,同时允许小屏幕压缩曲线区域的高度。 + setMinimumHeight(160); setMinimumWidth(420); } @@ -181,7 +184,8 @@ void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event) QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - painter.fillRect(rect(), QColor(240, 240, 240)); + // 保持全宽白色绘图区,曲线和实测标记沿用原有红绿配色。 + painter.fillRect(rect(), Qt::white); const int marginLeft = 62; const int marginTop = 18; @@ -191,7 +195,7 @@ void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event) qMax(10, width() - marginLeft - marginRight), qMax(10, height() - marginTop - marginBottom)); - painter.fillRect(m_chartRect, QColor(252, 252, 252)); + painter.fillRect(m_chartRect, Qt::white); drawGrid(painter); drawAxes(painter); drawCurveLine(painter, m_bestPressure, QColor(250, 74, 74)); @@ -210,7 +214,7 @@ void nmAutomaticFittingCurveChart::drawGrid(QPainter& painter) int yStart = static_cast(qFloor(m_logRange.top())); int yEnd = static_cast(qCeil(m_logRange.bottom())); - painter.setPen(QPen(QColor(236, 236, 236), 1, Qt::SolidLine)); + painter.setPen(QPen(QColor(247, 248, 250), 1, Qt::SolidLine)); for(int decade = xStart; decade <= xEnd; ++decade) { for(int multiplier = 2; multiplier <= 9; ++multiplier) { double value = decade + qLn(static_cast(multiplier)) / qLn(10.0); @@ -237,7 +241,7 @@ void nmAutomaticFittingCurveChart::drawGrid(QPainter& painter) } } - painter.setPen(QPen(QColor(214, 214, 214), 1, Qt::SolidLine)); + painter.setPen(QPen(QColor(228, 232, 238), 1, Qt::SolidLine)); for(int x = xStart; x <= xEnd; ++x) { if(x < m_logRange.left() || x > m_logRange.right()) { continue; @@ -397,167 +401,237 @@ nmWxAutomaticfittingStart::nmWxAutomaticfittingStart(QWidget *parent) // 设置窗口属性 setWindowTitle(tr("Auto Fitting Progress")); - setMinimumSize(1000, 650); + // 初始尺寸随所在屏幕变化,最小尺寸交给实际控件布局决定。 + resize(sizeHint()); setModal(false); } +QSize nmWxAutomaticfittingStart::sizeHint() const +{ + // 加宽进程窗口,为曲线和完整日志行留出空间,同时保留屏幕边缘。 + QDesktopWidget* desktop = QApplication::desktop(); + if(desktop == nullptr) return iDlgBase::sizeHint(); + const QRect available = desktop->availableGeometry(parentWidget() ? parentWidget()->window() : this); + return QSize(available.width() * 75 / 100, available.height() * 65 / 100); +} + nmWxAutomaticfittingStart::~nmWxAutomaticfittingStart() { } void nmWxAutomaticfittingStart::setupUI() { - // 创建主要布局 - mainLayout = new QHBoxLayout(); - leftLayout = new QVBoxLayout(); - rightLayout = new QVBoxLayout(); - - // 设置左右布局 + // 曲线和日志左右并排,参数横排占满下方整行,底部使用一行汇总。 + setObjectName("autoFittingProgress"); + setFont(QFont("Microsoft YaHei", 9)); + setStyleSheet( + "#autoFittingProgress { background: #ffffff; color: #233247; }" + "#autoFittingProgress QGroupBox { background: #ffffff; color: #233247; border: none; margin-top: 0; padding-top: 0; font-weight: bold; }" + "#autoFittingProgress QLabel { color: #66768a; background: transparent; font-weight: normal; }" + "#autoFittingProgress QGroupBox#fittingSummary { padding-top: 0; }" + "#autoFittingProgress QGroupBox#fittingSummary QLabel { font-size: 14px; color: #46556b; }" + "#autoFittingProgress QWidget#parameterStrip { background: #ffffff; border-top: 1px solid #e3e8ef; }" + "#autoFittingProgress QWidget#parameterSeparator { background: #e3e8ef; }" + "#autoFittingProgress QLabel#parameterName { font-size: 12px; }" + "#autoFittingProgress QLabel#parameterValue { color: #233247; font-size: 18px; font-weight: 500; }" + "#autoFittingProgress QGroupBox#fittingSummary QLabel#summaryValue { color: #233247; }" + "#autoFittingProgress QGroupBox#fittingSummary QLabel#stateBadge { background: #edf3fc; color: #366baf; border-radius: 5px; padding: 4px 10px; font-size: 12px; }" + "#autoFittingProgress QGroupBox#fittingLog { padding-top: 0; }" + "#autoFittingProgress QLabel#sectionHeading { color: #233247; font-size: 13px; font-weight: bold; }" + "#autoFittingProgress QWidget#fittingFooter { background: #ffffff; border-top: 1px solid #e3e8ef; }" + "#autoFittingProgress QLabel#resultSummary { color: #66768a; font-size: 14px; }" + "#autoFittingProgress QLabel#stageStatus { color: #66768a; font-size: 12px; }" + "#autoFittingProgress QLabel#runSummary { color: #46556b; font-size: 14px; }" + "#autoFittingProgress QTextEdit { border: none; background: #ffffff; color: #233247; font-size: 13px; font-weight: normal; selection-background-color: #eaf1fb; selection-color: #233247; }" + "#autoFittingProgress QPushButton { background: #ffffff; color: #46556b; border: 1px solid #cfd7e2; border-radius: 5px; padding: 8px 22px; min-width: 64px; min-height: 20px; }" + "#autoFittingProgress QPushButton:hover { background: #edf3fc; border-color: #9bb7db; }" + "#autoFittingProgress QPushButton:pressed { background: #dce8f8; }" + "#autoFittingProgress QPushButton:disabled { background: #f0f2f5; color: #98a2b1; border-color: #e0e5ec; }" + "#autoFittingProgress QPushButton#logToggle { border: none; background: transparent; color: #738091; min-width: 0; padding: 3px 8px; }" + "#autoFittingProgress QPushButton#logToggle:hover { color: #396fc0; }" + "#autoFittingProgress QSplitter::handle { background: #ffffff; }"); + + // 控件复用原有数据更新逻辑,仅重新组织所属区域。 setupParameterArea(); - setupTableArea(); + setupParameterStrip(); setupChartArea(); - setupLogArea(); setupControlArea(); - - // 左侧布局 - leftLayout->addWidget(parameterGroup); - leftLayout->addStretch(); - leftLayout->addWidget(tableGroup); - - // 右侧布局 - rightSplitter = new QSplitter(Qt::Vertical, this); + setupLogArea(); + // 日志只占右侧较窄区域,两侧底边对齐,分隔条允许调整宽度。 + rightSplitter = new QSplitter(Qt::Horizontal, this); + rightSplitter->setHandleWidth(4); rightSplitter->addWidget(chartGroup); rightSplitter->addWidget(logGroup); - rightSplitter->setStretchFactor(0, 3); - rightSplitter->setStretchFactor(1, 2); - rightSplitter->setSizes(QList() << 320 << 260); - rightLayout->addWidget(rightSplitter); - - // 主布局 - mainLayout->addLayout(leftLayout, 1); - mainLayout->addLayout(rightLayout, 2); + rightSplitter->setStretchFactor(0, 1); + rightSplitter->setStretchFactor(1, 1); + rightSplitter->setChildrenCollapsible(false); + rightSplitter->setSizes(QList() << 650 << 350); // 控制区域布局 - QHBoxLayout *controlLayout = new QHBoxLayout(); - controlLayout->addWidget(progressBar); + QWidget* footer = new QWidget(this); + footer->setObjectName("fittingFooter"); + footer->setAttribute(Qt::WA_StyledBackground, true); + QHBoxLayout *controlLayout = new QHBoxLayout(footer); + controlLayout->setContentsMargins(22, 14, 22, 14); + controlLayout->setSpacing(24); + controlLayout->addWidget(resultSummary); + controlLayout->addWidget(runSummary); + controlLayout->addStretch(); + controlLayout->addWidget(progressBar, 1); controlLayout->addWidget(stopButton); QVBoxLayout *overallLayout = new QVBoxLayout(); - overallLayout->addLayout(mainLayout); - overallLayout->addLayout(controlLayout); + overallLayout->setContentsMargins(0, 0, 0, 0); + overallLayout->setSpacing(0); + overallLayout->addWidget(parameterGroup); + overallLayout->addWidget(rightSplitter, 1); + overallLayout->addWidget(parameterStrip); + overallLayout->addWidget(footer); setLayout(overallLayout); } void nmWxAutomaticfittingStart::setupParameterArea() { - parameterGroup = new QGroupBox(tr("Parameter Display"), this); - parameterGroup->setFixedWidth(280); + parameterGroup = new QGroupBox(this); + parameterGroup->setObjectName("fittingSummary"); QGridLayout *paramLayout = new QGridLayout(parameterGroup); + paramLayout->setContentsMargins(22, 19, 22, 17); + paramLayout->setHorizontalSpacing(12); + paramLayout->setVerticalSpacing(8); // 创建标签 algorithmTypeLabel = new QLabel(tr("Algorithm:")); - currentIterationLabel = new QLabel(tr("Current Iteration:")); - maxIterationLabel = new QLabel(tr("Max Iteration:")); + currentIterationLabel = new QLabel(tr("Iterations:")); currentComfortLabel = new QLabel(tr("Current Error:")); bestComfortLabel = new QLabel(tr("Best Error:")); - targetPrecisionLabel = new QLabel(tr("Target Precision:")); + targetPrecisionLabel = new QLabel(tr("Target error:")); targetWellLabel = new QLabel(tr("Target Well:")); // 创建数值显示标签 algorithmTypeValue = new QLabel("PSO"); - currentIterationValue = new QLabel("0"); - maxIterationValue = new QLabel("100"); + stateBadge = new QLabel(tr("Waiting for fitting to start")); + stateBadge->setObjectName("stateBadge"); + currentIterationValue = new QLabel("0 / 100"); + currentIterationValue->setObjectName("summaryValue"); currentComfortValue = new QLabel("N/A"); bestComfortValue = new QLabel("N/A"); targetPrecisionValue = new QLabel("0.001"); targetWellValue = new QLabel("N/A"); - - // 添加到布局 - paramLayout->addWidget(algorithmTypeLabel, 0, 0); - paramLayout->addWidget(algorithmTypeValue, 0, 1); - paramLayout->addWidget(currentIterationLabel, 1, 0); - paramLayout->addWidget(currentIterationValue, 1, 1); - paramLayout->addWidget(maxIterationLabel, 2, 0); - paramLayout->addWidget(maxIterationValue, 2, 1); - paramLayout->addWidget(currentComfortLabel, 3, 0); - paramLayout->addWidget(currentComfortValue, 3, 1); - paramLayout->addWidget(bestComfortLabel, 4, 0); - paramLayout->addWidget(bestComfortValue, 4, 1); - paramLayout->addWidget(targetPrecisionLabel, 5, 0); - paramLayout->addWidget(targetPrecisionValue, 5, 1); - paramLayout->addWidget(targetWellLabel, 6, 0); - paramLayout->addWidget(targetWellValue, 6, 1); + algorithmTypeValue->setObjectName("summaryValue"); + targetWellValue->setObjectName("summaryValue"); + targetPrecisionValue->setObjectName("summaryValue"); + + // 顶部统一显示累计迭代数和上限;PSO 的额外误差信息放在第二行。 + paramLayout->addWidget(targetWellLabel, 0, 0); + paramLayout->addWidget(targetWellValue, 0, 1); + paramLayout->addWidget(algorithmTypeLabel, 0, 2); + paramLayout->addWidget(algorithmTypeValue, 0, 3); + paramLayout->addWidget(targetPrecisionLabel, 0, 4); + paramLayout->addWidget(targetPrecisionValue, 0, 5); + paramLayout->addWidget(currentIterationLabel, 0, 6); + paramLayout->addWidget(currentIterationValue, 0, 7); + paramLayout->addWidget(stateBadge, 0, 9, Qt::AlignRight | Qt::AlignVCenter); + paramLayout->addWidget(currentComfortLabel, 1, 0); + paramLayout->addWidget(currentComfortValue, 1, 1); + paramLayout->addWidget(bestComfortLabel, 1, 2); + paramLayout->addWidget(bestComfortValue, 1, 3); + paramLayout->setColumnStretch(8, 1); + targetWellValue->setWordWrap(true); parameterGroup->setLayout(paramLayout); } -void nmWxAutomaticfittingStart::setupTableArea() +void nmWxAutomaticfittingStart::setupParameterStrip() { - tableGroup = new QGroupBox(tr("Parameter Table"), this); - tableGroup->setFixedWidth(300); - - QVBoxLayout *tableLayout = new QVBoxLayout(tableGroup); - - tableWidget = new QTableWidget(0, 2, this); - tableWidget->setHorizontalHeaderLabels(QStringList() << tr("Parameter Name") << tr("Optimal Value")); - - // 设置表格属性 - tableWidget->setAlternatingRowColors(true); - tableWidget->setSelectionBehavior(QAbstractItemView::SelectRows); - tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers); - tableWidget->verticalHeader()->setVisible(false); - tableWidget->setMinimumHeight(310); - - // 设置列宽 - QHeaderView* header = tableWidget->horizontalHeader(); - header->setResizeMode(0, QHeaderView::ResizeToContents); - header->setResizeMode(1, QHeaderView::Stretch); - - // 设置表格填充整个区域 - tableWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - // 隐藏滚动条 - tableWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - - tableLayout->addWidget(tableWidget); - tableGroup->setLayout(tableLayout); + // 参数独占曲线和日志下方的整行,横向均分空间,不保留尾部空白。 + parameterStrip = new QWidget(this); + parameterStrip->setObjectName("parameterStrip"); + parameterStrip->setAttribute(Qt::WA_StyledBackground, true); + parameterStrip->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + // 标题与参数共用网格,竖线跨越两行,上下留白均为 4 像素。 + parameterLayout = new QGridLayout(parameterStrip); + parameterLayout->setContentsMargins(22, 4, 22, 4); + parameterLayout->setHorizontalSpacing(0); + parameterLayout->setVerticalSpacing(2); } void nmWxAutomaticfittingStart::setupChartArea() { - chartGroup = new QGroupBox(tr("Fitting Curve"), this); + // 曲线上方仅保留标题。 + chartGroup = new QGroupBox(this); QVBoxLayout* chartLayout = new QVBoxLayout(chartGroup); + QLabel* heading = new QLabel(tr("Fitting Curve"), chartGroup); + heading->setObjectName("sectionHeading"); + chartLayout->addWidget(heading); curveChart = new nmAutomaticFittingCurveChart(chartGroup); curveChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - chartLayout->setMargin(6); + chartLayout->setContentsMargins(22, 17, 22, 12); chartLayout->addWidget(curveChart); chartGroup->setLayout(chartLayout); - chartGroup->setMinimumHeight(240); + chartGroup->setMinimumHeight(200); } void nmWxAutomaticfittingStart::setupLogArea() { - logGroup = new QGroupBox(tr("Log Window"), this); + // 日志标题始终可见,正文收起时仍继续接收日志。 + logGroup = new QGroupBox(this); + logGroup->setObjectName("fittingLog"); QVBoxLayout *logLayout = new QVBoxLayout(logGroup); + logLayout->setContentsMargins(18, 13, 22, 16); + logLayout->setSpacing(8); + logLayout->setAlignment(Qt::AlignTop); + QHBoxLayout* logHeader = new QHBoxLayout(); + QLabel* heading = new QLabel(tr("Fitting process"), logGroup); + heading->setObjectName("sectionHeading"); + logToggle = new QPushButton(tr("Collapse"), logGroup); + logToggle->setObjectName("logToggle"); + logToggle->setAutoDefault(false); + logHeader->addWidget(heading); + logHeader->addStretch(); + logHeader->addWidget(logToggle); + logLayout->addLayout(logHeader); + // 阶段状态放到标题下方,避免挤占窄日志区的标题和按钮。 + logLayout->addWidget(stageStatus); + connect(logToggle, SIGNAL(clicked()), this, SLOT(onToggleLog())); logTextEdit = new QTextEdit(this); logTextEdit->setReadOnly(true); - logTextEdit->setFont(QFont("Consolas", 9)); + // 小屏幕允许缩短可见日志区域,完整内容仍可通过滚动查看。 + logTextEdit->setMinimumHeight(48); + logTextEdit->setMinimumWidth(180); + logTextEdit->setFont(font()); + // 正文与标题左对齐,沿用窗口白底,不额外绘制日志底色。 + logTextEdit->document()->setDocumentMargin(0); // 添加初始日志 addLogMessage(tr("System initialization completed")); addLogMessage(tr("Waiting for fitting to start")); - logLayout->addWidget(logTextEdit); + logLayout->addWidget(logTextEdit, 1); logGroup->setLayout(logLayout); } void nmWxAutomaticfittingStart::setupControlArea() { + // 运行状态放在拟合过程标题下方;底部汇总保持单行。 + stageStatus = new QLabel(tr("Waiting for fitting to start"), this); + stageStatus->setObjectName("stageStatus"); + stageStatus->setWordWrap(true); + stageStatus->hide(); + resultSummary = new QLabel(this); + resultSummary->setObjectName("resultSummary"); + resultSummary->setTextFormat(Qt::RichText); + resultSummary->setWordWrap(false); + resultSummary->hide(); + runSummary = new QLabel(this); + runSummary->setObjectName("runSummary"); + runSummary->setWordWrap(false); + runSummary->hide(); // 进度条 progressBar = new QProgressBar(this); progressBar->setRange(0, 100); @@ -571,7 +645,6 @@ void nmWxAutomaticfittingStart::setupControlArea() // 停止按钮 stopButton = new QPushButton(tr("Stop"), this); - stopButton->setFixedSize(80, 30); stopButton->setEnabled(false); // 连接信号槽 @@ -583,10 +656,10 @@ void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitPSO* autoFitte m_autoFitterPSO = autoFitter; m_autoFitterLM = nullptr; m_algorithmName = "PSO"; + updateAlgorithmDisplay(); // 更新算法类型显示 algorithmTypeValue->setText("PSO"); - algorithmTypeValue->setStyleSheet("QLabel { color: blue; font-weight: bold; }"); if (m_autoFitterPSO) { connect(m_autoFitterPSO, SIGNAL(progressUpdated(int, double)), @@ -612,13 +685,15 @@ void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitLM* autoFitter m_autoFitterPSO = nullptr; m_autoFitterLM = autoFitter; m_algorithmName = "LM"; + updateAlgorithmDisplay(); // LM 日志由优化器统一组织,清除通用初始化提示。 logTextEdit->clear(); algorithmTypeValue->setText(m_algorithmName); - algorithmTypeValue->setStyleSheet("QLabel { color: blue; font-weight: bold; }"); if (m_autoFitterLM) { + connect(m_autoFitterLM, SIGNAL(fittingStageChanged(int)), + this, SLOT(onLMStageChanged(int))); connect(m_autoFitterLM, SIGNAL(progressUpdated(int, double)), this, SLOT(onFittingProgress(int, double))); connect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)), @@ -634,14 +709,48 @@ void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitLM* autoFitter } } +void nmWxAutomaticfittingStart::updateAlgorithmDisplay() +{ + // 迭代数始终在顶部显示,PSO 保留原有额外误差和进度条。 + const bool lm = m_autoFitterLM != nullptr; + QLabel* psoLabels[] = { currentComfortLabel, currentComfortValue, + bestComfortLabel, bestComfortValue }; + for(int i = 0; i < 4; ++i) psoLabels[i]->setVisible(!lm); + progressBar->setVisible(!lm); + stageStatus->setVisible(lm); + resultSummary->setVisible(lm); + if(lm) updateCurrentError(m_autoFitterLM->getBestFitness()); + if(lm) onLMStageChanged(0); +} + +void nmWxAutomaticfittingStart::onLMStageChanged(int stage) +{ + // 仅更新拟合过程区域的运行状态,不在曲线上方增加阶段栏。 + const QString stages[] = { tr("Calculating initial error..."), tr("Height alignment"), + tr("Shape adjustment"), tr("Total-error adjustment") }; + stageStatus->setText(stages[stage]); +} + +void nmWxAutomaticfittingStart::onToggleLog() +{ + // 收起正文时把横向空间让给曲线,保留标题和按钮以便重新展开。 + const bool collapse = !logTextEdit->isHidden(); + if(collapse) expandedLogSizes = rightSplitter->sizes(); + logTextEdit->setVisible(!collapse); + logToggle->setText(collapse ? tr("Expand") : tr("Collapse")); + logGroup->layout()->activate(); + logGroup->setMaximumWidth(collapse ? logGroup->minimumSizeHint().width() : QWIDGETSIZE_MAX); + if(!collapse && !expandedLogSizes.isEmpty()) rightSplitter->setSizes(expandedLogSizes); +} + void nmWxAutomaticfittingStart::setFittingParameters(int maxIterations, double targetError, const QString& wellName) { m_maxIterations = maxIterations; m_targetError = targetError; m_wellName = wellName; - maxIterationValue->setText(QString::number(maxIterations)); - targetPrecisionValue->setText(formatScientific(targetError)); + currentIterationValue->setText(QString("0 / %1").arg(maxIterations)); + targetPrecisionValue->setText(QString::number(targetError, 'g', 6)); targetWellValue->setText(wellName); progressBar->setRange(0, maxIterations); @@ -654,6 +763,7 @@ void nmWxAutomaticfittingStart::setFittingParameters(int maxIterations, double t void nmWxAutomaticfittingStart::markFittingStarted() { m_startTime = QDateTime::currentDateTime(); + stateBadge->setText(tr("Fitting in progress")); const QString algorithmName = m_algorithmName; QString timestamp = m_startTime.toString("yyyy-MM-dd hh:mm:ss"); @@ -663,12 +773,56 @@ void nmWxAutomaticfittingStart::markFittingStarted() .arg(timestamp)); } -void nmWxAutomaticfittingStart::setSelectedParameters(const QStringList& parameterNames) +void nmWxAutomaticfittingStart::setSelectedParameters(const QStringList& parameterNames, + const QStringList& parameterUnits) { - m_selectedParameters = parameterNames; + // 仅在选择变化时重建标签,拟合期间只更新数值,避免布局反复抖动。 + QLayoutItem* item = nullptr; + while((item = parameterLayout->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + parameterValues.clear(); + for(int column = 0; column < parameterLayout->columnCount(); ++column) { + parameterLayout->setColumnStretch(column, 0); + } + QLabel* heading = new QLabel(tr("Current parameters"), parameterStrip); + heading->setObjectName("sectionHeading"); + parameterLayout->addWidget(heading, 0, 0); + for(int i = 0; i < parameterNames.size(); ++i) { + // 名称和单位在上、数值在下,相邻参数用细竖线分隔。 + QWidget* cell = new QWidget(parameterStrip); + cell->setAttribute(Qt::WA_StyledBackground, true); + QVBoxLayout* cellLayout = new QVBoxLayout(cell); + cellLayout->setContentsMargins(12, 0, 12, 0); + cellLayout->setSpacing(2); + const QString unit = parameterUnits.value(i); + QLabel* name = new QLabel(parameterNames[i] + (unit.isEmpty() ? QString() : " / " + unit), cell); + name->setObjectName("parameterName"); + name->setWordWrap(true); + name->setAlignment(Qt::AlignCenter); + QLabel* value = new QLabel("--", cell); + value->setObjectName("parameterValue"); + value->setAlignment(Qt::AlignCenter); + value->setTextInteractionFlags(Qt::TextSelectableByMouse); + cellLayout->addWidget(name, 1); + cellLayout->addWidget(value); + parameterLayout->addWidget(cell, 1, i * 2); + parameterLayout->setColumnStretch(i * 2, 1); + if(i > 0) { + // 分隔线独立于参数单元格,避免标题导致上下间距不一致。 + QWidget* separator = new QWidget(parameterStrip); + separator->setObjectName("parameterSeparator"); + separator->setAttribute(Qt::WA_StyledBackground, true); + separator->setFixedWidth(1); + parameterLayout->addWidget(separator, 0, i * 2 - 1, 2, 1); + } + parameterValues.append(value); + } + parameterStrip->setVisible(!parameterNames.isEmpty()); - // 立即更新参数表格 - updateParameterTable(); + // 立即显示当前已接受的参数值。 + updateParameterValues(); const QString algorithmName = m_algorithmName; if(!m_autoFitterLM) addLogMessage(tr("%1 selected parameters: %2").arg(algorithmName).arg(parameterNames.join(", "))); @@ -690,17 +844,17 @@ void nmWxAutomaticfittingStart::setPseudoPressureMode(bool enabled) void nmWxAutomaticfittingStart::onFittingProgress(int iteration, double fitness) { - // LM 预调整没有固定总步数,单独显示阶段;整体阶段从局部迭代 0 开始计进度。 - if (m_autoFitterLM && iteration < 0) { - progressBar->setValue(0); - progressBar->setFormat(tr("Pre-adjustment")); - currentIterationValue->setText(tr("Pre-adjustment")); + // LM 各阶段显示累计迭代次数;达到上限之前仍可能提前收敛,不显示完成百分比。 + currentIterationValue->setText(QString("%1 / %2").arg(qMax(0, iteration)).arg(m_maxIterations)); + if(m_autoFitterLM) { + updateCurrentError(fitness); + return; } else { - int displayIteration = qMax(m_autoFitterLM ? 0 : 1, qMin(iteration, m_maxIterations)); + int displayIteration = qMax(1, qMin(iteration, m_maxIterations)); progressBar->setValue(displayIteration); double progress = (double)displayIteration / m_maxIterations * 100; progressBar->setFormat(QString("%1%").arg(progress, 0, 'f', 1)); - currentIterationValue->setText(QString::number(displayIteration)); + currentIterationValue->setText(QString("%1 / %2").arg(displayIteration).arg(m_maxIterations)); } currentComfortValue->setText(formatScientific(fitness)); @@ -713,8 +867,8 @@ void nmWxAutomaticfittingStart::onFittingProgress(int iteration, double fitness) if(!m_autoFitterLM) addLogMessage(tr("Better solution found: %1").arg(formatScientific(fitness))); - // 更新参数表格 - updateParameterTable(); + // 更新参数值。 + updateParameterValues(); } else { bestComfortValue->setStyleSheet("QLabel { color: black; }"); } @@ -724,11 +878,20 @@ void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& m { m_isFinished = true; m_isFinalizing = false; + stateBadge->setText(success ? tr("Fitting ended") : tr("Fitting failed")); + stateBadge->setStyleSheet(success + ? "QLabel { color: #28765f; background: #edf7f2; }" + : "QLabel { color: #a04438; background: #fff0ed; }"); const QString algorithmName = m_algorithmName; // 更新状态 - if (success) { + if(m_autoFitterLM) { + stageStatus->setText(success ? tr("Fitting ended") : tr("Fitting failed")); + // 完成区直接读取优化器数值,失败评价显示占位符,不暴露惩罚值。 + updateCurrentError(m_autoFitterLM->getBestFitness()); + stageStatus->hide(); + } else if (success) { progressBar->setValue(m_maxIterations); // 区分正常完成和用户停止 @@ -781,8 +944,8 @@ void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& m disconnect(stopButton, SIGNAL(clicked()), this, SLOT(onStopButtonClicked())); connect(stopButton, SIGNAL(clicked()), this, SLOT(close())); - // 最终更新参数表格 - updateParameterTable(); + // 同步最终参数值。 + updateParameterValues(); // 添加完成时间戳 QDateTime endTime = QDateTime::currentDateTime(); @@ -803,7 +966,13 @@ void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& m // LM 的调用次数由优化器汇总,这里只在结束时显示一次总耗时。 if(m_autoFitterLM) { - addLogMessage(tr("Total time: %1 min %2 s").arg(minutes).arg(remainSec, 0, 'f', 1)); + // 不足一分钟只显示秒,窗口汇总和完成日志共用同一耗时文本。 + const QString elapsed = minutes == 0 ? tr("%1 s").arg(seconds, 0, 'f', 1) + : tr("%1 min %2 s").arg(minutes).arg(remainSec, 0, 'f', 1); + addLogMessage(tr("Total time: %1").arg(elapsed)); + runSummary->setText(tr("Solver calls: %1 | Total time: %2") + .arg(m_autoFitterLM->getSolverCallCount()).arg(elapsed)); + runSummary->show(); } else { addLogMessage(tr("Total fitting time: %1 s (%2 min %3 s), total solver calls: %4") .arg(seconds, 0, 'f', 2).arg(minutes).arg(remainSec, 0, 'f', 2).arg(totalEvaluations)); @@ -816,6 +985,8 @@ void nmWxAutomaticfittingStart::onFinalizingStarted() { // 最终完整求解负责生成正式快照,此阶段不能再次停止或关闭进度窗口。 m_isFinalizing = true; + stateBadge->setText(tr("Finalizing...")); + if(m_autoFitterLM) stageStatus->setText(tr("Generating final result...")); stopButton->setEnabled(false); stopButton->setText(tr("Finalizing...")); addLogMessage(tr("Generating the final result with the current best parameters...")); @@ -837,6 +1008,8 @@ void nmWxAutomaticfittingStart::onStopButtonClicked() // 先锁定按钮,等优化循环退出后由 finalizingStarted() 切换到最终求解状态。 stopButton->setEnabled(false); stopButton->setText(tr("Stopping...")); + stateBadge->setText(tr("Stopping...")); + if(m_autoFitterLM) stageStatus->setText(tr("Stopping...")); if (m_autoFitterPSO) { m_autoFitterPSO->stopFitting(); } else if (m_autoFitterLM) { @@ -847,40 +1020,27 @@ void nmWxAutomaticfittingStart::onStopButtonClicked() } } -void nmWxAutomaticfittingStart::updateParameterTable() +void nmWxAutomaticfittingStart::updateParameterValues() { - // 设置表格行数 - tableWidget->setRowCount(m_selectedParameters.size()); - - // 更新表格内容 - for (int i = 0; i < m_selectedParameters.size(); ++i) { - // 参数名 - QTableWidgetItem* nameItem = new QTableWidgetItem(m_selectedParameters.at(i)); - nameItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - tableWidget->setItem(i, 0, nameItem); - - // 最优参数值 - QString valueText = "N/A"; - - if (m_autoFitterPSO) { - QVector bestSolution = m_autoFitterPSO->getBestSolution(); - if (i < bestSolution.size()) { - valueText = formatScientific(bestSolution[i]); - } - } else if (m_autoFitterLM) { - QVector bestSolution = m_autoFitterLM->getBestSolution(); - if (i < bestSolution.size()) { - valueText = formatScientific(bestSolution[i]); - } - } - - QTableWidgetItem* valueItem = new QTableWidgetItem(valueText); - valueItem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - tableWidget->setItem(i, 1, valueItem); + // 使用当前已接受参数,普通数值优先显示十进制,极小数仍保留有效位。 + const QVector values = m_autoFitterLM ? m_autoFitterLM->getBestSolution() + : (m_autoFitterPSO ? m_autoFitterPSO->getBestSolution() : QVector()); + for(int i = 0; i < parameterValues.size(); ++i) { + parameterValues[i]->setText(i < values.size() ? QString::number(values[i], 'g', 6) : "--"); + parameterValues[i]->setToolTip(i < values.size() ? QString::number(values[i], 'g', 15) : QString()); } } +void nmWxAutomaticfittingStart::updateCurrentError(double fitness) +{ + // 仅展示当前已接受参数对应的总误差,未获得有效结果时显示占位符。 + const QString value = nmAutoFitChartIsFinite(fitness) && fitness >= 0.0 && fitness < 1.0e9 + ? QString::number(fitness, 'f', 6) : "--"; + resultSummary->setText(tr("Current error: %1") + .arg(QString("%1").arg(value))); + resultSummary->show(); +} + void nmWxAutomaticfittingStart::addLogMessage(const QString& message) { // 获取滚动条 @@ -957,4 +1117,9 @@ void nmWxAutomaticfittingStart::onBestCurveUpdated(QVector > tar } curveChart->setBestLogLogData(bestData, iteration, fitness); + // 形状改善时总误差可能上升,参数值仍须同步到当前已接受曲线。 + if(m_autoFitterLM) { + updateParameterValues(); + updateCurrentError(fitness); + } }