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/nmCalculationAutoFitMetrics...

477 lines
14 KiB
C++

#include "nmCalculationAutoFitPSO.h"
#include <QtCore/qmath.h>
#include <algorithm>
#include <cmath>
#ifdef Q_OS_WIN
#include <float.h>
#endif
namespace {
static bool autoFitMetricIsFinite(double value)
{
#ifdef Q_OS_WIN
return _finite(value) != 0;
#else
return std::isfinite(value);
#endif
}
static void autoFitAppendMetricReason(QString* reasons,
const QString& reason)
{
if(!reasons || reason.isEmpty() || reasons->contains(reason)) {
return;
}
if(!reasons->isEmpty()) {
reasons->append(';');
}
reasons->append(reason);
}
struct AutoFitPressurePoint
{
double timeHr;
double pressureMpa;
};
static bool autoFitPressurePointLess(const AutoFitPressurePoint& left,
const AutoFitPressurePoint& right)
{
return left.timeHr < right.timeHr;
}
// 清理压力曲线并按时间升序排列。相同时间只保留最后一个值,避免插值区间为零。
static QVector<AutoFitPressurePoint> autoFitNormalizePressureCurve(
const QVector<QVector<double> >& pressureData)
{
QVector<AutoFitPressurePoint> points;
if(pressureData.size() < 2) {
return points;
}
const int count = qMin(pressureData[0].size(), pressureData[1].size());
points.reserve(count);
for(int i = 0; i < count; ++i) {
const double timeHr = pressureData[0][i];
const double pressureMpa = pressureData[1][i];
if(!autoFitMetricIsFinite(timeHr) || timeHr <= 0.0 ||
!autoFitMetricIsFinite(pressureMpa)) {
continue;
}
AutoFitPressurePoint point;
point.timeHr = timeHr;
point.pressureMpa = pressureMpa;
points.append(point);
}
std::sort(points.begin(), points.end(), autoFitPressurePointLess);
QVector<AutoFitPressurePoint> uniquePoints;
uniquePoints.reserve(points.size());
for(int i = 0; i < points.size(); ++i) {
if(!uniquePoints.isEmpty() &&
qAbs(uniquePoints.last().timeHr - points[i].timeHr) <=
qMax(1.0e-14, points[i].timeHr * 1.0e-12)) {
uniquePoints.last() = points[i];
} else {
uniquePoints.append(points[i]);
}
}
return uniquePoints;
}
// 压力在 log10(t) 坐标中线性插值,与七算例统一误差协议保持一致。
static bool autoFitInterpolatePressure(
const QVector<AutoFitPressurePoint>& points,
double logTime,
double* pressureMpa)
{
if(!pressureMpa || points.size() < 2) {
return false;
}
const double timeHr = qPow(10.0, logTime);
if(timeHr < points.first().timeHr || timeHr > points.last().timeHr) {
return false;
}
int left = 0;
int right = points.size() - 1;
while(right - left > 1) {
const int middle = left + (right - left) / 2;
if(points[middle].timeHr <= timeHr) {
left = middle;
} else {
right = middle;
}
}
if(qAbs(timeHr - points[left].timeHr) <=
qMax(1.0e-14, timeHr * 1.0e-12)) {
*pressureMpa = points[left].pressureMpa;
return true;
}
if(qAbs(timeHr - points[right].timeHr) <=
qMax(1.0e-14, timeHr * 1.0e-12)) {
*pressureMpa = points[right].pressureMpa;
return true;
}
const double leftLogTime = qLn(points[left].timeHr) / qLn(10.0);
const double rightLogTime = qLn(points[right].timeHr) / qLn(10.0);
const double denominator = rightLogTime - leftLogTime;
if(denominator <= 0.0) {
return false;
}
const double ratio = (logTime - leftLogTime) / denominator;
*pressureMpa = points[left].pressureMpa +
(points[right].pressureMpa - points[left].pressureMpa) * ratio;
return autoFitMetricIsFinite(*pressureMpa);
}
// 在统一时间网格上按 Bourdet 三点公式计算 d(DeltaP)/d(ln t)。首尾点
// 没有完整邻点,保持 NaN 并且不参与导数 RMSE。
static bool autoFitCalculateBourdetDerivative(
const QVector<double>& timeHr,
const QVector<double>& deltaPMpa,
QVector<double>* derivativeMpa,
QString* invalidReason,
bool* hasInvalidDerivative)
{
if(hasInvalidDerivative) {
*hasInvalidDerivative = false;
}
if(!derivativeMpa || timeHr.size() != deltaPMpa.size() ||
timeHr.size() < 3) {
if(invalidReason) {
*invalidReason = "INSUFFICIENT_DERIVATIVE_POINTS";
}
return false;
}
const double invalidValue = std::numeric_limits<double>::quiet_NaN();
derivativeMpa->fill(invalidValue, timeHr.size());
for(int i = 1; i < timeHr.size() - 1; ++i) {
const double leftInterval = qLn(timeHr[i] / timeHr[i - 1]);
const double rightInterval = qLn(timeHr[i + 1] / timeHr[i]);
const double totalInterval = leftInterval + rightInterval;
if(!autoFitMetricIsFinite(leftInterval) || leftInterval <= 0.0 ||
!autoFitMetricIsFinite(rightInterval) || rightInterval <= 0.0 ||
!autoFitMetricIsFinite(totalInterval) || totalInterval <= 0.0) {
if(invalidReason) {
*invalidReason = "INVALID_TIME_INTERVAL";
}
return false;
}
const double leftSlope =
(deltaPMpa[i] - deltaPMpa[i - 1]) / leftInterval;
const double rightSlope =
(deltaPMpa[i + 1] - deltaPMpa[i]) / rightInterval;
const double derivative =
leftSlope * rightInterval / totalInterval +
rightSlope * leftInterval / totalInterval;
if(!autoFitMetricIsFinite(derivative) || derivative <= 0.0) {
// 单点异常不破坏其余 Bourdet 点。该位置保持 NaN由调用方从
// 双对数导数 RMSE 中剔除,同时保留整条曲线的数据质量失败标志。
if(hasInvalidDerivative) {
*hasInvalidDerivative = true;
}
continue;
}
(*derivativeMpa)[i] = derivative;
}
return true;
}
} // namespace
AutoFitCurveMetrics::AutoFitCurveMetrics()
: valid(false)
, passed(false)
, coverage(std::numeric_limits<double>::quiet_NaN())
, pressureRmseMpa(std::numeric_limits<double>::quiet_NaN())
, pressureMaxAbsErrorMpa(std::numeric_limits<double>::quiet_NaN())
, logDeltaPRmseDecade(std::numeric_limits<double>::quiet_NaN())
, logDerivativeRmseDecade(std::numeric_limits<double>::quiet_NaN())
, unifiedCurveError(std::numeric_limits<double>::quiet_NaN())
, sampleCount(0)
, validDerivativeCount(0)
{
}
AutoFitParameterResult::AutoFitParameterResult()
: initialValue(std::numeric_limits<double>::quiet_NaN())
, lowerBound(std::numeric_limits<double>::quiet_NaN())
, upperBound(std::numeric_limits<double>::quiet_NaN())
, finalValue(std::numeric_limits<double>::quiet_NaN())
{
}
AutoFitRunResult::AutoFitRunResult()
: ompThreads(-1)
, iluReuseSteps(-1)
, optimizationWallTimeMs(-1)
, workflowWallTimeMs(-1)
, solverTimeSumMs(0)
, finalSolverTimeMs(-1)
, iterationCount(0)
, parameterEvaluationCount(0)
, modelSolverCallCount(0)
, finalSolverCallCount(0)
, solverSuccessCount(0)
, solverFailureCount(0)
, solverTimeoutCount(0)
, optimizationPebiCount(-1)
, finalPebiCount(-1)
, pebiCount(-1)
, finalSolverStatus("NOT_RUN")
, initialPressureMpa(std::numeric_limits<double>::quiet_NaN())
, initialInternalError(std::numeric_limits<double>::quiet_NaN())
, finalInternalError(std::numeric_limits<double>::quiet_NaN())
{
}
void AutoFitRunResult::recordOptimizationSolverCall(bool success,
bool timeout,
qint64 solveTimeMs,
int pebiCount)
{
++modelSolverCallCount;
if(timeout) {
++solverTimeoutCount;
} else if(success) {
++solverSuccessCount;
} else {
++solverFailureCount;
}
if(solveTimeMs >= 0) {
solverTimeSumMs += solveTimeMs;
}
if(pebiCount >= 0) {
optimizationPebiCount = pebiCount;
this->pebiCount = pebiCount;
}
}
void AutoFitRunResult::recordFinalSolverCall(bool success,
bool timeout,
qint64 solveTimeMs,
int pebiCount)
{
++finalSolverCallCount;
finalSolverTimeMs = solveTimeMs >= 0 ? solveTimeMs : -1;
finalPebiCount = pebiCount >= 0 ? pebiCount : -1;
if(pebiCount >= 0) {
this->pebiCount = pebiCount;
}
if(timeout) {
finalSolverStatus = "TIMEOUT";
} else {
finalSolverStatus = success ? "SUCCESS" : "FAILED";
}
}
AutoFitRunResult nmCalculationAutoFitPSO::getLastRunResult() const
{
return m_lastRunResult;
}
AutoFitCurveMetrics nmCalculationAutoFitPSO::calculateUnifiedCurveMetrics(
const QVector<QVector<double> >& targetPressureData,
const QVector<QVector<double> >& fittedPressureData,
double initialPressureMpa,
int sampleCount)
{
AutoFitCurveMetrics metrics;
if(!autoFitMetricIsFinite(initialPressureMpa)) {
metrics.invalidReason = "INVALID_INITIAL_PRESSURE";
return metrics;
}
if(sampleCount < 3) {
metrics.invalidReason = "INVALID_SAMPLE_COUNT";
return metrics;
}
const QVector<AutoFitPressurePoint> targetPoints =
autoFitNormalizePressureCurve(targetPressureData);
const QVector<AutoFitPressurePoint> fittedPoints =
autoFitNormalizePressureCurve(fittedPressureData);
if(targetPoints.size() < 2) {
metrics.invalidReason = "MISSING_TARGET_PRESSURE_DATA";
return metrics;
}
if(fittedPoints.size() < 2) {
metrics.invalidReason = "MISSING_FITTED_PRESSURE_DATA";
return metrics;
}
const double targetMinLogTime =
qLn(targetPoints.first().timeHr) / qLn(10.0);
const double targetMaxLogTime =
qLn(targetPoints.last().timeHr) / qLn(10.0);
const double fittedMinLogTime =
qLn(fittedPoints.first().timeHr) / qLn(10.0);
const double fittedMaxLogTime =
qLn(fittedPoints.last().timeHr) / qLn(10.0);
const double targetLogSpan = targetMaxLogTime - targetMinLogTime;
if(!autoFitMetricIsFinite(targetLogSpan) || targetLogSpan <= 0.0) {
metrics.invalidReason = "INVALID_TARGET_TIME_RANGE";
return metrics;
}
const double commonMinLogTime = qMax(targetMinLogTime, fittedMinLogTime);
const double commonMaxLogTime = qMin(targetMaxLogTime, fittedMaxLogTime);
const double commonLogSpan = commonMaxLogTime - commonMinLogTime;
metrics.coverage = qBound(0.0, commonLogSpan / targetLogSpan, 1.0);
if(!autoFitMetricIsFinite(commonLogSpan) || commonLogSpan <= 0.0) {
metrics.invalidReason = "NO_COMMON_TIME_RANGE";
return metrics;
}
metrics.sampleCount = sampleCount;
metrics.timeHr.reserve(sampleCount);
metrics.targetPressureMpa.reserve(sampleCount);
metrics.fittedPressureMpa.reserve(sampleCount);
metrics.targetDeltaPMpa.reserve(sampleCount);
metrics.fittedDeltaPMpa.reserve(sampleCount);
double pressureSquaredSum = 0.0;
double pressureMaxAbsError = 0.0;
double logDeltaPSquaredSum = 0.0;
int validLogDeltaPCount = 0;
bool hasInvalidDeltaP = false;
for(int i = 0; i < sampleCount; ++i) {
const double ratio = sampleCount > 1
? static_cast<double>(i) / static_cast<double>(sampleCount - 1)
: 0.0;
const double logTime = commonMinLogTime + commonLogSpan * ratio;
double targetPressure = 0.0;
double fittedPressure = 0.0;
if(!autoFitInterpolatePressure(targetPoints, logTime, &targetPressure) ||
!autoFitInterpolatePressure(fittedPoints, logTime, &fittedPressure)) {
metrics.invalidReason = "PRESSURE_INTERPOLATION_FAILED";
return metrics;
}
const double timeHr = qPow(10.0, logTime);
const double targetDeltaP = qAbs(initialPressureMpa - targetPressure);
const double fittedDeltaP = qAbs(initialPressureMpa - fittedPressure);
metrics.timeHr.append(timeHr);
metrics.targetPressureMpa.append(targetPressure);
metrics.fittedPressureMpa.append(fittedPressure);
metrics.targetDeltaPMpa.append(targetDeltaP);
metrics.fittedDeltaPMpa.append(fittedDeltaP);
const double pressureResidual = fittedPressure - targetPressure;
pressureSquaredSum += pressureResidual * pressureResidual;
pressureMaxAbsError = qMax(pressureMaxAbsError, qAbs(pressureResidual));
if(!autoFitMetricIsFinite(targetDeltaP) || targetDeltaP <= 0.0 ||
!autoFitMetricIsFinite(fittedDeltaP) || fittedDeltaP <= 0.0) {
hasInvalidDeltaP = true;
continue;
}
const double logResidual =
qLn(fittedDeltaP / targetDeltaP) / qLn(10.0);
if(autoFitMetricIsFinite(logResidual)) {
logDeltaPSquaredSum += logResidual * logResidual;
++validLogDeltaPCount;
} else {
hasInvalidDeltaP = true;
}
}
metrics.pressureRmseMpa =
qSqrt(pressureSquaredSum / static_cast<double>(sampleCount));
metrics.pressureMaxAbsErrorMpa = pressureMaxAbsError;
if(validLogDeltaPCount > 0) {
metrics.logDeltaPRmseDecade = qSqrt(
logDeltaPSquaredSum /
static_cast<double>(validLogDeltaPCount));
} else {
autoFitAppendMetricReason(&metrics.invalidReason,
"NO_VALID_LOG_DELTA_P_SAMPLES");
}
if(hasInvalidDeltaP) {
autoFitAppendMetricReason(&metrics.invalidReason,
"NON_POSITIVE_OR_INVALID_DELTA_P");
}
QString targetDerivativeReason;
QString fittedDerivativeReason;
bool targetHasInvalidDerivative = false;
bool fittedHasInvalidDerivative = false;
if(!autoFitCalculateBourdetDerivative(metrics.timeHr,
metrics.targetDeltaPMpa,
&metrics.targetDerivativeMpa,
&targetDerivativeReason,
&targetHasInvalidDerivative)) {
autoFitAppendMetricReason(&metrics.invalidReason,
targetDerivativeReason);
return metrics;
}
if(!autoFitCalculateBourdetDerivative(metrics.timeHr,
metrics.fittedDeltaPMpa,
&metrics.fittedDerivativeMpa,
&fittedDerivativeReason,
&fittedHasInvalidDerivative)) {
autoFitAppendMetricReason(&metrics.invalidReason,
fittedDerivativeReason);
return metrics;
}
double logDerivativeSquaredSum = 0.0;
for(int i = 1; i < sampleCount - 1; ++i) {
const double targetDerivative = metrics.targetDerivativeMpa[i];
const double fittedDerivative = metrics.fittedDerivativeMpa[i];
if(!autoFitMetricIsFinite(targetDerivative) ||
targetDerivative <= 0.0 ||
!autoFitMetricIsFinite(fittedDerivative) ||
fittedDerivative <= 0.0) {
continue;
}
const double logResidual =
qLn(fittedDerivative / targetDerivative) / qLn(10.0);
if(!autoFitMetricIsFinite(logResidual)) {
targetHasInvalidDerivative = true;
continue;
}
logDerivativeSquaredSum += logResidual * logResidual;
++metrics.validDerivativeCount;
}
if(metrics.validDerivativeCount > 0) {
metrics.logDerivativeRmseDecade = qSqrt(
logDerivativeSquaredSum /
static_cast<double>(metrics.validDerivativeCount));
} else {
autoFitAppendMetricReason(&metrics.invalidReason,
"NO_VALID_LOG_DERIVATIVE_SAMPLES");
}
if(targetHasInvalidDerivative || fittedHasInvalidDerivative) {
autoFitAppendMetricReason(&metrics.invalidReason,
"NON_POSITIVE_OR_INVALID_DERIVATIVE");
}
if(autoFitMetricIsFinite(metrics.logDeltaPRmseDecade) &&
autoFitMetricIsFinite(metrics.logDerivativeRmseDecade)) {
metrics.unifiedCurveError = qSqrt(
(metrics.logDeltaPRmseDecade * metrics.logDeltaPRmseDecade +
metrics.logDerivativeRmseDecade * metrics.logDerivativeRmseDecade) /
2.0);
}
if(metrics.coverage < 0.95) {
autoFitAppendMetricReason(&metrics.invalidReason,
"TIME_COVERAGE_BELOW_0_95");
}
metrics.valid = metrics.invalidReason.isEmpty();
metrics.passed = metrics.valid &&
metrics.logDeltaPRmseDecade <= 0.02 &&
metrics.logDerivativeRmseDecade <= 0.02;
return metrics;
}