Files
lammps/tools/lammps-gui/chartviewer.cpp
Axel Kohlmeyer cfcbad9e38 back to 5 sigma
2024-08-25 16:07:22 -04:00

1019 lines
32 KiB
C++

/* ----------------------------------------------------------------------
LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator
https://www.lammps.org/, Sandia National Laboratories
LAMMPS development team: developers@lammps.org
Copyright (2003) Sandia Corporation. Under the terms of Contract
DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
certain rights in this software. This software is distributed under
the GNU General Public License.
See the README file in the top-level LAMMPS directory.
------------------------------------------------------------------------- */
#include "chartviewer.h"
#include "lammpsgui.h"
#include <QAction>
#include <QApplication>
#include <QChart>
#include <QCloseEvent>
#include <QComboBox>
#include <QEvent>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QKeySequence>
#include <QLabel>
#include <QLayout>
#include <QLineSeries>
#include <QMenu>
#include <QMenuBar>
#include <QPushButton>
#include <QSettings>
#include <QSpacerItem>
#include <QSpinBox>
#include <QTextStream>
#include <QTime>
#include <QVBoxLayout>
#include <QValueAxis>
#include <QVariant>
#include <cmath>
using namespace QtCharts;
ChartWindow::ChartWindow(const QString &_filename, QWidget *parent) :
QWidget(parent), menu(new QMenuBar), file(new QMenu("&File")), saveAsAct(nullptr),
exportCsvAct(nullptr), exportDatAct(nullptr), exportYamlAct(nullptr), closeAct(nullptr),
stopAct(nullptr), quitAct(nullptr), filename(_filename)
{
auto *top = new QHBoxLayout;
menu->addMenu(file);
menu->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
// workaround for incorrect highlight bug on macOS
auto *dummy = new QPushButton(QIcon(), "");
dummy->hide();
do_raw = true;
do_smooth = true;
smooth = new QComboBox;
smooth->addItem("Raw");
smooth->addItem("Smooth");
smooth->addItem("Both");
smooth->setCurrentIndex(2);
smooth->show();
window = new QSpinBox;
window->setRange(5, 999);
window->setValue(10);
window->setEnabled(true);
window->setToolTip("Smoothing Window Size");
order = new QSpinBox;
order->setRange(1, 20);
order->setValue(4);
order->setEnabled(true);
order->setToolTip("Smoothing Order");
auto *normal = new QPushButton(QIcon(":/icons/gtk-zoom-fit.png"), "");
normal->setToolTip("Reset zoom to normal");
columns = new QComboBox;
top->addWidget(menu);
top->addSpacerItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum));
top->addWidget(dummy);
top->addWidget(new QLabel("Plot:"));
top->addWidget(smooth);
top->addWidget(new QLabel(" Smooth:"));
top->addWidget(window);
top->addWidget(order);
top->addWidget(new QLabel(" "));
top->addWidget(normal);
top->addWidget(new QLabel(" Data:"));
top->addWidget(columns);
saveAsAct = file->addAction("&Save Graph As...", this, &ChartWindow::saveAs);
saveAsAct->setIcon(QIcon(":/icons/document-save-as.png"));
exportCsvAct = file->addAction("&Export data to CSV...", this, &ChartWindow::exportCsv);
exportCsvAct->setIcon(QIcon(":/icons/application-calc.png"));
exportDatAct = file->addAction("Export data to &Gnuplot...", this, &ChartWindow::exportDat);
exportDatAct->setIcon(QIcon(":/icons/application-plot.png"));
exportYamlAct = file->addAction("Export data to &YAML...", this, &ChartWindow::exportYaml);
exportYamlAct->setIcon(QIcon(":/icons/yaml-file-icon.png"));
file->addSeparator();
stopAct = file->addAction("Stop &Run", this, &ChartWindow::stop_run);
stopAct->setIcon(QIcon(":/icons/process-stop.png"));
stopAct->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Slash));
closeAct = file->addAction("&Close", this, &QWidget::close);
closeAct->setIcon(QIcon(":/icons/window-close.png"));
closeAct->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_W));
quitAct = file->addAction("&Quit", this, &ChartWindow::quit);
quitAct->setIcon(QIcon(":/icons/application-exit.png"));
quitAct->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_Q));
auto *layout = new QVBoxLayout;
layout->addLayout(top);
setLayout(layout);
connect(smooth, SIGNAL(currentIndexChanged(int)), this, SLOT(select_smooth(int)));
connect(window, &QAbstractSpinBox::editingFinished, this, &ChartWindow::update_smooth);
connect(order, &QAbstractSpinBox::editingFinished, this, &ChartWindow::update_smooth);
connect(window, QOverload<int>::of(&QSpinBox::valueChanged), this, &ChartWindow::update_smooth);
connect(order, QOverload<int>::of(&QSpinBox::valueChanged), this, &ChartWindow::update_smooth);
connect(normal, &QPushButton::released, this, &ChartWindow::reset_zoom);
connect(columns, SIGNAL(currentIndexChanged(int)), this, SLOT(change_chart(int)));
installEventFilter(this);
QSettings settings;
resize(settings.value("chartx", 500).toInt(), settings.value("charty", 320).toInt());
}
int ChartWindow::get_step() const
{
if (charts.size() > 0) {
auto *v = charts[0];
if (v)
return (int)v->get_step(v->get_count() - 1);
else
return -1;
} else {
return -1;
}
}
void ChartWindow::reset_charts()
{
while (layout()->count() > 1) {
auto *item = layout()->takeAt(1);
if (item) {
layout()->removeItem(item);
delete item->widget();
delete item;
}
}
charts.clear();
columns->clear();
}
void ChartWindow::add_chart(const QString &title, int index)
{
auto *chart = new ChartViewer(title, index);
layout()->addWidget(chart);
columns->addItem(title, index);
columns->show();
// hide all but the first chart added
if (charts.size() > 0) chart->hide();
charts.append(chart);
}
void ChartWindow::add_data(int step, double data, int index)
{
for (auto &c : charts)
if (c->get_index() == index) c->add_data(step, data);
}
void ChartWindow::quit()
{
LammpsGui *main = nullptr;
for (QWidget *widget : QApplication::topLevelWidgets())
if (widget->objectName() == "LammpsGui") main = dynamic_cast<LammpsGui *>(widget);
if (main) main->quit();
}
void ChartWindow::reset_zoom()
{
int choice = columns->currentData().toInt();
if ((choice >= 0) && (choice < charts.size())) {
charts[choice]->update_smooth();
charts[choice]->reset_zoom();
}
}
void ChartWindow::stop_run()
{
LammpsGui *main = nullptr;
for (QWidget *widget : QApplication::topLevelWidgets())
if (widget->objectName() == "LammpsGui") main = dynamic_cast<LammpsGui *>(widget);
if (main) main->stop_run();
}
void ChartWindow::select_smooth(int)
{
switch (smooth->currentIndex()) {
case 0:
do_raw = true;
do_smooth = false;
break;
case 1:
do_raw = false;
do_smooth = true;
break;
case 2: // fallthrough
default:
do_raw = true;
do_smooth = true;
break;
}
window->setEnabled(do_smooth);
order->setEnabled(do_smooth);
update_smooth();
}
void ChartWindow::update_smooth()
{
int wval = window->value();
int oval = order->value();
for (auto &c : charts)
c->smooth_param(do_raw, do_smooth, wval, oval);
}
void ChartWindow::saveAs()
{
if (charts.empty()) return;
QString defaultname = filename + "." + columns->currentText() + ".png";
if (filename.isEmpty()) defaultname = columns->currentText() + ".png";
QString fileName = QFileDialog::getSaveFileName(this, "Save Chart as Image", defaultname,
"Image Files (*.jpg *.png *.bmp *.ppm)");
if (!fileName.isEmpty()) {
int choice = columns->currentData().toInt();
for (auto &c : charts)
if (choice == c->get_index()) c->grab().save(fileName);
}
}
void ChartWindow::exportDat()
{
if (charts.empty()) return;
QString defaultname = filename + ".dat";
if (filename.isEmpty()) defaultname = "lammpsdata.dat";
QString fileName = QFileDialog::getSaveFileName(this, "Save Chart as Gnuplot data", defaultname,
"Image Files (*.dat)");
if (!fileName.isEmpty()) {
QFile file(fileName);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
constexpr int fw = 16;
out.setFieldAlignment(QTextStream::AlignRight);
out.setRealNumberPrecision(8);
out << "# Thermodynamic data from " << filename << "\n";
out << "# Step";
for (auto &c : charts)
out << qSetFieldWidth(0) << ' ' << qSetFieldWidth(fw) << c->get_title();
out << qSetFieldWidth(0) << '\n';
int lines = charts[0]->get_count();
for (int i = 0; i < lines; ++i) {
// timestep
out << qSetFieldWidth(0) << ' ' << qSetFieldWidth(fw) << charts[0]->get_step(i);
for (auto &c : charts)
out << qSetFieldWidth(0) << ' ' << qSetFieldWidth(fw) << c->get_data(i);
out << qSetFieldWidth(0) << '\n';
}
file.close();
}
}
}
void ChartWindow::exportCsv()
{
if (charts.empty()) return;
QString defaultname = filename + ".csv";
if (filename.isEmpty()) defaultname = "lammpsdata.csv";
QString fileName = QFileDialog::getSaveFileName(this, "Save Chart as CSV data", defaultname,
"Image Files (*.csv)");
if (!fileName.isEmpty()) {
QFile file(fileName);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
out.setRealNumberPrecision(8);
out << "Step";
for (auto &c : charts)
out << ',' << c->get_title();
out << '\n';
int lines = charts[0]->get_count();
for (int i = 0; i < lines; ++i) {
// timestep
out << charts[0]->get_step(i);
for (auto &c : charts)
out << ',' << c->get_data(i);
out << '\n';
}
file.close();
}
}
}
void ChartWindow::exportYaml()
{
if (charts.empty()) return;
QString defaultname = filename + ".yaml";
if (filename.isEmpty()) defaultname = "lammpsdata.yaml";
QString fileName = QFileDialog::getSaveFileName(this, "Save Chart as YAML data", defaultname,
"Image Files (*.yaml, *.yml)");
if (!fileName.isEmpty()) {
QFile file(fileName);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream out(&file);
out.setRealNumberPrecision(8);
out << "---\n";
out << "keywords: ['Step'";
for (auto &c : charts)
out << ", " << c->get_title();
out << "]\n";
out << "data: \n";
int lines = charts[0]->get_count();
for (int i = 0; i < lines; ++i) {
// timestep
out << " - [" << charts[0]->get_step(i);
// data
for (auto &c : charts)
out << ", " << c->get_data(i);
out << "]\n";
}
out << "...\n";
file.close();
}
}
}
void ChartWindow::change_chart(int)
{
int choice = columns->currentData().toInt();
for (auto &c : charts) {
if (choice == c->get_index())
c->show();
else
c->hide();
}
}
void ChartWindow::closeEvent(QCloseEvent *event)
{
QSettings settings;
if (!isMaximized()) {
settings.setValue("chartx", width());
settings.setValue("charty", height());
}
QWidget::closeEvent(event);
}
// event filter to handle "Ambiguous shortcut override" issues
bool ChartWindow::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::ShortcutOverride) {
auto *keyEvent = dynamic_cast<QKeyEvent *>(event);
if (!keyEvent) return QWidget::eventFilter(watched, event);
if (keyEvent->modifiers().testFlag(Qt::ControlModifier) && keyEvent->key() == '/') {
stop_run();
event->accept();
return true;
}
if (keyEvent->modifiers().testFlag(Qt::ControlModifier) && keyEvent->key() == 'W') {
close();
event->accept();
return true;
}
}
return QWidget::eventFilter(watched, event);
}
/* -------------------------------------------------------------------- */
ChartViewer::ChartViewer(const QString &title, int _index, QWidget *parent) :
QChartView(parent), last_step(-1), index(_index), window(10), order(4), chart(new QChart),
series(new QLineSeries), smooth(nullptr), xaxis(new QValueAxis), yaxis(new QValueAxis),
do_raw(true), do_smooth(true)
{
chart->legend()->hide();
chart->addAxis(xaxis, Qt::AlignBottom);
chart->addAxis(yaxis, Qt::AlignLeft);
xaxis->setTitleText("Time step");
xaxis->setTickCount(5);
xaxis->setLabelFormat("%d");
yaxis->setTickCount(5);
xaxis->setMinorTickCount(5);
yaxis->setMinorTickCount(5);
yaxis->setTitleText(title);
series->setName(title);
setRenderHint(QPainter::Antialiasing);
setChart(chart);
setRubberBand(QChartView::RectangleRubberBand);
last_update = QTime::currentTime();
update_smooth();
}
/* -------------------------------------------------------------------- */
ChartViewer::~ChartViewer()
{
delete xaxis;
delete yaxis;
delete smooth;
delete series;
delete chart;
}
/* -------------------------------------------------------------------- */
void ChartViewer::add_data(int step, double data)
{
if (last_step < step) {
last_step = step;
// do not add data that deviates by more than 4 sigma from the average
// over the last 5 to 20 data items. this is a hack to work around
// getting corrupted data from lammps_get_last_thermo()
const auto &points = series->points();
const auto count = points.count();
if (count > 4) {
double ysum = 0.0;
double ysumsq = 0.0;
int first = count - 20;
if (first < 0) first = 0;
for (int i = first; i < count; ++i) {
double val = points[i].y();
ysum += val;
ysumsq += val * val;
}
const double num = count - first;
const double avg = ysum / num;
const double avgsq = ysumsq / num;
if (fabs(data - avg) > (5.0 * sqrt(avgsq - (avg * avg)))) return;
}
series->append(step, data);
QSettings settings;
// update the chart display only after at least updchart milliseconds have passed
if (last_update.msecsTo(QTime::currentTime()) > settings.value("updchart", "500").toInt()) {
last_update = QTime::currentTime();
update_smooth();
reset_zoom();
}
}
}
/* -------------------------------------------------------------------- */
void ChartViewer::reset_zoom()
{
auto points = series->points();
// get min/max for plot
qreal xmin = 1.0e100;
qreal xmax = -1.0e100;
qreal ymin = 1.0e100;
qreal ymax = -1.0e100;
for (auto &p : points) {
xmin = qMin(xmin, p.x());
xmax = qMax(xmax, p.x());
ymin = qMin(ymin, p.y());
ymax = qMax(ymax, p.y());
}
// if plotting the smoothed plot, check for its min/max values, too
if (smooth) {
auto spoints = smooth->points();
for (auto &p : spoints) {
xmin = qMin(xmin, p.x());
xmax = qMax(xmax, p.x());
ymin = qMin(ymin, p.y());
ymax = qMax(ymax, p.y());
}
}
// avoid (nearly) empty ranges
double deltax = xmax - xmin;
if ((deltax / ((xmax == 0.0) ? 1.0 : xmax)) < 1.0e-10) {
if ((xmin == 0.0) || (xmax == 0.0)) {
xmin = -0.025;
xmax = 0.025;
} else {
xmin -= 0.025 * fabs(xmin);
xmax += 0.025 * fabs(xmax);
}
}
double deltay = ymax - ymin;
if ((deltay / ((ymax == 0.0) ? 1.0 : ymax)) < 1.0e-10) {
if ((ymin == 0.0) || (ymax == 0.0)) {
ymin = -0.025;
ymax = 0.025;
} else {
ymin -= 0.025 * fabs(ymin);
ymax += 0.025 * fabs(ymax);
}
}
xaxis->setRange(xmin, xmax);
yaxis->setRange(ymin, ymax);
}
/* -------------------------------------------------------------------- */
void ChartViewer::smooth_param(bool _do_raw, bool _do_smooth, int _window, int _order)
{
// turn off raw plot
if (!_do_raw) {
if (do_raw) chart->removeSeries(series);
}
// turn off smooth plot
if (!_do_smooth) {
if (smooth) {
chart->removeSeries(smooth);
delete smooth;
smooth = nullptr;
}
}
do_raw = _do_raw;
do_smooth = _do_smooth;
window = _window;
order = _order;
update_smooth();
}
/* -------------------------------------------------------------------- */
// update smooth plot data
static QList<QPointF> calc_sgsmooth(const QList<QPointF> &input, const int window, const int order);
void ChartViewer::update_smooth()
{
auto allseries = chart->series();
if (do_raw) {
// add raw data if not in chart
if (!allseries.contains(series)) {
series->setPen(QPen(QBrush(QColor(100, 150, 255)), 3, Qt::SolidLine, Qt::RoundCap));
chart->addSeries(series);
series->attachAxis(xaxis);
series->attachAxis(yaxis);
}
}
if (do_smooth) {
if (series->count() > (2 * window)) {
if (!smooth) {
smooth = new QLineSeries;
smooth->setPen(QPen(QBrush(QColor(255, 125, 125)), 3, Qt::SolidLine, Qt::RoundCap));
chart->addSeries(smooth);
smooth->attachAxis(xaxis);
smooth->attachAxis(yaxis);
}
smooth->clear();
smooth->append(calc_sgsmooth(series->points(), window, order));
}
}
}
//! default convergence
static constexpr double TINY_FLOAT = 1.0e-300;
//! comfortable array of doubles
typedef std::vector<double> float_vect;
//! comfortable array of ints;
typedef std::vector<int> int_vect;
// savitzky golay smoothing.
static float_vect sg_smooth(const float_vect &v, const int w, const int deg);
QList<QPointF> calc_sgsmooth(const QList<QPointF> &input, int window, int order)
{
const int ndat = input.count();
if (ndat < 2 * window + 2) window = ndat / 2 - 1;
if (window > 1) {
float_vect in(ndat);
QList<QPointF> rv;
for (int i = 0; i < ndat; ++i) {
in[i] = input[i].y();
}
float_vect out = sg_smooth(in, window, order);
for (int i = 0; i < ndat; ++i) {
rv.append(QPointF(input[i].x(), out[i]));
}
return rv;
} else {
return input;
}
}
/*! matrix class.
*
* This is a matrix class derived from a vector of float_vects. Note that
* the matrix elements indexed [row][column] with indices starting at 0 (c
* style). Also note that because of its design looping through rows should
* be faster than looping through columns.
*
* \brief two dimensional floating point array
*/
class float_mat : public std::vector<float_vect> {
private:
//! disable the default constructor
explicit float_mat() {};
//! disable assignment operator until it is implemented.
float_mat &operator=(const float_mat &) { return *this; };
public:
//! constructor with sizes
float_mat(const std::size_t rows, const std::size_t cols, const double def = 0.0);
//! copy constructor for matrix
float_mat(const float_mat &m);
//! copy constructor for vector
float_mat(const float_vect &v);
//! use default destructor
// ~float_mat() {};
//! get size
int nr_rows(void) const { return size(); };
//! get size
int nr_cols(void) const { return front().size(); };
};
// constructor with sizes
float_mat::float_mat(const std::size_t rows, const std::size_t cols, const double defval) :
std::vector<float_vect>(rows)
{
for (std::size_t i = 0; i < rows; ++i) {
(*this)[i].resize(cols, defval);
}
#if 0
if ((rows < 1) || (cols < 1)) {
char buffer[1024];
sprintf(buffer, "cannot build matrix with %d rows and %d columns\n",
rows, cols);
sgs_error(buffer);
}
#endif
}
// copy constructor for matrix
float_mat::float_mat(const float_mat &m) : std::vector<float_vect>(m.size())
{
float_mat::iterator inew = begin();
float_mat::const_iterator iold = m.begin();
for (/* empty */; iold < m.end(); ++inew, ++iold) {
const auto oldsz = iold->size();
inew->resize(oldsz);
const float_vect oldvec(*iold);
*inew = oldvec;
}
}
// copy constructor for vector
float_mat::float_mat(const float_vect &v) : std::vector<float_vect>(1)
{
const auto oldsz = v.size();
front().resize(oldsz);
front() = v;
}
//////////////////////
// Helper functions //
//////////////////////
//! permute() orders the rows of A to match the integers in the index array.
void permute(float_mat &A, int_vect &idx)
{
int_vect i(idx.size());
for (int j = 0; j < A.nr_rows(); ++j) {
i[j] = j;
}
// loop over permuted indices
for (int j = 0; j < A.nr_rows(); ++j) {
if (i[j] != idx[j]) {
// search only the remaining indices
for (int k = j + 1; k < A.nr_rows(); ++k) {
if (i[k] == idx[j]) {
std::swap(A[j], A[k]); // swap the rows and
i[k] = i[j]; // the elements of
i[j] = idx[j]; // the ordered index.
break; // next j
}
}
}
}
}
/*! \brief Implicit partial pivoting.
*
* The function looks for pivot element only in rows below the current
* element, A[idx[row]][column], then swaps that row with the current one in
* the index map. The algorithm is for implicit pivoting (i.e., the pivot is
* chosen as if the max coefficient in each row is set to 1) based on the
* scaling information in the vector scale. The map of swapped indices is
* recorded in swp. The return value is +1 or -1 depending on whether the
* number of row swaps was even or odd respectively. */
static int partial_pivot(float_mat &A, const std::size_t row, const std::size_t col,
float_vect &scale, int_vect &idx, double tol)
{
if (tol <= 0.0) tol = TINY_FLOAT;
int swapNum = 1;
// default pivot is the current position, [row,col]
std::size_t pivot = row;
double piv_elem = fabs(A[idx[row]][col]) * scale[idx[row]];
// loop over possible pivots below current
for (int j = row + 1; j < A.nr_rows(); ++j) {
const double tmp = fabs(A[idx[j]][col]) * scale[idx[j]];
// if this elem is larger, then it becomes the pivot
if (tmp > piv_elem) {
pivot = j;
piv_elem = tmp;
}
}
#if 0
if(piv_elem < tol) {
sgs_error("partial_pivot(): Zero pivot encountered.\n")
#endif
if (pivot > row) { // bring the pivot to the diagonal
int j = idx[row]; // reorder swap array
idx[row] = idx[pivot];
idx[pivot] = j;
swapNum = -swapNum; // keeping track of odd or even swap
}
return swapNum;
}
/*! \brief Perform backward substitution.
*
* Solves the system of equations A*b=a, ASSUMING that A is upper
* triangular. If diag==1, then the diagonal elements are additionally
* assumed to be 1. Note that the lower triangular elements are never
* checked, so this function is valid to use after a LU-decomposition in
* place. A is not modified, and the solution, b, is returned in a. */
static void lu_backsubst(float_mat &A, float_mat &a, bool diag = false)
{
for (int r = (A.nr_rows() - 1); r >= 0; --r) {
for (int c = (A.nr_cols() - 1); c > r; --c) {
for (int k = 0; k < A.nr_cols(); ++k) {
a[r][k] -= A[r][c] * a[c][k];
}
}
if (!diag) {
for (int k = 0; k < A.nr_cols(); ++k) {
a[r][k] /= A[r][r];
}
}
}
}
/*! \brief Perform forward substitution.
*
* Solves the system of equations A*b=a, ASSUMING that A is lower
* triangular. If diag==1, then the diagonal elements are additionally
* assumed to be 1. Note that the upper triangular elements are never
* checked, so this function is valid to use after a LU-decomposition in
* place. A is not modified, and the solution, b, is returned in a. */
static void lu_forwsubst(float_mat &A, float_mat &a, bool diag = true)
{
for (int r = 0; r < A.nr_rows(); ++r) {
for (int c = 0; c < r; ++c) {
for (int k = 0; k < A.nr_cols(); ++k) {
a[r][k] -= A[r][c] * a[c][k];
}
}
if (!diag) {
for (int k = 0; k < A.nr_cols(); ++k) {
a[r][k] /= A[r][r];
}
}
}
}
/*! \brief Performs LU factorization in place.
*
* This is Crout's algorithm (cf., Num. Rec. in C, Section 2.3). The map of
* swapped indeces is recorded in idx. The return value is +1 or -1
* depending on whether the number of row swaps was even or odd
* respectively. idx must be preinitialized to a valid set of indices
* (e.g., {1,2, ... ,A.nr_rows()}). */
static int lu_factorize(float_mat &A, int_vect &idx, double tol = TINY_FLOAT)
{
if (tol <= 0.0) tol = TINY_FLOAT;
#if 0
if ((A.nr_rows() == 0) || (A.nr_rows() != A.nr_cols())) {
sgs_error("lu_factorize(): cannot handle empty "
"or nonsquare matrices.\n");
return 0;
}
#endif
float_vect scale(A.nr_rows()); // implicit pivot scaling
for (int i = 0; i < A.nr_rows(); ++i) {
double maxval = 0.0;
for (int j = 0; j < A.nr_cols(); ++j) {
if (fabs(A[i][j]) > maxval) maxval = fabs(A[i][j]);
}
if (maxval == 0.0) {
#if 0
sgs_error("lu_factorize(): zero pivot found.\n");
#endif
return 0;
}
scale[i] = 1.0 / maxval;
}
int swapNum = 1;
for (int c = 0; c < A.nr_cols(); ++c) { // loop over columns
swapNum *= partial_pivot(A, c, c, scale, idx, tol); // bring pivot to diagonal
for (int r = 0; r < A.nr_rows(); ++r) { // loop over rows
int lim = (r < c) ? r : c;
for (int j = 0; j < lim; ++j) {
A[idx[r]][c] -= A[idx[r]][j] * A[idx[j]][c];
}
if (r > c) A[idx[r]][c] /= A[idx[c]][c];
}
}
permute(A, idx);
return swapNum;
}
/*! \brief Solve a system of linear equations.
* Solves the inhomogeneous matrix problem with lu-decomposition. Note that
* inversion may be accomplished by setting a to the identity_matrix. */
static float_mat lin_solve(const float_mat &A, const float_mat &a, double tol = TINY_FLOAT)
{
float_mat B(A);
float_mat b(a);
int_vect idx(B.nr_rows());
for (int j = 0; j < B.nr_rows(); ++j) {
idx[j] = j; // init row swap label array
}
lu_factorize(B, idx, tol); // get the lu-decomp.
permute(b, idx); // sort the inhomogeneity to match the lu-decomp
lu_forwsubst(B, b); // solve the forward problem
lu_backsubst(B, b); // solve the backward problem
return b;
}
///////////////////////
// related functions //
///////////////////////
//! Returns the inverse of a matrix using LU-decomposition.
static float_mat invert(const float_mat &A)
{
const int n = A.size();
float_mat E(n, n, 0.0);
float_mat B(A);
for (int i = 0; i < n; ++i) {
E[i][i] = 1.0;
}
return lin_solve(B, E);
}
//! returns the transposed matrix.
static float_mat transpose(const float_mat &a)
{
float_mat res(a.nr_cols(), a.nr_rows());
for (int i = 0; i < a.nr_rows(); ++i) {
for (int j = 0; j < a.nr_cols(); ++j) {
res[j][i] = a[i][j];
}
}
return res;
}
//! matrix multiplication.
float_mat operator*(const float_mat &a, const float_mat &b)
{
float_mat res(a.nr_rows(), b.nr_cols());
#if 0
if (a.nr_cols() != b.nr_rows()) {
sgs_error("incompatible matrices in multiplication\n");
return res;
}
#endif
for (int i = 0; i < a.nr_rows(); ++i) {
for (int j = 0; j < b.nr_cols(); ++j) {
double sum(0.0);
for (int k = 0; k < a.nr_cols(); ++k) {
sum += a[i][k] * b[k][j];
}
res[i][j] = sum;
}
}
return res;
}
//! calculate savitzky golay coefficients.
static float_vect sg_coeff(const float_vect &b, const std::size_t deg)
{
const std::size_t rows(b.size());
const std::size_t cols(deg + 1);
float_mat A(rows, cols);
float_vect res(rows);
// generate input matrix for least squares fit
for (std::size_t i = 0; i < rows; ++i) {
for (std::size_t j = 0; j < cols; ++j) {
A[i][j] = pow(double(i), double(j));
}
}
float_mat c(invert(transpose(A) * A) * (transpose(A) * transpose(b)));
for (std::size_t i = 0; i < b.size(); ++i) {
res[i] = c[0][0];
for (std::size_t j = 1; j <= deg; ++j) {
res[i] += c[j][0] * pow(double(i), double(j));
}
}
return res;
}
/*! \brief savitzky golay smoothing.
*
* This method means fitting a polynome of degree 'deg' to a sliding window
* of width 2w+1 throughout the data. The needed coefficients are
* generated dynamically by doing a least squares fit on a "symmetric" unit
* vector of size 2w+1, e.g. for w=2 b=(0,0,1,0,0). evaluating the polynome
* yields the sg-coefficients. at the border non symmectric vectors b are
* used. */
float_vect sg_smooth(const float_vect &v, const int width, const int deg)
{
float_vect res(v.size(), 0.0);
#if 0
if ((width < 1) || (deg < 0) || (v.size() < (2 * width + 2))) {
sgs_error("sgsmooth: parameter error.\n");
return res;
}
#endif
const int window = 2 * width + 1;
const int endidx = v.size() - 1;
// do a regular sliding window average
if (deg == 0) {
// handle border cases first because we need different coefficients
for (int i = 0; i < width; ++i) {
const double scale = 1.0 / double(i + 1);
const float_vect c1(width, scale);
for (int j = 0; j <= i; ++j) {
res[i] += c1[j] * v[j];
res[endidx - i] += c1[j] * v[endidx - j];
}
}
// now loop over rest of data. reusing the "symmetric" coefficients.
const double scale = 1.0 / double(window);
const float_vect c2(window, scale);
for (std::size_t i = 0; i <= (v.size() - window); ++i) {
for (int j = 0; j < window; ++j) {
res[i + width] += c2[j] * v[i + j];
}
}
return res;
}
// handle border cases first because we need different coefficients
for (int i = 0; i < width; ++i) {
float_vect b1(window, 0.0);
b1[i] = 1.0;
const float_vect c1(sg_coeff(b1, deg));
for (int j = 0; j < window; ++j) {
res[i] += c1[j] * v[j];
res[endidx - i] += c1[j] * v[endidx - j];
}
}
// now loop over rest of data. reusing the "symmetric" coefficients.
float_vect b2(window, 0.0);
b2[width] = 1.0;
const float_vect c2(sg_coeff(b2, deg));
for (std::size_t i = 0; i <= (v.size() - window); ++i) {
for (int j = 0; j < window; ++j) {
res[i + width] += c2[j] * v[i + j];
}
}
return res;
}
// Local Variables:
// c-basic-offset: 4
// End: