|
|
#include "nmWxAutomaticfittingStart.h"
|
|
|
#include <QtGui/QApplication>
|
|
|
#include <QDesktopWidget>
|
|
|
#include <QtCore/QEvent>
|
|
|
#include <QtGui/QMouseEvent>
|
|
|
#include <QtGui/QFont>
|
|
|
#include <QScrollBar>
|
|
|
#include <QTextDocument>
|
|
|
#include <QList>
|
|
|
#include <QPainter>
|
|
|
#include <QPaintEvent>
|
|
|
#include <QPainterPath>
|
|
|
#include <QtCore/qmath.h>
|
|
|
#include <cmath>
|
|
|
#ifdef Q_OS_WIN
|
|
|
#include <float.h>
|
|
|
#endif
|
|
|
|
|
|
static const double kTargetCrossHalfSize = 1.0;
|
|
|
static const double kTargetCircleRadius = 1.0;
|
|
|
|
|
|
static bool nmAutoFitChartIsFinite(double value)
|
|
|
{
|
|
|
#ifdef Q_OS_WIN
|
|
|
return _finite(value) != 0;
|
|
|
#else
|
|
|
return std::isfinite(value);
|
|
|
#endif
|
|
|
}
|
|
|
|
|
|
nmAutomaticFittingCurveChart::nmAutomaticFittingCurveChart(QWidget* parent)
|
|
|
: QWidget(parent)
|
|
|
, m_logRange(0.0, 0.0, 1.0, 1.0)
|
|
|
, m_bestIteration(0)
|
|
|
, m_bestFitness(0.0)
|
|
|
, m_pseudoPressureMode(false)
|
|
|
{
|
|
|
// 保留绘图空间,同时允许小屏幕压缩曲线区域的高度。
|
|
|
setMinimumHeight(160);
|
|
|
setMinimumWidth(420);
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::setPseudoPressureMode(bool enabled)
|
|
|
{
|
|
|
// 该状态只控制纵轴标题, 不改变目标曲线、最优曲线或误差数据.
|
|
|
m_pseudoPressureMode = enabled;
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::setTargetLogLogData(const QVector<QVector<double> >& data)
|
|
|
{
|
|
|
m_targetPressure = extractCurve(data, 1);
|
|
|
m_targetDerivative = extractCurve(data, 2);
|
|
|
rebuildRange();
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::setBestLogLogData(const QVector<QVector<double> >& data, int iteration, double fitness)
|
|
|
{
|
|
|
m_bestPressure = extractCurve(data, 1);
|
|
|
m_bestDerivative = extractCurve(data, 2);
|
|
|
m_bestIteration = iteration;
|
|
|
m_bestFitness = fitness;
|
|
|
rebuildRange();
|
|
|
update();
|
|
|
}
|
|
|
|
|
|
QVector<QPointF> nmAutomaticFittingCurveChart::extractCurve(const QVector<QVector<double> >& data, int yIndex) const
|
|
|
{
|
|
|
QVector<QPointF> points;
|
|
|
|
|
|
if(data.size() <= yIndex || data[0].isEmpty() || data[yIndex].isEmpty()) {
|
|
|
return points;
|
|
|
}
|
|
|
|
|
|
int count = qMin(data[0].size(), data[yIndex].size());
|
|
|
points.reserve(count);
|
|
|
|
|
|
for(int i = 0; i < count; ++i) {
|
|
|
double x = data[0][i];
|
|
|
double y = data[yIndex][i];
|
|
|
|
|
|
if(nmAutoFitChartIsFinite(x) && nmAutoFitChartIsFinite(y) && x > 0.0 && y > 0.0) {
|
|
|
points.append(QPointF(x, y));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return points;
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::rebuildRange()
|
|
|
{
|
|
|
bool hasPoint = false;
|
|
|
double minX = 0.0;
|
|
|
double maxX = 0.0;
|
|
|
double minY = 0.0;
|
|
|
double maxY = 0.0;
|
|
|
|
|
|
QVector<QVector<QPointF> > allCurves;
|
|
|
allCurves << m_targetPressure << m_targetDerivative << m_bestPressure << m_bestDerivative;
|
|
|
|
|
|
for(int c = 0; c < allCurves.size(); ++c) {
|
|
|
const QVector<QPointF>& points = allCurves[c];
|
|
|
|
|
|
for(int i = 0; i < points.size(); ++i) {
|
|
|
double lx = qLn(points[i].x()) / qLn(10.0);
|
|
|
double ly = qLn(points[i].y()) / qLn(10.0);
|
|
|
|
|
|
if(!hasPoint) {
|
|
|
minX = maxX = lx;
|
|
|
minY = maxY = ly;
|
|
|
hasPoint = true;
|
|
|
} else {
|
|
|
minX = qMin(minX, lx);
|
|
|
maxX = qMax(maxX, lx);
|
|
|
minY = qMin(minY, ly);
|
|
|
maxY = qMax(maxY, ly);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if(!hasPoint) {
|
|
|
m_logRange = QRectF(0.0, 0.0, 1.0, 1.0);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if(qAbs(maxX - minX) < 1e-8) {
|
|
|
minX -= 0.5;
|
|
|
maxX += 0.5;
|
|
|
}
|
|
|
|
|
|
if(qAbs(maxY - minY) < 1e-8) {
|
|
|
minY -= 0.5;
|
|
|
maxY += 0.5;
|
|
|
}
|
|
|
|
|
|
double padX = (maxX - minX) * 0.06;
|
|
|
double padY = (maxY - minY) * 0.10;
|
|
|
m_logRange = QRectF(minX - padX, minY - padY, (maxX - minX) + 2.0 * padX, (maxY - minY) + 2.0 * padY);
|
|
|
}
|
|
|
|
|
|
QPointF nmAutomaticFittingCurveChart::dataToScreen(const QPointF& point) const
|
|
|
{
|
|
|
double lx = qLn(point.x()) / qLn(10.0);
|
|
|
double ly = qLn(point.y()) / qLn(10.0);
|
|
|
double xRatio = (lx - m_logRange.left()) / qMax(1e-12, m_logRange.width());
|
|
|
double yRatio = (ly - m_logRange.top()) / qMax(1e-12, m_logRange.height());
|
|
|
|
|
|
return QPointF(m_chartRect.left() + xRatio * m_chartRect.width(),
|
|
|
m_chartRect.bottom() - yRatio * m_chartRect.height());
|
|
|
}
|
|
|
|
|
|
QString nmAutomaticFittingCurveChart::formatPowerTickLabel(int exponent) const
|
|
|
{
|
|
|
if(exponent == 0) {
|
|
|
return "1";
|
|
|
}
|
|
|
|
|
|
if(exponent == 1) {
|
|
|
return "10";
|
|
|
}
|
|
|
|
|
|
QString expStr = QString::number(exponent);
|
|
|
|
|
|
expStr.replace("-", QString(QChar(0x207B))); // ⁻
|
|
|
expStr.replace("0", QString(QChar(0x2070))); // ⁰
|
|
|
expStr.replace("1", QString(QChar(0x00B9))); // ¹
|
|
|
expStr.replace("2", QString(QChar(0x00B2))); // ²
|
|
|
expStr.replace("3", QString(QChar(0x00B3))); // ³
|
|
|
expStr.replace("4", QString(QChar(0x2074))); // ⁴
|
|
|
expStr.replace("5", QString(QChar(0x2075))); // ⁵
|
|
|
expStr.replace("6", QString(QChar(0x2076))); // ⁶
|
|
|
expStr.replace("7", QString(QChar(0x2077))); // ⁷
|
|
|
expStr.replace("8", QString(QChar(0x2078))); // ⁸
|
|
|
expStr.replace("9", QString(QChar(0x2079))); // ⁹
|
|
|
|
|
|
return QString("10") + expStr;
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event)
|
|
|
{
|
|
|
Q_UNUSED(event);
|
|
|
|
|
|
QPainter painter(this);
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
|
// 保持全宽白色绘图区,曲线和实测标记沿用原有红绿配色。
|
|
|
painter.fillRect(rect(), Qt::white);
|
|
|
|
|
|
const int marginLeft = 62;
|
|
|
const int marginTop = 18;
|
|
|
const int marginRight = 16;
|
|
|
const int marginBottom = 42;
|
|
|
m_chartRect = QRectF(marginLeft, marginTop,
|
|
|
qMax(10, width() - marginLeft - marginRight),
|
|
|
qMax(10, height() - marginTop - marginBottom));
|
|
|
|
|
|
painter.fillRect(m_chartRect, Qt::white);
|
|
|
drawGrid(painter);
|
|
|
drawAxes(painter);
|
|
|
drawCurveLine(painter, m_bestPressure, QColor(250, 74, 74));
|
|
|
drawCurveLine(painter, m_bestDerivative, QColor(71, 252, 71));
|
|
|
|
|
|
drawCrossMarkers(painter, m_targetPressure, QColor(71, 252, 71));
|
|
|
drawCircleMarkers(painter, m_targetDerivative, QColor(250, 74, 74));
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::drawGrid(QPainter& painter)
|
|
|
{
|
|
|
painter.save();
|
|
|
|
|
|
int xStart = static_cast<int>(qFloor(m_logRange.left()));
|
|
|
int xEnd = static_cast<int>(qCeil(m_logRange.right()));
|
|
|
int yStart = static_cast<int>(qFloor(m_logRange.top()));
|
|
|
int yEnd = static_cast<int>(qCeil(m_logRange.bottom()));
|
|
|
|
|
|
painter.setPen(QPen(QColor(247, 248, 250), 1, Qt::SolidLine));
|
|
|
for(int decade = xStart; decade <= xEnd; ++decade) {
|
|
|
for(int multiplier = 2; multiplier <= 9; ++multiplier) {
|
|
|
double value = decade + qLn(static_cast<double>(multiplier)) / qLn(10.0);
|
|
|
if(value <= m_logRange.left() || value >= m_logRange.right()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
double ratio = (value - m_logRange.left()) / qMax(1e-12, m_logRange.width());
|
|
|
double sx = m_chartRect.left() + ratio * m_chartRect.width();
|
|
|
painter.drawLine(QPointF(sx, m_chartRect.top()), QPointF(sx, m_chartRect.bottom()));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
for(int decade = yStart; decade <= yEnd; ++decade) {
|
|
|
for(int multiplier = 2; multiplier <= 9; ++multiplier) {
|
|
|
double value = decade + qLn(static_cast<double>(multiplier)) / qLn(10.0);
|
|
|
if(value <= m_logRange.top() || value >= m_logRange.bottom()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
double ratio = (value - m_logRange.top()) / qMax(1e-12, m_logRange.height());
|
|
|
double sy = m_chartRect.bottom() - ratio * m_chartRect.height();
|
|
|
painter.drawLine(QPointF(m_chartRect.left(), sy), QPointF(m_chartRect.right(), sy));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
painter.setPen(QPen(QColor(228, 232, 238), 1, Qt::SolidLine));
|
|
|
for(int x = xStart; x <= xEnd; ++x) {
|
|
|
if(x < m_logRange.left() || x > m_logRange.right()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
double ratio = (x - m_logRange.left()) / qMax(1e-12, m_logRange.width());
|
|
|
double sx = m_chartRect.left() + ratio * m_chartRect.width();
|
|
|
painter.drawLine(QPointF(sx, m_chartRect.top()), QPointF(sx, m_chartRect.bottom()));
|
|
|
}
|
|
|
|
|
|
for(int y = yStart; y <= yEnd; ++y) {
|
|
|
if(y < m_logRange.top() || y > m_logRange.bottom()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
double ratio = (y - m_logRange.top()) / qMax(1e-12, m_logRange.height());
|
|
|
double sy = m_chartRect.bottom() - ratio * m_chartRect.height();
|
|
|
painter.drawLine(QPointF(m_chartRect.left(), sy), QPointF(m_chartRect.right(), sy));
|
|
|
}
|
|
|
|
|
|
painter.restore();
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::drawAxes(QPainter& painter)
|
|
|
{
|
|
|
painter.save();
|
|
|
painter.setPen(QPen(Qt::black, 1));
|
|
|
painter.drawRect(m_chartRect);
|
|
|
|
|
|
QFontMetrics fm(painter.font());
|
|
|
int xStart = static_cast<int>(qFloor(m_logRange.left()));
|
|
|
int xEnd = static_cast<int>(qCeil(m_logRange.right()));
|
|
|
int yStart = static_cast<int>(qFloor(m_logRange.top()));
|
|
|
int yEnd = static_cast<int>(qCeil(m_logRange.bottom()));
|
|
|
|
|
|
for(int x = xStart; x <= xEnd; ++x) {
|
|
|
if(x < m_logRange.left() || x > m_logRange.right()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
double ratio = (x - m_logRange.left()) / qMax(1e-12, m_logRange.width());
|
|
|
double sx = m_chartRect.left() + ratio * m_chartRect.width();
|
|
|
QString label = formatPowerTickLabel(x);
|
|
|
painter.drawLine(QPointF(sx, m_chartRect.bottom()), QPointF(sx, m_chartRect.bottom() + 5));
|
|
|
painter.drawText(QPointF(sx - fm.width(label) / 2.0, m_chartRect.bottom() + 22), label);
|
|
|
}
|
|
|
|
|
|
for(int y = yStart; y <= yEnd; ++y) {
|
|
|
if(y < m_logRange.top() || y > m_logRange.bottom()) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
double ratio = (y - m_logRange.top()) / qMax(1e-12, m_logRange.height());
|
|
|
double sy = m_chartRect.bottom() - ratio * m_chartRect.height();
|
|
|
QString label = formatPowerTickLabel(y);
|
|
|
painter.drawLine(QPointF(m_chartRect.left() - 5, sy), QPointF(m_chartRect.left(), sy));
|
|
|
painter.drawText(QPointF(m_chartRect.left() - fm.width(label) - 9, sy + fm.height() / 3.0), label);
|
|
|
}
|
|
|
|
|
|
QString xTitle = tr("Time (hr)");
|
|
|
painter.drawText(QPointF(m_chartRect.center().x() - fm.width(xTitle) / 2.0, height() - 8), xTitle);
|
|
|
|
|
|
painter.save();
|
|
|
const QString yTitle = m_pseudoPressureMode
|
|
|
// 气井目标和结果均为拟压力时显示对应量纲.
|
|
|
? tr("m(p) (MPa^2/mPa.s)")
|
|
|
: tr("Pressure (MPa)");
|
|
|
painter.translate(13, m_chartRect.center().y());
|
|
|
painter.rotate(-90);
|
|
|
painter.drawText(QPointF(-fm.width(yTitle) / 2.0, 0), yTitle);
|
|
|
painter.restore();
|
|
|
|
|
|
painter.restore();
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::drawCurveLine(QPainter& painter, const QVector<QPointF>& points, const QColor& color)
|
|
|
{
|
|
|
if(points.size() < 2) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
painter.save();
|
|
|
painter.setClipRect(m_chartRect.adjusted(-1, -1, 1, 1));
|
|
|
painter.setPen(QPen(color, 2));
|
|
|
|
|
|
QPainterPath path;
|
|
|
QPointF first = dataToScreen(points[0]);
|
|
|
path.moveTo(first);
|
|
|
|
|
|
for(int i = 1; i < points.size(); ++i) {
|
|
|
path.lineTo(dataToScreen(points[i]));
|
|
|
}
|
|
|
|
|
|
painter.drawPath(path);
|
|
|
painter.restore();
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::drawCrossMarkers(QPainter& painter, const QVector<QPointF>& points, const QColor& color)
|
|
|
{
|
|
|
if(points.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
painter.save();
|
|
|
painter.setClipRect(m_chartRect.adjusted(-1, -1, 1, 1));
|
|
|
painter.setPen(QPen(color, 1));
|
|
|
|
|
|
int step = qMax(1, points.size() / 220);
|
|
|
const double size = kTargetCrossHalfSize;
|
|
|
|
|
|
for(int i = 0; i < points.size(); i += step) {
|
|
|
QPointF p = dataToScreen(points[i]);
|
|
|
painter.drawLine(QPointF(p.x() - size, p.y() - size), QPointF(p.x() + size, p.y() + size));
|
|
|
painter.drawLine(QPointF(p.x() - size, p.y() + size), QPointF(p.x() + size, p.y() - size));
|
|
|
}
|
|
|
|
|
|
painter.restore();
|
|
|
}
|
|
|
|
|
|
void nmAutomaticFittingCurveChart::drawCircleMarkers(QPainter& painter, const QVector<QPointF>& points, const QColor& color)
|
|
|
{
|
|
|
if(points.isEmpty()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
painter.save();
|
|
|
painter.setClipRect(m_chartRect.adjusted(-1, -1, 1, 1));
|
|
|
painter.setPen(QPen(color, 1));
|
|
|
painter.setBrush(Qt::NoBrush);
|
|
|
|
|
|
int step = qMax(1, points.size() / 220);
|
|
|
|
|
|
for(int i = 0; i < points.size(); i += step) {
|
|
|
painter.drawEllipse(dataToScreen(points[i]), kTargetCircleRadius, kTargetCircleRadius);
|
|
|
}
|
|
|
|
|
|
painter.restore();
|
|
|
}
|
|
|
|
|
|
nmWxAutomaticfittingStart::nmWxAutomaticfittingStart(QWidget *parent)
|
|
|
: iDlgBase(parent)
|
|
|
, rightSplitter(nullptr)
|
|
|
, chartGroup(nullptr)
|
|
|
, curveChart(nullptr)
|
|
|
, m_autoFitterPSO(nullptr)
|
|
|
, m_autoFitterLM(nullptr)
|
|
|
, m_algorithmName("PSO")
|
|
|
, m_maxIterations(100)
|
|
|
, m_targetError(0.001)
|
|
|
, m_wellName("")
|
|
|
, m_isFinished(false)
|
|
|
, m_isFinalizing(false)
|
|
|
, m_bestFitnessEver(1e10)
|
|
|
, m_startTime()
|
|
|
{
|
|
|
setupUI();
|
|
|
|
|
|
// 设置窗口属性
|
|
|
setWindowTitle(tr("Auto Fitting Progress"));
|
|
|
// 初始尺寸随所在屏幕变化,最小尺寸交给实际控件布局决定。
|
|
|
resize(sizeHint());
|
|
|
setModal(false);
|
|
|
}
|
|
|
|
|
|
QSize nmWxAutomaticfittingStart::sizeHint() const
|
|
|
{
|
|
|
// 加宽进程窗口,为曲线和完整日志行留出空间,同时保留屏幕边缘。
|
|
|
QDesktopWidget* desktop = QApplication::desktop();
|
|
|
if(desktop == nullptr) return iDlgBase::sizeHint();
|
|
|
const QRect available = desktop->availableGeometry(parentWidget() ? parentWidget()->window() : this);
|
|
|
return QSize(available.width() * 75 / 100, available.height() * 65 / 100);
|
|
|
}
|
|
|
|
|
|
nmWxAutomaticfittingStart::~nmWxAutomaticfittingStart()
|
|
|
{
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setupUI()
|
|
|
{
|
|
|
// 曲线和日志左右并排,参数横排占满下方整行,底部使用一行汇总。
|
|
|
setObjectName("autoFittingProgress");
|
|
|
setFont(QFont("Microsoft YaHei", 9));
|
|
|
setStyleSheet(
|
|
|
"#autoFittingProgress { background: #ffffff; color: #233247; }"
|
|
|
"#autoFittingProgress QGroupBox { background: #ffffff; color: #233247; border: none; margin-top: 0; padding-top: 0; font-weight: bold; }"
|
|
|
"#autoFittingProgress QLabel { color: #66768a; background: transparent; font-weight: normal; }"
|
|
|
"#autoFittingProgress QGroupBox#fittingSummary { padding-top: 0; }"
|
|
|
"#autoFittingProgress QGroupBox#fittingSummary QLabel { font-size: 14px; color: #46556b; }"
|
|
|
"#autoFittingProgress QWidget#parameterStrip { background: #ffffff; border-top: 1px solid #e3e8ef; }"
|
|
|
"#autoFittingProgress QWidget#parameterSeparator { background: #e3e8ef; }"
|
|
|
"#autoFittingProgress QLabel#parameterName { font-size: 12px; }"
|
|
|
"#autoFittingProgress QLabel#parameterValue { color: #233247; font-size: 18px; font-weight: 500; }"
|
|
|
"#autoFittingProgress QGroupBox#fittingSummary QLabel#summaryValue { color: #233247; }"
|
|
|
"#autoFittingProgress QGroupBox#fittingSummary QLabel#stateBadge { background: #edf3fc; color: #366baf; border-radius: 5px; padding: 4px 10px; font-size: 12px; }"
|
|
|
"#autoFittingProgress QGroupBox#fittingLog { padding-top: 0; }"
|
|
|
"#autoFittingProgress QLabel#sectionHeading { color: #233247; font-size: 13px; font-weight: bold; }"
|
|
|
"#autoFittingProgress QWidget#fittingFooter { background: #ffffff; border-top: 1px solid #e3e8ef; }"
|
|
|
"#autoFittingProgress QLabel#resultSummary { color: #66768a; font-size: 14px; }"
|
|
|
"#autoFittingProgress QLabel#stageStatus { color: #66768a; font-size: 12px; }"
|
|
|
"#autoFittingProgress QLabel#runSummary { color: #46556b; font-size: 14px; }"
|
|
|
"#autoFittingProgress QTextEdit { border: none; background: #ffffff; color: #233247; font-size: 13px; font-weight: normal; selection-background-color: #eaf1fb; selection-color: #233247; }"
|
|
|
"#autoFittingProgress QPushButton { background: #ffffff; color: #46556b; border: 1px solid #cfd7e2; border-radius: 5px; padding: 8px 22px; min-width: 64px; min-height: 20px; }"
|
|
|
"#autoFittingProgress QPushButton:hover { background: #edf3fc; border-color: #9bb7db; }"
|
|
|
"#autoFittingProgress QPushButton:pressed { background: #dce8f8; }"
|
|
|
"#autoFittingProgress QPushButton:disabled { background: #f0f2f5; color: #98a2b1; border-color: #e0e5ec; }"
|
|
|
"#autoFittingProgress QPushButton#logToggle { border: none; background: transparent; color: #738091; min-width: 0; padding: 3px 8px; }"
|
|
|
"#autoFittingProgress QPushButton#logToggle:hover { color: #396fc0; }"
|
|
|
"#autoFittingProgress QSplitter::handle { background: #ffffff; }");
|
|
|
|
|
|
// 控件复用原有数据更新逻辑,仅重新组织所属区域。
|
|
|
setupParameterArea();
|
|
|
setupParameterStrip();
|
|
|
setupChartArea();
|
|
|
setupControlArea();
|
|
|
setupLogArea();
|
|
|
// 日志只占右侧较窄区域,两侧底边对齐,分隔条允许调整宽度。
|
|
|
rightSplitter = new QSplitter(Qt::Horizontal, this);
|
|
|
rightSplitter->setHandleWidth(4);
|
|
|
rightSplitter->addWidget(chartGroup);
|
|
|
rightSplitter->addWidget(logGroup);
|
|
|
rightSplitter->setStretchFactor(0, 1);
|
|
|
rightSplitter->setStretchFactor(1, 1);
|
|
|
rightSplitter->setChildrenCollapsible(false);
|
|
|
rightSplitter->setSizes(QList<int>() << 650 << 350);
|
|
|
|
|
|
// 控制区域布局
|
|
|
QWidget* footer = new QWidget(this);
|
|
|
footer->setObjectName("fittingFooter");
|
|
|
footer->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
QHBoxLayout *controlLayout = new QHBoxLayout(footer);
|
|
|
controlLayout->setContentsMargins(22, 14, 22, 14);
|
|
|
controlLayout->setSpacing(24);
|
|
|
controlLayout->addWidget(resultSummary);
|
|
|
controlLayout->addWidget(runSummary);
|
|
|
controlLayout->addStretch();
|
|
|
controlLayout->addWidget(progressBar, 1);
|
|
|
controlLayout->addWidget(stopButton);
|
|
|
|
|
|
QVBoxLayout *overallLayout = new QVBoxLayout();
|
|
|
overallLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
overallLayout->setSpacing(0);
|
|
|
overallLayout->addWidget(parameterGroup);
|
|
|
overallLayout->addWidget(rightSplitter, 1);
|
|
|
overallLayout->addWidget(parameterStrip);
|
|
|
overallLayout->addWidget(footer);
|
|
|
|
|
|
setLayout(overallLayout);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setupParameterArea()
|
|
|
{
|
|
|
parameterGroup = new QGroupBox(this);
|
|
|
parameterGroup->setObjectName("fittingSummary");
|
|
|
|
|
|
QGridLayout *paramLayout = new QGridLayout(parameterGroup);
|
|
|
paramLayout->setContentsMargins(22, 19, 22, 17);
|
|
|
paramLayout->setHorizontalSpacing(12);
|
|
|
paramLayout->setVerticalSpacing(8);
|
|
|
|
|
|
// 创建标签
|
|
|
algorithmTypeLabel = new QLabel(tr("Algorithm:"));
|
|
|
currentIterationLabel = new QLabel(tr("Iterations:"));
|
|
|
currentComfortLabel = new QLabel(tr("Current Error:"));
|
|
|
bestComfortLabel = new QLabel(tr("Best Error:"));
|
|
|
targetPrecisionLabel = new QLabel(tr("Target error:"));
|
|
|
targetWellLabel = new QLabel(tr("Target Well:"));
|
|
|
|
|
|
// 创建数值显示标签
|
|
|
algorithmTypeValue = new QLabel("PSO");
|
|
|
stateBadge = new QLabel(tr("Waiting for fitting to start"));
|
|
|
stateBadge->setObjectName("stateBadge");
|
|
|
currentIterationValue = new QLabel("0 / 100");
|
|
|
currentIterationValue->setObjectName("summaryValue");
|
|
|
currentComfortValue = new QLabel("N/A");
|
|
|
bestComfortValue = new QLabel("N/A");
|
|
|
targetPrecisionValue = new QLabel("0.001");
|
|
|
targetWellValue = new QLabel("N/A");
|
|
|
algorithmTypeValue->setObjectName("summaryValue");
|
|
|
targetWellValue->setObjectName("summaryValue");
|
|
|
targetPrecisionValue->setObjectName("summaryValue");
|
|
|
|
|
|
// 顶部统一显示累计迭代数和上限;PSO 的额外误差信息放在第二行。
|
|
|
paramLayout->addWidget(targetWellLabel, 0, 0);
|
|
|
paramLayout->addWidget(targetWellValue, 0, 1);
|
|
|
paramLayout->addWidget(algorithmTypeLabel, 0, 2);
|
|
|
paramLayout->addWidget(algorithmTypeValue, 0, 3);
|
|
|
paramLayout->addWidget(targetPrecisionLabel, 0, 4);
|
|
|
paramLayout->addWidget(targetPrecisionValue, 0, 5);
|
|
|
paramLayout->addWidget(currentIterationLabel, 0, 6);
|
|
|
paramLayout->addWidget(currentIterationValue, 0, 7);
|
|
|
paramLayout->addWidget(stateBadge, 0, 9, Qt::AlignRight | Qt::AlignVCenter);
|
|
|
paramLayout->addWidget(currentComfortLabel, 1, 0);
|
|
|
paramLayout->addWidget(currentComfortValue, 1, 1);
|
|
|
paramLayout->addWidget(bestComfortLabel, 1, 2);
|
|
|
paramLayout->addWidget(bestComfortValue, 1, 3);
|
|
|
paramLayout->setColumnStretch(8, 1);
|
|
|
targetWellValue->setWordWrap(true);
|
|
|
|
|
|
parameterGroup->setLayout(paramLayout);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setupParameterStrip()
|
|
|
{
|
|
|
// 参数独占曲线和日志下方的整行,横向均分空间,不保留尾部空白。
|
|
|
parameterStrip = new QWidget(this);
|
|
|
parameterStrip->setObjectName("parameterStrip");
|
|
|
parameterStrip->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
parameterStrip->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
|
|
// 标题与参数共用网格,竖线跨越两行,上下留白均为 4 像素。
|
|
|
parameterLayout = new QGridLayout(parameterStrip);
|
|
|
parameterLayout->setContentsMargins(22, 4, 22, 4);
|
|
|
parameterLayout->setHorizontalSpacing(0);
|
|
|
parameterLayout->setVerticalSpacing(2);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setupChartArea()
|
|
|
{
|
|
|
// 曲线上方仅保留标题。
|
|
|
chartGroup = new QGroupBox(this);
|
|
|
QVBoxLayout* chartLayout = new QVBoxLayout(chartGroup);
|
|
|
QLabel* heading = new QLabel(tr("Fitting Curve"), chartGroup);
|
|
|
heading->setObjectName("sectionHeading");
|
|
|
chartLayout->addWidget(heading);
|
|
|
|
|
|
curveChart = new nmAutomaticFittingCurveChart(chartGroup);
|
|
|
curveChart->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
|
|
|
|
|
chartLayout->setContentsMargins(22, 17, 22, 12);
|
|
|
chartLayout->addWidget(curveChart);
|
|
|
chartGroup->setLayout(chartLayout);
|
|
|
chartGroup->setMinimumHeight(200);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setupLogArea()
|
|
|
{
|
|
|
// 日志标题始终可见,正文收起时仍继续接收日志。
|
|
|
logGroup = new QGroupBox(this);
|
|
|
logGroup->setObjectName("fittingLog");
|
|
|
|
|
|
QVBoxLayout *logLayout = new QVBoxLayout(logGroup);
|
|
|
logLayout->setContentsMargins(18, 13, 22, 16);
|
|
|
logLayout->setSpacing(8);
|
|
|
logLayout->setAlignment(Qt::AlignTop);
|
|
|
QHBoxLayout* logHeader = new QHBoxLayout();
|
|
|
QLabel* heading = new QLabel(tr("Fitting process"), logGroup);
|
|
|
heading->setObjectName("sectionHeading");
|
|
|
logToggle = new QPushButton(tr("Collapse"), logGroup);
|
|
|
logToggle->setObjectName("logToggle");
|
|
|
logToggle->setAutoDefault(false);
|
|
|
logHeader->addWidget(heading);
|
|
|
logHeader->addStretch();
|
|
|
logHeader->addWidget(logToggle);
|
|
|
logLayout->addLayout(logHeader);
|
|
|
// 阶段状态放到标题下方,避免挤占窄日志区的标题和按钮。
|
|
|
logLayout->addWidget(stageStatus);
|
|
|
connect(logToggle, SIGNAL(clicked()), this, SLOT(onToggleLog()));
|
|
|
|
|
|
logTextEdit = new QTextEdit(this);
|
|
|
logTextEdit->setReadOnly(true);
|
|
|
// 小屏幕允许缩短可见日志区域,完整内容仍可通过滚动查看。
|
|
|
logTextEdit->setMinimumHeight(48);
|
|
|
logTextEdit->setMinimumWidth(180);
|
|
|
logTextEdit->setFont(font());
|
|
|
// 正文与标题左对齐,沿用窗口白底,不额外绘制日志底色。
|
|
|
logTextEdit->document()->setDocumentMargin(0);
|
|
|
|
|
|
// 添加初始日志
|
|
|
addLogMessage(tr("System initialization completed"));
|
|
|
addLogMessage(tr("Waiting for fitting to start"));
|
|
|
|
|
|
logLayout->addWidget(logTextEdit, 1);
|
|
|
logGroup->setLayout(logLayout);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setupControlArea()
|
|
|
{
|
|
|
// 运行状态放在拟合过程标题下方;底部汇总保持单行。
|
|
|
stageStatus = new QLabel(tr("Waiting for fitting to start"), this);
|
|
|
stageStatus->setObjectName("stageStatus");
|
|
|
stageStatus->setWordWrap(true);
|
|
|
stageStatus->hide();
|
|
|
resultSummary = new QLabel(this);
|
|
|
resultSummary->setObjectName("resultSummary");
|
|
|
resultSummary->setTextFormat(Qt::RichText);
|
|
|
resultSummary->setWordWrap(false);
|
|
|
resultSummary->hide();
|
|
|
runSummary = new QLabel(this);
|
|
|
runSummary->setObjectName("runSummary");
|
|
|
runSummary->setWordWrap(false);
|
|
|
runSummary->hide();
|
|
|
// 进度条
|
|
|
progressBar = new QProgressBar(this);
|
|
|
progressBar->setRange(0, 100);
|
|
|
progressBar->setValue(0);
|
|
|
progressBar->setTextVisible(true);
|
|
|
// 设置进度条为绿色
|
|
|
progressBar->setStyleSheet(
|
|
|
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }"
|
|
|
"QProgressBar::chunk { background-color: #4CAF50; }"
|
|
|
);
|
|
|
|
|
|
// 停止按钮
|
|
|
stopButton = new QPushButton(tr("Stop"), this);
|
|
|
stopButton->setEnabled(false);
|
|
|
|
|
|
// 连接信号槽
|
|
|
connect(stopButton, SIGNAL(clicked()), this, SLOT(onStopButtonClicked()));
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitPSO* autoFitter)
|
|
|
{
|
|
|
m_autoFitterPSO = autoFitter;
|
|
|
m_autoFitterLM = nullptr;
|
|
|
m_algorithmName = "PSO";
|
|
|
updateAlgorithmDisplay();
|
|
|
|
|
|
// 更新算法类型显示
|
|
|
algorithmTypeValue->setText("PSO");
|
|
|
|
|
|
if (m_autoFitterPSO) {
|
|
|
connect(m_autoFitterPSO, SIGNAL(progressUpdated(int, double)),
|
|
|
this, SLOT(onFittingProgress(int, double)));//更新进度条、当前迭代数、当前误差
|
|
|
connect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)),
|
|
|
this, SLOT(onFittingFinished(bool, QString)));//显示结束状态
|
|
|
connect(m_autoFitterPSO, SIGNAL(finalizingStarted()),
|
|
|
this, SLOT(onFinalizingStarted()));
|
|
|
connect(m_autoFitterPSO, SIGNAL(logMessageGenerated(QString)),
|
|
|
this, SLOT(onLogMessageReceived(QString)));//把 PSO 内部日志显示到窗口
|
|
|
connect(m_autoFitterPSO, SIGNAL(bestCurveUpdated(QVector<QVector<double> >,QVector<QVector<double> >,int,double)),
|
|
|
this, SLOT(onBestCurveUpdated(QVector<QVector<double> >,QVector<QVector<double> >,int,double)));//把目标曲线和当前最优模拟曲线画出来
|
|
|
|
|
|
// 启用停止按钮
|
|
|
stopButton->setEnabled(true);
|
|
|
|
|
|
addLogMessage(tr("PSO auto fitting started"));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitLM* autoFitter)
|
|
|
{
|
|
|
m_autoFitterPSO = nullptr;
|
|
|
m_autoFitterLM = autoFitter;
|
|
|
m_algorithmName = "LM";
|
|
|
updateAlgorithmDisplay();
|
|
|
// LM 日志由优化器统一组织,清除通用初始化提示。
|
|
|
logTextEdit->clear();
|
|
|
|
|
|
algorithmTypeValue->setText(m_algorithmName);
|
|
|
|
|
|
if (m_autoFitterLM) {
|
|
|
connect(m_autoFitterLM, SIGNAL(fittingStageChanged(int)),
|
|
|
this, SLOT(onLMStageChanged(int)));
|
|
|
connect(m_autoFitterLM, SIGNAL(progressUpdated(int, double)),
|
|
|
this, SLOT(onFittingProgress(int, double)));
|
|
|
connect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)),
|
|
|
this, SLOT(onFittingFinished(bool, QString)));
|
|
|
connect(m_autoFitterLM, SIGNAL(finalizingStarted()),
|
|
|
this, SLOT(onFinalizingStarted()));
|
|
|
connect(m_autoFitterLM, SIGNAL(logMessageGenerated(QString)),
|
|
|
this, SLOT(onLogMessageReceived(QString)));
|
|
|
connect(m_autoFitterLM, SIGNAL(bestCurveUpdated(QVector<QVector<double> >,QVector<QVector<double> >,int,double)),
|
|
|
this, SLOT(onBestCurveUpdated(QVector<QVector<double> >,QVector<QVector<double> >,int,double)));
|
|
|
|
|
|
stopButton->setEnabled(true);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::updateAlgorithmDisplay()
|
|
|
{
|
|
|
// 迭代数始终在顶部显示,PSO 保留原有额外误差和进度条。
|
|
|
const bool lm = m_autoFitterLM != nullptr;
|
|
|
QLabel* psoLabels[] = { currentComfortLabel, currentComfortValue,
|
|
|
bestComfortLabel, bestComfortValue };
|
|
|
for(int i = 0; i < 4; ++i) psoLabels[i]->setVisible(!lm);
|
|
|
progressBar->setVisible(!lm);
|
|
|
stageStatus->setVisible(lm);
|
|
|
resultSummary->setVisible(lm);
|
|
|
if(lm) updateCurrentError(m_autoFitterLM->getBestFitness());
|
|
|
if(lm) onLMStageChanged(0);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onLMStageChanged(int stage)
|
|
|
{
|
|
|
// 仅更新拟合过程区域的运行状态,不在曲线上方增加阶段栏。
|
|
|
const QString stages[] = { tr("Calculating initial error..."), tr("Height alignment"),
|
|
|
tr("Shape adjustment"), tr("Total-error adjustment") };
|
|
|
stageStatus->setText(stages[stage]);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onToggleLog()
|
|
|
{
|
|
|
// 收起正文时把横向空间让给曲线,保留标题和按钮以便重新展开。
|
|
|
const bool collapse = !logTextEdit->isHidden();
|
|
|
if(collapse) expandedLogSizes = rightSplitter->sizes();
|
|
|
logTextEdit->setVisible(!collapse);
|
|
|
logToggle->setText(collapse ? tr("Expand") : tr("Collapse"));
|
|
|
logGroup->layout()->activate();
|
|
|
logGroup->setMaximumWidth(collapse ? logGroup->minimumSizeHint().width() : QWIDGETSIZE_MAX);
|
|
|
if(!collapse && !expandedLogSizes.isEmpty()) rightSplitter->setSizes(expandedLogSizes);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setFittingParameters(int maxIterations, double targetError, const QString& wellName)
|
|
|
{
|
|
|
m_maxIterations = maxIterations;
|
|
|
m_targetError = targetError;
|
|
|
m_wellName = wellName;
|
|
|
|
|
|
currentIterationValue->setText(QString("0 / %1").arg(maxIterations));
|
|
|
targetPrecisionValue->setText(QString::number(targetError, 'g', 6));
|
|
|
targetWellValue->setText(wellName);
|
|
|
|
|
|
progressBar->setRange(0, maxIterations);
|
|
|
|
|
|
const QString algorithmName = m_algorithmName;
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("%1 fitting parameters set: MaxIterations=%2, TargetAccuracy=%3, TargetWell=%4")
|
|
|
.arg(algorithmName).arg(maxIterations).arg(formatScientific(targetError)).arg(wellName));
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::markFittingStarted()
|
|
|
{
|
|
|
m_startTime = QDateTime::currentDateTime();
|
|
|
stateBadge->setText(tr("Fitting in progress"));
|
|
|
|
|
|
const QString algorithmName = m_algorithmName;
|
|
|
QString timestamp = m_startTime.toString("yyyy-MM-dd hh:mm:ss");
|
|
|
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("=== %1 Fitting Session Started at %2 ===")
|
|
|
.arg(algorithmName)
|
|
|
.arg(timestamp));
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setSelectedParameters(const QStringList& parameterNames,
|
|
|
const QStringList& parameterUnits)
|
|
|
{
|
|
|
// 仅在选择变化时重建标签,拟合期间只更新数值,避免布局反复抖动。
|
|
|
QLayoutItem* item = nullptr;
|
|
|
while((item = parameterLayout->takeAt(0)) != nullptr) {
|
|
|
delete item->widget();
|
|
|
delete item;
|
|
|
}
|
|
|
parameterValues.clear();
|
|
|
for(int column = 0; column < parameterLayout->columnCount(); ++column) {
|
|
|
parameterLayout->setColumnStretch(column, 0);
|
|
|
}
|
|
|
QLabel* heading = new QLabel(tr("Current parameters"), parameterStrip);
|
|
|
heading->setObjectName("sectionHeading");
|
|
|
parameterLayout->addWidget(heading, 0, 0);
|
|
|
for(int i = 0; i < parameterNames.size(); ++i) {
|
|
|
// 名称和单位在上、数值在下,相邻参数用细竖线分隔。
|
|
|
QWidget* cell = new QWidget(parameterStrip);
|
|
|
cell->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
QVBoxLayout* cellLayout = new QVBoxLayout(cell);
|
|
|
cellLayout->setContentsMargins(12, 0, 12, 0);
|
|
|
cellLayout->setSpacing(2);
|
|
|
const QString unit = parameterUnits.value(i);
|
|
|
QLabel* name = new QLabel(parameterNames[i] + (unit.isEmpty() ? QString() : " / " + unit), cell);
|
|
|
name->setObjectName("parameterName");
|
|
|
name->setWordWrap(true);
|
|
|
name->setAlignment(Qt::AlignCenter);
|
|
|
QLabel* value = new QLabel("--", cell);
|
|
|
value->setObjectName("parameterValue");
|
|
|
value->setAlignment(Qt::AlignCenter);
|
|
|
value->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
|
cellLayout->addWidget(name, 1);
|
|
|
cellLayout->addWidget(value);
|
|
|
parameterLayout->addWidget(cell, 1, i * 2);
|
|
|
parameterLayout->setColumnStretch(i * 2, 1);
|
|
|
if(i > 0) {
|
|
|
// 分隔线独立于参数单元格,避免标题导致上下间距不一致。
|
|
|
QWidget* separator = new QWidget(parameterStrip);
|
|
|
separator->setObjectName("parameterSeparator");
|
|
|
separator->setAttribute(Qt::WA_StyledBackground, true);
|
|
|
separator->setFixedWidth(1);
|
|
|
parameterLayout->addWidget(separator, 0, i * 2 - 1, 2, 1);
|
|
|
}
|
|
|
parameterValues.append(value);
|
|
|
}
|
|
|
parameterStrip->setVisible(!parameterNames.isEmpty());
|
|
|
|
|
|
// 立即显示当前已接受的参数值。
|
|
|
updateParameterValues();
|
|
|
|
|
|
const QString algorithmName = m_algorithmName;
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("%1 selected parameters: %2").arg(algorithmName).arg(parameterNames.join(", ")));
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setTargetLogLogData(const QVector<QVector<double> >& targetData)
|
|
|
{
|
|
|
if(curveChart) {
|
|
|
curveChart->setTargetLogLogData(targetData);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::setPseudoPressureMode(bool enabled)
|
|
|
{
|
|
|
if(curveChart) {
|
|
|
curveChart->setPseudoPressureMode(enabled);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onFittingProgress(int iteration, double fitness)
|
|
|
{
|
|
|
// LM 各阶段显示累计迭代次数;达到上限之前仍可能提前收敛,不显示完成百分比。
|
|
|
currentIterationValue->setText(QString("%1 / %2").arg(qMax(0, iteration)).arg(m_maxIterations));
|
|
|
if(m_autoFitterLM) {
|
|
|
updateCurrentError(fitness);
|
|
|
return;
|
|
|
} else {
|
|
|
int displayIteration = qMax(1, qMin(iteration, m_maxIterations));
|
|
|
progressBar->setValue(displayIteration);
|
|
|
double progress = (double)displayIteration / m_maxIterations * 100;
|
|
|
progressBar->setFormat(QString("%1%").arg(progress, 0, 'f', 1));
|
|
|
currentIterationValue->setText(QString("%1 / %2").arg(displayIteration).arg(m_maxIterations));
|
|
|
}
|
|
|
|
|
|
currentComfortValue->setText(formatScientific(fitness));
|
|
|
|
|
|
// 更新最佳适应度
|
|
|
if (fitness < m_bestFitnessEver) {
|
|
|
m_bestFitnessEver = fitness;
|
|
|
bestComfortValue->setText(formatScientific(fitness));
|
|
|
bestComfortValue->setStyleSheet("QLabel { color: green; font-weight: bold; }");
|
|
|
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("Better solution found: %1").arg(formatScientific(fitness)));
|
|
|
|
|
|
// 更新参数值。
|
|
|
updateParameterValues();
|
|
|
} else {
|
|
|
bestComfortValue->setStyleSheet("QLabel { color: black; }");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& message)
|
|
|
{
|
|
|
m_isFinished = true;
|
|
|
m_isFinalizing = false;
|
|
|
stateBadge->setText(success ? tr("Fitting ended") : tr("Fitting failed"));
|
|
|
stateBadge->setStyleSheet(success
|
|
|
? "QLabel { color: #28765f; background: #edf7f2; }"
|
|
|
: "QLabel { color: #a04438; background: #fff0ed; }");
|
|
|
|
|
|
const QString algorithmName = m_algorithmName;
|
|
|
|
|
|
// 更新状态
|
|
|
if(m_autoFitterLM) {
|
|
|
stageStatus->setText(success ? tr("Fitting ended") : tr("Fitting failed"));
|
|
|
// 完成区直接读取优化器数值,失败评价显示占位符,不暴露惩罚值。
|
|
|
updateCurrentError(m_autoFitterLM->getBestFitness());
|
|
|
stageStatus->hide();
|
|
|
} else if (success) {
|
|
|
progressBar->setValue(m_maxIterations);
|
|
|
|
|
|
// 区分正常完成和用户停止
|
|
|
if(message.contains("stopped by user", Qt::CaseInsensitive)) {
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("%1 fitting stopped by user: %2").arg(algorithmName).arg(message));
|
|
|
|
|
|
// 更新进度条显示停止状态
|
|
|
progressBar->setFormat(tr("Stopped: 100%"));
|
|
|
progressBar->setStyleSheet(
|
|
|
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }"
|
|
|
"QProgressBar::chunk { background-color: #FF9800; }" // 橙色表示停止
|
|
|
);
|
|
|
} else if(message.contains("target achieved", Qt::CaseInsensitive)) {
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("%1 fitting completed successfully - target achieved: %2").arg(algorithmName).arg(message));
|
|
|
|
|
|
// 更新进度条显示成功状态
|
|
|
progressBar->setFormat(tr("Target Achieved: 100%"));
|
|
|
progressBar->setStyleSheet(
|
|
|
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }"
|
|
|
"QProgressBar::chunk { background-color: #4CAF50; }" // 绿色表示成功
|
|
|
);
|
|
|
} else {
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("%1 fitting completed successfully: %2").arg(algorithmName).arg(message));
|
|
|
|
|
|
// 更新进度条显示完成状态
|
|
|
progressBar->setFormat(tr("Completed: 100%"));
|
|
|
progressBar->setStyleSheet(
|
|
|
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }"
|
|
|
"QProgressBar::chunk { background-color: #2196F3; }" // 蓝色表示完成
|
|
|
);
|
|
|
}
|
|
|
} else {
|
|
|
// 真正的失败情况
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("%1 fitting failed: %2").arg(algorithmName).arg(message));
|
|
|
|
|
|
// 更新进度条显示失败状态
|
|
|
int currentProgress = progressBar->value();
|
|
|
progressBar->setFormat(tr("Failed (%1%)").arg((double)currentProgress / m_maxIterations * 100, 0, 'f', 1));
|
|
|
progressBar->setStyleSheet(
|
|
|
"QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }"
|
|
|
"QProgressBar::chunk { background-color: #F44336; }" // 红色表示失败
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// 禁用停止按钮
|
|
|
stopButton->setEnabled(true);
|
|
|
stopButton->setText(tr("Close"));
|
|
|
|
|
|
// 连接关闭功能
|
|
|
disconnect(stopButton, SIGNAL(clicked()), this, SLOT(onStopButtonClicked()));
|
|
|
connect(stopButton, SIGNAL(clicked()), this, SLOT(close()));
|
|
|
|
|
|
// 同步最终参数值。
|
|
|
updateParameterValues();
|
|
|
|
|
|
// 添加完成时间戳
|
|
|
QDateTime endTime = QDateTime::currentDateTime();
|
|
|
QString timestamp = endTime.toString("yyyy-MM-dd hh:mm:ss");
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("=== %1 Fitting Session Ended at %2 ===")
|
|
|
.arg(algorithmName).arg(timestamp));
|
|
|
|
|
|
// 如果有开始时间,则计算总耗时
|
|
|
if (m_startTime.isValid()) {
|
|
|
qint64 ms = m_startTime.msecsTo(endTime);
|
|
|
double seconds = ms / 1000.0;
|
|
|
|
|
|
int minutes = static_cast<int>(seconds / 60.0);
|
|
|
double remainSec = seconds - minutes * 60.0;
|
|
|
const int totalEvaluations = m_autoFitterLM
|
|
|
? m_autoFitterLM->getTotalEvaluations()
|
|
|
: (m_autoFitterPSO ? m_autoFitterPSO->getTotalEvaluations() : 0);
|
|
|
|
|
|
// LM 的调用次数由优化器汇总,这里只在结束时显示一次总耗时。
|
|
|
if(m_autoFitterLM) {
|
|
|
// 不足一分钟只显示秒,窗口汇总和完成日志共用同一耗时文本。
|
|
|
const QString elapsed = minutes == 0 ? tr("%1 s").arg(seconds, 0, 'f', 1)
|
|
|
: tr("%1 min %2 s").arg(minutes).arg(remainSec, 0, 'f', 1);
|
|
|
addLogMessage(tr("Total time: %1").arg(elapsed));
|
|
|
runSummary->setText(tr("Solver calls: %1 | Total time: %2")
|
|
|
.arg(m_autoFitterLM->getSolverCallCount()).arg(elapsed));
|
|
|
runSummary->show();
|
|
|
} else {
|
|
|
addLogMessage(tr("Total fitting time: %1 s (%2 min %3 s), total solver calls: %4")
|
|
|
.arg(seconds, 0, 'f', 2).arg(minutes).arg(remainSec, 0, 'f', 2).arg(totalEvaluations));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onFinalizingStarted()
|
|
|
{
|
|
|
// 最终完整求解负责生成正式快照,此阶段不能再次停止或关闭进度窗口。
|
|
|
m_isFinalizing = true;
|
|
|
stateBadge->setText(tr("Finalizing..."));
|
|
|
if(m_autoFitterLM) stageStatus->setText(tr("Generating final result..."));
|
|
|
stopButton->setEnabled(false);
|
|
|
stopButton->setText(tr("Finalizing..."));
|
|
|
addLogMessage(tr("Generating the final result with the current best parameters..."));
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onStopButtonClicked()
|
|
|
{
|
|
|
const bool isRunning = (m_autoFitterPSO && m_autoFitterPSO->isRunning()) ||
|
|
|
(m_autoFitterLM && m_autoFitterLM->isRunning());
|
|
|
|
|
|
if (isRunning) {
|
|
|
const QString algorithmName = m_algorithmName;
|
|
|
int ret = QMessageBox::question(this, tr("Confirm Stop"),
|
|
|
tr("Are you sure you want to stop the %1 fitting process?").arg(algorithmName),
|
|
|
QMessageBox::Yes | QMessageBox::No,
|
|
|
QMessageBox::No);
|
|
|
|
|
|
if (ret == QMessageBox::Yes) {
|
|
|
// 先锁定按钮,等优化循环退出后由 finalizingStarted() 切换到最终求解状态。
|
|
|
stopButton->setEnabled(false);
|
|
|
stopButton->setText(tr("Stopping..."));
|
|
|
stateBadge->setText(tr("Stopping..."));
|
|
|
if(m_autoFitterLM) stageStatus->setText(tr("Stopping..."));
|
|
|
if (m_autoFitterPSO) {
|
|
|
m_autoFitterPSO->stopFitting();
|
|
|
} else if (m_autoFitterLM) {
|
|
|
m_autoFitterLM->stopFitting();
|
|
|
}
|
|
|
if(!m_autoFitterLM) addLogMessage(tr("User requested to stop %1 fitting").arg(algorithmName));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::updateParameterValues()
|
|
|
{
|
|
|
// 使用当前已接受参数,普通数值优先显示十进制,极小数仍保留有效位。
|
|
|
const QVector<double> values = m_autoFitterLM ? m_autoFitterLM->getBestSolution()
|
|
|
: (m_autoFitterPSO ? m_autoFitterPSO->getBestSolution() : QVector<double>());
|
|
|
for(int i = 0; i < parameterValues.size(); ++i) {
|
|
|
parameterValues[i]->setText(i < values.size() ? QString::number(values[i], 'g', 6) : "--");
|
|
|
parameterValues[i]->setToolTip(i < values.size() ? QString::number(values[i], 'g', 15) : QString());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::updateCurrentError(double fitness)
|
|
|
{
|
|
|
// 仅展示当前已接受参数对应的总误差,未获得有效结果时显示占位符。
|
|
|
const QString value = nmAutoFitChartIsFinite(fitness) && fitness >= 0.0 && fitness < 1.0e9
|
|
|
? QString::number(fitness, 'f', 6) : "--";
|
|
|
resultSummary->setText(tr("Current error: %1")
|
|
|
.arg(QString("<span style=\"color:#233247; font-weight:500;\">%1</span>").arg(value)));
|
|
|
resultSummary->show();
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::addLogMessage(const QString& message)
|
|
|
{
|
|
|
// 获取滚动条
|
|
|
QScrollBar* scrollBar = logTextEdit->verticalScrollBar();
|
|
|
|
|
|
// 记录当前是否在底部
|
|
|
bool wasAtBottom = (scrollBar->value() == scrollBar->maximum());
|
|
|
|
|
|
// 添加消息
|
|
|
logTextEdit->append(message);
|
|
|
|
|
|
// 只有当用户之前在底部时才自动滚动到底部
|
|
|
if (wasAtBottom) {
|
|
|
scrollBar->setValue(scrollBar->maximum());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
QString nmWxAutomaticfittingStart::formatScientific(double value)
|
|
|
{
|
|
|
return QString::number(value, 'e', 3);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::closeEvent(QCloseEvent *event)
|
|
|
{
|
|
|
const bool isRunning = (m_autoFitterPSO && m_autoFitterPSO->isRunning()) ||
|
|
|
(m_autoFitterLM && m_autoFitterLM->isRunning());
|
|
|
const QString algorithmName = m_algorithmName;
|
|
|
if(m_isFinalizing && !m_isFinished) {
|
|
|
QMessageBox::information(
|
|
|
this,
|
|
|
tr("Finalizing"),
|
|
|
tr("The final result is being generated. Please wait for the calculation to finish."));
|
|
|
event->ignore();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (isRunning && !m_isFinished) {
|
|
|
int ret = QMessageBox::question(this, tr("Confirm Close"),
|
|
|
tr("%1 fitting is running. Do you want to stop it and close the window?").arg(algorithmName),
|
|
|
QMessageBox::Yes | QMessageBox::No,
|
|
|
QMessageBox::No);
|
|
|
|
|
|
if (ret == QMessageBox::Yes) {
|
|
|
if (m_autoFitterPSO) {
|
|
|
m_autoFitterPSO->stopFitting();
|
|
|
} else if (m_autoFitterLM) {
|
|
|
m_autoFitterLM->stopFitting();
|
|
|
}
|
|
|
event->accept();
|
|
|
} else {
|
|
|
event->ignore();
|
|
|
}
|
|
|
} else {
|
|
|
event->accept();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onLogMessageReceived(const QString& message)
|
|
|
{
|
|
|
addLogMessage(message);
|
|
|
}
|
|
|
|
|
|
void nmWxAutomaticfittingStart::onBestCurveUpdated(QVector<QVector<double> > targetData,
|
|
|
QVector<QVector<double> > bestData,
|
|
|
int iteration,
|
|
|
double fitness)
|
|
|
{
|
|
|
if(!curveChart) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if(!targetData.isEmpty()) {
|
|
|
curveChart->setTargetLogLogData(targetData);
|
|
|
}
|
|
|
|
|
|
curveChart->setBestLogLogData(bestData, iteration, fitness);
|
|
|
// 形状改善时总误差可能上升,参数值仍须同步到当前已接受曲线。
|
|
|
if(m_autoFitterLM) {
|
|
|
updateParameterValues();
|
|
|
updateCurrentError(fitness);
|
|
|
}
|
|
|
}
|