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++

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 "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;
}