diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm
index 51dbc19b..409f506c 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 eb5e59ba..92d6829e 100644
--- a/Bin/Config/Lang/cn/nmNum_cn.ts
+++ b/Bin/Config/Lang/cn/nmNum_cn.ts
@@ -664,6 +664,34 @@ Reason: %1
nmCalculationAutoFitLM
+
+ Failed to refresh the sampling layer from the current curve.
+ 无法根据当前曲线刷新采样层级。
+
+
+ LM sampling refined: %1 / %2 target points; full-target error: %3
+ LM 采样加密:%1 / %2 个目标点;全目标点误差:%3
+
+
+ LM sampling: layered target points (%1 / %2); acceptance uses all valid target points.
+ LM 采样方式:目标点分层采样(%1 / %2 点),使用全部有效目标点验收。
+
+
+ LM sampling: fixed 80 points (original mode).
+ LM 采样方式:固定 80 点(原模式)。
+
+
+ Full-target comparison error: initial=%1; final=%2
+ 全目标点对比误差:初始=%1;最终=%2
+
+
+ Unavailable
+ 不可用
+
+
+ Budget exhausted before the full sampling layer; convergence is not confirmed.
+ 预算已耗尽,尚未进入完整采样层,未确认收敛。
+
=== User Stop Request Received ===
=== 用户停止请求已接收 ===
@@ -3794,6 +3822,22 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We
nmWxAutomaticFitting
+
+ LM sampling:
+ LM 采样方式:
+
+
+ Fixed sampling (80 points)
+ 固定采样(80 点)
+
+
+ Layered target sampling
+ 目标点分层采样
+
+
+ Layered sampling uses target-point subsets for directions and all valid target points for acceptance.
+ 逐层增加目标曲线采样点,并始终使用全部有效目标点判断拟合是否改善。
+
Automatic fitting
自动拟合
diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h
index bea69df0..b2048714 100644
--- a/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h
+++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h
@@ -12,7 +12,7 @@
#include "nmCalculation_global.h"
// 固定对数时间窗口的误差诊断。时间边界为不含重叠区的基础边界;
-// rmsError 是局部加权均方根,energy 是对 total 平方的贡献。
+// rmsError 是局部加权均方根,energy 是对当前层残差能量的贡献。
struct AutoFitTimeWindowLM {
double timeMin;
double timeMax;
@@ -25,7 +25,7 @@ struct AutoFitTimeWindowLM {
{}
};
-// 双对数曲线误差分解。total 是 LM 候选接受和排序的唯一依据,
+// 双对数曲线误差分解。total 是候选接受和排序的统一依据;分层模式使用全部目标点,
// 时间窗口和残差用于 Fisher 选参,其余诊断量用于解释曲线失配。
struct AutoFitObjectiveBreakdownLM {
bool valid;
@@ -33,6 +33,11 @@ struct AutoFitObjectiveBreakdownLM {
double pressureLoss;
double derivativeLoss;
QVector residualVector;
+ // 分层模式的残差属于当前层,total 属于全部目标点;固定模式坐标数组为空。
+ QVector sampleCoordinates;
+ int samplingStride;
+ int fullPointCount;
+ double layerError;
QVector timeWindows;
double verticalCommonBias;
double verticalLoss;
@@ -51,6 +56,9 @@ struct AutoFitObjectiveBreakdownLM {
, total(1.0e10)
, pressureLoss(std::numeric_limits::quiet_NaN())
, derivativeLoss(std::numeric_limits::quiet_NaN())
+ , samplingStride(1)
+ , fullPointCount(80)
+ , layerError(1.0e10)
, verticalCommonBias(std::numeric_limits::quiet_NaN())
, verticalLoss(std::numeric_limits::quiet_NaN())
, verticalReliable(false)
@@ -97,6 +105,7 @@ public:
int getTotalEvaluations() const;
void resetOptimizer();
void setTargetWellName(const QString& wellName);
+ void setLayeredSamplingEnabled(bool enabled);
signals:
void progressUpdated(int iteration, double bestFitness);
@@ -191,6 +200,9 @@ private:
double m_comparisonTimeMin;
double m_comparisonTimeMax;
QString m_targetWellName;
+ // 此开关由拟合界面传入;每轮重置层级,固定模式仍使用原来的 80 点目标。
+ bool m_layeredSampling;
+ int m_samplingStride;
int m_maxIterations;
double m_targetError;
diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h
index 3f526c4d..4c6c6947 100644
--- a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h
+++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h
@@ -82,6 +82,8 @@ private:
QLineEdit* m_errorLimitEdit;
QComboBox* m_targetWellCombo;
QComboBox* m_algorithmCombo;
+ QLabel* m_samplingLabel;
+ QComboBox* m_samplingCombo;
QLabel* m_surrogateLabel;
QComboBox* m_surrogateCombo;
diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp
index 8e4ae45b..fece2678 100644
--- a/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp
+++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp
@@ -53,10 +53,52 @@ static double autoFitTimeWindowWeight(double coordinate, int windowIndex)
return weight;
}
+// 目标时间点构成嵌套子集。窗口中心、重叠区两端及边界使用原始点两侧作锚点。
+static QVector autoFitSamplingIndices(const QVector& coordinates, int stride)
+{
+ QVector selected(coordinates.size(), false);
+ for(int i = 0; i < selected.size(); i += stride) {
+ selected[i] = true;
+ }
+ selected[0] = true;
+ selected[selected.size() - 1] = true;
+ for(int k = 0; k < kAutoFitTimeWindowCount; ++k) {
+ const double width = 1.0 / kAutoFitTimeWindowCount;
+ const double anchors[] = {(k + 0.5) * width,
+ k * width - width * kAutoFitTimeWindowOverlapRatio * 0.5,
+ k * width, k * width + width * kAutoFitTimeWindowOverlapRatio * 0.5};
+ for(int a = 0; a < 4; ++a) {
+ const int right = static_cast(std::lower_bound(
+ coordinates.begin(), coordinates.end(), anchors[a]) - coordinates.begin());
+ if(right < selected.size()) selected[right] = true;
+ if(right > 0) selected[right - 1] = true;
+ }
+ }
+ QVector indices;
+ for(int i = 0; i < selected.size(); ++i) {
+ if(selected[i]) indices.append(i);
+ }
+ return indices;
+}
+
+// 在归一化 log(time) 上使用梯形积分权重,避免原始数据密集段被重复放大。
+static QVector autoFitLogTimeWeights(const QVector& coordinates)
+{
+ QVector weights(coordinates.size(), 0.0);
+ for(int i = 1; i < coordinates.size(); ++i) {
+ const double halfWidth = 0.5 * (coordinates[i] - coordinates[i - 1]);
+ weights[i - 1] += halfWidth;
+ weights[i] += halfWidth;
+ }
+ return weights;
+}
+
static QVector calculateAutoFitTimeWindows(
- const QVector& residualVector, double timeMin, double timeMax)
+ const QVector& residualVector, double timeMin, double timeMax,
+ const QVector& coordinates = QVector(),
+ const QVector& timeWeights = QVector())
{
- // 残差前后两半分别为压力和导数,已包含各占一半及采样点数的归一化。
+ // 残差前后两半分别为压力和导数,已包含各占一半及时间采样权重的归一化。
const int pointCount = residualVector.size() / 2;
const double logMin = qLn(timeMin);
const double logSpan = qLn(timeMax) - logMin;
@@ -69,13 +111,16 @@ static QVector calculateAutoFitTimeWindows(
: qExp(logMin + logSpan * (k + 1) / kAutoFitTimeWindowCount);
for(int i = 0; i < pointCount; ++i) {
const double weight = autoFitTimeWindowWeight(
- static_cast(i) / (pointCount - 1), k);
- window.weightSum += weight;
+ coordinates.isEmpty() ? static_cast(i) / (pointCount - 1)
+ : coordinates[i], k);
+ window.weightSum += weight * (timeWeights.isEmpty() ? 1.0 : timeWeights[i]);
window.energy += weight *
(residualVector[i] * residualVector[i] +
residualVector[pointCount + i] * residualVector[pointCount + i]);
}
- window.rmsError = qSqrt(window.energy * pointCount / window.weightSum);
+ window.rmsError = window.weightSum > 0.0
+ ? qSqrt(window.energy * (timeWeights.isEmpty() ? pointCount : 1.0)
+ / window.weightSum) : 0.0;
}
return windows;
}
@@ -262,9 +307,11 @@ struct TrustRegionEvaluation
static bool trustRegionResidualsValid(
const AutoFitObjectiveBreakdownLM& breakdown)
{
- // 损失函数固定使用 80 个压力点和 80 个导数点。严格校验长度,避免
- // Jacobian 沿用旧维度后访问另一候选的短残差向量。
- if(!breakdown.valid || breakdown.residualVector.size() != 160) {
+ // 固定模式仍要求 160 维;分层模式按当前时间点数校验,切层后重建 J。
+ const int pointCount = breakdown.sampleCoordinates.isEmpty()
+ ? 80 : breakdown.sampleCoordinates.size();
+ if(!breakdown.valid || pointCount < 3 ||
+ breakdown.residualVector.size() != 2 * pointCount) {
return false;
}
@@ -429,9 +476,10 @@ struct TrustRegionFisher
static QVector buildTrustRegionFisher(
const QVector >& jacobian,
const QVector& residual,
- const QVector& columnValid)
+ const QVector& columnValid,
+ const QVector& coordinates = QVector())
{
- // 残差和 J 已包含压力/导数及点数归一化,只再乘一次窗口权重。
+ // 残差和 J 已包含压力/导数及时间采样权重,只再乘一次窗口权重。
// 前半段是压力,后半段是导数;同一时间点的两行使用相同权重。
const int dimensions = columnValid.size();
const int pointCount = residual.size() / 2;
@@ -441,7 +489,8 @@ static QVector buildTrustRegionFisher(
TrustRegionFisher& local = information[k];
for(int row = 0; row < residual.size(); ++row) {
const double weight = autoFitTimeWindowWeight(
- static_cast(row % pointCount) / (pointCount - 1), k);
+ coordinates.isEmpty() ? static_cast(row % pointCount) / (pointCount - 1)
+ : coordinates[row % pointCount], k);
for(int p = 0; p < dimensions; ++p) {
if(!columnValid[p]) {
continue;
@@ -685,6 +734,8 @@ nmCalculationAutoFitLM::nmCalculationAutoFitLM(QObject* parent)
, m_globalBestFitness(1e10)
, m_comparisonTimeMin(0.0)
, m_comparisonTimeMax(0.0)
+ , m_layeredSampling(false)
+ , m_samplingStride(1)
, m_maxIterations(100)
, m_targetError(0.001)
, m_totalEvaluations(0)
@@ -864,6 +915,7 @@ void nmCalculationAutoFitLM::resetOptimizer()
m_userInitialObjectiveBreakdown = AutoFitObjectiveBreakdownLM();
m_comparisonTimeMin = 0.0;
m_comparisonTimeMax = 0.0;
+ m_samplingStride = m_layeredSampling ? 4 : 1;
m_currentIteration = 0;
m_totalEvaluations = 0;
m_successfulEvaluations = 0;
@@ -878,6 +930,14 @@ void nmCalculationAutoFitLM::resetOptimizer()
DEBUG_OUT("LM optimizer reset");
}
+void nmCalculationAutoFitLM::setLayeredSamplingEnabled(bool enabled)
+{
+ // 拟合运行期间不允许改变残差定义;新一轮由 resetOptimizer 初始化层级。
+ if(!m_isRunning) {
+ m_layeredSampling = enabled;
+ }
+}
+
void nmCalculationAutoFitLM::setTargetWellName(const QString& wellName)
{
// 目标井名是贯穿拟合流程的关键索引:
@@ -982,6 +1042,8 @@ void nmCalculationAutoFitLM::writeTraceHeader()
<< prefix + "weight_sum" << prefix + "rms_error" << prefix + "energy";
}
+ cols << "sampling_mode" << "sampling_stride" << "sampling_points"
+ << "full_target_points" << "layer_objective";
QTextStream out(&m_traceFile);
out << cols.join(",") << "\n";
}
@@ -1018,12 +1080,14 @@ void nmCalculationAutoFitLM::writeTraceMetaFile()
QTextStream out(&metaFile);
out << "{\n";
- out << " \"schema_version\": 3,\n";
+ out << " \"schema_version\": 4,\n";
out << " \"trace_type\": \"finite_difference_lm_trust_region\",\n";
out << " \"run_id\": " << jsonEscape(m_traceRunId) << ",\n";
out << " \"created_at\": "
<< jsonEscape(QDateTime::currentDateTime().toString(Qt::ISODate)) << ",\n";
out << " \"trace_csv\": " << jsonEscape(QFileInfo(m_traceFilePath).fileName()) << ",\n";
+ out << " \"sampling_mode\": "
+ << jsonEscape(m_layeredSampling ? "layered_target" : "fixed_80") << ",\n";
out << " \"target\": {\n";
out << " \"well_name\": " << jsonEscape(m_targetWellName) << ",\n";
out << " \"time\": " << jsonDoubleArray(targetTime) << ",\n";
@@ -1128,6 +1192,11 @@ void nmCalculationAutoFitLM::writeTraceRow(
}
}
+ cols << (m_layeredSampling ? "layered_target" : "fixed_80")
+ << QString::number(objectiveBreakdown ? objectiveBreakdown->samplingStride : m_samplingStride)
+ << (objectiveBreakdown ? QString::number(objectiveBreakdown->residualVector.size() / 2) : QString())
+ << (objectiveBreakdown ? QString::number(objectiveBreakdown->fullPointCount) : QString())
+ << (objectiveBreakdown ? traceNumber(objectiveBreakdown->layerError) : QString());
QTextStream out(&m_traceFile);
out << cols.join(",") << "\n";
m_traceFile.flush();
@@ -1428,6 +1497,29 @@ bool nmCalculationAutoFitLM::startAutoFitting()
finalReason = runTrustRegionFitting();
validateAndProtectFinalResult();
+ // 两种模式共用全目标点比较指标,仅重算已有曲线,不增加真实求解次数。
+ if(m_comparisonTimeMin > 0.0 && !m_globalBestLogLogData.isEmpty()) {
+ const bool savedMode = m_layeredSampling;
+ const int savedStride = m_samplingStride;
+ const AutoFitObjectiveBreakdownLM savedBreakdown = m_lastObjectiveBreakdown;
+ m_layeredSampling = true;
+ m_samplingStride = 1;
+ const double comparisonFinal = calculateLogLogCurveError(m_targetLogLogData, m_globalBestLogLogData);
+ const double comparisonInitial = m_userInitialLogLogData.isEmpty() ? 1.0e10
+ : calculateLogLogCurveError(m_targetLogLogData, m_userInitialLogLogData);
+ m_layeredSampling = savedMode;
+ m_samplingStride = savedStride;
+ m_lastObjectiveBreakdown = savedBreakdown;
+ if(comparisonFinal < 1.0e9) {
+ emit logMessageGenerated(tr("Full-target comparison error: initial=%1; final=%2")
+ .arg(comparisonInitial < 1.0e9 ? QString::number(comparisonInitial, 'e', 6) : tr("Unavailable"))
+ .arg(comparisonFinal, 0, 'e', 6));
+ }
+ if(savedMode && savedStride > 1 && finalReason == LM_MAX_ITERATIONS) {
+ emit logMessageGenerated(tr("Budget exhausted before the full sampling layer; convergence is not confirmed."));
+ }
+ }
+
if(!m_globalBestPosition.isEmpty() && m_globalBestObjectiveBreakdown.valid) {
// 精英保护之后记录最终行,保证轨迹与实际写回参数一致。
writeTraceRow(m_currentIteration,
@@ -1441,8 +1533,8 @@ bool nmCalculationAutoFitLM::startAutoFitting()
&m_globalBestObjectiveBreakdown);
}
- if(finalReason != LM_USER_STOPPED &&
- m_globalBestFitness < m_targetError) {
+ if(finalReason != LM_USER_STOPPED && finalReason != LM_OPTIMIZATION_FAILED &&
+ (!m_layeredSampling || m_samplingStride == 1) && m_globalBestFitness < m_targetError) {
finalReason = LM_TARGET_ACHIEVED;
}
} catch(const std::exception& e) {
@@ -1714,7 +1806,7 @@ bool nmCalculationAutoFitLM::evaluateTrustRegionPoint(
}
// evaluateFitness() 会写入 DataManager 并调用真实求解器。这里统一统计
- // 真实评价次数和耗时,同时严格要求固定残差、诊断结构和结果曲线均有效。
+ // 真实评价次数和耗时,同时要求当前层残差、误差结构和结果曲线均有效。
QTime timer;
timer.start();
*fitness = evaluateFitness(parameters);
@@ -1776,7 +1868,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
bool globalFallbackAttempted = false;
StopReasonLM stopReason = LM_MAX_ITERATIONS;
- // jacobian 的行对应固定 160 维残差,列对应用户勾选的参数。
+ // jacobian 的行对应当前采样层的残差,列对应用户勾选的参数。
// Fisher 直接复用残差 Jacobian,上下/左右/形状诊断仅保留用于结果说明。
QVector > jacobian;
QVector jacobianColumnValid(dimensions, false);
@@ -1887,6 +1979,55 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
// 有效改善始终相对“上一次有效改善后的误差”累计判断,避免一连串微小
// 下降每次都清零计数;累计达到门槛后才开始新的有效改善基准。
double effectiveImprovementBaseline = current.fitness;
+ int fullDataRejections = 0;
+ bool samplingRefreshFailed = false;
+ auto promoteSampling = [&](bool complete) -> bool {
+ if(!m_layeredSampling || m_samplingStride == 1 || m_shouldStop) {
+ return false;
+ }
+ const int previousStride = m_samplingStride;
+ const AutoFitObjectiveBreakdownLM previousBreakdown = current.breakdown;
+ m_samplingStride = complete ? 1 : m_samplingStride / 2;
+ const double refreshedFitness = calculateLogLogCurveError(m_targetLogLogData, current.curve);
+ if(refreshedFitness >= 1.0e9 || !trustRegionResidualsValid(m_lastObjectiveBreakdown)) {
+ m_samplingStride = previousStride;
+ m_lastObjectiveBreakdown = previousBreakdown;
+ m_lastError = tr("Failed to refresh the sampling layer from the current curve.");
+ samplingRefreshFailed = true;
+ return false;
+ }
+ current.fitness = refreshedFitness;
+ current.breakdown = m_lastObjectiveBreakdown;
+ publishAcceptedPoint(current);
+ restoreEvaluationState(current);
+ jacobian.clear();
+ jacobianColumnValid.fill(false);
+ rebuildRequested = true;
+ trustRadius = 0.12;
+ damping = 1.0e-2;
+ consecutiveRejectedSteps = 0;
+ consecutiveIneffectiveSteps = 0;
+ acceptedSinceRebuild = 0;
+ movementSinceRebuild = 0.0;
+ modelRebuiltAtMinimumRadius = false;
+ stagnationConfirmationRequested = false;
+ attemptedWindows.fill(false);
+ globalFallbackAttempted = false;
+ fullDataRejections = 0;
+ effectiveImprovementBaseline = current.fitness;
+ emit logMessageGenerated(tr("LM sampling refined: %1 / %2 target points; full-target error: %3")
+ .arg(current.breakdown.residualVector.size() / 2)
+ .arg(current.breakdown.fullPointCount).arg(current.fitness, 0, 'e', 4));
+ writeTraceRow(m_currentIteration, -1, "sampling_refinement", current.parameters,
+ current.fitness, true, 0, "rebuild_required", ¤t.breakdown);
+ return true;
+ };
+
+ emit logMessageGenerated(m_layeredSampling
+ ? tr("LM sampling: layered target points (%1 / %2); acceptance uses all valid target points.")
+ .arg(current.breakdown.residualVector.size() / 2).arg(current.breakdown.fullPointCount)
+ : tr("LM sampling: fixed 80 points (original mode)."));
+
auto registerEffectiveImprovement = [&](double fitness) -> bool {
const double requiredImprovement = qMax(
effectiveAbsoluteImprovement,
@@ -1941,7 +2082,8 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
.arg(maximumIneffectiveSteps));
if(current.fitness < m_targetError) {
- return LM_TARGET_ACHIEVED;
+ promoteSampling(true);
+ return samplingRefreshFailed ? LM_OPTIMIZATION_FAILED : LM_TARGET_ACHIEVED;
}
// 在同一个真实工作点逐参数做单边差分。首选可用空间更大的方向;只有该方向
@@ -2122,16 +2264,26 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
if(!processPauseAndStop()) {
break;
}
+ if(m_layeredSampling && m_samplingStride > 1) {
+ const bool reserveFinalBudget = maximumEvaluations - m_totalEvaluations <= 2 * (dimensions + 1);
+ const int layerDeadline = qMax(1, m_maxIterations * (m_samplingStride == 4 ? 1 : 2) / 3);
+ if(reserveFinalBudget || iteration >= layerDeadline) {
+ promoteSampling(reserveFinalBudget);
+ if(samplingRefreshFailed) break;
+ }
+ }
if(rebuildRequested) {
const bool confirmingStagnation =
stagnationConfirmationRequested;
if(!rebuildSensitivity()) {
+ if(promoteSampling(false)) continue;
stopReason = m_shouldStop
? LM_USER_STOPPED
: LM_LOCAL_OPTIMUM;
break;
}
if(current.fitness < m_targetError) {
+ promoteSampling(true);
stopReason = LM_TARGET_ACHIEVED;
break;
}
@@ -2142,6 +2294,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
const bool rebuildEffective =
registerEffectiveImprovement(current.fitness);
if(confirmingStagnation && !rebuildEffective) {
+ if(promoteSampling(false)) continue;
emit logMessageGenerated(
tr("Sensitivity rebuild produced no effective improvement; "
"local convergence detected"));
@@ -2152,7 +2305,8 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
// 每轮从最新 J 和当前残差重算窗口 Fisher,包含有限差分与割线更新的变化。
const QVector information = buildTrustRegionFisher(
- jacobian, current.breakdown.residualVector, jacobianColumnValid);
+ jacobian, current.breakdown.residualVector, jacobianColumnValid,
+ current.breakdown.sampleCoordinates);
const TrustRegionFisher& global = information[kAutoFitTimeWindowCount];
const int dominantComponent = trustRegionDominantComponent(
current.breakdown, diagnosisThreshold); // 仅用于现有诊断日志。
@@ -2211,6 +2365,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
if(selectedColumns.isEmpty()) {
if(trustRadius <= minimumTrustRadius * 1.01 &&
modelRebuiltAtMinimumRadius) {
+ if(promoteSampling(false)) continue;
stopReason = LM_LOCAL_OPTIMUM;
break;
}
@@ -2218,6 +2373,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
damping = qMin(1.0e8, damping * 4.0);
rebuildRequested = true;
if(recordIneffectiveStep()) {
+ if(promoteSampling(false)) continue;
stopReason = LM_LOCAL_OPTIMUM;
break;
}
@@ -2273,6 +2429,7 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
break;
}
if(recordIneffectiveStep()) {
+ if(promoteSampling(false)) continue;
stopReason = LM_LOCAL_OPTIMUM;
break;
}
@@ -2290,11 +2447,18 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
coordinateStep);
// reductionRatio 衡量局部线性模型的可信度:接近 1 表示预测准确;
// 值较小表示虽然可能下降,但模型低估了非线性,需要收紧下一步。
- double actualReduction = 0.5 *
- (current.fitness * current.fitness -
- candidate.fitness * candidate.fitness);
+ double actualReduction = m_layeredSampling
+ ? 0.5 * (trustRegionSquaredNorm(current.breakdown.residualVector) -
+ trustRegionSquaredNorm(candidate.breakdown.residualVector))
+ : 0.5 * (current.fitness * current.fitness - candidate.fitness * candidate.fitness);
double reductionRatio = actualReduction / predictedReduction;
bool accepted = candidate.fitness < current.fitness;
+ // 粗层认为下降而完整数据不认可时累计,连续两次就提前加密。
+ if(m_layeredSampling && m_samplingStride > 1 && !accepted && actualReduction > 0.0) {
+ ++fullDataRejections;
+ } else {
+ fullDataRejections = 0;
+ }
QString componentName = trustRegionComponentName(dominantComponent);
if(accepted) {
@@ -2378,17 +2542,23 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
.arg(accepted ? tr("accepted") : tr("rejected")));
emit progressUpdated(iteration + 1, m_globalBestFitness);
- if(stopReason == LM_LOCAL_OPTIMUM) {
- break;
+ if(stopReason == LM_LOCAL_OPTIMUM || fullDataRejections >= 2) {
+ if(promoteSampling(false)) {
+ stopReason = LM_MAX_ITERATIONS;
+ continue;
+ }
+ if(stopReason == LM_LOCAL_OPTIMUM || samplingRefreshFailed) break;
}
if(current.fitness < m_targetError) {
+ promoteSampling(true);
stopReason = LM_TARGET_ACHIEVED;
break;
}
if(selectedWindow < 0 && trustRadius <= minimumTrustRadius * 1.01 &&
consecutiveRejectedSteps >= 2) {
if(modelRebuiltAtMinimumRadius) {
+ if(promoteSampling(false)) continue;
stopReason = LM_LOCAL_OPTIMUM;
break;
}
@@ -2404,8 +2574,10 @@ StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
if(m_shouldStop) {
return LM_USER_STOPPED;
}
+ if(samplingRefreshFailed) return LM_OPTIMIZATION_FAILED;
if(current.fitness < m_targetError) {
- return LM_TARGET_ACHIEVED;
+ promoteSampling(true);
+ return samplingRefreshFailed ? LM_OPTIMIZATION_FAILED : LM_TARGET_ACHIEVED;
}
if(stopReason == LM_CONSECUTIVE_FAILURES ||
stopReason == LM_LOCAL_OPTIMUM ||
@@ -3221,9 +3393,9 @@ double nmCalculationAutoFitLM::calculateLogLogCurveError(
// 仅首次有效评价使用交集建立基准;失败试算不能冻结区间,后续候选
// 必须覆盖完整基准,不允许靠丢失首尾点缩小误差或改变窗口位置。
const bool comparisonRangeFixed = m_comparisonTimeMin > 0.0;
- const double overlapMinX = comparisonRangeFixed
+ double overlapMinX = comparisonRangeFixed
? m_comparisonTimeMin : qMax(targetMinX, resultMinX);
- const double overlapMaxX = comparisonRangeFixed
+ double overlapMaxX = comparisonRangeFixed
? m_comparisonTimeMax : qMin(targetMaxX, resultMaxX);
if(overlapMinX >= overlapMaxX) {
return invalidLoss;
@@ -3234,6 +3406,86 @@ double nmCalculationAutoFitLM::calculateLogLogCurveError(
return invalidLoss;
}
+ if(m_layeredSampling) {
+ // 完整基准只取公共范围内的目标原始时间点;模拟点数不改变评价标准。
+ QVector times;
+ QVector pressureResidual;
+ QVector derivativeResidual;
+ for(int i = 0; i < targetPressure.size(); ++i) {
+ const double time = targetPressure[i].x();
+ if(time < overlapMinX || time > overlapMaxX) {
+ continue;
+ }
+ double pressure = 0.0;
+ double derivative = 0.0;
+ if(!interpolateLogValue(resultPressure, time, &pressure) ||
+ !interpolateLogValue(resultDerivative, time, &derivative)) {
+ return invalidLoss;
+ }
+ times.append(time);
+ pressureResidual.append(pressure - qLn(qMax(targetPressure[i].y(), valueFloor)));
+ derivativeResidual.append(derivative - qLn(targetDerivative[i].y()));
+ }
+ if(times.size() < 3) {
+ return invalidLoss;
+ }
+ overlapMinX = times.first();
+ overlapMaxX = times.last();
+ QVector coordinates;
+ const double logMin = qLn(overlapMinX);
+ const double logSpan = qLn(overlapMaxX) - logMin;
+ for(int i = 0; i < times.size(); ++i) {
+ coordinates.append((qLn(times[i]) - logMin) / logSpan);
+ }
+
+ // 数据较少时直接全量。补点只引用原始目标点,且各层共享同一组锚点。
+ const int stride = times.size() <= 41 ? 1 : m_samplingStride;
+ const QVector indices = autoFitSamplingIndices(coordinates, stride);
+ const QVector fullWeights = autoFitLogTimeWeights(coordinates);
+ QVector layerCoordinates;
+ for(int i = 0; i < indices.size(); ++i) {
+ layerCoordinates.append(coordinates[indices[i]]);
+ }
+ const QVector layerWeights = autoFitLogTimeWeights(layerCoordinates);
+ AutoFitObjectiveBreakdownLM breakdown;
+ breakdown.sampleCoordinates = layerCoordinates;
+ breakdown.samplingStride = stride;
+ breakdown.fullPointCount = times.size();
+ double pressureEnergy = 0.0;
+ double derivativeEnergy = 0.0;
+ for(int i = 0; i < times.size(); ++i) {
+ pressureEnergy += fullWeights[i] * pressureResidual[i] * pressureResidual[i];
+ derivativeEnergy += fullWeights[i] * derivativeResidual[i] * derivativeResidual[i];
+ }
+ breakdown.pressureLoss = qSqrt(pressureEnergy);
+ breakdown.derivativeLoss = qSqrt(derivativeEnergy);
+ breakdown.total = qSqrt(0.5 * (pressureEnergy + derivativeEnergy));
+ for(int component = 0; component < 2; ++component) {
+ for(int i = 0; i < indices.size(); ++i) {
+ const double residual = component == 0
+ ? pressureResidual[indices[i]] : derivativeResidual[indices[i]];
+ breakdown.residualVector.append(qSqrt(0.5 * layerWeights[i]) * residual);
+ }
+ }
+ breakdown.layerError = qSqrt(trustRegionSquaredNorm(breakdown.residualVector));
+ breakdown.valid = isFiniteNumber(breakdown.total) && breakdown.total < 1.0e9;
+ if(!trustRegionResidualsValid(breakdown)) {
+ return invalidLoss;
+ }
+ breakdown.timeWindows = calculateAutoFitTimeWindows(
+ breakdown.residualVector, overlapMinX, overlapMaxX,
+ layerCoordinates, layerWeights);
+ m_lastObjectiveBreakdown = breakdown;
+ // 首次完整评价有效后再冻结区间和实际层级,失败候选不能改变基准。
+ m_samplingStride = stride;
+ if(!comparisonRangeFixed) {
+ m_comparisonTimeMin = overlapMinX;
+ m_comparisonTimeMax = overlapMaxX;
+ writeTraceMetaFile();
+ }
+ return breakdown.total;
+ }
+
QVector commonX(numPoints);
QVector commonLogX(numPoints);
QVector targetLogPressure(numPoints);
@@ -3856,6 +4108,7 @@ double nmCalculationAutoFitLM::calculateLogLogCurveError(
breakdown.total = qSqrt(
0.5 * breakdown.pressureLoss * breakdown.pressureLoss +
0.5 * breakdown.derivativeLoss * breakdown.derivativeLoss);
+ breakdown.layerError = breakdown.total;
breakdown.valid =
isFiniteNumber(breakdown.total) &&
breakdown.total >= 0.0;
diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp
index 24200afd..1f0e2f3c 100644
--- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp
+++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp
@@ -724,6 +724,13 @@ void nmWxAutomaticFitting::setupControlPanel()
m_surrogateCombo->setCurrentIndex(automaticFittingData.getSurrogateScreeningEnabled() ? 1 : 0);
m_surrogateCombo->setMaximumWidth(160);
m_surrogateCombo->setMinimumWidth(160);
+ // 分层采样用于 LM 对比试验,每次打开默认使用原来的固定 80 点模式。
+ m_samplingLabel = new QLabel(tr("LM sampling:"));
+ m_samplingCombo = new QComboBox();
+ m_samplingCombo->addItem(tr("Fixed sampling (80 points)"));
+ m_samplingCombo->addItem(tr("Layered target sampling"));
+ m_samplingCombo->setMinimumWidth(180);
+ m_samplingCombo->setToolTip(tr("Layered sampling uses target-point subsets for directions and all valid target points for acceptance."));
connect(m_algorithmCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onAlgorithmChanged(int)));
onAlgorithmChanged(m_algorithmCombo->currentIndex());
@@ -781,6 +788,10 @@ void nmWxAutomaticFitting::setupControlPanel()
m_controlLayout->addWidget(m_surrogateCombo);
m_controlLayout->addSpacing(15);
+ m_controlLayout->addWidget(m_samplingLabel);
+ m_controlLayout->addWidget(m_samplingCombo);
+ m_controlLayout->addSpacing(15);
+
m_controlLayout->addWidget(iterationLabel);
m_controlLayout->addWidget(m_iterationEdit);
m_controlLayout->addSpacing(15);
@@ -802,6 +813,8 @@ void nmWxAutomaticFitting::onAlgorithmChanged(int index)
const bool usePSO = (index == 0);
m_surrogateLabel->setEnabled(usePSO);
m_surrogateCombo->setEnabled(usePSO);
+ m_samplingLabel->setEnabled(!usePSO);
+ m_samplingCombo->setEnabled(!usePSO);
}
void nmWxAutomaticFitting::setupButtons()
@@ -1189,6 +1202,7 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ
m_autoFitterLM = new nmCalculationAutoFitLM(this);
m_autoFitterLM->setTargetLogLogData(targetData);
m_autoFitterLM->setTargetWellName(targetWellName);
+ m_autoFitterLM->setLayeredSamplingEnabled(m_samplingCombo->currentIndex() == 1);
} else {
DEBUG_UI("Creating PSO auto fitter");
m_autoFitterPSO = new nmCalculationAutoFitPSO(this);