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/nmSubWxs/nmWxAutomaticFittingStart.cpp

1126 lines
40 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 "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);
}
}