You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp

4365 lines
184 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "nmCalculationAutoFitLM.h"
#include "nmCalculationDllPebiSolverTask.h"
#include "nmCalculationUtils.h"
#include "nmDataAnalyzeManager.h"
#include "nmDataWellBase.h"
#include "nmDataVerticalFracturedWell.h"
#include "nmDataHorizontalFracturedWell.h"
#include "nmDataReservoir.h"
#include "nmDataAutomaticFitting.h"
#include <QApplication>
#include <QDebug>
#include <QTime>
#include <QDir>
#include <QTextStream>
#include <QFileInfo>
#include <QDateTime>
#include <QtCore/qmath.h>
#include <cmath>
#include <algorithm>
#include <limits>
#ifdef Q_OS_WIN
#include <windows.h>
#include <float.h>
#define DEBUG_OUT(msg) OutputDebugStringA(QString("[AutoFitLM] %1\n").arg(msg).toLocal8Bit().data())
#endif
static const bool kAutoFitDiagnosticTraceEnabled = true;
static const int kAutoFitTimeWindowCount = 4;
static const double kAutoFitTimeWindowOverlapRatio = 0.20;
static const int kAutoFitIntervalsPerDecade = 20;
static int autoFitShapeLag(int pointCount)
{
// 保持斜率跨度约为完整对数时间范围的 10%,四舍五入到间隔数且至少跨一个间隔。
return qMax(1, qRound((pointCount - 1) * 0.10));
}
static double autoFitTimeWindowWeight(double coordinate, int windowIndex)
{
// 重叠区以基础边界为中心,总宽度为窗口宽度的 20%;相邻权重互补。
const double width = 1.0 / kAutoFitTimeWindowCount;
const double overlap = width * kAutoFitTimeWindowOverlapRatio;
const double halfOverlap = overlap * 0.5;
const double pi = 3.14159265358979323846;
double weight = 1.0;
if(windowIndex > 0) {
double u = qBound(0.0,
(coordinate - windowIndex * width + halfOverlap) / overlap,
1.0);
weight *= 0.5 * (1.0 - qCos(pi * u));
}
if(windowIndex + 1 < kAutoFitTimeWindowCount) {
double u = qBound(0.0,
(coordinate - (windowIndex + 1) * width + halfOverlap) / overlap,
1.0);
weight *= 0.5 * (1.0 + qCos(pi * u));
}
return weight;
}
static QVector<AutoFitTimeWindowLM> calculateAutoFitTimeWindows(
const QVector<double>& residualVector, double timeMin, double timeMax)
{
// 残差前后两半分别为压力和导数,已包含各占一半及时间采样权重的归一化。
const int pointCount = residualVector.size() / 2;
const double logMin = qLn(timeMin);
const double logSpan = qLn(timeMax) - logMin;
QVector<AutoFitTimeWindowLM> windows(kAutoFitTimeWindowCount);
for(int k = 0; k < windows.size(); ++k) {
AutoFitTimeWindowLM& window = windows[k];
window.timeMin = k == 0 ? timeMin
: qExp(logMin + logSpan * k / kAutoFitTimeWindowCount);
window.timeMax = k + 1 == windows.size() ? timeMax
: qExp(logMin + logSpan * (k + 1) / kAutoFitTimeWindowCount);
for(int i = 0; i < pointCount; ++i) {
const double weight = autoFitTimeWindowWeight(
static_cast<double>(i) / (pointCount - 1), k);
window.weightSum += weight;
window.energy += weight *
(residualVector[i] * residualVector[i] +
residualVector[pointCount + i] * residualVector[pointCount + i]);
}
window.rmsError = window.weightSum > 0.0
? qSqrt(window.energy * pointCount / window.weightSum) : 0.0;
}
return windows;
}
static inline bool isFiniteNumber(double value)
{
#ifdef Q_OS_WIN
return _finite(value) != 0;
#else
return std::isfinite(value);
#endif
}
// 从嵌套 RMSE 中提取被消除的独立误差贡献。
static double nestedRmsContribution(double reducedModelLoss,
double fullModelLoss)
{
return qSqrt(qMax(0.0,
reducedModelLoss * reducedModelLoss -
fullModelLoss * fullModelLoss));
}
static inline void msleep(int ms)
{
#ifdef Q_OS_WIN
Sleep(ms);
#else
Q_UNUSED(ms);
#endif
}
static QString csvEscape(const QString& text)
{
QString escaped = text;
escaped.replace("\"", "\"\"");
return QString("\"%1\"").arg(escaped);
}
static QString traceNumber(double value)
{
return isFiniteNumber(value) ? QString::number(value, 'g', 17) : QString();
}
static QString traceParamAt(const QVector<double>& params, int index)
{
return (index >= 0 && index < params.size())
? traceNumber(params[index])
: QString();
}
static QString jsonEscape(const QString& text)
{
QString escaped = text;
escaped.replace("\\", "\\\\");
escaped.replace("\"", "\\\"");
escaped.replace("\b", "\\b");
escaped.replace("\f", "\\f");
escaped.replace("\n", "\\n");
escaped.replace("\r", "\\r");
escaped.replace("\t", "\\t");
return QString("\"%1\"").arg(escaped);
}
static QString jsonNumber(double value)
{
return isFiniteNumber(value) ? QString::number(value, 'g', 17) : QString("null");
}
static QString jsonDoubleArray(const QVector<double>& values)
{
QStringList items;
for(int i = 0; i < values.size(); ++i) {
items << jsonNumber(values[i]);
}
return QString("[%1]").arg(items.join(","));
}
static QString jsonIntArray(const QVector<int>& values)
{
QStringList items;
for(int i = 0; i < values.size(); ++i) {
items << QString::number(values[i]);
}
return QString("[%1]").arg(items.join(","));
}
static QString jsonBoolArray(const QVector<bool>& values)
{
QStringList items;
for(int i = 0; i < values.size(); ++i) {
items << (values[i] ? "true" : "false");
}
return QString("[%1]").arg(items.join(","));
}
static QString jsonStringArray(const QStringList& values)
{
QStringList items;
for(int i = 0; i < values.size(); ++i) {
items << jsonEscape(values[i]);
}
return QString("[%1]").arg(items.join(","));
}
// 信赖域搜索统一在 [0, 1] 内部坐标工作。正值参数使用对数坐标,使内部相同步长
// 表示近似相同的相对变化,避免 k、C、Dfc 等跨数量级参数被线性尺度支配;
// skin 可为负数、Swi 的物理意义是线性比例,因此二者保持有界线性坐标。
static bool useTrustRegionLogScale(int parameterIndex, double lower, double upper)
{
return parameterIndex != 1 && parameterIndex != 4 &&
lower > 0.0 && upper > lower;
}
static double toTrustRegionCoordinate(double value,
int parameterIndex,
double lower,
double upper)
{
// 所有进入优化器的物理值先投影到用户上下界,再转换成无量纲坐标。
// 这样有限差分步长、信赖半径和参数间相关性可以在统一尺度上比较。
value = qMax(lower, qMin(upper, value));
if(useTrustRegionLogScale(parameterIndex, lower, upper)) {
return (qLn(value) - qLn(lower)) / (qLn(upper) - qLn(lower));
}
return upper > lower ? (value - lower) / (upper - lower) : 0.0;
}
static double fromTrustRegionCoordinate(double coordinate,
int parameterIndex,
double lower,
double upper)
{
coordinate = qMax(0.0, qMin(1.0, coordinate));
// 端点直接返回原始边界,避免对数或线性逆变换的舍入误差造成越界。
if(coordinate <= 0.0) {
return lower;
}
if(coordinate >= 1.0) {
return upper;
}
double value;
if(useTrustRegionLogScale(parameterIndex, lower, upper)) {
value = qExp(qLn(lower) + coordinate * (qLn(upper) - qLn(lower)));
} else {
value = lower + coordinate * (upper - lower);
}
// 内部点转换后也限制到实际上下界,保证候选参数通过严格的边界检查。
return qMax(lower, qMin(upper, value));
}
// 一次真实求解的完整快照。除了参数和总误差,还保存内部坐标、诊断分量和
// 双对数曲线,因此拒绝候选后可以完整恢复上一个已接受工作点。
struct TrustRegionEvaluation
{
QVector<double> parameters;
QVector<double> coordinates;
AutoFitObjectiveBreakdownLM breakdown;
QVector<QVector<double> > curve;
double fitness;
int elapsedMs;
bool valid;
TrustRegionEvaluation()
: fitness(1.0e10)
, elapsedMs(-1)
, valid(false)
{}
};
// LM 残差长度由本轮固定比较区间决定,所有残差必须有限。
static bool trustRegionResidualsValid(
const AutoFitObjectiveBreakdownLM& breakdown)
{
// 压力和导数共用采样时间,两个通道点数相同且各至少三个点。
if(!breakdown.valid || breakdown.residualVector.size() < 6 ||
breakdown.residualVector.size() % 2 != 0) {
return false;
}
for(int i = 0; i < breakdown.residualVector.size(); ++i) {
if(!isFiniteNumber(breakdown.residualVector[i])) {
return false;
}
}
const int pointCount = breakdown.residualVector.size() / 2;
const int slopeCount = pointCount - autoFitShapeLag(pointCount);
if(breakdown.shapeResiduals.size() != 2 * slopeCount || !isFiniteNumber(breakdown.shapeLoss)) return false;
for(int i = 0; i < breakdown.shapeResiduals.size(); ++i) {
if(!isFiniteNumber(breakdown.shapeResiduals[i])) return false;
}
if(breakdown.earlyValueResiduals.size() != 2 * pointCount ||
breakdown.earlyParallelResiduals.size() != 2 * slopeCount ||
!isFiniteNumber(breakdown.earlyValueLoss) || !isFiniteNumber(breakdown.earlyParallelLoss) ||
!isFiniteNumber(breakdown.earlyParallelBias)) return false;
for(int i = 0; i < breakdown.earlyValueResiduals.size(); ++i) {
if(!isFiniteNumber(breakdown.earlyValueResiduals[i])) return false;
}
for(int i = 0; i < breakdown.earlyParallelResiduals.size(); ++i) {
if(!isFiniteNumber(breakdown.earlyParallelResiduals[i])) return false;
}
return true;
}
// 数值、整体形状和前期形状残差共用一次求解,联合缓存供各子阶段复用。
static QVector<double> trustRegionFullResidual(const AutoFitObjectiveBreakdownLM& objective)
{
QVector<double> residual = objective.residualVector;
residual += objective.shapeResiduals;
residual += objective.earlyValueResiduals;
residual += objective.earlyParallelResiduals;
return residual;
}
// 计算向量二范数的平方,避免在只比较能量或计算正规方程时反复开方。
static double trustRegionSquaredNorm(const QVector<double>& values)
{
double sum = 0.0;
for(int i = 0; i < values.size(); ++i) {
sum += values[i] * values[i];
}
return sum;
}
// 数值残差已含 sqrt(0.5 * 窗口权重),相减并换底后得到 log10 间距的加权 RMS。
// 不减去平均间距,保留“两条曲线整体离得太远”的信息;共同纵移会自然抵消。
static double trustRegionEarlyGapLoss(const AutoFitObjectiveBreakdownLM& objective)
{
const int count = objective.earlyValueResiduals.size() / 2;
double energy = 0.0;
for(int i = 0; i < count; ++i) {
const double difference = objective.earlyValueResiduals[i] - objective.earlyValueResiduals[count + i];
energy += 2.0 * difference * difference;
}
return qSqrt(energy) / qLn(10.0);
}
// 前期带符号的平均间距差:正值表示模拟比目标分得更开,负值表示更靠近。
// 复用现有加权数值残差,恢复第一窗口的归一化权重,不用 RMS 推断偏差符号。
static double trustRegionEarlyGapBias(const AutoFitObjectiveBreakdownLM& objective)
{
const int count = objective.earlyValueResiduals.size() / 2;
QVector<double> weights(count, 0.0);
double weightSum = 0.0;
for(int i = 0; i < count; ++i) {
weights[i] = autoFitTimeWindowWeight(static_cast<double>(i) / (count - 1), 0) *
((i == 0 || i == count - 1) ? 0.5 : 1.0);
weightSum += weights[i];
}
double bias = 0.0;
for(int i = 0; i < count; ++i) {
bias += qSqrt(2.0 * weights[i] / weightSum) *
(objective.earlyValueResiduals[i] - objective.earlyValueResiduals[count + i]);
}
return bias / qLn(10.0);
}
// 第一窗口的形状能量与局部 Fisher 使用相同的中心时间和重叠权重,
// 压力、导数同时参与;不重新插值或调用求解器。
static double trustRegionEarlyShapeEnergy(const AutoFitObjectiveBreakdownLM& objective)
{
const QVector<double>& residual = objective.shapeResiduals;
const int pointCount = objective.residualVector.size() / 2;
const int lag = autoFitShapeLag(pointCount);
const int slopeCount = pointCount - lag;
double energy = 0.0;
for(int row = 0; row < residual.size(); ++row) {
const double coordinate = (row % slopeCount + lag * 0.5) / (pointCount - 1);
energy += autoFitTimeWindowWeight(coordinate, 0) * residual[row] * residual[row];
}
return energy;
}
// 前期复用整体形状的双曲线斜率残差,只按第一窗口重新加权并归一化。
// 单独平移任一曲线不改变此指标;前期数值误差仅作诊断,不参与井储、表皮验收。
static void populateEarlyWellboreMetrics(AutoFitObjectiveBreakdownLM* objective,
const QVector<double> targetLogs[2], const QVector<double> resultLogs[2], double logTimeSpan)
{
const int count = targetLogs[0].size();
QVector<double> weights(count, 0.0);
double weightSum = 0.0;
for(int i = 0; i < count; ++i) {
weights[i] = autoFitTimeWindowWeight(static_cast<double>(i) / (count - 1), 0) *
((i == 0 || i == count - 1) ? 0.5 : 1.0);
weightSum += weights[i];
}
objective->earlyValueResiduals.fill(0.0, 2 * count);
for(int i = 0; i < count; ++i) {
const double weight = weights[i] / weightSum;
for(int component = 0; component < 2; ++component) {
const int row = component * count + i;
const double scale = qSqrt(0.5 * weight);
objective->earlyValueResiduals[row] = scale * (resultLogs[component][i] - targetLogs[component][i]);
}
}
objective->earlyValueLoss = qSqrt(trustRegionSquaredNorm(objective->earlyValueResiduals));
// 复用整体形状的斜率跨度,窗口权重取区间中心,随采样数量同步变化。
const int lag = autoFitShapeLag(count);
const int slopeCount = count - lag;
const double logTimeStep = logTimeSpan * lag / (count - 1);
QVector<double> parallelWeights(slopeCount, 0.0);
double parallelWeightSum = 0.0;
for(int i = 0; i < slopeCount; ++i) {
parallelWeights[i] = autoFitTimeWindowWeight((i + lag * 0.5) / (count - 1), 0);
parallelWeightSum += parallelWeights[i];
}
objective->earlyParallelResiduals.fill(0.0, 2 * slopeCount);
objective->earlyParallelBias = 0.0;
for(int i = 0; i < slopeCount; ++i) {
const double resultSlopeDifference = ((resultLogs[0][i + lag] - resultLogs[0][i]) -
(resultLogs[1][i + lag] - resultLogs[1][i])) / logTimeStep;
const double targetSlopeDifference = ((targetLogs[0][i + lag] - targetLogs[0][i]) -
(targetLogs[1][i + lag] - targetLogs[1][i])) / logTimeStep;
const double relativeSlopeError = resultSlopeDifference - targetSlopeDifference;
const double weight = parallelWeights[i] / parallelWeightSum;
objective->earlyParallelBias += weight * relativeSlopeError;
// 整体残差已含 sqrt(0.5 / slopeCount),换成第一窗口权重后仍保持双曲线等权。
// 压力、导数分别匹配目标;相对斜率偏差只保留为诊断,不参与前期验收。
const double scale = qSqrt(slopeCount * weight);
objective->earlyParallelResiduals[i] = scale * objective->shapeResiduals[i];
objective->earlyParallelResiduals[slopeCount + i] = scale * objective->shapeResiduals[slopeCount + i];
}
objective->earlyParallelLoss = qSqrt(trustRegionSquaredNorm(objective->earlyParallelResiduals));
}
// 计算同维向量内积;维度不一致表示局部模型无效,返回零让调用方放弃修正。
static double trustRegionDotProduct(const QVector<double>& left,
const QVector<double>& right)
{
if(left.size() != right.size()) {
return 0.0;
}
double sum = 0.0;
for(int i = 0; i < left.size(); ++i) {
sum += left[i] * right[i];
}
return sum;
}
// 求解选中参数对应的阻尼正规方程。参数最多七维,使用带部分主元的
// 高斯消元处理该小矩阵,并在主元退化时明确返回失败。
static bool solveTrustRegionLinearSystem(
QVector<QVector<double> > matrix,
QVector<double> rightHandSide,
QVector<double>* solution)
{
if(!solution || matrix.isEmpty() ||
matrix.size() != rightHandSide.size()) {
return false;
}
const int size = matrix.size();
for(int i = 0; i < size; ++i) {
if(matrix[i].size() != size) {
return false;
}
}
for(int column = 0; column < size; ++column) {
int pivotRow = column;
double pivotMagnitude = qAbs(matrix[column][column]);
for(int row = column + 1; row < size; ++row) {
double magnitude = qAbs(matrix[row][column]);
if(magnitude > pivotMagnitude) {
pivotMagnitude = magnitude;
pivotRow = row;
}
}
if(pivotMagnitude <= 1.0e-14) {
return false;
}
if(pivotRow != column) {
qSwap(matrix[pivotRow], matrix[column]);
qSwap(rightHandSide[pivotRow], rightHandSide[column]);
}
for(int row = column + 1; row < size; ++row) {
double factor = matrix[row][column] /
matrix[column][column];
matrix[row][column] = 0.0;
for(int nextColumn = column + 1;
nextColumn < size; ++nextColumn) {
matrix[row][nextColumn] -=
factor * matrix[column][nextColumn];
}
rightHandSide[row] -= factor * rightHandSide[column];
}
}
solution->fill(0.0, size);
for(int row = size - 1; row >= 0; --row) {
double value = rightHandSide[row];
for(int column = row + 1; column < size; ++column) {
value -= matrix[row][column] * (*solution)[column];
}
double pivot = matrix[row][row];
if(qAbs(pivot) <= 1.0e-14) {
return false;
}
(*solution)[row] = value / pivot;
if(!isFiniteNumber((*solution)[row])) {
return false;
}
}
return true;
}
// Fisher 仅作为当前归一化坐标下的局部信息矩阵,不用于统计置信区间。
struct TrustRegionFisher
{
QVector<QVector<double> > matrix;
QVector<double> gradient;
explicit TrustRegionFisher(int dimensions = 0)
: matrix(dimensions, QVector<double>(dimensions, 0.0))
, gradient(dimensions, 0.0)
{}
};
static QVector<TrustRegionFisher> buildTrustRegionFisher(
const QVector<QVector<double> >& jacobian,
const QVector<double>& residual,
const QVector<bool>& columnValid,
const QVector<double>& coordinates = QVector<double>())
{
// 残差和 J 已包含压力/导数及时间采样权重,只再乘一次窗口权重。
// 前半段是压力,后半段是导数;同一时间点的两行使用相同权重。
const int dimensions = columnValid.size();
const int pointCount = residual.size() / 2;
QVector<TrustRegionFisher> information(
kAutoFitTimeWindowCount + 1, TrustRegionFisher(dimensions));
for(int k = 0; k < kAutoFitTimeWindowCount; ++k) {
TrustRegionFisher& local = information[k];
for(int row = 0; row < residual.size(); ++row) {
const double weight = autoFitTimeWindowWeight(
coordinates.isEmpty() ? static_cast<double>(row % pointCount) / (pointCount - 1)
: coordinates[row % pointCount], k);
for(int p = 0; p < dimensions; ++p) {
if(!columnValid[p]) {
continue;
}
local.gradient[p] += weight * jacobian[row][p] * residual[row];
for(int q = 0; q <= p; ++q) {
if(columnValid[q]) {
local.matrix[p][q] +=
weight * jacobian[row][p] * jacobian[row][q];
}
}
}
}
// 互补窗口之和恢复全局 J^T*J 和 J^T*r,不能对各窗口再单独平均。
TrustRegionFisher& global = information[kAutoFitTimeWindowCount];
for(int p = 0; p < dimensions; ++p) {
global.gradient[p] += local.gradient[p];
for(int q = 0; q <= p; ++q) {
local.matrix[q][p] = local.matrix[p][q];
global.matrix[p][q] += local.matrix[p][q];
global.matrix[q][p] = global.matrix[p][q];
}
}
}
return information;
}
static bool buildTrustRegionFisherStep(
const TrustRegionFisher& global,
const QVector<int>& selected,
const QVector<double>& coordinates,
double damping, double trustRadius, double minimumStep,
QVector<double>* step, double* predictedReduction)
{
// 复用原有全局 LM 方程、阻尼缩放、退化回退、信赖半径和边界投影。
const int dimensions = coordinates.size();
const int count = selected.size();
QVector<QVector<double> > normal(count, QVector<double>(count, 0.0));
QVector<double> rhs(count, 0.0);
for(int i = 0; i < count; ++i) {
const int p = selected[i];
rhs[i] = -global.gradient[p];
for(int j = 0; j < count; ++j) {
normal[i][j] = global.matrix[p][selected[j]];
}
normal[i][i] += damping * qMax(1.0e-10, normal[i][i]);
}
QVector<double> solution;
bool solved = solveTrustRegionLinearSystem(normal, rhs, &solution);
step->fill(0.0, dimensions);
if(solved) {
for(int i = 0; i < count; ++i) {
(*step)[selected[i]] = solution[i];
}
}
auto projectAndPredict = [&]() -> bool {
const double norm = qSqrt(trustRegionSquaredNorm(*step));
if(!isFiniteNumber(norm) || norm < minimumStep) return false;
if(norm > trustRadius) {
for(int p = 0; p < dimensions; ++p) (*step)[p] *= trustRadius / norm;
}
for(int p = 0; p < dimensions; ++p) {
(*step)[p] = qBound(0.0, coordinates[p] + (*step)[p], 1.0) - coordinates[p];
}
// 只比较投影后可执行步长的下降,阻尼项不属于真实拟合目标。
*predictedReduction = -trustRegionDotProduct(global.gradient, *step);
for(int p = 0; p < dimensions; ++p) {
for(int q = 0; q < dimensions; ++q) {
*predictedReduction -= 0.5 * (*step)[p] * global.matrix[p][q] * (*step)[q];
}
}
return isFiniteNumber(*predictedReduction) && *predictedReduction > 1.0e-14 &&
qSqrt(trustRegionSquaredNorm(*step)) >= minimumStep;
};
if(solved && projectAndPredict()) return true;
// 联合解可能被边界投影破坏;此时尝试可行梯度方向,不直接判为没有下降方向。
step->fill(0.0, dimensions);
for(int i = 0; i < count; ++i) {
const int p = selected[i];
double direction = -global.gradient[p];
if((coordinates[p] <= minimumStep && direction < 0.0) ||
(coordinates[p] >= 1.0 - minimumStep && direction > 0.0)) direction = 0.0;
(*step)[p] = direction;
}
const double norm = qSqrt(trustRegionSquaredNorm(*step));
if(!isFiniteNumber(norm) || norm < minimumStep) return false;
for(int p = 0; p < dimensions; ++p) {
(*step)[p] = qBound(0.0, coordinates[p] + (*step)[p] * trustRadius / norm, 1.0) - coordinates[p];
}
const double linearReduction = -trustRegionDotProduct(global.gradient, *step);
double curvature = 0.0;
for(int p = 0; p < dimensions; ++p) {
for(int q = 0; q < dimensions; ++q) {
curvature += (*step)[p] * global.matrix[p][q] * (*step)[q];
}
}
if(!isFiniteNumber(linearReduction) || !isFiniteNumber(curvature) || linearReduction <= 0.0) return false;
// 沿投影梯度最小化局部二次模型,只缩步,不突破信赖域或参数边界。
const double scale = curvature > 0.0 ? qMin(1.0, linearReduction / curvature) : 1.0;
for(int p = 0; p < dimensions; ++p) (*step)[p] *= scale;
return projectAndPredict();
}
// 每次得到有效真实候选后,使用满足最新割线条件的秩一修正更新完整残差
// Jacobian。这样模型吸收了刚得到的真实变化,又不必立即逐参数重新试算。
static void updateTrustRegionJacobian(
QVector<QVector<double> >* jacobian,
const QVector<double>& oldResidual,
const QVector<double>& newResidual,
const QVector<double>& coordinateStep)
{
if(!jacobian || jacobian->size() != oldResidual.size() ||
oldResidual.size() != newResidual.size()) {
return;
}
double denominator = trustRegionSquaredNorm(coordinateStep);
if(denominator <= 1.0e-12) {
return;
}
for(int row = 0; row < jacobian->size(); ++row) {
if((*jacobian)[row].size() != coordinateStep.size()) {
return;
}
double predictedChange = 0.0;
for(int column = 0; column < coordinateStep.size(); ++column) {
predictedChange +=
(*jacobian)[row][column] * coordinateStep[column];
}
double correction =
(newResidual[row] - oldResidual[row] - predictedChange) /
denominator;
for(int column = 0; column < coordinateStep.size(); ++column) {
(*jacobian)[row][column] +=
correction * coordinateStep[column];
}
}
}
static QStringList traceParameterNames()
{
QStringList names;
names << "k"
<< "skin"
<< "wellboreC"
<< "phi"
<< "Swi"
<< "Dfc"
<< "fractureHalfLength";
return names;
}
nmCalculationAutoFitLM::nmCalculationAutoFitLM(QObject* parent)
: QObject(parent)
, m_isRunning(false)
, m_shouldStop(false)
, m_isFinalizing(false)
, m_currentIteration(0)
, m_globalBestFitness(1e10)
, m_comparisonTimeMin(0.0)
, m_comparisonTimeMax(0.0)
, m_maxIterations(100)
, m_targetError(0.001)
, m_totalEvaluations(0)
, m_successfulEvaluations(0)
, m_evaluationInProgress(0)
, m_consecutiveFailures(0)
, m_userInitialFitness(1e10)
, m_maxConsecutiveFailures(3)
, m_hasValidUserSolution(false)
, m_targetWellName("")
, m_traceRunId("")
, m_traceFilePath("")
, m_traceMetaFilePath("")
{
// 单次运行目录在输入校验通过后创建,避免只打开界面也产生临时文件。
DEBUG_OUT("LM automatic fitting calculator initialized");
}
// 析构函数:停止仍在进行的拟合、关闭 trace 文件并清理临时目录。
// 自动拟合可能在 UI 线程中被窗口关闭打断,因此析构时要尽量温和地等待当前评价结束;
// 如果等待超时,再强制清除运行标志,避免对象销毁后还有信号回调访问成员变量。
nmCalculationAutoFitLM::~nmCalculationAutoFitLM()
{
if(m_isRunning) {
m_shouldStop = true;
int waitCount = 0;
while(m_isRunning && waitCount < 100) {
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 50);
msleep(50);
waitCount++;
}
if(m_isRunning) {
DEBUG_OUT("Force stopping LM fitting after timeout");
m_isRunning = false;
}
}
closeTraceFile();
cleanupTemporaryDirectory();
disconnect(this, nullptr, nullptr, nullptr);
}
// ===== 临时目录工具 =====
//
// 真实求解器和井曲线 CSV 共用一个受控的单次运行目录。创建失败时不允许
// 回退到程序目录,否则后续递归清理可能删除安装文件。
bool nmCalculationAutoFitLM::initializeTemporaryDirectory()
{
nmCalculationUtils::cleanupStaleAutoFitTemporaryDirectories();
m_tempDirectory = nmCalculationUtils::createAutoFitTemporaryDirectory();
if(m_tempDirectory.isEmpty()) {
DEBUG_OUT("Failed to create LM auto-fit temporary directory");
return false;
}
m_traceRunId = QFileInfo(m_tempDirectory).fileName();
DEBUG_OUT(QString("Initialized temp directory: %1").arg(m_tempDirectory));
return true;
}
void nmCalculationAutoFitLM::cleanupTemporaryDirectory()
{
if(m_tempDirectory.isEmpty()) {
return;
}
const QString directoryPath = m_tempDirectory;
m_tempDirectory.clear();
if(nmCalculationUtils::removeAutoFitTemporaryDirectory(directoryPath)) {
DEBUG_OUT("Temp directory cleaned up successfully");
} else {
DEBUG_OUT(QString("Failed to clean auto-fit temporary directory: %1")
.arg(directoryPath));
}
}
// ==================== 公共接口方法 ====================
void nmCalculationAutoFitLM::setTargetLogLogData(const QVector<QVector<double> >& targetData)
{
// 目标曲线由界面层从目标井 history log-log 传入。
// 约定 targetData[0]=time,targetData[1]=pressure,targetData[2]=pressure derivative。
m_targetLogLogData = targetData;
m_comparisonTimeMin = 0.0;
m_comparisonTimeMax = 0.0;
DEBUG_OUT(QString("Target LogLog data set: %1 arrays").arg(targetData.size()));
if(targetData.size() >= 3) {
DEBUG_OUT(QString("LogLog data points: X=%1, Y1=%2, Y2=%3")
.arg(targetData[0].size())
.arg(targetData[1].size())
.arg(targetData[2].size()));
}
}
void nmCalculationAutoFitLM::stopFitting()
{
if(m_isFinalizing) {
emit logMessageGenerated(
tr("The final full-field calculation is already running"));
return;
}
// 用户点击停止时只设置请求标志,让 LM 主循环和求解器等待逻辑自然退出。
if(m_isRunning) {
emit logMessageGenerated(tr("=== User Stop Request Received ==="));
emit logMessageGenerated(tr("Gracefully stopping LM automatic fitting..."));
m_shouldStop = true;
// 此槽由求解等待循环派发,必须立即返回,外层循环才能转发取消请求。
if(m_evaluationInProgress > 0) {
emit logMessageGenerated(tr("Waiting for current solver evaluation to stop..."));
}
emit logMessageGenerated(tr("LM automatic fitting stop request processed"));
} else {
emit logMessageGenerated(tr("Stop request received but optimization is not running"));
}
}
bool nmCalculationAutoFitLM::isRunning() const
{
// UI 查询当前是否处于自动拟合运行状态。
return m_isRunning;
}
int nmCalculationAutoFitLM::getCurrentIteration() const
{
// UI 进度条和日志展示用的当前迭代序号。
return m_currentIteration;
}
int nmCalculationAutoFitLM::getTotalEvaluations() const
{
// 返回本轮拟合实际调用真实求解器的总次数,供完成日志展示。
return m_totalEvaluations;
}
QVector<double> nmCalculationAutoFitLM::getBestSolution() const
{
// 返回紧凑的“启用参数向量”,顺序与 m_enabledParamIndices 一致。
return m_globalBestPosition;
}
double nmCalculationAutoFitLM::getBestFitness() const
{
// 当前全局最优真实误差。越小越好,1e10 附近通常表示尚无有效解。
return m_globalBestFitness;
}
AutoFitObjectiveBreakdownLM nmCalculationAutoFitLM::getLastObjectiveBreakdown() const
{
// 返回最近一次损失评价的误差分解,供界面或后续优化逻辑读取。
return m_lastObjectiveBreakdown;
}
QString nmCalculationAutoFitLM::getLastError() const
{
// 上一次失败的人类可读错误信息,主要给 UI 层弹窗或日志使用。
return m_lastError;
}
void nmCalculationAutoFitLM::resetOptimizer()
{
// 清空一次运行产生的状态,但不销毁对象。
// 配置字段会在 startAutoFitting() 中重新从 DataManager 读取;
// trace 文件先关闭,避免新一轮 run 继续写到旧 CSV。
closeTraceFile();
m_globalBestPosition.clear();
m_globalBestFitness = 1e10;
m_globalBestObjectiveBreakdown = AutoFitObjectiveBreakdownLM();
m_lastEvaluatedLogLogData.clear();
m_globalBestLogLogData.clear();
m_lastObjectiveBreakdown = AutoFitObjectiveBreakdownLM();
m_userInitialLogLogData.clear();
m_userInitialObjectiveBreakdown = AutoFitObjectiveBreakdownLM();
m_comparisonTimeMin = 0.0;
m_comparisonTimeMax = 0.0;
m_currentIteration = 0;
m_totalEvaluations = 0;
m_successfulEvaluations = 0;
m_lastError.clear();
m_initialValues.clear();
m_userInitialSolution.clear();
m_userInitialFitness = 1e10;
m_hasValidUserSolution = false;
m_traceMetaFilePath.clear();
m_isFinalizing = false;
DEBUG_OUT("LM optimizer reset");
}
void nmCalculationAutoFitLM::setTargetWellName(const QString& wellName)
{
// 目标井名是贯穿拟合流程的关键索引:
// 读目标曲线、写 skin/wellboreC、求解后取 resultLogLog 都依赖这个名字。
m_targetWellName = wellName;
}
void nmCalculationAutoFitLM::initializeTraceFile()
{
closeTraceFile();
m_traceFilePath.clear();
m_traceMetaFilePath.clear();
if(!kAutoFitDiagnosticTraceEnabled) {
return;
}
// 诊断轨迹在所有构建中启用,并与源码、安装目录完全分离。
if(m_traceRunId.isEmpty()) {
m_traceRunId = QString("%1-%2")
.arg(QString::number(QCoreApplication::applicationPid()))
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"));
}
QDir traceDir(QDir(QDir::tempPath()).absoluteFilePath("WTAI/AutoFitTrace/LM"));
if(!traceDir.exists() && !QDir().mkpath(traceDir.absolutePath())) {
DEBUG_OUT(QString("Failed to create LM trace directory: %1").arg(traceDir.absolutePath()));
m_traceFilePath.clear();
m_traceMetaFilePath.clear();
return;
}
m_traceFilePath = traceDir.absoluteFilePath(
QString("lm_trust_region_trace_%1.csv").arg(m_traceRunId));
m_traceMetaFilePath = traceDir.absoluteFilePath(
QString("lm_trust_region_trace_%1.meta.json").arg(m_traceRunId));
m_traceFile.setFileName(m_traceFilePath);
if(!m_traceFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
DEBUG_OUT(QString("Failed to open LM trace file: %1").arg(m_traceFilePath));
m_traceFilePath.clear();
m_traceMetaFilePath.clear();
return;
}
writeTraceHeader();
writeTraceMetaFile();
emit logMessageGenerated(tr("LM fitting trace: %1").arg(m_traceFilePath));
if(!m_traceMetaFilePath.isEmpty()) {
emit logMessageGenerated(
tr("LM fitting trace metadata: %1").arg(m_traceMetaFilePath));
}
}
void nmCalculationAutoFitLM::closeTraceFile()
{
if(m_traceFile.isOpen()) {
m_traceFile.flush();
m_traceFile.close();
}
}
void nmCalculationAutoFitLM::writeTraceHeader()
{
if(!m_traceFile.isOpen()) {
return;
}
QStringList cols;
cols << "run_id"
<< "iteration"
<< "parameter_index"
<< "phase"
<< "k"
<< "skin"
<< "wellboreC"
<< "phi"
<< "Swi"
<< "Dfc"
<< "fractureHalfLength"
<< "solver_objective"
<< "solver_success"
<< "elapsed_ms"
<< "decision"
<< "enabled_param_indices"
<< "pressure_loss"
<< "derivative_loss"
<< "vertical_common_bias"
<< "vertical_loss"
<< "vertical_reliable"
<< "horizontal_physical_shift"
<< "horizontal_loss"
<< "horizontal_reliable"
<< "shape_loss"
<< "late_trend_loss"
<< "late_slope_bias"
<< "late_trend_reliable"
<< "registration_ambiguous";
for(int k = 0; k < kAutoFitTimeWindowCount; ++k) {
const QString prefix = QString("window_%1_").arg(k + 1);
cols << prefix + "time_min" << prefix + "time_max"
<< prefix + "weight_sum" << prefix + "rms_error" << prefix + "energy";
}
cols << "sampling_mode" << "sampling_points"
<< "pressure_vertical_bias" << "derivative_vertical_bias" << "first_window_shape_loss"
<< "early_value_loss" << "early_parallel_loss" << "early_parallel_bias" << "early_gap_loss" << "early_gap_bias";
QTextStream out(&m_traceFile);
out << cols.join(",") << "\n";
}
void nmCalculationAutoFitLM::writeTraceMetaFile()
{
if(m_traceMetaFilePath.isEmpty()) {
return;
}
QFile metaFile(m_traceMetaFilePath);
if(!metaFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
DEBUG_OUT(QString("Failed to open LM trace meta file: %1").arg(m_traceMetaFilePath));
m_traceMetaFilePath.clear();
return;
}
QStringList parameterNames = traceParameterNames();
QStringList enabledNames;
for(int i = 0; i < m_enabledParamIndices.size(); ++i) {
int paramIndex = m_enabledParamIndices[i];
enabledNames << ((paramIndex >= 0 && paramIndex < parameterNames.size())
? parameterNames[paramIndex]
: QString::number(paramIndex));
}
QVector<double> initialFullParams = buildTraceParameterVector(m_initialValues);
QVector<double> targetTime = m_targetLogLogData.size() > 0
? m_targetLogLogData[0] : QVector<double>();
QVector<double> targetPressure = m_targetLogLogData.size() > 1
? m_targetLogLogData[1] : QVector<double>();
QVector<double> targetDerivative = m_targetLogLogData.size() > 2
? m_targetLogLogData[2] : QVector<double>();
QTextStream out(&metaFile);
out << "{\n";
out << " \"schema_version\": 52,\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 << " \"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";
out << " \"difference_policy\": \"coordinate_step_cap_0.04_with_half_trust_radius_in_both_LM_phases\",\n";
out << " \"difference_failure_policy\": \"halve_before_opposite_direction\",\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("log_uniform_per_decade") << ",\n";
out << " \"sampling_intervals_per_decade\": " << kAutoFitIntervalsPerDecade << ",\n";
out << " \"target\": {\n";
out << " \"well_name\": " << jsonEscape(m_targetWellName) << ",\n";
out << " \"time\": " << jsonDoubleArray(targetTime) << ",\n";
out << " \"pressure\": " << jsonDoubleArray(targetPressure) << ",\n";
out << " \"derivative\": " << jsonDoubleArray(targetDerivative) << "\n";
out << " },\n";
out << " \"lm\": {\n";
out << " \"max_iterations\": " << m_maxIterations << ",\n";
out << " \"target_error\": " << jsonNumber(m_targetError) << "\n";
out << " },\n";
// 区间尚未冻结时写 null;首次有效评价后重写元数据,保存实际窗口基准。
out << " \"time_windows\": {\n";
out << " \"count\": " << kAutoFitTimeWindowCount << ",\n";
out << " \"overlap_ratio\": " << jsonNumber(kAutoFitTimeWindowOverlapRatio) << ",\n";
out << " \"comparison_time_min\": "
<< (m_comparisonTimeMin > 0.0 ? jsonNumber(m_comparisonTimeMin) : QString("null")) << ",\n";
out << " \"comparison_time_max\": "
<< (m_comparisonTimeMax > 0.0 ? jsonNumber(m_comparisonTimeMax) : QString("null")) << "\n";
out << " },\n";
out << " \"parameters\": {\n";
out << " \"names\": " << jsonStringArray(parameterNames) << ",\n";
out << " \"enabled_indices\": " << jsonIntArray(m_enabledParamIndices) << ",\n";
out << " \"enabled_names\": " << jsonStringArray(enabledNames) << ",\n";
out << " \"selected_flags\": " << jsonBoolArray(m_parameterSelected) << ",\n";
out << " \"lower\": " << jsonDoubleArray(m_parameterLower) << ",\n";
out << " \"upper\": " << jsonDoubleArray(m_parameterUpper) << ",\n";
out << " \"initial_selected\": " << jsonDoubleArray(m_initialValues) << ",\n";
out << " \"initial_full\": " << jsonDoubleArray(initialFullParams) << "\n";
out << " }\n";
out << "}\n";
metaFile.flush();
metaFile.close();
}
void nmCalculationAutoFitLM::writeTraceRow(
int iteration,
int parameterIndex,
const QString& phase,
const QVector<double>& parameters,
double solverObjective,
bool solverSuccess,
int elapsedMs,
const QString& decision,
const AutoFitObjectiveBreakdownLM* objectiveBreakdown)
{
if(!m_traceFile.isOpen()) {
return;
}
QVector<double> fullParams = buildTraceParameterVector(parameters);
QStringList enabledIndices;
for(int i = 0; i < m_enabledParamIndices.size(); ++i) {
enabledIndices << QString::number(m_enabledParamIndices[i]);
}
QStringList cols;
cols << csvEscape(m_traceRunId)
<< QString::number(iteration)
<< QString::number(parameterIndex)
<< csvEscape(phase);
for(int i = 0; i < 7; ++i) {
cols << traceParamAt(fullParams, i);
}
cols << traceNumber(solverObjective)
<< QString::number(solverSuccess ? 1 : 0)
<< QString::number(elapsedMs)
<< csvEscape(decision)
<< csvEscape(enabledIndices.join(";"));
if(objectiveBreakdown && objectiveBreakdown->valid) {
cols << traceNumber(objectiveBreakdown->pressureLoss)
<< traceNumber(objectiveBreakdown->derivativeLoss)
<< traceNumber(objectiveBreakdown->verticalCommonBias)
<< traceNumber(objectiveBreakdown->verticalLoss)
<< QString::number(objectiveBreakdown->verticalReliable ? 1 : 0)
<< traceNumber(objectiveBreakdown->horizontalPhysicalShift)
<< traceNumber(objectiveBreakdown->horizontalLoss)
<< QString::number(objectiveBreakdown->horizontalReliable ? 1 : 0)
<< traceNumber(objectiveBreakdown->shapeLoss)
<< traceNumber(objectiveBreakdown->lateDerivativeTrendLoss)
<< traceNumber(objectiveBreakdown->lateDerivativeSlopeBias)
<< QString::number(objectiveBreakdown->lateDerivativeTrendReliable ? 1 : 0)
<< QString::number(objectiveBreakdown->registrationAmbiguous ? 1 : 0);
} else {
for(int i = 0; i < 13; ++i) {
cols << QString();
}
}
// 无效候选也补齐窗口列,保证轨迹每行结构一致。
for(int k = 0; k < kAutoFitTimeWindowCount; ++k) {
if(objectiveBreakdown && objectiveBreakdown->valid &&
k < objectiveBreakdown->timeWindows.size()) {
const AutoFitTimeWindowLM& window = objectiveBreakdown->timeWindows[k];
cols << traceNumber(window.timeMin) << traceNumber(window.timeMax)
<< traceNumber(window.weightSum) << traceNumber(window.rmsError)
<< traceNumber(window.energy);
} else {
for(int i = 0; i < 5; ++i) {
cols << QString();
}
}
}
cols << "log_uniform_per_decade"
<< (objectiveBreakdown ? QString::number(objectiveBreakdown->residualVector.size() / 2) : QString())
<< (objectiveBreakdown ? traceNumber(objectiveBreakdown->pressureVerticalBias) : QString())
<< (objectiveBreakdown ? traceNumber(objectiveBreakdown->derivativeVerticalBias) : QString())
<< (objectiveBreakdown && objectiveBreakdown->valid
? traceNumber(qSqrt(trustRegionEarlyShapeEnergy(*objectiveBreakdown))) : QString())
<< (objectiveBreakdown && objectiveBreakdown->valid ? traceNumber(objectiveBreakdown->earlyValueLoss) : QString())
<< (objectiveBreakdown && objectiveBreakdown->valid ? traceNumber(objectiveBreakdown->earlyParallelLoss) : QString())
<< (objectiveBreakdown && objectiveBreakdown->valid ? traceNumber(objectiveBreakdown->earlyParallelBias) : QString())
<< (objectiveBreakdown && objectiveBreakdown->valid ? traceNumber(trustRegionEarlyGapLoss(*objectiveBreakdown)) : QString())
<< (objectiveBreakdown && objectiveBreakdown->valid ? traceNumber(trustRegionEarlyGapBias(*objectiveBreakdown)) : QString());
QTextStream out(&m_traceFile);
out << cols.join(",") << "\n";
m_traceFile.flush();
}
void nmCalculationAutoFitLM::emitRunSummary(bool success, StopReasonLM finalReason)
{
// 汇总仅描述 LM 迭代和真实求解器评价。
emit logMessageGenerated(tr("=== LM Run Summary ==="));
emit logMessageGenerated(tr("Stop reason: %1").arg(getStopReasonDescription(finalReason)));
emit logMessageGenerated(
tr("Result: %1, final error=%2, iterations=%3, evaluations=%4 (successful=%5, failed=%6)")
.arg(success ? tr("SUCCESS") : tr("FAILED"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1)
.arg(m_totalEvaluations)
.arg(m_successfulEvaluations)
.arg(m_totalEvaluations - m_successfulEvaluations));
if(!m_traceFilePath.isEmpty()) {
emit logMessageGenerated(tr("Artifacts: trace=%1").arg(m_traceFilePath));
}
if(!m_traceMetaFilePath.isEmpty()) {
emit logMessageGenerated(tr("Artifacts: trace_meta=%1").arg(m_traceMetaFilePath));
}
}
QVector<double> nmCalculationAutoFitLM::buildTraceParameterVector(const QVector<double>& selectedParameters) const
{
// 将 LM 内部使用的“启用参数向量”还原成完整 7 维参数向量。
// 未启用的参数从当前 DataManager 读取,启用的参数用 selectedParameters 覆盖。
// trace CSV 和 meta 使用该完整向量记录一次候选评价。
QVector<double> fullParams(7, 0.0);
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
if(dataManager) {
nmDataReservoir reservoirData = dataManager->getReservoirDataCopy();
fullParams[0] = reservoirData.getPermeability().getValue().toDouble();
fullParams[3] = reservoirData.getPorosity().getValue().toDouble();
fullParams[4] = reservoirData.getSwi().getValue().toDouble();
nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName);
if(pTargetWell) {
nmDataPerforation* perforation = pTargetWell->getPerforation(0);
if(perforation) {
fullParams[1] = perforation->getSkin().getValue().toDouble();
}
fullParams[2] = pTargetWell->getWellboreStorage().getValue().toDouble();
// Dfc 只存在于两类压裂井,普通井在完整向量中保持为 0。
if(pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) {
nmDataVerticalFracturedWell* fracturedWell =
dynamic_cast<nmDataVerticalFracturedWell*>(pTargetWell);
if(fracturedWell) {
fullParams[5] = fracturedWell->getDfc().getValue().toDouble();
fullParams[6] = fracturedWell->getFractureHalfLength().getValue().toDouble();
}
} else if(pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) {
nmDataHorizontalFracturedWell* fracturedWell =
dynamic_cast<nmDataHorizontalFracturedWell*>(pTargetWell);
if(fracturedWell) {
fullParams[5] = fracturedWell->getDfc().getValue().toDouble();
fullParams[6] = fracturedWell->getFractureHalfLength().getValue().toDouble();
}
}
}
}
for(int i = 0; i < selectedParameters.size() && i < m_enabledParamIndices.size(); ++i) {
int paramIndex = m_enabledParamIndices[i];
if(paramIndex >= 0 && paramIndex < fullParams.size()) {
fullParams[paramIndex] = selectedParameters[i];
}
}
return fullParams;
}
// ==================== 数据加载方法 ====================
bool nmCalculationAutoFitLM::loadAllConfigFromDataManager()
{
// 统一从 DataManager 加载本次运行所需配置。
// UI 层只负责把用户选择保存到 nmDataAutomaticFitting,本类从这里开始完全数据驱动。
try {
loadOptimizationConfig();
loadParameterBounds();
extractUserInitialValues(); // 直接提取初始值,无需条件判断
return true;
} catch(...) {
m_lastError = tr("Failed to load configuration from data manager");
return false;
}
}
void nmCalculationAutoFitLM::loadOptimizationConfig()
{
// LM 只读取迭代次数和目标误差,其他数值控制保留在现有算法实现中。
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy();
m_maxIterations = fittingData.getIterationCount().getValue().toInt();
m_targetError = fittingData.getErrorTolerance().getValue().toDouble();
DEBUG_OUT(QString("Loaded LM config: iterations=%1, error=%2")
.arg(m_maxIterations).arg(m_targetError));
}
void nmCalculationAutoFitLM::loadParameterBounds()
{
// 读取用户勾选的拟合参数及上下界。
//
// 这里构建三个核心数组:
// - m_parameterSelected[7]:完整参数体系中每个参数是否参与拟合;
// - m_parameterLower/Upper[7]:完整参数体系的搜索上下界;
// - m_enabledParamIndices:把粒子内部紧凑向量映射回完整参数索引。
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy();
// 获取参数选择状态
m_parameterSelected.resize(7);
m_parameterSelected[0] = fittingData.getPermeabilitySelected();
m_parameterSelected[1] = fittingData.getSkinSelected();
m_parameterSelected[2] = fittingData.getWellboreStorageSelected();
m_parameterSelected[3] = fittingData.getPorositySelected();
m_parameterSelected[4] = false; // 含水饱和度固定,保留索引以兼容物理参数和追踪记录。
m_parameterSelected[5] = fittingData.getFractureConductivitySelected();
m_parameterSelected[6] = fittingData.getFractureHalfLengthSelected();
// 获取参数边界
m_parameterLower.resize(7);
m_parameterUpper.resize(7);
m_parameterLower[0] = fittingData.getPermeabilityMin().getValue().toDouble();
m_parameterUpper[0] = fittingData.getPermeabilityMax().getValue().toDouble();
m_parameterLower[1] = fittingData.getSkinMin().getValue().toDouble();
m_parameterUpper[1] = fittingData.getSkinMax().getValue().toDouble();
m_parameterLower[2] = fittingData.getWellboreStorageMin().getValue().toDouble();
m_parameterUpper[2] = fittingData.getWellboreStorageMax().getValue().toDouble();
m_parameterLower[3] = fittingData.getPorosityMin().getValue().toDouble();
m_parameterUpper[3] = fittingData.getPorosityMax().getValue().toDouble();
m_parameterLower[4] = dataManager->getReservoirDataCopy().getSwi().getValue().toDouble();
m_parameterUpper[4] = m_parameterLower[4];
m_parameterLower[5] = fittingData.getFractureConductivityMin().getValue().toDouble();
m_parameterUpper[5] = fittingData.getFractureConductivityMax().getValue().toDouble();
m_parameterLower[6] = fittingData.getFractureHalfLengthMin().getValue().toDouble();
m_parameterUpper[6] = fittingData.getFractureHalfLengthMax().getValue().toDouble();
// 更新启用参数索引
m_enabledParamIndices.clear();
for(int i = 0; i < m_parameterSelected.size(); ++i) {
if(m_parameterSelected[i]) {
m_enabledParamIndices.append(i);
}
}
DEBUG_OUT(QString("Loaded parameter bounds: %1 enabled parameters")
.arg(m_enabledParamIndices.size()));
}
// ==================== 自动拟合核心方法 ====================
bool nmCalculationAutoFitLM::startAutoFitting()
{
// 总入口只负责准备数据、调用有限差分 + LM/信赖域,并写回最终结果。
StopReasonLM finalReason = LM_CONTINUE_OPTIMIZATION;
if(m_isRunning) {
m_lastError = tr("Auto fitting is already running");
return false;
}
try {
if(!loadAllConfigFromDataManager()) {
emit logMessageGenerated(tr("ERROR: Failed to load configuration from data manager"));
return false;
}
emit logMessageGenerated(tr("Algorithm: LM"));
const int enabledParams = getEnabledParameterCount();
emit logMessageGenerated(tr("Enabled parameters count: %1").arg(enabledParams));
if(enabledParams == 0) {
m_lastError = tr("No parameters enabled for optimization");
emit logMessageGenerated(tr("ERROR: No parameters enabled for optimization"));
return false;
}
if(m_targetLogLogData.size() < 3) {
m_lastError = tr("Target LogLog data is empty or insufficient");
emit logMessageGenerated(tr("ERROR: Target LogLog data is empty or insufficient"));
return false;
}
if(m_targetLogLogData[0].size() != m_targetLogLogData[1].size() ||
m_targetLogLogData[0].size() != m_targetLogLogData[2].size()) {
m_lastError = tr("Target LogLog data arrays have inconsistent sizes");
emit logMessageGenerated(tr("ERROR: Target LogLog data arrays have inconsistent sizes"));
return false;
}
if(m_targetWellName.isEmpty()) {
m_lastError = tr("Target well name is empty");
emit logMessageGenerated(tr("ERROR: Target well name is empty"));
return false;
}
emit logMessageGenerated(
tr("Target data validation passed (%1 data points)")
.arg(m_targetLogLogData[0].size()));
// resetOptimizer() 会清空运行状态,因此先保存从 DataManager 提取的初始值。
QVector<double> savedInitialValues = m_initialValues;
resetOptimizer();
// 输入校验通过后,为本次拟合创建新的临时计算目录。
cleanupTemporaryDirectory();
if(!initializeTemporaryDirectory()) {
m_lastError = tr("Cannot create the automatic fitting temporary directory");
emit logMessageGenerated(tr("ERROR: %1").arg(m_lastError));
return false;
}
m_isRunning = true;
m_shouldStop = false;
m_isFinalizing = false;
m_currentIteration = 0;
m_consecutiveFailures = 0;
m_initialValues = savedInitialValues;
initializeTraceFile();
// 先用真实求解器评价用户当前模型,供最终精英保护使用。
if(!savedInitialValues.isEmpty()) {
m_userInitialSolution = savedInitialValues;
emit logMessageGenerated(tr("=== Evaluating Initial Solution (Elite Protection) ==="));
QString paramStr = tr("Initial parameters: ");
for(int i = 0; i < m_userInitialSolution.size(); ++i) {
paramStr += QString("[%1]=%2 ")
.arg(i).arg(m_userInitialSolution[i], 0, 'f', 6);
}
emit logMessageGenerated(paramStr);
try {
emit logMessageGenerated(
tr("Starting initial solution evaluation..."));
QTime initialEvalTimer;
initialEvalTimer.start();
m_totalEvaluations++;
m_userInitialFitness = evaluateFitness(m_userInitialSolution);
const int initialEvalElapsedMs = initialEvalTimer.elapsed();
if(m_userInitialFitness < 1e9) {
m_successfulEvaluations++;
m_hasValidUserSolution = true;
m_globalBestFitness = m_userInitialFitness;
m_globalBestPosition = m_userInitialSolution;
m_userInitialLogLogData = m_lastEvaluatedLogLogData;
m_globalBestLogLogData = m_userInitialLogLogData;
m_userInitialObjectiveBreakdown = m_lastObjectiveBreakdown;
m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown;
emit logMessageGenerated(tr("Initial solution evaluation successful"));
emit logMessageGenerated(
tr("Initial Error: %1").arg(m_userInitialFitness, 0, 'e', 4));
emit bestCurveUpdated(m_targetLogLogData,
m_globalBestLogLogData,
0,
m_globalBestFitness);
} else {
m_hasValidUserSolution = false;
emit logMessageGenerated(tr("Initial solution evaluation failed"));
}
writeTraceRow(-1,
-1,
"initial_solution",
m_userInitialSolution,
m_userInitialFitness,
m_userInitialFitness < 1e9,
initialEvalElapsedMs,
m_hasValidUserSolution ? "valid" : "invalid",
m_hasValidUserSolution
? &m_userInitialObjectiveBreakdown : nullptr);
} catch(...) {
m_hasValidUserSolution = false;
emit logMessageGenerated(tr("Exception during initial solution evaluation"));
}
m_initialValues = savedInitialValues;
}
finalReason = runTrustRegionFitting();
validateAndProtectFinalResult();
if(!m_globalBestPosition.isEmpty() && m_globalBestObjectiveBreakdown.valid) {
// 精英保护之后记录最终行,保证轨迹与实际写回参数一致。
writeTraceRow(m_currentIteration,
-1,
"trust_region_final",
m_globalBestPosition,
m_globalBestFitness,
m_globalBestFitness < 1.0e9,
-1,
"final_result",
&m_globalBestObjectiveBreakdown);
}
if(finalReason != LM_USER_STOPPED && finalReason != LM_OPTIMIZATION_FAILED &&
m_globalBestFitness < m_targetError) {
finalReason = LM_TARGET_ACHIEVED;
}
} catch(const std::exception& e) {
m_lastError = QString(tr("Critical exception in automatic fitting: %1")).arg(e.what());
emit logMessageGenerated(tr("CRITICAL ERROR: %1").arg(e.what()));
closeTraceFile();
cleanupTemporaryDirectory();
m_isFinalizing = false;
m_isRunning = false;
emit fittingFinished(false, m_lastError);
return false;
} catch(...) {
m_lastError = tr("Unknown critical exception in automatic fitting");
emit logMessageGenerated(tr("CRITICAL ERROR: Unknown exception in automatic fitting"));
closeTraceFile();
cleanupTemporaryDirectory();
m_isFinalizing = false;
m_isRunning = false;
emit fittingFinished(false, m_lastError);
return false;
}
// 只有最终完整求解实际执行并提交快照后才能置为成功。
bool finalFullSolverSucceeded = false;
bool finalFullSolverExecuted = false;
if(!m_globalBestPosition.isEmpty()) {
try {
// Stop 只结束优化迭代;从这里开始必须用当前最优参数生成并发布正式快照。
m_isFinalizing = true;
emit finalizingStarted();
emit logMessageGenerated(tr("Applying optimized parameters to model..."));
applyParametersToDataManager(m_globalBestPosition);
// 裂缝参数会改变网格输入;标记失效后,最终求解任务会基于新快照重建网格。
const bool fractureGridParameterSelected =
(m_parameterSelected.size() > 5 && m_parameterSelected[5]) ||
(m_parameterSelected.size() > 6 && m_parameterSelected[6]);
if(fractureGridParameterSelected) {
nmDataAnalyzeManager* dataManager =
nmDataAnalyzeManager::getCurrentInstance();
if(!dataManager) {
throw std::runtime_error("Data manager is unavailable");
}
dataManager->invalidatePebiGrid();
}
emit logMessageGenerated(
tr("Running final full-field calculation with optimized parameters..."));
finalFullSolverExecuted = true;
finalFullSolverSucceeded = runFinalFullSolver();
if(finalFullSolverSucceeded) {
emit logMessageGenerated(
tr("Final full-field calculation completed successfully"));
} else {
m_lastError =
tr("Optimized parameters were found, but the final full-field calculation failed");
emit logMessageGenerated(
tr("ERROR: Final full-field calculation failed"));
}
saveOptimizationResult();
emit logMessageGenerated(tr("=== Optimization Results ==="));
emit logMessageGenerated(
tr("Final error: %1").arg(m_globalBestFitness, 0, 'e', 4));
emit logMessageGenerated(
tr("Total iterations: %1").arg(m_currentIteration + 1));
emit logMessageGenerated(
tr("Total evaluations: %1 (successful: %2)")
.arg(m_totalEvaluations).arg(m_successfulEvaluations));
QString finalParams = tr("Optimized parameters: ");
for(int i = 0; i < m_globalBestPosition.size(); ++i) {
finalParams += QString("[%1]=%2 ")
.arg(i).arg(m_globalBestPosition[i], 0, 'f', 6);
}
emit logMessageGenerated(finalParams);
if(finalFullSolverExecuted && finalFullSolverSucceeded) {
emit logMessageGenerated(
tr("Parameters and full-field results applied successfully to data manager"));
} else if(!finalFullSolverExecuted) {
emit logMessageGenerated(
tr("Optimized parameters applied to data manager"));
}
} catch(const std::exception& e) {
finalFullSolverSucceeded = false;
m_lastError = tr("Failed to apply final parameters: %1").arg(e.what());
emit logMessageGenerated(
tr("ERROR: Failed to apply final parameters: %1").arg(e.what()));
} catch(...) {
finalFullSolverSucceeded = false;
m_lastError = tr("Failed to apply final parameters due to unknown error");
emit logMessageGenerated(
tr("ERROR: Unknown error applying final parameters"));
}
}
m_isFinalizing = false;
m_isRunning = false;
bool success = false;
QString message;
if(finalReason == LM_TARGET_ACHIEVED) {
success = true;
message = QString(tr("Target achieved. Best error: %1, Iterations: %2"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING SUCCESSFUL ==="));
} else if(finalReason == LM_TRUE_CONVERGENCE) {
success = true;
message = QString(
tr("Automatic fitting converged to a stable solution. Best error: %1, Iterations: %2"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING CONVERGED ==="));
} else if(finalReason == LM_LOCAL_OPTIMUM) {
success = true;
message = QString(
tr("Automatic fitting reached a local optimum. Best error: %1, Iterations: %2"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(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"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING - MAX ITERATIONS ==="));
} else if(finalReason == LM_USER_STOPPED) {
success = true;
message = QString(tr("Stopped by user. Best error: %1, Iterations: %2"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING STOPPED BY USER ==="));
} else if(finalReason == LM_CONSECUTIVE_FAILURES) {
message = QString(
tr("Automatic fitting failed due to consecutive failures. Best error: %1, Iterations: %2"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING FAILED ==="));
} else {
message = QString(
tr("Automatic fitting ended unexpectedly. Best error: %1, Iterations: %2"))
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_currentIteration + 1);
emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING - UNKNOWN END ==="));
}
if(!finalFullSolverSucceeded) {
success = false;
message = m_lastError;
}
emitRunSummary(success, finalReason);
QApplication::processEvents();
msleep(200);
QApplication::processEvents();
closeTraceFile();
cleanupTemporaryDirectory();
emit fittingFinished(success, message);
return success;
}
void nmCalculationAutoFitLM::extractUserInitialValues()
{
// 从当前项目模型读取用户已有初始参数。
// 只提取用户勾选的参数,并按 m_enabledParamIndices 的顺序写入 m_initialValues。
// 这些值用于初始解真实评价、LM 起点和最终精英保护。
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
nmDataReservoir reservoirData = dataManager->getReservoirDataCopy();
//QVector<nmDataWellBase*> wells = dataManager->getWellDataList();
nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName);
m_initialValues.clear();
// 按照启用参数的顺序提取初始值。井参数来自目标井,储层参数来自 reservoirData。
for(int i = 0; i < m_enabledParamIndices.size(); ++i) {
int paramIndex = m_enabledParamIndices[i];
double initialValue = 0.0;
switch(paramIndex) {
case 0: // 渗透率
initialValue = reservoirData.getPermeability().getValue().toDouble();
break;
case 1: // 表皮系数
if(pTargetWell) {
initialValue = pTargetWell->getPerforation(0)->getSkin().getValue().toDouble();
}
break;
case 2: // 井筒储集系数
if(pTargetWell) {
initialValue = pTargetWell->getWellboreStorage().getValue().toDouble();
}
break;
case 3: // 孔隙度
initialValue = reservoirData.getPorosity().getValue().toDouble();
break;
case 4: // 初始含水饱和度
initialValue = reservoirData.getSwi().getValue().toDouble();
break;
case 5: // 裂缝导流能力
if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) {
nmDataVerticalFracturedWell* fracturedWell =
dynamic_cast<nmDataVerticalFracturedWell*>(pTargetWell);
if(fracturedWell) {
initialValue = fracturedWell->getDfc().getValue().toDouble();
}
} else if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) {
nmDataHorizontalFracturedWell* fracturedWell =
dynamic_cast<nmDataHorizontalFracturedWell*>(pTargetWell);
if(fracturedWell) {
initialValue = fracturedWell->getDfc().getValue().toDouble();
}
}
break;
case 6: // 裂缝半长
if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) {
nmDataVerticalFracturedWell* fracturedWell =
dynamic_cast<nmDataVerticalFracturedWell*>(pTargetWell);
if(fracturedWell) {
initialValue = fracturedWell->getFractureHalfLength().getValue().toDouble();
}
} else if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) {
nmDataHorizontalFracturedWell* fracturedWell =
dynamic_cast<nmDataHorizontalFracturedWell*>(pTargetWell);
if(fracturedWell) {
initialValue = fracturedWell->getFractureHalfLength().getValue().toDouble();
}
}
break;
}
m_initialValues.append(initialValue);
}
DEBUG_OUT(QString("Extracted %1 user initial values").arg(m_initialValues.size()));
for(int i = 0; i < m_initialValues.size(); ++i) {
DEBUG_OUT(QString(" Initial[%1] = %2").arg(i).arg(m_initialValues[i], 0, 'e', 3));
}
// 验证初始值
if(!validateInitialValues()) {
DEBUG_OUT("Warning: Some initial values are outside parameter bounds");
}
}
bool nmCalculationAutoFitLM::evaluateTrustRegionPoint(
const QVector<double>& parameters,
double* fitness,
AutoFitObjectiveBreakdownLM* breakdown,
QVector<QVector<double> >* curve,
int* elapsedMs)
{
if(!fitness || !breakdown || !curve || !elapsedMs || m_shouldStop) {
return false;
}
// evaluateFitness() 会写入 DataManager 并调用真实求解器。这里统一统计
// 真实评价次数和耗时,同时要求固定采样残差、误差结构和结果曲线均有效。
QTime timer;
timer.start();
*fitness = evaluateFitness(parameters, false);
*elapsedMs = timer.elapsed();
*breakdown = m_lastObjectiveBreakdown;
*curve = m_lastEvaluatedLogLogData;
++m_totalEvaluations;
bool valid = isFiniteNumber(*fitness) && *fitness < 1.0e9 &&
breakdown->valid &&
trustRegionResidualsValid(*breakdown) &&
!curve->isEmpty();
if(valid) {
++m_successfulEvaluations;
}
return valid;
}
StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting()
{
const int dimensions = getEnabledParameterCount();
if(dimensions <= 0 || m_enabledParamIndices.size() != dimensions) {
m_lastError = tr("No valid parameters are available for trust-region fitting");
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;
// 下列步长均位于归一化内部坐标: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% 才算有效改善。
// 更小的下降仍保留为最佳解,但不能反复清除停滞状态、延长拟合时间。
const double effectiveRelativeImprovement = 2.0e-3;
const double effectiveAbsoluteImprovement = 1.0e-5;
const int maximumIneffectiveSteps = 3;
// damping 是 LM 阻尼;拒绝或预测失准时增大,真实下降与预测一致时减小。
// 按连续预测失准刷新 Jacobian;接受步数只作兜底,不因累计移动距离强制重建。
double trustRadius = 0.12;
double damping = 1.0e-2;
int consecutiveRejectedSteps = 0;
int consecutiveSolverFailures = 0;
int acceptedSinceRebuild = 0;
int consecutiveIneffectiveSteps = 0;
int consecutivePoorPredictions = 0;
bool rebuildRequested = true;
// 跟踪文件保留稳定英文原因,界面输出时再翻译,避免受本地编码影响。
const char* rebuildReason = QT_TR_NOOP("initial sensitivity model");
bool modelRebuiltAtMinimumRadius = false;
bool stagnationConfirmationRequested = false;
bool globalFallbackAttempted = false;
StopReasonLM stopReason = LM_MAX_ITERATIONS;
// jacobian 的行对应固定采样的残差,列对应用户勾选的参数。
// Fisher 按当前目标取数值或形状残差行,两段均使用所有勾选参数。
QVector<QVector<double> > jacobian;
QVector<bool> jacobianColumnValid(dimensions, false);
// 参数向量的顺序始终与 m_enabledParamIndices 一致,不能按完整参数索引
// 直接访问;下面两个转换函数集中维护这层映射关系。
auto coordinatesFromParameters = [&](const QVector<double>& parameters)
-> QVector<double> {
QVector<double> coordinates(dimensions, 0.0);
for(int i = 0; i < dimensions; ++i) {
int parameterIndex = m_enabledParamIndices[i];
coordinates[i] = toTrustRegionCoordinate(
parameters[i], parameterIndex,
m_parameterLower[parameterIndex],
m_parameterUpper[parameterIndex]);
}
return coordinates;
};
auto parametersFromCoordinates = [&](const QVector<double>& coordinates)
-> QVector<double> {
QVector<double> parameters(dimensions, 0.0);
for(int i = 0; i < dimensions; ++i) {
int parameterIndex = m_enabledParamIndices[i];
parameters[i] = fromTrustRegionCoordinate(
coordinates[i], parameterIndex,
m_parameterLower[parameterIndex],
m_parameterUpper[parameterIndex]);
}
return parameters;
};
auto restoreEvaluationState = [&](const TrustRegionEvaluation& evaluation) {
// evaluateFitness() 会把试算参数写入 DataManager。无论候选是否接受,
// 下一次计算前都恢复到唯一的已接受工作点,防止失败试算污染后续求解。
applyParametersToDataManager(evaluation.parameters);
m_lastObjectiveBreakdown = evaluation.breakdown;
m_lastEvaluatedLogLogData = evaluation.curve;
};
// 各阶段只发布通过当前目标和约束检查的工作点;曲线和诊断快照必须
// 与参数同步更新,防止界面显示或最终精英保护使用错配的数据。
auto publishAcceptedPoint = [&](const TrustRegionEvaluation& evaluation) {
m_globalBestPosition = evaluation.parameters;
m_globalBestFitness = evaluation.fitness;
m_globalBestObjectiveBreakdown = evaluation.breakdown;
m_globalBestLogLogData = evaluation.curve;
emit bestCurveUpdated(m_targetLogLogData,
m_globalBestLogLogData,
m_currentIteration + 1,
m_globalBestFitness);
};
auto processPauseAndStop = [&]() -> bool {
QApplication::processEvents();
return !m_shouldStop;
};
// current 始终代表唯一已接受工作点。优先复用启动阶段已经真实验证的
// 用户初始解,避免在信赖域入口重复调用一次昂贵求解器。
TrustRegionEvaluation current;
if(m_hasValidUserSolution &&
m_globalBestPosition.size() == dimensions &&
trustRegionResidualsValid(m_globalBestObjectiveBreakdown) &&
!m_globalBestLogLogData.isEmpty()) {
current.parameters = m_globalBestPosition;
current.coordinates = coordinatesFromParameters(current.parameters);
current.breakdown = m_globalBestObjectiveBreakdown;
current.curve = m_globalBestLogLogData;
current.fitness = m_globalBestFitness;
current.elapsedMs = 0;
current.valid = true;
} else {
// 用户初始解无效时只做一次确定性的范围中点回退;所有正值参数在对数
// 坐标取中点,避免线性中点过分偏向跨数量级范围的上界。
current.coordinates.fill(0.5, dimensions);
current.parameters = parametersFromCoordinates(current.coordinates);
current.valid = evaluateTrustRegionPoint(
current.parameters,
&current.fitness,
&current.breakdown,
&current.curve,
&current.elapsedMs);
writeTraceRow(-1, -1,
"trust_region_midpoint",
current.parameters,
current.fitness,
current.valid,
current.elapsedMs,
current.valid ? "midpoint_valid" : "midpoint_invalid",
current.valid ? &current.breakdown : nullptr);
if(!current.valid) {
m_lastError = tr("The initial solution and parameter-range midpoint are both invalid");
return m_shouldStop
? LM_USER_STOPPED
: LM_OPTIMIZATION_FAILED;
}
publishAcceptedPoint(current);
}
restoreEvaluationState(current);
emit progressUpdated(-1, current.fitness);
emit logMessageGenerated(tr("=== Starting LM Main Loop ==="));
emit logMessageGenerated(
tr("LM starting point error: %1; evaluation budget per LM phase: %2")
.arg(current.fitness, 0, 'e', 4)
.arg(phaseEvaluationBudget));
// 高度预调整只改变用户勾选的渗透率。ln(k) 的初始变化由有符号高度差
// 给出,真实求解后用割线估计修正;拒绝时缩步,不让其他参数补偿高度。
const int permeabilityColumn = m_enabledParamIndices.indexOf(0);
double heightBaseline = current.breakdown.verticalLoss;
int heightStagnation = 0;
double heightScale = 1.0;
double heightSlope = -1.0;
if(permeabilityColumn >= 0) {
emit logMessageGenerated(tr("LM stage 1: align curve height using permeability only; no fixed evaluation limit, effective improvement threshold 10%."));
// 不限制试算次数,由高度对齐、改善停滞及可行步长决定何时结束。
while(processPauseAndStop()) {
const double bias = current.breakdown.verticalCommonBias;
if(!current.breakdown.verticalReliable || qAbs(bias) <= 0.01 || heightStagnation >= 2) break;
const double k = current.parameters[permeabilityColumn];
if(k <= 0.0 || m_parameterLower[0] <= 0.0 || m_parameterUpper[0] <= m_parameterLower[0]) break;
const double logRange = qLn(m_parameterUpper[0]) - qLn(m_parameterLower[0]);
const double change = qBound(-0.30 * logRange, -bias / heightSlope * heightScale, 0.30 * logRange);
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;
candidate.valid = evaluateTrustRegionPoint(candidate.parameters, &candidate.fitness,
&candidate.breakdown, &candidate.curve, &candidate.elapsedMs);
// 第一阶段只按上下偏移改善验收,不限制形状;仍防止两条曲线一高一低互相抵消。
const bool accepted = candidate.valid && candidate.breakdown.verticalReliable &&
candidate.breakdown.verticalLoss < current.breakdown.verticalLoss;
if(candidate.valid) {
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,
candidate.valid, candidate.elapsedMs, accepted ? "accepted_height" : "rejected_height",
candidate.valid ? &candidate.breakdown : nullptr);
if(accepted) {
current = candidate;
publishAcceptedPoint(current);
} else {
heightScale *= 0.5;
}
restoreEvaluationState(current);
if(heightBaseline - current.breakdown.verticalLoss >= qMax(1.0e-4, 0.10 * heightBaseline)) {
heightBaseline = current.breakdown.verticalLoss;
heightStagnation = 0;
} else ++heightStagnation;
emit logMessageGenerated(tr("Permeability alignment: k=%1, height error=%2, shape error=%3, result=%4")
.arg(candidate.parameters[permeabilityColumn], 0, 'g', 6)
.arg(candidate.breakdown.verticalLoss, 0, 'e', 4).arg(candidate.breakdown.shapeLoss, 0, 'e', 4)
.arg(accepted ? tr("accepted") : tr("rejected")));
}
QString heightReason = tr("no feasible permeability step or step too small");
if(m_shouldStop) heightReason = tr("stopped by user");
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");
else if(current.parameters[permeabilityColumn] <= m_parameterLower[0] ||
current.parameters[permeabilityColumn] >= m_parameterUpper[0]) heightReason = tr("permeability reached its bound");
emit logMessageGenerated(tr("Permeability alignment ended: %1").arg(heightReason));
}
if(m_shouldStop) return LM_USER_STOPPED;
// 原逐参数第二阶段已移除;同一个联合 LM 先匹配整体形状,再匹配总误差。
bool shapeStage = true;
auto stageError = [&](const TrustRegionEvaluation& point) -> double {
return shapeStage ? point.breakdown.shapeLoss : point.fitness;
};
auto acceptable = [&](const TrustRegionEvaluation& point, const TrustRegionEvaluation& base) -> bool {
return point.valid && stageError(point) < stageError(base);
};
auto acceptPoint = [&](const TrustRegionEvaluation& point) {
// 参数、曲线和诊断同步发布,两个目标仅改变候选验收指标。
current = point;
publishAcceptedPoint(current);
restoreEvaluationState(current);
};
double effectiveImprovementBaseline = stageError(current);
emit logMessageGenerated(tr("LM sampling: %1 points (%2 intervals per log-time decade).")
.arg(current.breakdown.residualVector.size() / 2).arg(kAutoFitIntervalsPerDecade));
auto registerEffectiveImprovement = [&](double fitness) -> bool {
const double requiredImprovement = qMax(
effectiveAbsoluteImprovement,
qAbs(effectiveImprovementBaseline) * effectiveRelativeImprovement);
const double improvement = effectiveImprovementBaseline - fitness;
if(improvement < requiredImprovement) {
return false;
}
// 有效改善后更新累计基准,并清除停滞计数。
globalFallbackAttempted = false;
effectiveImprovementBaseline = fitness;
consecutiveIneffectiveSteps = 0;
stagnationConfirmationRequested = false;
return true;
};
// 两个目标均沿用原 LM 的有效改善门槛及重建后停滞确认。
auto recordIneffectiveStep = [&]() -> bool {
++consecutiveIneffectiveSteps;
if(!globalFallbackAttempted) {
return false;
}
globalFallbackAttempted = false;
if(consecutiveIneffectiveSteps < maximumIneffectiveSteps) {
return false;
}
if(stagnationConfirmationRequested) {
return true;
}
stagnationConfirmationRequested = true;
rebuildRequested = true;
rebuildReason = QT_TR_NOOP("confirm stagnation after ineffective steps");
emit logMessageGenerated(
tr("No effective improvement for %1 consecutive steps; "
"rebuilding sensitivity model for confirmation")
.arg(consecutiveIneffectiveSteps));
consecutiveIneffectiveSteps = 0;
return false;
};
emit logMessageGenerated(
tr("LM effective improvement threshold: max(%1, %2% of baseline error); "
"%3 consecutive ineffective steps trigger convergence confirmation")
.arg(effectiveAbsoluteImprovement, 0, 'e', 2)
.arg(effectiveRelativeImprovement * 100.0, 0, 'f', 2)
.arg(maximumIneffectiveSteps));
// 在同一个真实工作点逐参数做单边差分。失败先在原方向缩步,再反向尝试;
// 正常情况下每列仍只需一次真实求解,重试也计入总评价预算。
auto rebuildSensitivity = [&]() -> bool {
// 形状段也受原 LM 求解额度约束,差分范围及重试规则与总误差段相同。
const int evaluationLimit = maximumEvaluations;
const TrustRegionEvaluation base = current;
const QVector<double> baseResidual = trustRegionFullResidual(base.breakdown);
const int residualCount = baseResidual.size();
if(residualCount <= 0) {
return false;
}
jacobian = QVector<QVector<double> >(
residualCount, QVector<double>(dimensions, 0.0));
jacobianColumnValid.fill(false, dimensions);
TrustRegionEvaluation bestProbe;
int bestProbeColumn = -1;
double bestProbeDelta = 0.0;
// 两段共用原 LM 的局部差分尺度,避免目标切换同时改变灵敏度算法。
const double finiteDifferenceStep = qMin(
sensitivityStep, qMax(5.0e-3, trustRadius * 0.5));
for(int column = 0;
column < dimensions &&
m_totalEvaluations < evaluationLimit &&
processPauseAndStop();
++column) {
const int parameterIndex = m_enabledParamIndices[column];
const double lower = m_parameterLower[parameterIndex];
const double upper = m_parameterUpper[parameterIndex];
if(upper <= lower) continue;
// 表皮包含零和负值,沿用原 LM 按局部物理尺度扰动的方式。
double localStep = finiteDifferenceStep;
if(parameterIndex == 1) {
localStep = qMin(localStep, 0.02 * qMax(0.1, qAbs(base.parameters[column])) / (upper - lower));
}
double positiveRoom = 1.0 - base.coordinates[column];
double negativeRoom = base.coordinates[column];
double preferredSign = positiveRoom >= negativeRoom ? 1.0 : -1.0;
bool columnBuilt = false;
double probeStep = localStep;
for(int directionAttempt = 0;
directionAttempt < 2 &&
!columnBuilt &&
m_totalEvaluations < evaluationLimit && processPauseAndStop();
++directionAttempt) {
double direction = directionAttempt == 0
? preferredSign : -preferredSign;
double availableRoom = direction > 0.0
? positiveRoom : negativeRoom;
double deltaMagnitude = qMin(probeStep, availableRoom);
for(int shrinkAttempt = 0; shrinkAttempt < 3 && !columnBuilt &&
m_totalEvaluations < evaluationLimit && processPauseAndStop(); ++shrinkAttempt) {
if(deltaMagnitude < minimumCoordinateStep) {
break;
}
const bool canShrink = shrinkAttempt < 2 && deltaMagnitude * 0.5 >= minimumCoordinateStep;
TrustRegionEvaluation probe;
probe.coordinates = base.coordinates;
probe.coordinates[column] += direction * deltaMagnitude;
probe.parameters = parametersFromCoordinates(probe.coordinates);
probe.valid = evaluateTrustRegionPoint(
probe.parameters,
&probe.fitness,
&probe.breakdown,
&probe.curve,
&probe.elapsedMs);
QString decision = probe.valid
? "sensitivity_valid"
: (canShrink ? "sensitivity_retry_smaller" : (directionAttempt == 0
? "sensitivity_retry_opposite"
: "sensitivity_invalid"));
writeTraceRow(m_currentIteration,
column,
"trust_region_sensitivity",
probe.parameters,
probe.fitness,
probe.valid,
probe.elapsedMs,
(shapeStage ? "shape_" : "total_") + decision,
probe.valid ? &probe.breakdown : nullptr);
if(!probe.valid) {
restoreEvaluationState(base);
if(!canShrink) break;
deltaMagnitude *= 0.5;
probeStep = deltaMagnitude;
continue;
}
double delta = probe.coordinates[column] -
base.coordinates[column];
if(qAbs(delta) < minimumCoordinateStep ||
trustRegionFullResidual(probe.breakdown).size() != residualCount) {
restoreEvaluationState(base);
break;
}
// 第 column 列是固定残差向量相对内部参数坐标的有限差分:
// J[:,column] = (r_probe-r_base)/delta。
const QVector<double> probeResidual = trustRegionFullResidual(probe.breakdown);
for(int row = 0; row < residualCount; ++row) {
jacobian[row][column] = (probeResidual[row] - baseResidual[row]) / delta;
}
jacobianColumnValid[column] = true;
columnBuilt = true;
// 沿用原 LM 的缓存探针验收,仅按当前段的目标选择更优探针。
if(acceptable(probe, base) &&
(!bestProbe.valid || stageError(probe) < stageError(bestProbe))) {
bestProbe = probe;
bestProbeColumn = column;
bestProbeDelta = delta;
}
restoreEvaluationState(base);
}
}
}
int validColumnCount = 0;
for(int i = 0; i < jacobianColumnValid.size(); ++i) {
if(jacobianColumnValid[i]) {
++validColumnCount;
}
}
// 两段都要求至少一列有效灵敏度,零梯度仍交给原 LM 停滞判断。
if(validColumnCount == 0 || m_shouldStop) {
restoreEvaluationState(base);
return false;
}
// 两段均保留原 LM 接受更优缓存探针的行为。
// 所有列先基于同一个 base 建完,再用已知割线平移 Jacobian,避免边算边移动基点。
if(bestProbe.valid && bestProbeColumn >= 0) {
QVector<double> acceptedStep(dimensions, 0.0);
acceptedStep[bestProbeColumn] = bestProbeDelta;
updateTrustRegionJacobian(
&jacobian,
trustRegionFullResidual(base.breakdown),
trustRegionFullResidual(bestProbe.breakdown),
acceptedStep);
acceptPoint(bestProbe);
writeTraceRow(m_currentIteration,
bestProbeColumn,
"trust_region_sensitivity_accept",
current.parameters,
current.fitness,
true,
0,
shapeStage ? "shape_accepted_cached_probe" : "total_accepted_cached_probe",
&current.breakdown);
emit logMessageGenerated(
tr("Sensitivity probe accepted: current objective error=%1")
.arg(stageError(current), 0, 'e', 4));
} else {
restoreEvaluationState(current);
}
acceptedSinceRebuild = 0;
consecutivePoorPredictions = 0;
consecutiveRejectedSteps = 0;
rebuildRequested = false;
// 若重建过程中接受了试算点,当前模型已通过割线平移而不是在新点完整
// 重算;再遇到最小半径停滞时仍允许做一次真正的新点重建。
modelRebuiltAtMinimumRadius =
trustRadius <= minimumTrustRadius * 1.01 &&
!bestProbe.valid;
emit logMessageGenerated(
tr("Sensitivity model rebuilt: %1/%2 parameter columns valid")
.arg(validColumnCount)
.arg(dimensions));
return true;
};
int completedIterations = 0;
for(int phase = 0; phase < 2 && !m_shouldStop; ++phase) {
shapeStage = phase == 0;
phaseIterations = 0;
maximumEvaluations = m_totalEvaluations + phaseEvaluationBudget;
stopReason = LM_MAX_ITERATIONS;
effectiveImprovementBaseline = stageError(current);
trustRadius = 0.12;
damping = 0.01;
consecutiveRejectedSteps = 0;
consecutiveSolverFailures = 0;
acceptedSinceRebuild = 0;
consecutiveIneffectiveSteps = 0;
consecutivePoorPredictions = 0;
modelRebuiltAtMinimumRadius = false;
stagnationConfirmationRequested = false;
globalFallbackAttempted = false;
jacobian.clear();
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 logMessageGenerated(shapeStage
? tr("Joint LM shape phase: adjust all selected parameters by full shape error.")
: tr("Joint LM total phase: adjust all selected parameters by total error."));
emit logMessageGenerated(tr("LM phase budget: %1 iterations, %2 evaluations.")
.arg(m_maxIterations).arg(phaseEvaluationBudget));
writeTraceRow(completedIterations, -1, "stage_switch", current.parameters, current.fitness,
true, 0, shapeStage ? "height_to_shape_lm" : "shape_to_total", &current.breakdown);
for(int iteration = completedIterations;
!m_shouldStop && phaseIterations < m_maxIterations && m_totalEvaluations < maximumEvaluations;
++iteration) {
m_currentIteration = iteration;
completedIterations = iteration + 1;
if(!processPauseAndStop()) break;
if(stageError(current) < m_targetError) {
stopReason = LM_TARGET_ACHIEVED;
break;
}
if(rebuildRequested) {
emit logMessageGenerated(tr("Rebuilding sensitivity model: %1").arg(tr(rebuildReason)));
writeTraceRow(m_currentIteration, -1, "sensitivity_rebuild", current.parameters,
current.fitness, true, 0, rebuildReason, &current.breakdown);
if(!rebuildSensitivity()) {
stopReason = m_shouldStop
? LM_USER_STOPPED
: LM_LOCAL_OPTIMUM;
break;
}
if(stageError(current) < m_targetError) {
stopReason = LM_TARGET_ACHIEVED;
break;
}
if(m_totalEvaluations >= maximumEvaluations) {
stopReason = LM_MAX_ITERATIONS;
break;
}
// 差分探针不等于 LM 候选;重建后继续按当前阶段求步并真实评价,再确认停滞。
registerEffectiveImprovement(stageError(current));
}
// 两段分别计数;无可行方向也消耗一次尝试,避免无限缩步重建。
++phaseIterations;
const QVector<double> objectiveResidual = shapeStage
? current.breakdown.shapeResiduals : current.breakdown.residualVector;
const int rowOffset = shapeStage ? current.breakdown.residualVector.size() : 0;
const QVector<QVector<double> > objectiveJacobian = jacobian.mid(rowOffset, objectiveResidual.size());
QVector<double> objectiveCoordinates;
if(shapeStage) {
// Fisher 窗口与形状残差使用同一组斜率区间中心。
const int pointCount = current.breakdown.residualVector.size() / 2;
const int lag = autoFitShapeLag(pointCount);
for(int i = 0; i < pointCount - lag; ++i)
objectiveCoordinates.append((i + lag * 0.5) / (pointCount - 1));
}
const QVector<TrustRegionFisher> information = buildTrustRegionFisher(
objectiveJacobian, objectiveResidual, jacobianColumnValid, objectiveCoordinates);
// 两段都使用当前目标的全局信息,不按时间窗口另行选参。
const TrustRegionFisher& global = information[kAutoFitTimeWindowCount];
QVector<int> selectedColumns;
QVector<double> coordinateStep;
double predictedReduction = 0.0;
// 两段都联合调整全部有效自由参数,只切换残差及其对应的 Jacobian 行。
for(int column = 0; column < dimensions; ++column) {
if(jacobianColumnValid[column] && global.matrix[column][column] > 0.0)
selectedColumns.append(column);
}
globalFallbackAttempted = true;
if(selectedColumns.isEmpty() || !buildTrustRegionFisherStep(
global, selectedColumns, current.coordinates, damping,
trustRadius, minimumCoordinateStep, &coordinateStep, &predictedReduction)) {
selectedColumns.clear();
}
// 当前阶段没有可行下降步时,收缩半径并进入原有重建/收敛处理。
if(selectedColumns.isEmpty()) {
if(trustRadius <= minimumTrustRadius * 1.01 &&
modelRebuiltAtMinimumRadius) {
stopReason = LM_LOCAL_OPTIMUM;
break;
}
trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5);
damping = qMin(1.0e8, damping * 4.0);
rebuildRequested = true;
rebuildReason = QT_TR_NOOP("no feasible descent step");
if(recordIneffectiveStep()) {
stopReason = LM_LOCAL_OPTIMUM;
break;
}
continue;
}
double stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep));
QVector<double> candidateCoordinates = current.coordinates;
for(int column = 0; column < dimensions; ++column) {
candidateCoordinates[column] += coordinateStep[column];
}
// 窗口和参数索引仅写入已有跟踪文件,不向拟合日志窗口增加分段信息。
QStringList selectedParameterIndices;
for(int i = 0; i < selectedColumns.size(); ++i) {
selectedParameterIndices << QString::number(m_enabledParamIndices[selectedColumns[i]]);
}
QString selectionName = QString(shapeStage ? "shape_global_params_" : "total_global_params_") +
selectedParameterIndices.join("_");
TrustRegionEvaluation candidate;
candidate.coordinates = candidateCoordinates;
candidate.parameters = parametersFromCoordinates(candidate.coordinates);
candidate.valid = evaluateTrustRegionPoint(
candidate.parameters,
&candidate.fitness,
&candidate.breakdown,
&candidate.curve,
&candidate.elapsedMs);
if(m_shouldStop) {
restoreEvaluationState(current);
stopReason = LM_USER_STOPPED;
break;
}
if(!candidate.valid) {
// 求解失败的候选不能改变 current。先完整恢复上一个已接受参数和
// 对应误差快照,再缩小信赖域;连续失败达到上限才终止整个拟合。
++consecutiveSolverFailures;
++consecutiveRejectedSteps;
consecutivePoorPredictions = 0;
trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5);
damping = qMin(1.0e8, damping * 4.0);
writeTraceRow(m_currentIteration,
-1,
"trust_region_candidate",
candidate.parameters,
candidate.fitness,
false,
candidate.elapsedMs,
"solver_invalid_" + selectionName,
nullptr);
restoreEvaluationState(current);
if(consecutiveSolverFailures >= 2) {
rebuildRequested = true;
rebuildReason = QT_TR_NOOP("2 consecutive solver failures");
}
if(consecutiveSolverFailures >= m_maxConsecutiveFailures) {
stopReason = LM_CONSECUTIVE_FAILURES;
break;
}
if(recordIneffectiveStep()) {
stopReason = LM_LOCAL_OPTIMUM;
break;
}
continue;
}
if(m_shouldStop) {
restoreEvaluationState(current);
stopReason = LM_USER_STOPPED;
break;
}
consecutiveSolverFailures = 0;
// 有效候选即使最终被拒绝,也提供了一条真实割线,可用于修正下一轮
// 局部模型;是否成为新工作点由当前阶段的目标和约束共同决定。
const AutoFitObjectiveBreakdownLM oldBreakdown = current.breakdown;
updateTrustRegionJacobian(
&jacobian,
trustRegionFullResidual(oldBreakdown),
trustRegionFullResidual(candidate.breakdown),
coordinateStep);
// reductionRatio 衡量局部线性模型的可信度:接近 1 表示预测准确;
// 值较小表示虽然可能下降,但模型低估了非线性,需要收紧下一步。
const double objectiveEnergy = 0.5 * trustRegionSquaredNorm(objectiveResidual);
const QVector<double> candidateResidual = shapeStage
? candidate.breakdown.shapeResiduals : candidate.breakdown.residualVector;
const double candidateEnergy = 0.5 * trustRegionSquaredNorm(candidateResidual);
double actualReduction = objectiveEnergy - candidateEnergy;
double reductionRatio = predictedReduction > 1.0e-14 ? actualReduction / predictedReduction : 0.0;
// 使用固定采样和同一阶段目标比较预测与实际改善。接近收敛时的微小
// 预测量交给停滞逻辑处理,避免比例数值波动反复触发昂贵的全参数重建。
const double predictionFloor = qMax(1.0e-14, objectiveEnergy * 1.0e-8);
const bool poorPrediction = predictedReduction > predictionFloor &&
(!isFiniteNumber(reductionRatio) || reductionRatio < 0.25);
consecutivePoorPredictions = poorPrediction ? consecutivePoorPredictions + 1 : 0;
if(consecutivePoorPredictions >= 2) {
rebuildRequested = true;
rebuildReason = QT_TR_NOOP("2 consecutive steps with actual improvement below 25% of prediction");
}
bool accepted = acceptable(candidate, current);
if(accepted) {
// 当前阶段接受候选后同步发布参数、曲线和诊断。
// 模型预测可靠时减小阻尼并可扩大半径,预测较差时保守收缩。
acceptPoint(candidate);
++acceptedSinceRebuild;
consecutiveRejectedSteps = 0;
// 两段共用原 LM 阻尼与信赖半径更新,预测比不替代目标误差验收。
if(reductionRatio > 0.75) {
damping = qMax(1.0e-8, damping * 0.5);
if(stepNorm >= trustRadius * 0.8) {
trustRadius = qMin(
maximumTrustRadius, trustRadius * 1.6);
}
} else if(reductionRatio > 0.25) {
damping = qMax(1.0e-8, damping * 0.8);
} else {
damping = qMin(1.0e8, damping * 2.0);
trustRadius = qMax(
minimumTrustRadius, trustRadius * 0.75);
}
if(acceptedSinceRebuild >= 10 && !rebuildRequested) {
rebuildRequested = true;
rebuildReason = QT_TR_NOOP("10 accepted steps since last rebuild");
}
modelRebuiltAtMinimumRadius = false;
} else {
// 拒绝时参数恢复到 current;沿用原 LM 的阻尼和半径收缩规则,
// 模型是否重建仍由预测质量判断,不因更换目标而改变。
++consecutiveRejectedSteps;
damping = qMin(1.0e8, damping * 4.0);
trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5);
restoreEvaluationState(current);
}
writeTraceRow(m_currentIteration,
-1,
"trust_region_candidate",
candidate.parameters,
candidate.fitness,
true,
candidate.elapsedMs,
accepted
? "accepted_" + selectionName
: "rejected_" + selectionName,
&candidate.breakdown);
QString componentDisplayName = shapeStage ? tr("shape deviation") : tr("total error");
emit logMessageGenerated(
tr("Iteration %1: focus=%2, parameters=%3, error=%4, result=%5")
.arg(iteration + 1)
.arg(componentDisplayName)
.arg(selectedColumns.size())
.arg(stageError(candidate), 0, 'e', 4)
.arg(accepted ? tr("accepted") : tr("rejected")));
emit progressUpdated(phaseIterations, m_globalBestFitness);
const bool effectiveImprovement = registerEffectiveImprovement(stageError(current));
if(!effectiveImprovement && recordIneffectiveStep()) stopReason = LM_LOCAL_OPTIMUM;
if(stopReason == LM_LOCAL_OPTIMUM) break;
if(stageError(current) < m_targetError) {
stopReason = LM_TARGET_ACHIEVED;
break;
}
if(trustRadius <= minimumTrustRadius * 1.01 &&
consecutiveRejectedSteps >= 2 && consecutivePoorPredictions >= 2) {
if(modelRebuiltAtMinimumRadius) {
stopReason = LM_LOCAL_OPTIMUM;
break;
}
rebuildRequested = true;
rebuildReason = QT_TR_NOOP("inaccurate model at minimum trust radius");
}
}
if(shapeStage) shapeIterations = phaseIterations;
else totalIterations = phaseIterations;
if(m_shouldStop || stopReason == LM_CONSECUTIVE_FAILURES || stopReason == LM_OPTIMIZATION_FAILED)
break;
// 形状达标、确认停滞或用完额度后,保留当前参数进入总误差段。
if(shapeStage) {
emit logMessageGenerated(tr("Shape LM phase ended: %1").arg(getStopReasonDescription(stopReason)));
writeTraceRow(m_currentIteration, -1, "lm_phase_end", current.parameters, current.fitness,
true, 0, QString("shape_stop_reason_%1").arg(static_cast<int>(stopReason)), &current.breakdown);
}
}
if(completedIterations > 0) {
m_currentIteration = completedIterations - 1;
}
restoreEvaluationState(current);
emit logMessageGenerated(tr("Adaptive fitting counts: %1 shape LM iterations, %2 total LM iterations, %3 total evaluations.")
.arg(shapeIterations).arg(totalIterations).arg(m_totalEvaluations));
if(m_shouldStop) {
return LM_USER_STOPPED;
}
if(!shapeStage && current.fitness < m_targetError) {
return LM_TARGET_ACHIEVED;
}
if(stopReason == LM_CONSECUTIVE_FAILURES ||
stopReason == LM_LOCAL_OPTIMUM ||
stopReason == LM_OPTIMIZATION_FAILED) {
return stopReason;
}
return LM_MAX_ITERATIONS;
}
double nmCalculationAutoFitLM::evaluateFitness(const QVector<double>& parameters, bool retrySolver)
{
// LM 候选评价函数,也是自动拟合最核心的闭环:
// 1. 校验候选参数是否在用户设置的上下界和基本物理范围内;
// 2. 将参数写入 DataManager 的储层/目标井对象;
// 3. 调用真实数值求解器,生成模拟结果;
// 4. 从本次求解任务读取目标井 result log-log 曲线;
// 5. 与目标 history log-log 曲线计算误差,误差越小代表拟合越好。
//
// 返回 1e10 表示候选评价失败或结果不可用。
const QString funcName = QString("evaluateError[%1]").arg(m_currentIteration);
static int callCount = 0;
callCount++;
m_lastEvaluatedLogLogData.clear();
m_lastObjectiveBreakdown = AutoFitObjectiveBreakdownLM();
try {
DEBUG_OUT(QString("%1: Call #%2 - Starting evaluation with %3 parameters")
.arg(funcName).arg(callCount).arg(parameters.size()));
// 打印参数值
QString paramStr = "Parameters: ";
for(int i = 0; i < parameters.size(); ++i) {
paramStr += QString("[%1]=%2 ").arg(i).arg(parameters[i], 0, 'f', 6);
}
DEBUG_OUT(QString("%1: %2").arg(funcName).arg(paramStr));
// 1. 参数有效性检查。这里先拦截明显非法的候选,
// 避免把非有限数、越界值或极端危险值传给求解器。
if(!validateParameters(parameters)) {
DEBUG_OUT(QString("%1: Call #%2 - VALIDATION FAILED").arg(funcName).arg(callCount));
// 详细检查每个参数
for(int i = 0; i < parameters.size(); ++i) {
if(!isFiniteNumber(parameters[i])) {
DEBUG_OUT(QString(" -> Param[%1] is NOT finite: %2").arg(i).arg(parameters[i]));
}
if(i < m_enabledParamIndices.size()) {
int paramIndex = m_enabledParamIndices[i];
if(paramIndex >= 0 && paramIndex < m_parameterLower.size()) {
double lower = m_parameterLower[paramIndex];
double upper = m_parameterUpper[paramIndex];
if(parameters[i] < lower) {
DEBUG_OUT(QString(" -> Param[%1]=%2 < lower bound %3")
.arg(i).arg(parameters[i]).arg(lower));
}
if(parameters[i] > upper) {
DEBUG_OUT(QString(" -> Param[%1]=%2 > upper bound %3")
.arg(i).arg(parameters[i]).arg(upper));
}
// 检查危险值。这些条件不是严格物理模型定义,
// 而是工程保护:避免求解器在明显异常输入下崩溃或返回无意义曲线。
switch(paramIndex) {
case 0: // 渗透率
if(parameters[i] <= 1e-6) {
DEBUG_OUT(QString(" -> REJECTED: Permeability too small: %1").arg(parameters[i]));
}
break;
case 2: // 井筒储集系数
if(parameters[i] <= 1e-8) {
DEBUG_OUT(QString(" -> REJECTED: Wellbore storage too small: %1").arg(parameters[i]));
}
break;
case 3: // 孔隙度
if(parameters[i] <= 1e-4 || parameters[i] >= 0.95) {
DEBUG_OUT(QString(" -> REJECTED: Unrealistic porosity: %1").arg(parameters[i]));
}
break;
}
}
}
}
return 1e10;
}
DEBUG_OUT(QString("%1: Call #%2 - Parameters validated OK").arg(funcName).arg(callCount));
// 2. 数据管理器检查。后续参数写回和求解器组装都依赖当前 DataManager。
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
if(!dataManager) {
DEBUG_OUT(QString("%1: Call #%2 - DataManager is NULL").arg(funcName).arg(callCount));
return 1e10;
}
DEBUG_OUT(QString("%1: Call #%2 - DataManager OK").arg(funcName).arg(callCount));
// 3. 应用参数。parameters 的顺序与 m_enabledParamIndices 对齐,
// applyParametersToDataManager() 会把它们拆分写入储层参数和目标井参数。
try {
DEBUG_OUT(QString("%1: Call #%2 - Applying parameters...").arg(funcName).arg(callCount));
applyParametersToDataManager(parameters);
DEBUG_OUT(QString("%1: Call #%2 - Parameters applied successfully").arg(funcName).arg(callCount));
} catch(const std::exception& e) {
DEBUG_OUT(QString("%1: Call #%2 - FAILED to apply parameters: %3")
.arg(funcName).arg(callCount).arg(e.what()));
return 1e10;
}
// Dfc 和裂缝半长属于网格输入。标记网格失效,使下一次任务基于当前参数快照重建。
const bool fractureGridParameterSelected =
(m_parameterSelected.size() > 5 && m_parameterSelected[5]) ||
(m_parameterSelected.size() > 6 && m_parameterSelected[6]);
if(fractureGridParameterSelected) {
dataManager->invalidatePebiGrid();
}
// 4. 运行求解器。真实求解器偶发失败时允许重试,避免一次 DLL 调用异常
// 直接让初始评价失败;迭代候选不重复相同参数,交给信赖域缩步。
QVector<QVector<double>> solverResult;
const int maxRetries = retrySolver ? 2 : 0;
bool solverSuccess = false;
for(int retry = 0; retry <= maxRetries; ++retry) {
if(m_shouldStop) {
DEBUG_OUT(QString("%1: Call #%2 - User stop requested").arg(funcName).arg(callCount));
return 1e10;
}
try {
DEBUG_OUT(QString("%1: Call #%2 - Solver attempt %3/%4")
.arg(funcName).arg(callCount).arg(retry + 1).arg(maxRetries + 1));
if(retry > 0) {
DEBUG_OUT(QString("%1: Call #%2 - Retry delay...").arg(funcName).arg(callCount));
msleep(1000);
}
solverResult = runSolver();
// 详细检查求解器结果
if(solverResult.isEmpty()) {
DEBUG_OUT(QString("%1: Call #%2 - Solver returned EMPTY result").arg(funcName).arg(callCount));
} else {
DEBUG_OUT(QString("%1: Call #%2 - Solver returned %3 arrays")
.arg(funcName).arg(callCount).arg(solverResult.size()));
for(int i = 0; i < solverResult.size(); ++i) {
DEBUG_OUT(QString(" -> Array[%1] size: %2").arg(i).arg(solverResult[i].size()));
}
if(validateSolverResult(solverResult)) {
DEBUG_OUT(QString("%1: Call #%2 - Solver result VALIDATED on attempt %3")
.arg(funcName).arg(callCount).arg(retry + 1));
solverSuccess = true;
break;
} else {
DEBUG_OUT(QString("%1: Call #%2 - Solver result VALIDATION FAILED on attempt %3")
.arg(funcName).arg(callCount).arg(retry + 1));
}
}
} catch(const std::exception& e) {
DEBUG_OUT(QString("%1: Call #%2 - Solver EXCEPTION on attempt %3: %4")
.arg(funcName).arg(callCount).arg(retry + 1).arg(e.what()));
}
}
if(!solverSuccess) {
DEBUG_OUT(QString("%1: Call #%2 - ALL SOLVER ATTEMPTS FAILED").arg(funcName).arg(callCount));
return 1e10;
}
// 5. 获取 LogLog 数据。runSolverDll() 直接从求解任务复制目标井曲线,
// 不再依赖 DataManager 中可能被其它井或上一粒子改写的共享结果。
QVector<QVector<double>> resultLogLogData = m_lastEvaluatedLogLogData;
try {
if(!validateLogLogData(resultLogLogData)) {
DEBUG_OUT(QString("%1: Call #%2 - LogLog data VALIDATION FAILED")
.arg(funcName).arg(callCount));
// 详细输出LogLog数据问题
if(resultLogLogData.size() < 3) {
DEBUG_OUT(QString(" -> LogLog arrays count: %1 (need 3)")
.arg(resultLogLogData.size()));
} else {
DEBUG_OUT(QString(" -> LogLog array sizes: X=%1, Y1=%2, Y2=%3")
.arg(resultLogLogData[0].size())
.arg(resultLogLogData[1].size())
.arg(resultLogLogData[2].size()));
// 检查数据有效性
for(int i = 0; i < qMin(5, resultLogLogData[0].size()); ++i) {
if(!isFiniteNumber(resultLogLogData[0][i]) ||
!isFiniteNumber(resultLogLogData[1][i]) ||
!isFiniteNumber(resultLogLogData[2][i])) {
DEBUG_OUT(QString(" -> Invalid data at index %1: X=%2, Y1=%3, Y2=%4")
.arg(i)
.arg(resultLogLogData[0][i])
.arg(resultLogLogData[1][i])
.arg(resultLogLogData[2][i]));
}
}
}
return 1e10;
}
DEBUG_OUT(QString("%1: Call #%2 - LogLog data validated, size: %3")
.arg(funcName).arg(callCount).arg(resultLogLogData[0].size()));
} catch(const std::exception& e) {
DEBUG_OUT(QString("%1: Call #%2 - Error getting LogLog data: %3")
.arg(funcName).arg(callCount).arg(e.what()));
return 1e10;
}
// 6. 计算误差。这里比较的是目标井 history log-log 与当前模拟 result log-log。
double error;
try {
DEBUG_OUT(QString("%1: Call #%2 - Calculating error...")
.arg(funcName).arg(callCount));
error = calculateLogLogCurveError(m_targetLogLogData, resultLogLogData);
if(!isFiniteNumber(error) || error < 0) {
DEBUG_OUT(QString("%1: Call #%2 - INVALID error value: %3")
.arg(funcName).arg(callCount).arg(error));
return 1e10;
}
DEBUG_OUT(QString("%1: Call #%2 - SUCCESS! Error = %3")
.arg(funcName).arg(callCount).arg(error, 0, 'e', 6));
// 保存最后一次有效曲线,供 LM 候选评价和精英保护复用。
m_lastEvaluatedLogLogData = resultLogLogData;
} catch(const std::exception& e) {
DEBUG_OUT(QString("%1: Call #%2 - Error calculation FAILED: %3")
.arg(funcName).arg(callCount).arg(e.what()));
return 1e10;
}
return error;
} catch(const std::exception& e) {
DEBUG_OUT(QString("%1: Call #%2 - TOP-LEVEL EXCEPTION: %3")
.arg(funcName).arg(callCount).arg(e.what()));
return 1e10;
} catch(...) {
DEBUG_OUT(QString("%1: Call #%2 - UNKNOWN TOP-LEVEL EXCEPTION")
.arg(funcName).arg(callCount));
return 1e10;
}
}
// ==================== 参数应用方法 ====================
void nmCalculationAutoFitLM::applyParametersToDataManager(const QVector<double>& parameters)
{
// 将粒子的“启用参数向量”写回项目数据。
// parameters 的维度必须等于用户勾选的参数数量,顺序由 m_enabledParamIndices 决定。
// 这里不直接跑求解器,只负责把 DataManager 调整到该粒子对应的模型状态。
if(parameters.size() != getEnabledParameterCount()) {
return;
}
updateReservoirParameters(parameters);
updateWellParameters(parameters);
}
void nmCalculationAutoFitLM::updateReservoirParameters(const QVector<double>& parameters)
{
// 更新储层级参数。井级参数 skin/wellboreC 不在这里改,由 updateWellParameters() 负责。
// 这里先取 DataManager 中 reservoir 的副本,修改后再整体写回 DataManager。
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
nmDataReservoir reservoirData = dataManager->getReservoirDataCopy();
// paramIndex 是粒子 position 中的索引;i 是完整 7 个参数体系中的索引。
// 只有 m_parameterSelected[i] 为 true 时,才从 parameters 中消费一个值。
int paramIndex = 0;
for(int i = 0; i < m_parameterSelected.size(); ++i) {
if(m_parameterSelected[i] && paramIndex < parameters.size()) {
double value = parameters[paramIndex];
switch(i) {
case 0: // 渗透率
reservoirData.getPermeability().setValue(value);
break;
case 3: // 孔隙度
reservoirData.getPorosity().setValue(value);
break;
}
paramIndex++;
}
}
// 更新数据管理器
dataManager->updateReservoirData(reservoirData);
}
void nmCalculationAutoFitLM::updateWellParameters(const QVector<double>& parameters)
{
// 更新目标井上的拟合参数。目前井级可拟合参数主要是:
// - skin:写入第一个 perforation;
// - wellboreC:写入井筒储集系数;
// - Dfc/裂缝半长:只写入垂直压裂井或多段压裂水平井。
// 如果目标井不存在或没有射孔数据,这里只记录 debug,不抛异常。
nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance();
// 只更新当前目标井的井参数,避免多井项目中误改其他井。
//QVector<nmDataWellBase*> wells = dataManager->getWellDataList();
nmDataWellBase* pWell = dataManager->findWellByName(m_targetWellName);
if(!pWell) return;
// 先在参数副本中组装本次候选值,全部解析完成后再写回现有井对象。
// 这样不会触发整井赋值,也不会删除并重建井内已有的射孔对象。
nmDataPerforation* pPerforation = pWell->getPerforation(0);
nmDataAttribute skinAttr;
bool updateSkin = false;
if(pPerforation) {
skinAttr = pPerforation->getSkin();
}
nmDataAttribute wellboreAttr = pWell->getWellboreStorage();
bool updateWellboreStorage = false;
nmDataVerticalFracturedWell* pVerticalFracturedWell =
dynamic_cast<nmDataVerticalFracturedWell*>(pWell);
nmDataHorizontalFracturedWell* pHorizontalFracturedWell =
dynamic_cast<nmDataHorizontalFracturedWell*>(pWell);
nmDataAttribute dfcAttr;
nmDataAttribute fractureHalfLengthAttr;
if(pVerticalFracturedWell) {
dfcAttr = pVerticalFracturedWell->getDfc();
fractureHalfLengthAttr = pVerticalFracturedWell->getFractureHalfLength();
} else if(pHorizontalFracturedWell) {
dfcAttr = pHorizontalFracturedWell->getDfc();
fractureHalfLengthAttr = pHorizontalFracturedWell->getFractureHalfLength();
}
bool updateDfc = false;
bool updateFractureHalfLength = false;
int paramIndex = 0;
for(int i = 0; i < m_parameterSelected.size(); ++i) {
if(m_parameterSelected[i] && paramIndex < parameters.size()) {
double value = parameters[paramIndex];
switch(i) {
case 1: { // 表皮系数
if(pPerforation) {
skinAttr.setValue(value);
updateSkin = true;
}
}
break;
case 2: { // 井筒储集系数
wellboreAttr.setValue(value);
updateWellboreStorage = true;
}
break;
case 5: { // 裂缝导流能力
if(pVerticalFracturedWell || pHorizontalFracturedWell) {
dfcAttr.setValue(value);
updateDfc = true;
}
}
break;
case 6: { // 裂缝半长
if(pVerticalFracturedWell || pHorizontalFracturedWell) {
fractureHalfLengthAttr.setValue(value);
updateFractureHalfLength = true;
}
}
break;
}
paramIndex++;
}
}
if(updateSkin) {
pPerforation->getSkin().setValue(skinAttr.getValue());
}
if(updateWellboreStorage) {
pWell->getWellboreStorage().setValue(wellboreAttr.getValue());
}
if(pVerticalFracturedWell) {
if(updateDfc) {
pVerticalFracturedWell->getDfc().setValue(dfcAttr.getValue());
}
if(updateFractureHalfLength) {
pVerticalFracturedWell->getFractureHalfLength().setValue(
fractureHalfLengthAttr.getValue());
}
} else if(pHorizontalFracturedWell) {
if(updateDfc) {
pHorizontalFracturedWell->getDfc().setValue(dfcAttr.getValue());
}
if(updateFractureHalfLength) {
pHorizontalFracturedWell->getFractureHalfLength().setValue(
fractureHalfLengthAttr.getValue());
}
}
}
// ==================== 求解器相关方法 ====================
QVector<QVector<double>> nmCalculationAutoFitLM::runSolver()
{
// 真实求解器统一走 DLL 方式,返回值由 evaluateFitness() 继续校验。
return runSolverDll();
}
// ==================== 数据处理方法 ====================
// ==================== 算法辅助方法 ====================
void nmCalculationAutoFitLM::saveOptimizationResult()
{
// 当前函数只做日志记录。真正把最优参数写回项目数据的是
// startAutoFitting() 结束阶段的 applyParametersToDataManager(m_globalBestPosition)。
DEBUG_OUT(QString("Optimization result: error=%1, evaluations=%2/%3")
.arg(m_globalBestFitness, 0, 'e', 4)
.arg(m_successfulEvaluations)
.arg(m_totalEvaluations));
}
void nmCalculationAutoFitLM::validateAndProtectFinalResult()
{
// 最终精英保护只阻止无效结果或真正变差的结果。任何真实误差下降都应保留,
// 不能再用固定百分比门槛把已经找到的更优解恢复成初始值。
if(!m_hasValidUserSolution) {
emit logMessageGenerated(tr("No initial solution for elite protection"));
return;
}
emit logMessageGenerated(tr("=== Final Result Validation (Elite Protection) ==="));
// 使用已有的评估结果
double finalFitness = m_globalBestFitness;
double initialFitness = m_userInitialFitness;
emit logMessageGenerated(tr("Comparing results: Initial=%1, Final=%2")
.arg(initialFitness, 0, 'e', 4).arg(finalFitness, 0, 'e', 4));
bool finalValid = isFiniteNumber(finalFitness) &&
finalFitness < 1.0e9 &&
m_globalBestPosition.size() ==
m_userInitialSolution.size() &&
!m_globalBestLogLogData.isEmpty() &&
m_globalBestObjectiveBreakdown.valid;
if(finalValid) {
double improvement = initialFitness - finalFitness;
double relativeImprovement =
improvement / qMax(1.0e-10, qAbs(initialFitness));
emit logMessageGenerated(tr("Improvement: %1 (%2%)")
.arg(improvement, 0, 'e', 4)
.arg(relativeImprovement * 100, 0, 'f', 2));
}
if(!finalValid || finalFitness > initialFitness) {
emit logMessageGenerated(
tr("Elite protection triggered: final result is invalid or worse than initial"));
emit logMessageGenerated(tr("Restoring initial solution as final result"));
m_globalBestFitness = initialFitness;
m_globalBestPosition = m_userInitialSolution;
m_globalBestLogLogData = m_userInitialLogLogData;
m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown;
emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness);
emit logMessageGenerated(tr("Initial solution restored successfully"));
} else {
emit logMessageGenerated(
tr("Final result validated - solution is not worse than initial"));
}
}
// ==================== 工具方法 ====================
int nmCalculationAutoFitLM::getEnabledParameterCount() const
{
// 返回粒子维度,即用户勾选参与拟合的参数数量。
int count = 0;
for(int i = 0; i < m_parameterSelected.size(); ++i) {
if(m_parameterSelected[i]) count++;
}
return count;
}
// ==================== 验证和处理方法 ====================
bool nmCalculationAutoFitLM::validateParameters(const QVector<double>& parameters) const
{
// 参数物理范围已由拟合窗口统一校验;候选评价只检查维度、有限数和
// 用户设置的上下界,避免另一套硬编码阈值与实际搜索范围冲突。
if(parameters.size() != getEnabledParameterCount()) {
return false;
}
for(int i = 0; i < parameters.size(); ++i) {
if(!isFiniteNumber(parameters[i])) {
return false;
}
// 检查参数范围
if(i < m_enabledParamIndices.size()) {
int paramIndex = m_enabledParamIndices[i];
if(paramIndex >= 0 && paramIndex < m_parameterLower.size()) {
if(parameters[i] < m_parameterLower[paramIndex] ||
parameters[i] > m_parameterUpper[paramIndex]) {
return false;
}
}
}
}
return true;
}
bool nmCalculationAutoFitLM::validateLogLogData(const QVector<QVector<double>>& logLogData) const
{
// 校验双对数曲线结构。约定:
// logLogData[0]=time,logLogData[1]=pressure,logLogData[2]=pressure derivative。
// 三列必须长度一致,且至少有足够点数用于插值和误差计算。
if(logLogData.size() < 3) {
DEBUG_OUT("LogLog data has less than 3 arrays");
return false;
}
// 检查数组大小一致性
int size = logLogData[0].size();
if(size == 0) {
DEBUG_OUT("Empty LogLog data");
return false;
}
if(logLogData[1].size() != size || logLogData[2].size() != size) {
DEBUG_OUT(QString("LogLog data size mismatch: X=%1, Y1=%2, Y2=%3")
.arg(logLogData[0].size())
.arg(logLogData[1].size())
.arg(logLogData[2].size()));
return false;
}
// 检查最小数据点数
if(size < 5) {
DEBUG_OUT(QString("Too few LogLog data points: %1").arg(size));
return false;
}
// 数据有效性检查
for(int i = 0; i < size; ++i) {
if(!isFiniteNumber(logLogData[0][i]) ||
!isFiniteNumber(logLogData[1][i]) ||
!isFiniteNumber(logLogData[2][i])) {
DEBUG_OUT(QString("Invalid LogLog data at index %1").arg(i));
return false;
}
}
return true;
}
bool nmCalculationAutoFitLM::validateInitialValues() const
{
// 检查当前模型读取出的初始参数是否和用户勾选维度一致,并且在上下界内。
// 如果初始值越界,算法仍可继续,但日志会提示,因为精英保护可能不可用或效果变差。
if(m_initialValues.size() != m_enabledParamIndices.size()) {
DEBUG_OUT("Initial values count mismatch with enabled parameters");
return false;
}
bool allValid = true;
for(int i = 0; i < m_initialValues.size(); ++i) {
int paramIndex = m_enabledParamIndices[i];
double value = m_initialValues[i];
if(!isFiniteNumber(value)) {
DEBUG_OUT(QString("Initial value[%1] is not finite: %2").arg(i).arg(value));
allValid = false;
continue;
}
if(paramIndex < m_parameterLower.size() && paramIndex < m_parameterUpper.size()) {
double minVal = m_parameterLower[paramIndex];
double maxVal = m_parameterUpper[paramIndex];
if(value < minVal || value > maxVal) {
DEBUG_OUT(QString("Initial value[%1] = %2 is outside bounds [%3, %4]")
.arg(i).arg(value).arg(minVal).arg(maxVal));
allValid = false;
}
}
}
return allValid;
}
bool nmCalculationAutoFitLM::validateSolverResult(const QVector<QVector<double>>& result) const
{
// 校验求解器压力结果。这里检查的是 pressure result,至少需要 time 和 pressure 两列。
// result log-log 的结构会在 validateLogLogData() 中另行检查。
if(result.size() < 2) {
DEBUG_OUT("Solver result has less than 2 arrays");
return false;
}
if(result[0].size() != result[1].size()) {
DEBUG_OUT(QString("Size mismatch: X=%1, Y=%2").arg(result[0].size()).arg(result[1].size()));
return false;
}
if(result[0].size() == 0) {
DEBUG_OUT("Empty solver result");
return false;
}
// 检查最小数据点数
if(result[0].size() < 10) {
DEBUG_OUT(QString("Too few data points: %1").arg(result[0].size()));
return false;
}
// 数据有效性检查
for(int i = 0; i < result[0].size(); ++i) {
if(!isFiniteNumber(result[0][i]) || !isFiniteNumber(result[1][i])) {
DEBUG_OUT(QString("Invalid data at index %1: X=%2, Y=%3")
.arg(i).arg(result[0][i]).arg(result[1][i]));
return false;
}
}
// 检查X值单调性
bool isMonotonic = true;
for(int i = 1; i < result[0].size(); ++i) {
if(result[0][i] <= result[0][i - 1]) {
isMonotonic = false;
break;
}
}
if(!isMonotonic) {
DEBUG_OUT("X values are not monotonically increasing");
}
return true;
}
double nmCalculationAutoFitLM::calculateLogLogCurveError(
const QVector<QVector<double> >& target,
const QVector<QVector<double> >& result)
{
// 首次有效评价后固定公共时间范围。窗口与上下、左右、形状只负责诊断,
// 主目标仍由完整压力和导数残差计算,避免窗口重叠造成重复计权。
// 整个计算过程均位于 log(time)-log(value) 坐标。
const double invalidLoss = 1.0e10;
const double valueFloor = 1.0e-12;
m_lastObjectiveBreakdown = AutoFitObjectiveBreakdownLM();
if(!validateLogLogData(target) || !validateLogLogData(result)) {
return invalidLoss;
}
try {
// 非正导数无法进入双对数空间,跳过对应采样行,使用剩余有效点比较。
auto prepareCurve = [valueFloor](const QVector<QVector<double> >& data,
int firstIndex,
QVector<QPointF>* pressure,
QVector<QPointF>* derivative) -> bool {
if(!pressure || !derivative || data.size() < 3 ||
data[0].size() != data[1].size() ||
data[0].size() != data[2].size() ||
firstIndex < 0 || firstIndex >= data[0].size()) {
return false;
}
for(int i = firstIndex; i < data[0].size(); ++i) {
if(!isFiniteNumber(data[0][i]) ||
!isFiniteNumber(data[1][i]) ||
!isFiniteNumber(data[2][i]) ||
data[0][i] <= 0.0 ||
data[1][i] <= 0.0 ||
data[2][i] <= 0.0) {
continue;
}
pressure->append(QPointF(data[0][i], data[1][i]));
derivative->append(
QPointF(data[0][i], qMax(data[2][i], valueFloor)));
}
// 求解器输出可能不是严格升序,且同一时刻可能出现重复记录。
// 插值前统一排序并让后出现的记录覆盖同时间旧值,保证横坐标严格递增。
auto sortAndUnique = [](QVector<QPointF>* curve) {
std::stable_sort(
curve->begin(), curve->end(),
[](const QPointF& left, const QPointF& right) {
return left.x() < right.x();
});
QVector<QPointF> unique;
unique.reserve(curve->size());
for(int i = 0; i < curve->size(); ++i) {
if(unique.isEmpty() ||
curve->at(i).x() > unique.last().x()) {
unique.append(curve->at(i));
} else {
unique[unique.size() - 1] = curve->at(i);
}
}
*curve = unique;
};
sortAndUnique(pressure);
sortAndUnique(derivative);
return pressure->size() >= 3 && derivative->size() >= 3;
};
QVector<QPointF> targetPressure;
QVector<QPointF> targetDerivative;
QVector<QPointF> resultPressure;
QVector<QPointF> resultDerivative;
// 模拟结果已跳过 DLL 首点,因此误差计算同步忽略目标曲线首点。
if(!prepareCurve(target, 1, &targetPressure, &targetDerivative) ||
!prepareCurve(result, 0, &resultPressure, &resultDerivative)) {
return invalidLoss;
}
// 在双对数坐标中插值。二分定位用于后面的多次水平配准试算。
auto interpolateLogValue = [valueFloor](
const QVector<QPointF>& curve,
double x,
double* value) -> bool {
if(!value || curve.size() < 2 || x <= 0.0 ||
x < curve.first().x() || x > curve.last().x()) {
return false;
}
int low = 0;
int high = curve.size() - 1;
while(low < high) {
int middle = low + (high - low) / 2;
if(curve[middle].x() < x) {
low = middle + 1;
} else {
high = middle;
}
}
int right = qBound(1, low, curve.size() - 1);
int left = right - 1;
double leftLogX = qLn(curve[left].x());
double rightLogX = qLn(curve[right].x());
double denominator = rightLogX - leftLogX;
double leftLogY =
qLn(qMax(qAbs(curve[left].y()), valueFloor));
double rightLogY =
qLn(qMax(qAbs(curve[right].y()), valueFloor));
if(qAbs(denominator) <= 1.0e-12) {
*value = leftLogY;
} else {
double ratio = (qLn(x) - leftLogX) / denominator;
*value = leftLogY +
ratio * (rightLogY - leftLogY);
}
return isFiniteNumber(*value);
};
const double targetMinX = targetPressure.first().x();
const double targetMaxX = targetPressure.last().x();
const double resultMinX = resultPressure.first().x();
const double resultMaxX = resultPressure.last().x();
if(targetMinX <= 0.0 || targetMaxX <= targetMinX ||
resultMinX <= 0.0 || resultMaxX <= resultMinX) {
return invalidLoss;
}
// 仅首次有效评价使用交集建立基准;失败试算不能冻结区间,后续候选
// 必须覆盖完整基准,不允许靠丢失首尾点缩小误差或改变窗口位置。
const bool comparisonRangeFixed = m_comparisonTimeMin > 0.0;
double overlapMinX = comparisonRangeFixed
? m_comparisonTimeMin : qMax(targetMinX, resultMinX);
double overlapMaxX = comparisonRangeFixed
? m_comparisonTimeMax : qMin(targetMaxX, resultMaxX);
if(overlapMinX >= overlapMaxX) {
return invalidLoss;
}
if(targetMinX > overlapMinX || targetMaxX < overlapMaxX ||
resultMinX > overlapMinX || resultMaxX < overlapMaxX) {
DEBUG_OUT("Candidate does not cover the fixed LM comparison time range");
return invalidLoss;
}
// 所有指标共用每个对数时间数量级 20 个间隔,向上取整并包含两端,至少三个点。
// 比较区间在本轮固定,因此点数也固定;微小容差避免整数量级因舍入多取一点。
const double timeDecades = (qLn(overlapMaxX) - qLn(overlapMinX)) / qLn(10.0);
const int numPoints = qMax(3, qCeil(timeDecades * kAutoFitIntervalsPerDecade - 1.0e-10) + 1);
auto populateStageMetrics = [&](AutoFitObjectiveBreakdownLM* objective) -> bool {
// 阶段指标沿用相同网格,仅做插值评价,不增加求解点数或 DLL 调用。
// 斜率跨度保持约为全对数时域的 10%,压力和导数仍等权。
const int count = numPoints;
const int lag = autoFitShapeLag(count);
const double span = qLn(overlapMaxX) - qLn(overlapMinX);
QVector<double> residuals[2];
QVector<double> targetLogs[2], resultLogs[2];
double biases[2] = {0.0, 0.0};
for(int component = 0; component < 2; ++component) {
for(int i = 0; i < count; ++i) {
const double time = i == 0 ? overlapMinX : (i == count - 1 ? overlapMaxX
: qExp(qLn(overlapMinX) + span * i / (count - 1)));
double targetValue = 0.0, resultValue = 0.0;
if(!interpolateLogValue(component == 0 ? targetPressure : targetDerivative,
time, &targetValue) ||
!interpolateLogValue(component == 0 ? resultPressure : resultDerivative,
time, &resultValue)) return false;
const double residual = resultValue - targetValue;
residuals[component].append(residual);
targetLogs[component].append(targetValue);
resultLogs[component].append(resultValue);
// 对数时间梯形权重等价于互补窗口加权求和,避免密集段主导高度。
biases[component] += residual * ((i == 0 || i == count - 1) ? 0.5 : 1.0)
/ (count - 1);
}
}
objective->pressureVerticalBias = biases[0];
objective->derivativeVerticalBias = biases[1];
objective->verticalCommonBias = 0.5 * (biases[0] + biases[1]);
objective->verticalLoss = qAbs(objective->verticalCommonBias);
objective->verticalReliable = !(biases[0] * biases[1] < 0.0 &&
qMin(qAbs(biases[0]), qAbs(biases[1])) > 0.01);
objective->shapeResiduals.clear();
const double scale = qSqrt(0.5 / (count - lag));
for(int component = 0; component < 2; ++component) {
for(int i = 0; i < count - lag; ++i) {
objective->shapeResiduals.append(scale *
(residuals[component][i + lag] - residuals[component][i]) /
(span * lag / (count - 1)));
}
}
objective->shapeLoss = qSqrt(trustRegionSquaredNorm(objective->shapeResiduals));
populateEarlyWellboreMetrics(objective, targetLogs, resultLogs, span);
return isFiniteNumber(objective->shapeLoss);
};
QVector<double> commonX(numPoints);
QVector<double> commonLogX(numPoints);
QVector<double> targetLogPressure(numPoints);
QVector<double> targetLogDerivative(numPoints);
const double comparisonLogMinX = qLn(overlapMinX);
const double comparisonLogMaxX = qLn(overlapMaxX);
// 在公共时间范围内生成等距 log-time 网格,压力和导数各使用 numPoints 个残差。
for(int i = 0; i < numPoints; ++i) {
double logX = comparisonLogMinX +
static_cast<double>(i) *
(comparisonLogMaxX - comparisonLogMinX) /
(numPoints - 1);
commonLogX[i] = logX;
// 首尾直接使用原始端点,避免 exp(log(t)) 的舍入误差越过严格插值边界。
if(i == 0) {
commonX[i] = overlapMinX;
} else if(i == numPoints - 1) {
commonX[i] = overlapMaxX;
} else {
commonX[i] = qExp(logX);
}
if(!interpolateLogValue(
targetPressure, commonX[i],
&targetLogPressure[i]) ||
!interpolateLogValue(
targetDerivative, commonX[i],
&targetLogDerivative[i])) {
return invalidLoss;
}
}
QVector<double> pressureResidual(
numPoints, std::numeric_limits<double>::quiet_NaN());
QVector<double> derivativeResidual(
numPoints, std::numeric_limits<double>::quiet_NaN());
// 残差定义为“模拟减目标”:正值表示模拟曲线偏高,负值表示偏低。
for(int i = 0; i < numPoints; ++i) {
double resultLogPressure = 0.0;
double resultLogDerivative = 0.0;
if(!interpolateLogValue(
resultPressure, commonX[i],
&resultLogPressure) ||
!interpolateLogValue(
resultDerivative, commonX[i],
&resultLogDerivative)) {
// 已位于结果时间范围内却无法插值说明数据存在内部断点,不能补线。
return invalidLoss;
}
pressureResidual[i] =
resultLogPressure - targetLogPressure[i];
derivativeResidual[i] =
resultLogDerivative - targetLogDerivative[i];
}
AutoFitObjectiveBreakdownLM breakdown;
// 在指定中心附近计算普通均方根误差。
auto rmseAround = [](
const QVector<double>& values,
int begin,
int end,
double center) -> double {
double sum = 0.0;
int count = 0;
int validBegin = qMax(0, begin);
int validEnd =
qMin(end, static_cast<int>(values.size()));
for(int i = validBegin; i < validEnd; ++i) {
if(!isFiniteNumber(values[i])) {
continue;
}
double difference = values[i] - center;
sum += difference * difference;
++count;
}
return count > 0
? qSqrt(sum / count)
: std::numeric_limits<double>::quiet_NaN();
};
auto rmse = [&rmseAround](
const QVector<double>& values,
int begin,
int end) -> double {
return rmseAround(values, begin, end, 0.0);
};
// 普通算术平均中心保留上下偏差的符号。
auto meanCenterRange = [](
const QVector<double>& values,
int begin,
int end) -> double {
int validBegin = qMax(0, begin);
int validEnd =
qMin(end, static_cast<int>(values.size()));
double center = 0.0;
int count = 0;
for(int i = validBegin; i < validEnd; ++i) {
if(isFiniteNumber(values[i])) {
center += values[i];
++count;
}
}
if(count == 0) {
return std::numeric_limits<double>::quiet_NaN();
}
return center / count;
};
// 压力和导数合并后只求一个公共中心,表示两条曲线共同的上下位移。
// 分别去中心会把压力与导数之间真实的相对形状差异一并消除。
auto commonMeanCenterRange = [&meanCenterRange](
const QVector<double>& pressureValues,
const QVector<double>& derivativeValues,
int begin,
int end) -> double {
QVector<double> combined;
int validBegin = qMax(0, begin);
int validEnd = qMin(
end,
qMin(static_cast<int>(pressureValues.size()),
static_cast<int>(derivativeValues.size())));
combined.reserve(2 * qMax(0, validEnd - validBegin));
for(int i = validBegin; i < validEnd; ++i) {
if(isFiniteNumber(pressureValues[i])) {
combined.append(pressureValues[i]);
}
if(isFiniteNumber(derivativeValues[i])) {
combined.append(derivativeValues[i]);
}
}
return meanCenterRange(combined, 0, combined.size());
};
// 两个通道按能量等权合并,返回值与单通道 RMSE 保持同一量纲。
auto jointRmseAround = [&rmseAround](
const QVector<double>& pressureValues,
const QVector<double>& derivativeValues,
int begin,
int end,
double center) -> double {
double pressureLoss = rmseAround(
pressureValues, begin, end, center);
double derivativeLoss = rmseAround(
derivativeValues, begin, end, center);
if(!isFiniteNumber(pressureLoss) ||
!isFiniteNumber(derivativeLoss)) {
return std::numeric_limits<double>::quiet_NaN();
}
return qSqrt(0.5 *
(pressureLoss * pressureLoss +
derivativeLoss * derivativeLoss));
};
// 主目标始终使用未做上下或左右校正的完整曲线误差。
breakdown.pressureLoss =
rmse(pressureResidual, 0, numPoints);
breakdown.derivativeLoss =
rmse(derivativeResidual, 0, numPoints);
// 压力和导数各占一半权重。缩放后 residualVector 的二范数就是
// sqrt(0.5 * pressureLoss^2 + 0.5 * derivativeLoss^2)。
const double residualScale = qSqrt(0.5 / numPoints);
breakdown.residualVector.reserve(2 * numPoints);
for(int i = 0; i < numPoints; ++i) {
breakdown.residualVector.append(
residualScale *
pressureResidual[i]);
}
for(int i = 0; i < numPoints; ++i) {
breakdown.residualVector.append(
residualScale *
derivativeResidual[i]);
}
const double logGridStep =
(comparisonLogMaxX - comparisonLogMinX) /
(numPoints - 1);
const double resultLogMinX = qLn(resultMinX);
const double resultLogMaxX = qLn(resultMaxX);
// 左右配准只在所有候选位移都共同覆盖的固定区间比较,至少保留 80%
// 目标点;每个 log-time 网格间隔再细分为 8 份,提高位移分辨率。
const int minimumRegistrationPoints =
(numPoints * 4) / 5;
const int shiftSubdivisions = 8;
int maximumShiftIntervals = 4;
int registrationBegin = 0;
int registrationEnd = numPoints;
double maximumPhysicalShift =
maximumShiftIntervals * logGridStep;
// 所有位移候选使用同一组目标点。结果范围不足时逐步缩小最大位移,
// 但用于配准的固定公共区间不得少于目标网格的 80%。
auto updateRegistrationRange = [&](double maximumShift) {
registrationBegin = 0;
registrationEnd = numPoints;
while(registrationBegin < registrationEnd &&
commonLogX[registrationBegin] - maximumShift <
resultLogMinX - 1.0e-12) {
++registrationBegin;
}
while(registrationEnd > registrationBegin &&
commonLogX[registrationEnd - 1] + maximumShift >
resultLogMaxX + 1.0e-12) {
--registrationEnd;
}
};
updateRegistrationRange(maximumPhysicalShift);
while(maximumShiftIntervals > 0 &&
registrationEnd - registrationBegin <
minimumRegistrationPoints) {
--maximumShiftIntervals;
maximumPhysicalShift =
maximumShiftIntervals * logGridStep;
updateRegistrationRange(maximumPhysicalShift);
}
if(registrationEnd - registrationBegin <
minimumRegistrationPoints) {
return invalidLoss;
}
// physicalShift 为正表示模拟曲线偏右;对齐时在目标时刻右侧读取模拟值。
auto buildShiftResidual = [&](
double physicalShift,
int compareBegin,
int compareEnd,
QVector<double>* shiftedPressure,
QVector<double>* shiftedDerivative) -> bool {
if(!shiftedPressure || !shiftedDerivative) {
return false;
}
shiftedPressure->fill(
std::numeric_limits<double>::quiet_NaN(),
numPoints);
shiftedDerivative->fill(
std::numeric_limits<double>::quiet_NaN(),
numPoints);
int validBegin = qMax(0, compareBegin);
int validEnd = qMin(numPoints, compareEnd);
for(int i = validBegin; i < validEnd; ++i) {
double shiftedLogX =
commonLogX[i] + physicalShift;
if(shiftedLogX < resultLogMinX - 1.0e-12 ||
shiftedLogX >
resultLogMaxX + 1.0e-12) {
return false;
}
// 对数时间还原后再次限制到原始端点,避免 exp(log(t)) 的
// 舍入误差越过严格插值边界。
double shiftedX = qBound(
resultMinX,
qExp(qBound(resultLogMinX,
shiftedLogX,
resultLogMaxX)),
resultMaxX);
double resultLogPressure = 0.0;
double resultLogDerivative = 0.0;
if(!interpolateLogValue(
resultPressure, shiftedX,
&resultLogPressure) ||
!interpolateLogValue(
resultDerivative, shiftedX,
&resultLogDerivative)) {
return false;
}
(*shiftedPressure)[i] =
resultLogPressure - targetLogPressure[i];
(*shiftedDerivative)[i] =
resultLogDerivative - targetLogDerivative[i];
}
return true;
};
// 损失相同时优先选择绝对位移更小的候选,避免平坦 profile 在数值噪声
// 下无故偏向搜索边界。
auto isBetterProfileValue = [](
double loss,
double shift,
double bestLoss,
double bestShift) -> bool {
const double tolerance = 1.0e-12;
return loss < bestLoss - tolerance ||
(qAbs(loss - bestLoss) <= tolerance &&
qAbs(shift) < qAbs(bestShift));
};
const int halfShiftStepCount =
maximumShiftIntervals * shiftSubdivisions;
const double physicalShiftStep =
logGridStep / shiftSubdivisions;
double zeroShiftCenteredLoss =
std::numeric_limits<double>::quiet_NaN();
double bestCenteredLoss =
std::numeric_limits<double>::infinity();
double bestPhysicalShift = 0.0;
int bestShiftStep = 0;
double bestPressureLoss =
std::numeric_limits<double>::infinity();
double bestPressureShift = 0.0;
double bestDerivativeLoss =
std::numeric_limits<double>::infinity();
double bestDerivativeShift = 0.0;
QVector<double> profileLosses(
2 * halfShiftStepCount + 1,
std::numeric_limits<double>::quiet_NaN());
QVector<double> profileCommonBiases(
2 * halfShiftStepCount + 1,
std::numeric_limits<double>::quiet_NaN());
QVector<double> shiftedPressureResidual;
QVector<double> shiftedDerivativeResidual;
// 位移和公共上下偏移联合求解,避免“先扣上下还是先扣左右”的顺序依赖。
for(int shiftStep = -halfShiftStepCount;
shiftStep <= halfShiftStepCount;
++shiftStep) {
double physicalShift =
shiftStep * physicalShiftStep;
if(!buildShiftResidual(
physicalShift,
registrationBegin,
registrationEnd,
&shiftedPressureResidual,
&shiftedDerivativeResidual)) {
continue;
}
double commonBias = commonMeanCenterRange(
shiftedPressureResidual,
shiftedDerivativeResidual,
registrationBegin,
registrationEnd);
double centeredLoss = jointRmseAround(
shiftedPressureResidual,
shiftedDerivativeResidual,
registrationBegin,
registrationEnd,
commonBias);
double pressureBias = meanCenterRange(
shiftedPressureResidual,
registrationBegin,
registrationEnd);
double derivativeBias = meanCenterRange(
shiftedDerivativeResidual,
registrationBegin,
registrationEnd);
double pressureLoss = rmseAround(
shiftedPressureResidual,
registrationBegin,
registrationEnd,
pressureBias);
double derivativeLoss = rmseAround(
shiftedDerivativeResidual,
registrationBegin,
registrationEnd,
derivativeBias);
if(!isFiniteNumber(centeredLoss) ||
!isFiniteNumber(pressureLoss) ||
!isFiniteNumber(derivativeLoss)) {
continue;
}
int profileIndex = shiftStep + halfShiftStepCount;
profileLosses[profileIndex] = centeredLoss;
profileCommonBiases[profileIndex] = commonBias;
if(shiftStep == 0) {
zeroShiftCenteredLoss = centeredLoss;
}
if(isBetterProfileValue(
centeredLoss,
physicalShift,
bestCenteredLoss,
bestPhysicalShift)) {
bestCenteredLoss = centeredLoss;
bestPhysicalShift = physicalShift;
bestShiftStep = shiftStep;
}
if(isBetterProfileValue(
pressureLoss,
physicalShift,
bestPressureLoss,
bestPressureShift)) {
bestPressureLoss = pressureLoss;
bestPressureShift = physicalShift;
}
if(isBetterProfileValue(
derivativeLoss,
physicalShift,
bestDerivativeLoss,
bestDerivativeShift)) {
bestDerivativeLoss = derivativeLoss;
bestDerivativeShift = physicalShift;
}
}
if(!isFiniteNumber(zeroShiftCenteredLoss) ||
!isFiniteNumber(bestCenteredLoss) ||
!isFiniteNumber(bestPressureLoss) ||
!isFiniteNumber(bestDerivativeLoss)) {
return invalidLoss;
}
// horizontalGain 是“允许水平位移”相对“固定零位移”减少的均方能量。
// 只有改善足够明显且最优点不是边界,才把位移解释为可靠左右偏差。
double horizontalGain = nestedRmsContribution(
zeroShiftCenteredLoss, bestCenteredLoss);
double horizontalSignalThreshold =
qMax(1.0e-5, zeroShiftCenteredLoss * 0.02);
int bestProfileIndex = bestShiftStep + halfShiftStepCount;
double nearbyProfileLoss =
std::numeric_limits<double>::infinity();
int leftProfileIndex =
bestProfileIndex - shiftSubdivisions;
int rightProfileIndex =
bestProfileIndex + shiftSubdivisions;
if(leftProfileIndex >= 0 &&
leftProfileIndex < profileLosses.size() &&
isFiniteNumber(profileLosses[leftProfileIndex])) {
nearbyProfileLoss = qMin(
nearbyProfileLoss,
profileLosses[leftProfileIndex]);
}
if(rightProfileIndex >= 0 &&
rightProfileIndex < profileLosses.size() &&
isFiniteNumber(profileLosses[rightProfileIndex])) {
nearbyProfileLoss = qMin(
nearbyProfileLoss,
profileLosses[rightProfileIndex]);
}
double profileContrast = isFiniteNumber(nearbyProfileLoss)
? nestedRmsContribution(
nearbyProfileLoss,
bestCenteredLoss)
: 0.0;
bool flatRegistrationProfile =
profileContrast <= horizontalSignalThreshold;
// 平台曲线的 profile 也可能很平,但公共 bias 在各个位移下保持稳定,
// 此时仍能可靠判断上下。只有近优位移会明显改变 bias 才说明上下/左右不可辨识。
double minimumNearOptimalBias =
std::numeric_limits<double>::infinity();
double maximumNearOptimalBias =
-std::numeric_limits<double>::infinity();
for(int i = 0; i < profileLosses.size(); ++i) {
if(isFiniteNumber(profileLosses[i]) &&
isFiniteNumber(profileCommonBiases[i]) &&
profileLosses[i] <=
bestCenteredLoss + horizontalSignalThreshold) {
minimumNearOptimalBias = qMin(
minimumNearOptimalBias,
profileCommonBiases[i]);
maximumNearOptimalBias = qMax(
maximumNearOptimalBias,
profileCommonBiases[i]);
}
}
double nearOptimalBiasSpread =
isFiniteNumber(minimumNearOptimalBias) &&
isFiniteNumber(maximumNearOptimalBias)
? maximumNearOptimalBias - minimumNearOptimalBias
: std::numeric_limits<double>::infinity();
bool commonBiasStable = nearOptimalBiasSpread <= 1.0e-2;
bool horizontalAtBoundary =
halfShiftStepCount > 0 &&
qAbs(bestShiftStep) == halfShiftStepCount;
// 压力和导数通道分别求出的最佳位移若方向相反或相差过大,说明一个
// 单一水平平移无法解释两条曲线,此时标记配准歧义并禁用左右引导。
bool pressureShiftDetected =
qAbs(bestPressureShift) >=
0.5 * physicalShiftStep;
bool derivativeShiftDetected =
qAbs(bestDerivativeShift) >=
0.5 * physicalShiftStep;
bool channelShiftConflict =
pressureShiftDetected &&
derivativeShiftDetected &&
(bestPressureShift * bestDerivativeShift < 0.0 ||
qAbs(bestPressureShift - bestDerivativeShift) >
2.0 * logGridStep);
breakdown.horizontalLoss = horizontalGain;
breakdown.horizontalReliable =
maximumShiftIntervals > 0 &&
!horizontalAtBoundary &&
!channelShiftConflict &&
!flatRegistrationProfile &&
horizontalGain > horizontalSignalThreshold &&
qAbs(bestPhysicalShift) >=
0.5 * physicalShiftStep;
breakdown.registrationAmbiguous =
channelShiftConflict ||
(qAbs(bestPhysicalShift) >=
0.5 * physicalShiftStep &&
!breakdown.horizontalReliable) ||
(flatRegistrationProfile && !commonBiasStable);
breakdown.horizontalPhysicalShift =
breakdown.horizontalReliable
? bestPhysicalShift
: 0.0;
// 可信水平位移确定后,在该位移实际覆盖的最大区间重新计算上下和形状。
int diagnosticBegin = 0;
int diagnosticEnd = numPoints;
while(diagnosticBegin < diagnosticEnd &&
commonLogX[diagnosticBegin] +
breakdown.horizontalPhysicalShift <
resultLogMinX - 1.0e-12) {
++diagnosticBegin;
}
while(diagnosticEnd > diagnosticBegin &&
commonLogX[diagnosticEnd - 1] +
breakdown.horizontalPhysicalShift >
resultLogMaxX + 1.0e-12) {
--diagnosticEnd;
}
if(diagnosticEnd - diagnosticBegin <
minimumRegistrationPoints ||
!buildShiftResidual(
breakdown.horizontalPhysicalShift,
diagnosticBegin,
diagnosticEnd,
&shiftedPressureResidual,
&shiftedDerivativeResidual)) {
return invalidLoss;
}
double commonBias = commonMeanCenterRange(
shiftedPressureResidual,
shiftedDerivativeResidual,
diagnosticBegin,
diagnosticEnd);
double rawAlignedLoss = jointRmseAround(
shiftedPressureResidual,
shiftedDerivativeResidual,
diagnosticBegin,
diagnosticEnd,
0.0);
double centeredAlignedLoss = jointRmseAround(
shiftedPressureResidual,
shiftedDerivativeResidual,
diagnosticBegin,
diagnosticEnd,
commonBias);
if(!isFiniteNumber(commonBias) ||
!isFiniteNumber(rawAlignedLoss) ||
!isFiniteNumber(centeredAlignedLoss)) {
return invalidLoss;
}
// 原始对齐误差减去公共中心后的能量差定义为上下误差贡献。只有它相对
// 当前对齐误差足够明显,且配准无歧义时,公共 bias 才可用于有符号选参。
breakdown.verticalCommonBias = commonBias;
breakdown.verticalLoss = nestedRmsContribution(
rawAlignedLoss, centeredAlignedLoss);
breakdown.verticalReliable =
!breakdown.registrationAmbiguous &&
breakdown.verticalLoss >
qMax(1.0e-5, rawAlignedLoss * 0.02);
QVector<double> shapePressure(
numPoints, std::numeric_limits<double>::quiet_NaN());
QVector<double> shapeDerivative(
numPoints, std::numeric_limits<double>::quiet_NaN());
for(int i = diagnosticBegin; i < diagnosticEnd; ++i) {
if(isFiniteNumber(shiftedPressureResidual[i])) {
shapePressure[i] =
shiftedPressureResidual[i] - commonBias;
}
if(isFiniteNumber(shiftedDerivativeResidual[i])) {
shapeDerivative[i] =
shiftedDerivativeResidual[i] - commonBias;
}
}
// shapeLoss 是去除可信左右位移和公共均值中心后的剩余误差。
double shapePressureLoss = rmse(
shapePressure, diagnosticBegin, diagnosticEnd);
double shapeDerivativeLoss = rmse(
shapeDerivative, diagnosticBegin, diagnosticEnd);
if(!isFiniteNumber(shapePressureLoss) ||
!isFiniteNumber(shapeDerivativeLoss)) {
return invalidLoss;
}
breakdown.shapeLoss = qSqrt(
0.5 *
(shapePressureLoss * shapePressureLoss +
shapeDerivativeLoss * shapeDerivativeLoss));
// 现阶段不识别或单独调度晚期流动段;保留字段只为了维持现有 trace 列。
breakdown.lateDerivativeSlopeBias = 0.0;
breakdown.lateDerivativeTrendLoss = 0.0;
breakdown.lateDerivativeTrendReliable = false;
if(!isFiniteNumber(breakdown.pressureLoss) ||
!isFiniteNumber(breakdown.derivativeLoss) ||
!isFiniteNumber(breakdown.verticalCommonBias) ||
!isFiniteNumber(breakdown.verticalLoss) ||
!isFiniteNumber(breakdown.horizontalPhysicalShift) ||
!isFiniteNumber(breakdown.horizontalLoss) ||
!isFiniteNumber(breakdown.shapeLoss) ||
breakdown.residualVector.size() != 2 * numPoints) {
return invalidLoss;
}
// LM 总目标等于固定残差向量的二范数;压力和导数各占一半能量。
// total 的定义不变;阶段形状和高度指标在下方以固定网格单独计算。
breakdown.total = qSqrt(
0.5 * breakdown.pressureLoss * breakdown.pressureLoss +
0.5 * breakdown.derivativeLoss * breakdown.derivativeLoss);
breakdown.valid =
isFiniteNumber(breakdown.total) &&
breakdown.total >= 0.0;
if(breakdown.valid && breakdown.total < 1.0e9) {
breakdown.timeWindows = calculateAutoFitTimeWindows(
breakdown.residualVector, overlapMinX, overlapMaxX);
if(!comparisonRangeFixed) {
// 整个目标及诊断均有效后才提交基准,中点回退可重新建立区间。
m_comparisonTimeMin = overlapMinX;
m_comparisonTimeMax = overlapMaxX;
writeTraceMetaFile();
}
}
if(!populateStageMetrics(&breakdown)) return invalidLoss;
m_lastObjectiveBreakdown = breakdown;
DEBUG_OUT(
QString("LogLog objective: pressure=%1, derivative=%2, vertical=%3, horizontal=%4, shape=%5, ambiguous=%6, shift=%7, total=%8")
.arg(breakdown.pressureLoss, 0, 'e', 4)
.arg(breakdown.derivativeLoss, 0, 'e', 4)
.arg(breakdown.verticalLoss, 0, 'e', 4)
.arg(breakdown.horizontalLoss, 0, 'e', 4)
.arg(breakdown.shapeLoss, 0, 'e', 4)
.arg(breakdown.registrationAmbiguous)
.arg(breakdown.horizontalPhysicalShift, 0, 'e', 4)
.arg(breakdown.total, 0, 'e', 4));
return breakdown.valid
? qMin(1.0e9, breakdown.total)
: invalidLoss;
} catch(const std::exception& e) {
DEBUG_OUT(
QString("Exception in LogLog error calculation: %1")
.arg(e.what()));
return invalidLoss;
} catch(...) {
DEBUG_OUT("Unknown exception in LogLog error calculation");
return invalidLoss;
}
}
QVector<QVector<double>> nmCalculationAutoFitLM::runSolverDll()
{
// DLL 求解器路径。
// 这个函数负责把当前 DataManager 中的项目状态交给底层数值求解器,
// 数值求解仍包含全部计算井,以保留井间干扰;后处理只提取目标井曲线。
//
// 如果这里失败,通常需要优先检查:HX_NWTM.dll、license、网格/井数据是否完整、
// 目标井是否存在,以及 DataManager 中刚写入的参数是否导致求解器异常。
DEBUG_OUT("SOLVER DLL START");
// 创建任务时绑定当前分析,并在当前线程冻结本次求解所需的全部输入。
nmDataAnalyzeManager* dataManager =
nmDataAnalyzeManager::getCurrentInstance();
if(!dataManager || m_targetWellName.isEmpty()) {
DEBUG_OUT("Data manager or target well is unavailable");
return QVector<QVector<double> >();
}
if(m_evaluationInProgress > 0) {
DEBUG_OUT("DLL Solver already running, skipping");
return QVector<QVector<double>>();
}
++m_evaluationInProgress;
m_lastEvaluatedLogLogData.clear();
QVector<QVector<double>> result;
nmCalculationDllPebiSolverTask* dllTask = nullptr;
try {
DEBUG_OUT("Creating DLL solver task");
dllTask = new nmCalculationDllPebiSolverTask(
m_tempDirectory,
dataManager,
m_targetWellName);
if(m_shouldStop) {
DEBUG_OUT("Should stop - cleaning up and returning empty result");
delete dllTask;
--m_evaluationInProgress;
return result;
}
DEBUG_OUT("Starting DLL solver execution...");
// 异步执行 DLL 任务。循环等待期间持续 processEvents,保证界面不会完全卡死。
dllTask->start();
// 等待完成。最大等待 1 小时;用户停止使用任务已有的协作取消接口。
const int maxWait = 3600000; // 1h超时
const int checkInterval = 50;
QTime waitTimer;
waitTimer.start();
while(waitTimer.elapsed() < maxWait) {
// wait(timeout) 会在线程一完成时立即返回,避免原来固定 msleep(500)
// 带来的每次 0~500ms 额外等待;50ms 间隔仍可及时处理停止请求和界面事件。
if(dllTask->wait(checkInterval)) {
DEBUG_OUT("DLL solver task completed");
break;
}
// 等待期间也派发鼠标、键盘事件,让停止按钮能及时登记请求。
QApplication::processEvents(QEventLoop::AllEvents, checkInterval);
if(m_shouldStop) {
// DLL 没有中断接口,返回后丢弃结果,避免强制终止破坏其内部锁。
dllTask->requestCancel();
}
}
// 超时处理
if(dllTask->isRunning()) {
DEBUG_OUT("DLL solver task timeout, terminating...");
dllTask->terminate();
dllTask->wait(2000);
delete dllTask;
dllTask = nullptr;
--m_evaluationInProgress;
m_consecutiveFailures++;
return result;
}
// 线程结束后检查真实执行结果,防止失败时复用上一粒子的旧曲线。
dllTask->wait();
if(m_shouldStop) {
DEBUG_OUT("DLL solver evaluation cancelled by user");
delete dllTask;
dllTask = nullptr;
--m_evaluationInProgress;
return result;
}
if(!dllTask->wasSuccessful()) {
DEBUG_OUT("DLL solver task reported failure");
delete dllTask;
dllTask = nullptr;
--m_evaluationInProgress;
m_consecutiveFailures++;
return result;
}
// 任务结束后复制其局部结果,删除任务前不再持有任务内部引用。
QVector<QVector<double>> pressureResult = dllTask->getAutoFitResultPressure();
QVector<QVector<double>> logLogResult = dllTask->getAutoFitResultLogLog();
DEBUG_OUT(QString("DLL result verification - Pressure arrays: %1, LogLog arrays: %2")
.arg(pressureResult.size()).arg(logLogResult.size()));
if(pressureResult.size() >= 2) {
DEBUG_OUT(QString("Pressure result - Time points: %1, Pressure points: %2")
.arg(pressureResult[0].size()).arg(pressureResult[1].size()));
if(pressureResult[0].size() > 0) {
DEBUG_OUT(QString("Sample pressure data - Time[0]: %1, Time[last]: %2, P[0]: %3, P[last]: %4")
.arg(pressureResult[0][0])
.arg(pressureResult[0][pressureResult[0].size() - 1])
.arg(pressureResult[1][0])
.arg(pressureResult[1][pressureResult[1].size() - 1]));
}
}
// 数据有效性检查
if(pressureResult.size() >= 2
&& pressureResult[0].size() > 0
&& pressureResult[1].size() > 0
&& validateLogLogData(logLogResult)) {
result = pressureResult;
m_lastEvaluatedLogLogData = logLogResult;
DEBUG_OUT(QString("Got DLL solver result: %1 points").arg(result[0].size()));
m_consecutiveFailures = 0;
// 检查结果是否与之前不同。若连续粒子得到完全相同的压力曲线,
// 可能说明参数没有正确写入 DataManager,或求解器缓存/状态没有刷新。
static QVector<double> lastPressureResult;
bool isDifferentFromLast = false;
if(lastPressureResult.isEmpty() || lastPressureResult.size() != pressureResult[1].size()) {
isDifferentFromLast = true;
} else {
for(int i = 0; i < qMin(5, pressureResult[1].size()); ++i) {
if(qAbs(lastPressureResult[i] - pressureResult[1][i]) > 1e-12) {
isDifferentFromLast = true;
break;
}
}
}
if(isDifferentFromLast) {
DEBUG_OUT("RESULT VERIFICATION: Got NEW result data from DLL");
lastPressureResult = pressureResult[1];
} else {
DEBUG_OUT("!!! WARNING: Result data appears to be identical to previous run !!!");
}
} else {
DEBUG_OUT("DLL solver result is empty or invalid");
DEBUG_OUT(QString("Pressure result size: %1, Array sizes: %2, %3")
.arg(pressureResult.size())
.arg(pressureResult.size() > 0 ? pressureResult[0].size() : 0)
.arg(pressureResult.size() > 1 ? pressureResult[1].size() : 0));
m_consecutiveFailures++;
}
} catch(const std::bad_alloc& e) {
DEBUG_OUT(QString("Memory allocation failed in DLL solver: %1").arg(e.what()));
m_consecutiveFailures++;
} catch(const std::exception& e) {
DEBUG_OUT(QString("Exception in DLL solver: %1").arg(e.what()));
m_consecutiveFailures++;
} catch(...) {
DEBUG_OUT("Unknown exception in DLL solver");
m_consecutiveFailures++;
}
// 清理DLL任务
if(dllTask) {
if(dllTask->isRunning()) {
dllTask->terminate();
dllTask->wait(3000);
}
DEBUG_OUT("Cleaning up DLL solver task...");
delete dllTask;
dllTask = nullptr;
}
QApplication::processEvents(QEventLoop::AllEvents, 100);
--m_evaluationInProgress;
DEBUG_OUT(QString("SOLVER DLL END - ResultPoints: %1")
.arg(result.isEmpty() ? 0 : result[0].size()));
return result;
}
bool nmCalculationAutoFitLM::runFinalFullSolver()
{
// 不设置目标井名,任务按原完整模式保存全部井和网格结果。
nmDataAnalyzeManager* dataManager =
nmDataAnalyzeManager::getCurrentInstance();
if(!dataManager) {
DEBUG_OUT("Cannot start final full-field solver without a data manager");
return false;
}
if(m_evaluationInProgress > 0) {
DEBUG_OUT("Cannot start final full-field solver while another evaluation is running");
return false;
}
++m_evaluationInProgress;
nmCalculationDllPebiSolverTask dllTask(m_tempDirectory, dataManager);
dllTask.start();
const int maxWait = 3600000;
const int checkInterval = 50;
QTime waitTimer;
waitTimer.start();
while(waitTimer.elapsed() < maxWait) {
if(dllTask.wait(checkInterval)) {
break;
}
// 最终完整计算可能持续较长时间,此处需处理停止按钮事件。
QApplication::processEvents(QEventLoop::AllEvents, checkInterval);
if(!dllTask.isRunning()) {
break;
}
}
if(dllTask.isRunning()) {
DEBUG_OUT("Final full-field solver timeout, terminating task");
dllTask.terminate();
dllTask.wait(2000);
--m_evaluationInProgress;
return false;
}
dllTask.wait();
// 后台只生成局部结果快照;确认求解和输入版本均有效后再一次性写回当前分析。
const bool succeeded =
dllTask.wasSuccessful() && dllTask.commitResult(dataManager);
--m_evaluationInProgress;
return succeeded;
}
QString nmCalculationAutoFitLM::getStopReasonDescription(StopReasonLM reason) const
{
// 将停止枚举转成人类可读文本,用于日志和运行摘要。
switch(reason) {
case LM_TARGET_ACHIEVED:
return tr("Target error achieved");
case LM_TRUE_CONVERGENCE:
return tr("Algorithm converged to stable solution");
case LM_LOCAL_OPTIMUM:
return tr("Local optimum detected");
case LM_MAX_ITERATIONS:
return tr("Total-stage iteration or evaluation budget reached");
case LM_USER_STOPPED:
return tr("Stopped by user request");
case LM_CONSECUTIVE_FAILURES:
return tr("Too many consecutive failures");
case LM_OPTIMIZATION_FAILED:
return tr("Optimization failed");
default:
return tr("Unknown reason");
}
}