v1.0.0-rc.115 #22
@@ -4,6 +4,7 @@
|
||||
This is an UNSTABLE release and not recommended for production use (please use rc.111 instead).
|
||||
|
||||
* jfjoch_broker: Default spot finding settings can be configured via config JSON
|
||||
* jfjoch_viewer: FFT analysis of data in the dataset plot
|
||||
|
||||
### 1.0.0-rc.114
|
||||
This is an UNSTABLE release and not recommended for production use (please use rc.111 instead).
|
||||
|
||||
@@ -109,3 +109,10 @@ INSTALL(
|
||||
)
|
||||
|
||||
qt_import_plugins(jfjoch_viewer INCLUDE Qt::QXcbIntegrationPlugin)
|
||||
|
||||
IF(HAS_FFTW3_H AND FFTWF_LIBRARY)
|
||||
TARGET_LINK_LIBRARIES(jfjoch_viewer ${FFTWF_LIBRARY})
|
||||
MESSAGE(STATUS "FFT single-precision library found: ${FFTWF_LIBRARY}")
|
||||
ELSE()
|
||||
MESSAGE(WARNING "FFTW disabled")
|
||||
ENDIF()
|
||||
@@ -129,7 +129,7 @@ void JFJochViewerDatasetInfo::UpdatePlot() {
|
||||
} else if (val == 8)
|
||||
data = dataset->b_factor;
|
||||
|
||||
chart_view->loadValues(data, image_number, one_over_d2, dataset->experiment.GetGoniometer());
|
||||
chart_view->loadValues(data, image_number, one_over_d2, dataset.get());
|
||||
if (dataset->experiment.GetGridScan()) {
|
||||
stack->setCurrentWidget(grid_scan_image);
|
||||
grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2);
|
||||
|
||||
@@ -58,6 +58,18 @@ void JFJochDatasetInfoChartView::updateChart() {
|
||||
delete m_hoverLine;
|
||||
m_hoverLine = nullptr;
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
if (m_showFFT) {
|
||||
buildFFTChart();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
buildTimeDomainChart();
|
||||
}
|
||||
|
||||
void JFJochDatasetInfoChartView::buildTimeDomainChart() {
|
||||
if (values.size() >= binning) {
|
||||
// At least one full point
|
||||
|
||||
@@ -236,6 +248,7 @@ void JFJochDatasetInfoChartView::updateChart() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void JFJochDatasetInfoChartView::setBinning(int64_t val) {
|
||||
if (val >= 1) {
|
||||
binning = val;
|
||||
@@ -260,6 +273,14 @@ void JFJochDatasetInfoChartView::contextMenuEvent(QContextMenuEvent *event) {
|
||||
actXGoniometer->setChecked(m_xUseGoniometerAxis);
|
||||
actXGoniometer->setEnabled(goniometer_axis.has_value());
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
QAction *actShowFFT = menu.addAction("Show FFT (amplitude vs Hz)");
|
||||
actShowFFT->setCheckable(true);
|
||||
actShowFFT->setChecked(m_showFFT);
|
||||
// Require valid sampling interval
|
||||
actShowFFT->setEnabled(!values.empty() && image_time_us > 0.0);
|
||||
#endif
|
||||
|
||||
QAction *chosen = menu.exec(event->globalPos());
|
||||
if (chosen == copyXY) {
|
||||
QString out;
|
||||
@@ -278,7 +299,13 @@ void JFJochDatasetInfoChartView::contextMenuEvent(QContextMenuEvent *event) {
|
||||
} else if (chosen == actXGoniometer) {
|
||||
m_xUseGoniometerAxis = !m_xUseGoniometerAxis;
|
||||
updateChart();
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
} else if (chosen == actShowFFT) {
|
||||
m_showFFT = !m_showFFT;
|
||||
updateChart();
|
||||
#endif
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void JFJochDatasetInfoChartView::mouseMoveEvent(QMouseEvent *event) {
|
||||
@@ -286,6 +313,62 @@ void JFJochDatasetInfoChartView::mouseMoveEvent(QMouseEvent *event) {
|
||||
if (!series || values.empty())
|
||||
return;
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
if (m_showFFT && !m_fftFrequenciesHz.empty()) {
|
||||
// FFT mode: x is frequency in Hz
|
||||
const QPointF chartPos = chart()->mapToValue(event->pos(), series);
|
||||
double f = chartPos.x();
|
||||
if (!std::isfinite(f))
|
||||
return;
|
||||
|
||||
// If we only have DC, nothing meaningful to show
|
||||
if (m_fftFrequenciesHz.size() <= 1)
|
||||
return;
|
||||
|
||||
// Find nearest FFT bin, excluding k = 0 (DC component)
|
||||
int64_t bestIdx = -1;
|
||||
double bestDiff = std::numeric_limits<double>::infinity();
|
||||
for (size_t i = 1; i < m_fftFrequenciesHz.size(); ++i) {
|
||||
const double diff = std::abs(m_fftFrequenciesHz[i] - f);
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestIdx = static_cast<int64_t>(i);
|
||||
}
|
||||
}
|
||||
if (bestIdx < 1)
|
||||
return;
|
||||
|
||||
const double fBin = m_fftFrequenciesHz[static_cast<size_t>(bestIdx)];
|
||||
const double amp = m_fftMagnitudes[static_cast<size_t>(bestIdx)];
|
||||
|
||||
// Map x position of that bin to scene coords for vertical line
|
||||
const QRectF plotArea = chart()->plotArea();
|
||||
const QPointF ptOnChart = chart()->mapToPosition(QPointF(fBin, 0.0), series);
|
||||
|
||||
if (!m_hoverLine) {
|
||||
m_hoverLine = new QGraphicsLineItem;
|
||||
m_hoverLine->setPen(QPen(QColor(200, 0, 0, 150), 1.0));
|
||||
chart()->scene()->addItem(m_hoverLine);
|
||||
}
|
||||
|
||||
m_hoverLine->setLine(QLineF(ptOnChart.x(), plotArea.top(),
|
||||
ptOnChart.x(), plotArea.bottom()));
|
||||
|
||||
QString text = QString("f = %1 Hz, amplitude = %2")
|
||||
.arg(fBin, 0, 'g', 6)
|
||||
.arg(amp, 0, 'g', 6);
|
||||
emit writeStatusBar(text, 6000);
|
||||
|
||||
// No image loading in FFT mode
|
||||
m_hoverLoadTimer->stop();
|
||||
m_hoverPendingIdx = -1;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (values.empty())
|
||||
return;
|
||||
|
||||
// Map mouse position to chart coordinates
|
||||
const QPointF chartPos = chart()->mapToValue(event->pos(), series);
|
||||
double xVal = chartPos.x();
|
||||
@@ -366,3 +449,93 @@ void JFJochDatasetInfoChartView::onHoverLoadTimeout() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
void JFJochDatasetInfoChartView::buildFFTChart() {
|
||||
const size_t N = values.size();
|
||||
|
||||
if (N == 0 || !image_time_us.has_value() || image_time_us <= 0.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare input buffer (single precision, NaN/inf treated as 0)
|
||||
std::vector<float> in(N, 0.0f);
|
||||
for (size_t i = 0; i < N; ++i) {
|
||||
const double v = values[i];
|
||||
in[i] = std::isfinite(v) ? static_cast<float>(v) : 0.0f;
|
||||
}
|
||||
|
||||
const int n = static_cast<int>(N);
|
||||
const int nComplex = n / 2 + 1;
|
||||
|
||||
std::vector<fftwf_complex> out(static_cast<size_t>(nComplex));
|
||||
|
||||
fftwf_plan plan = fftwf_plan_dft_r2c_1d(
|
||||
n,
|
||||
in.data(),
|
||||
out.data(),
|
||||
FFTW_ESTIMATE);
|
||||
|
||||
if (!plan) {
|
||||
return;
|
||||
}
|
||||
|
||||
fftwf_execute(plan);
|
||||
fftwf_destroy_plan(plan);
|
||||
|
||||
// Compute amplitude spectrum and frequencies (0 .. Nyquist)
|
||||
m_fftMagnitudes.resize(static_cast<size_t>(nComplex));
|
||||
m_fftFrequenciesHz.resize(static_cast<size_t>(nComplex));
|
||||
|
||||
const double dt = image_time_us.value() * 1e-6; // seconds per sample
|
||||
const double fs = 1.0 / dt; // sampling frequency
|
||||
const double df = fs / static_cast<double>(n); // frequency resolution
|
||||
|
||||
for (int k = 0; k < nComplex; ++k) {
|
||||
const double re = out[static_cast<size_t>(k)][0];
|
||||
const double im = out[static_cast<size_t>(k)][1];
|
||||
const double mag = std::hypot(re, im); // amplitude
|
||||
|
||||
m_fftMagnitudes[static_cast<size_t>(k)] = mag;
|
||||
m_fftFrequenciesHz[static_cast<size_t>(k)] = static_cast<double>(k) * df;
|
||||
}
|
||||
|
||||
// Build chart series: X = frequency (Hz), Y = amplitude
|
||||
series = new QLineSeries(this);
|
||||
currentSeries = nullptr; // no "current image" marker in FFT mode
|
||||
|
||||
double magMin = std::numeric_limits<double>::infinity();
|
||||
double magMax = -std::numeric_limits<double>::infinity();
|
||||
|
||||
for (int k = 1; k < nComplex; ++k) {
|
||||
const double f = m_fftFrequenciesHz[static_cast<size_t>(k)];
|
||||
const double mag = m_fftMagnitudes[static_cast<size_t>(k)];
|
||||
series->append(f, mag);
|
||||
if (mag < magMin) magMin = mag;
|
||||
if (mag > magMax) magMax = mag;
|
||||
}
|
||||
|
||||
chart()->addSeries(series);
|
||||
chart()->createDefaultAxes();
|
||||
|
||||
QValueAxis *axisX = qobject_cast<QValueAxis *>(chart()->axisX(series));
|
||||
QValueAxis *axisY = qobject_cast<QValueAxis *>(chart()->axisY(series));
|
||||
|
||||
if (axisX) {
|
||||
axisX->setTitleText(QStringLiteral("Frequency (Hz)"));
|
||||
axisX->setLabelsVisible(true);
|
||||
}
|
||||
|
||||
if (axisY) {
|
||||
if (std::isfinite(magMin) && std::isfinite(magMax)) {
|
||||
if (!(magMax > magMin)) {
|
||||
magMax = magMin + 1.0;
|
||||
}
|
||||
axisY->setRange(magMin, magMax);
|
||||
}
|
||||
axisY->setTitleText(QStringLiteral("Amplitude"));
|
||||
axisY->setLabelsVisible(true);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#ifndef JFJOCH_JFJOCHCHARTVIEW_H
|
||||
#define JFJOCH_JFJOCHCHARTVIEW_H
|
||||
|
||||
#include <QtCharts/QChartView>
|
||||
#include <QtCharts/QValueAxis>
|
||||
#include <QtCharts/QLineSeries>
|
||||
@@ -10,7 +11,13 @@
|
||||
#include <QMouseEvent>
|
||||
#include <QGraphicsLineItem>
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
#include <fftw3.h>
|
||||
#endif
|
||||
|
||||
#include "../../common/GoniometerAxis.h"
|
||||
#include "../../reader/JFJochReaderDataset.h"
|
||||
|
||||
class JFJochDatasetInfoChartView : public QChartView {
|
||||
Q_OBJECT
|
||||
@@ -30,8 +37,26 @@ class JFJochDatasetInfoChartView : public QChartView {
|
||||
bool m_xUseGoniometerAxis = true;
|
||||
|
||||
std::optional<GoniometerAxis> goniometer_axis;
|
||||
std::optional<float> image_time_us;
|
||||
|
||||
void updateChart();
|
||||
|
||||
// Build regular (index or goniometer) chart
|
||||
void buildTimeDomainChart();
|
||||
|
||||
// FFT view toggle
|
||||
bool m_showFFT = false;
|
||||
|
||||
#ifdef JFJOCH_USE_FFTW
|
||||
// Cached FFT data for hover/status bar etc.
|
||||
void buildFFTChart();
|
||||
std::vector<double> m_fftMagnitudes;
|
||||
std::vector<double> m_fftFrequenciesHz;
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
|
||||
signals:
|
||||
void imageSelected(int64_t number);
|
||||
void writeStatusBar(QString string, int timeout_ms = 0);
|
||||
@@ -55,12 +80,21 @@ public slots:
|
||||
|
||||
public:
|
||||
template<class T>
|
||||
void loadValues(const std::vector<T> &input, int64_t image, bool one_over_d2, const std::optional<GoniometerAxis> &goniometer = {}) {
|
||||
void loadValues(const std::vector<T> &input,
|
||||
int64_t image,
|
||||
bool one_over_d2,
|
||||
const JFJochReaderDataset *dataset = nullptr) {
|
||||
m_yOneOverD = one_over_d2;
|
||||
|
||||
values.resize(input.size());
|
||||
|
||||
goniometer_axis = goniometer;
|
||||
if (dataset != nullptr) {
|
||||
goniometer_axis = dataset->experiment.GetGoniometer();
|
||||
image_time_us = dataset->experiment.GetImageTime().count();
|
||||
} else {
|
||||
goniometer_axis = {};
|
||||
image_time_us = {};
|
||||
}
|
||||
|
||||
for (int i = 0; i < input.size(); i++) {
|
||||
if (one_over_d2) {
|
||||
|
||||
Reference in New Issue
Block a user