< Summary

Information
Class: src
Assembly: holovibes
File(s): File 1: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\app_utils.cc
File 2: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\logger.cc
File 3: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\main.cc
File 4: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\field_help.cc
File 5: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tasks.cc
File 6: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tracer.cc
File 7: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tracer.hh
File 8: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tracer.hxx
File 9: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder.cc
File 10: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\manager.cc
File 11: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\manager.hh
File 12: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\settings.hh
File 13: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\validation.cc
File 14: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\settings_loader.cc
File 15: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\settings_loader.hh
File 16: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\tasks\sinks\display_signal_history.cc
File 17: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\tasks\sinks\display_tensor.cc
File 18: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\tasks\sinks\display_zernike_coefficients.cc
File 19: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\graph_visualizer_widget.cc
File 20: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\graph_visualizer_widget.hh
File 21: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\main_window.cc
File 22: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\main_window.hh
File 23: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\theme.cc
File 24: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\update_checker.cc
File 25: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\update_checker.hh
File 26: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\visualization_workspace.cc
File 27: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\visualization_workspace.hh
File 28: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\auto_focus_widget.cc
File 29: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\export_widget.cc
File 30: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\image_rendering_widget.cc
File 31: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\import_widget.cc
File 32: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\quiver_widget.cc
File 33: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\quiver_widget.hh
File 34: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\selected_widget_settings_panel.cc
File 35: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\selected_widget_settings_panel.hh
File 36: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\signal_history.cc
File 37: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\system_monitor_widget.cc
File 38: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\tensor_display_widget.cc
File 39: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\tensor_display_widget.hh
File 40: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\validation_style.hh
File 41: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\view_widget.cc
File 42: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\view_widget.hh
File 43: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\zernike_history_widget.cc
File 44: C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\zernike_history_widget.hh
Line coverage
6%
Covered lines: 397
Uncovered lines: 5840
Coverable lines: 6237
Total lines: 12585
Line coverage: 6.3%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\app_utils.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "app_utils.hh"
 16
 17#include <QApplication>
 18#include <QDebug>
 19#include <QDir>
 20#include <QDirIterator>
 21#include <QFile>
 22#include <QFileInfo>
 23#include <QStandardPaths>
 24
 25namespace holovibes::utils {
 26
 027static void copyResourcesRecursively(const QString &resourceRoot, const QString &targetRoot) {
 028  QDirIterator it(resourceRoot, QDir::NoFilter, QDirIterator::Subdirectories);
 029  const int    prefixLength = resourceRoot.length() + 1;
 30
 031  while (it.hasNext()) {
 032    QString   resourcePath = it.next();
 033    QFileInfo info(resourcePath);
 034    QString   relativePath = resourcePath.mid(prefixLength);
 035    QString   targetPath   = targetRoot + "/" + relativePath;
 36
 037    if (info.isDir()) {
 038      QDir().mkpath(targetPath);
 039    } else if (!QFile::exists(targetPath)) {
 040      QDir().mkpath(QFileInfo(targetPath).absolutePath());
 041      if (QFile::copy(resourcePath, targetPath)) {
 042        QFile::setPermissions(targetPath, QFile::permissions(targetPath) | QFileDevice::WriteOwner);
 043        qDebug() << "Copied" << resourcePath << "->" << targetPath;
 044      } else {
 045        qWarning() << "Failed to copy" << resourcePath << "->" << targetPath;
 46      }
 47    }
 048  }
 049}
 50
 051void setupAppData() {
 052  QString appDataBase = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 053  QString appDataPath = appDataBase + "/" + QCoreApplication::applicationVersion();
 54
 055  QDir dir(appDataPath);
 056  if (!dir.exists())
 057    dir.mkpath(".");
 58
 059  copyResourcesRecursively(":/resources/holovibes", appDataPath);
 060}
 61
 62} // namespace holovibes::utils

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\logger.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "logger.hh"
 16
 17#include <spdlog/sinks/stdout_color_sinks.h>
 18#include <spdlog/spdlog.h>
 19
 20namespace holovibes {
 21
 022std::shared_ptr<spdlog::logger> logger() {
 023  static std::shared_ptr<spdlog::logger> logger = [] {
 24    auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 25    sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [thread %t] [%^%l%$] %v");
 26
 27    auto log = std::make_shared<spdlog::logger>("holovibes", sink);
 28    log->set_level(spdlog::default_logger()->level());
 29    log->flush_on(spdlog::level::warn);
 30
 31    spdlog::register_logger(log);
 32
 33    return log;
 034  }();
 035  return logger;
 036}
 37
 38} // namespace holovibes

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\main.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include <QApplication>
 16#include <cstdlib>
 17#include <string_view>
 18
 19#include "app_utils.hh"
 20#include "bug.hh"
 21#include "holoflow/core/graph_spec.hh"
 22#include "holoflow/core/registry.hh"
 23#include "holoflow/runtime/compiler.hh"
 24#include "holoflow/runtime/graph_exec.hh"
 25#include "logger.hh"
 26#include "spdlog/common.h"
 27#include "ui/main_window.hh"
 28#include "ui/theme.hh"
 29#include "ui/widgets/quiver_widget.hh"
 30
 31#include "holonp/arange.hh"
 32
 133int main(int argc, char **argv) {
 34#if defined(HOLOFLOW_COVERAGE_PROBE)
 135  if (argc == 2 && std::string_view(argv[1]) == "--coverage-probe") {
 136    return EXIT_SUCCESS;
 37  }
 38#endif
 39
 040  spdlog::set_level(spdlog::level::debug);
 41
 42  holonp::ArangeSettings s{
 043      .start  = 0.0,
 044      .stop   = 10.0,
 045      .step   = 0.5,
 046      .dtype  = holoflow::core::DType::F32,
 047      .device = holoflow::core::MemLoc::Device,
 48  };
 49
 050  nlohmann::json j = s;
 51
 052  spdlog::info("ArangeSettings as JSON: {}", j.dump(2));
 53
 054  QCoreApplication::setOrganizationName("Digital Holography Foundation");
 055  QCoreApplication::setApplicationName("Holovibes");
 056  QCoreApplication::setApplicationVersion(HOLOVIBES_VERSION_SEMVER2);
 057  holovibes::utils::setupAppData();
 058  QApplication app(argc, argv);
 059  holovibes::ui::apply_dark_clinical_theme(app);
 60
 061  holovibes::ui::MainWindow main_window;
 062  app.setWindowIcon(QIcon(":/resources/holovibes/assets/holovibes_logo.png"));
 063  main_window.show();
 064  return QApplication::exec();
 165}

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\field_help.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "pipeline/field_help.hh"
 16
 17#include <array>
 18
 19#include "bug.hh"
 20
 21namespace holovibes::pipeline {
 22
 23namespace {
 24
 25constexpr auto kLoadPathConstraints               = std::to_array<const char *>({
 26    "Select an existing holofile when import source is file-based.",
 27});
 28constexpr auto kCameraConfigConstraints           = std::to_array<const char *>({
 29    "Select a readable JSON camera configuration file.",
 30});
 31constexpr auto kInputSamplingFrequencyConstraints = std::to_array<const char *>({
 32    "Must be strictly positive.",
 33    "Represents physical acquisition timing, not processing or playback throughput.",
 34});
 35constexpr auto kLoadBeginConstraints              = std::to_array<const char *>({
 36    "Must be strictly smaller than End Index.",
 37});
 38constexpr auto kLoadEndConstraints                = std::to_array<const char *>({
 39    "Must be strictly greater than Start Index.",
 40    "Must not exceed the number of frames in the holofile.",
 41});
 42constexpr auto kLoadBatchConstraints              = std::to_array<const char *>({
 43    "Must be strictly positive.",
 44    "Used as the recording batch size for raw recording.",
 45});
 46constexpr auto kFilter2DConstraints               = std::to_array<const char *>({
 47    "Angular Spectrum folds this filter into its propagation transfer function.",
 48    "Other propagation methods use a separate frequency-domain filtering stage.",
 49    "Uses the inner and outer radii as pixel distances in the spatial-frequency plane.",
 50});
 51constexpr auto kFilter2DInnerConstraints          = std::to_array<const char *>({
 52    "Must be non-negative.",
 53    "Must not exceed the outer radius when Filter 2D is enabled.",
 54});
 55constexpr auto kFilter2DOuterConstraints          = std::to_array<const char *>({
 56    "Must be non-negative.",
 57    "Must be greater than or equal to the inner radius when Filter 2D is enabled.",
 58});
 59constexpr auto kSpacialMethodConstraints          = std::to_array<const char *>({
 60    "Processed mode supports Fresnel Diffraction and Angular Spectrum.",
 61    "Angular Spectrum is disabled when Auto Focus is enabled.",
 62});
 63constexpr auto kAspPaddingConstraints             = std::to_array<const char *>({
 64    "Available only for Angular Spectrum propagation.",
 65    "Width and height must be at least the source dimensions.",
 66    "The number of pixels added on each axis must be even.",
 67});
 68constexpr auto kTimeMethodConstraints             = std::to_array<const char *>({
 69    "Processed mode requires a time transform.",
 70});
 71constexpr auto kTimeWindowConstraints             = std::to_array<const char *>({
 72    "Must be strictly positive.",
 73    "Time Stride must be a multiple of Time Window.",
 74});
 75constexpr auto kTimeStrideConstraints             = std::to_array<const char *>({
 76    "Must be strictly positive.",
 77    "Must be a multiple of Time Window.",
 78    "For holofile import, must not exceed the selected frame range.",
 79});
 80constexpr auto kTimeZBeginConstraints             = std::to_array<const char *>({
 81    "Defines the start of the selected temporal-frequency range.",
 82    "Must stay within the current time transform output.",
 83});
 84constexpr auto kTimeZEndConstraints               = std::to_array<const char *>({
 85    "Defines the end of the selected temporal-frequency range.",
 86    "Must be strictly greater than Z start and within the current time transform output.",
 87});
 88constexpr auto kView3DCutsConstraints             = std::to_array<const char *>({
 89    "Not currently supported by the pipeline.",
 90});
 91constexpr auto kAccumulationConstraints           = std::to_array<const char *>({
 92    "Must be strictly positive.",
 93    "Controls both reconstruction and Shack-Hartmann sliding windows.",
 94    "Values above one require a single auto-focus iteration.",
 95});
 96constexpr auto kFlatfieldCutoffConstraints        = std::to_array<const char *>({
 97    "Must be strictly positive.",
 98    "Physical period of the 50% amplitude transition convention, not a hard cutoff.",
 99    "Converted to anisotropic Gaussian sigmas using the current image pitch.",
 100});
 101constexpr auto kConvolutionConstraints            = std::to_array<const char *>({
 102    "Not currently supported by the pipeline.",
 103});
 104constexpr auto kRegistrationConstraints           = std::to_array<const char *>({
 105    "Not currently supported by the pipeline.",
 106});
 107constexpr auto kRecordingPathConstraints          = std::to_array<const char *>({
 108    "Required when recording is enabled.",
 109    "The parent directory must exist and be writable.",
 110});
 111constexpr auto kRecordingCountConstraints         = std::to_array<const char *>({
 112    "Must be strictly positive.",
 113    "Must be divisible by the effective pipeline batch size.",
 114});
 115constexpr auto kAutofocusNbSubapsConstraints      = std::to_array<const char *>({
 116    "Must be strictly positive.",
 117    "Must be odd.",
 118    "Must fit within the source dimensions.",
 119});
 120constexpr auto kAutofocusNbIterConstraints        = std::to_array<const char *>({
 121    "Must be strictly positive.",
 122    "Each pass applies one Shack-Hartmann phase correction.",
 123});
 124
 125const FieldHelp kFieldHelp[] = {
 126    {SettingsField::LoadPath, "Input File",
 127     "Holofile used as the acquisition source when camera mode is disabled.", kLoadPathConstraints},
 128    {SettingsField::CameraConfigPath, "Camera Config",
 129     "JSON configuration file used to initialize the selected camera source.",
 130     kCameraConfigConstraints},
 131    {SettingsField::InputSamplingFrequency, "Input Sampling Frequency",
 132     "Physical camera frame sampling frequency used to derive Zernike history timing.",
 133     kInputSamplingFrequencyConstraints},
 134    {SettingsField::LoadBegin, "Start Index",
 135     "First input frame included in the imported holofile range.", kLoadBeginConstraints},
 136    {SettingsField::LoadEnd, "End Index", "Frame index just after the imported holofile range.",
 137     kLoadEndConstraints},
 138    {SettingsField::LoadBatch, "Batch Size",
 139     "Number of frames acquired or loaded together by the source stage.", kLoadBatchConstraints},
 140    {SettingsField::Filter2D, "Filter 2D",
 141     "Enables radial frequency-domain filtering of the propagated complex field.",
 142     kFilter2DConstraints},
 143    {SettingsField::Filter2DInnerRadius, "Filter 2D Inner Radius",
 144     "Inner cutoff radius in pixels. Frequencies inside this radius are attenuated.",
 145     kFilter2DInnerConstraints},
 146    {SettingsField::Filter2DOuterRadius, "Filter 2D Outer Radius",
 147     "Outer cutoff radius in pixels. Frequencies outside this radius are attenuated.",
 148     kFilter2DOuterConstraints},
 149    {SettingsField::SpacialMethod, "Space Transform",
 150     "Spatial propagation method applied after temporal analysis.", kSpacialMethodConstraints},
 151    {SettingsField::AspPadding, "ASP Padding",
 152     "Centered zero-padding resolution used for Angular Spectrum propagation.",
 153     kAspPaddingConstraints},
 154    {SettingsField::TimeMethod, "Time Transform",
 155     "Temporal transform used to convert frame windows into the analysis domain.",
 156     kTimeMethodConstraints},
 157    {SettingsField::TimeWindow, "Time Window", "Number of frames in each temporal analysis window.",
 158     kTimeWindowConstraints},
 159    {SettingsField::TimeStride, "Time Stride",
 160     "Number of frames between successive temporal analysis windows.", kTimeStrideConstraints},
 161    {SettingsField::TimeZBegin, "Z Start",
 162     "Start of the selected temporal-frequency range displayed or processed.",
 163     kTimeZBeginConstraints},
 164    {SettingsField::TimeZEnd, "Z Width",
 165     "Extent of the selected temporal-frequency range displayed or processed.",
 166     kTimeZEndConstraints},
 167    {SettingsField::View3DCuts, "3D Cuts",
 168     "Enables XZ and YZ cut views derived from the processed volume.", kView3DCutsConstraints},
 169    {SettingsField::PpAccumulation, "Accumulation",
 170     "Number of temporal samples in the reconstruction and Shack-Hartmann sliding averages.",
 171     kAccumulationConstraints},
 172    {SettingsField::PpFlatfieldCutoffPeriod, "Flatfield Cutoff",
 173     "Physical cutoff period used to derive the Gaussian background subtraction scale.",
 174     kFlatfieldCutoffConstraints},
 175    {SettingsField::PpConvolution, "Convolution",
 176     "Applies a convolution kernel during post-processing.", kConvolutionConstraints},
 177    {SettingsField::PpRegistration, "Registration",
 178     "Aligns successive output frames during post-processing.", kRegistrationConstraints},
 179    {SettingsField::RecordingPath, "Recording Path",
 180     "Output path used when recording raw or processed data.", kRecordingPathConstraints},
 181    {SettingsField::RecordingCount, "Recording Count",
 182     "Number of frames to record before the writer stops automatically.",
 183     kRecordingCountConstraints},
 184    {SettingsField::AutofocusNbSubaps, "Auto-focus Sub-apertures",
 185     "Number of Shack-Hartmann sub-apertures per image dimension.", kAutofocusNbSubapsConstraints},
 186    {SettingsField::AutofocusNbIter, "Auto-focus Iterations",
 187     "Number of Shack-Hartmann correction passes.", kAutofocusNbIterConstraints},
 188};
 189
 190} // namespace
 191
 0192const FieldHelp &get_field_help(SettingsField field) {
 0193  for (const auto &help : kFieldHelp) {
 0194    if (help.field == field) {
 0195      return help;
 196    }
 0197  }
 198
 0199  HOLOVIBES_BUG("Missing field help for settings field {}", static_cast<int>(field));
 0200  HOLOVIBES_UNREACHABLE();
 0201}
 202
 203} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tasks.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "graph_builder_tasks.hh"
 16
 17#include "bug.hh"
 18
 19namespace holovibes::pipeline {
 20
 21#define DEFINE_SOURCE_SYNC_NODE(fn_name, node_name_str, kind_str, SettingsType)                    \
 22  GraphBuilderTasks::TDesc GraphBuilderTasks::fn_name(SettingsType s) {                            \
 23    return std::move(make_source_sync_node(node_name_str, kind_str, kind_str, s).at(0));           \
 24  }
 25
 26#define DEFINE_UNARY_SYNC_NODE(fn_name, node_name_str, kind_str, SettingsType)                     \
 27  GraphBuilderTasks::TDesc GraphBuilderTasks::fn_name(const TDesc &X, SettingsType s) {            \
 28    return std::move(make_unary_sync_node(node_name_str, kind_str, kind_str, X, s).at(0));         \
 29  }
 30
 31#define DEFINE_SINK_SYNC_NODE(fn_name, node_name_str, kind_str, SettingsType)                      \
 32  void GraphBuilderTasks::fn_name(const TDesc &X, SettingsType s) {                                \
 33    make_unary_sync_node(node_name_str, kind_str, kind_str, X, s);                                 \
 34  }
 35
 36#define DEFINE_NARY_SYNC_NODE(fn_name, node_name_str, kind_str, SettingsType)                      \
 37  GraphBuilderTasks::TDesc GraphBuilderTasks::fn_name(std::span<const TDesc> Xs, SettingsType s) { \
 38    return std::move(make_nary_sync_node(node_name_str, kind_str, kind_str, Xs, s).at(0));         \
 39  }
 40
 41#define DEFINE_UNARY_ASYNC_NODE(fn_name, node_name_str, kind_str, SettingsType)                    \
 42  GraphBuilderTasks::TDesc GraphBuilderTasks::fn_name(const TDesc &X, SettingsType s) {            \
 43    return std::move(make_unary_async_node(node_name_str, kind_str, kind_str, X, s).at(0));        \
 44  }
 45
 46// clang-format off
 047DEFINE_SOURCE_SYNC_NODE(holofile_read,                          "source",                              "Holofile",      
 048DEFINE_SOURCE_SYNC_NODE(empty,                                  "empty",                               "Empty",         
 049DEFINE_SOURCE_SYNC_NODE(zeros,                                  "zeros",                               "Zeros",         
 050DEFINE_SOURCE_SYNC_NODE(asarray,                                "asarray",                             "AsArray",       
 051DEFINE_SOURCE_SYNC_NODE(arange,                                 "arange",                              "Arange",        
 052DEFINE_UNARY_SYNC_NODE (ascontiguousarray,                      "ascontiguousarray",                   "AsContiguousArra
 053DEFINE_UNARY_SYNC_NODE (copy,                                   "copy",                                "Copy",          
 054DEFINE_SOURCE_SYNC_NODE(ametek_s710_euresys_coaxlink_octo,      "source",   "AmetekS710EuresysCoaxlinkOcto",   holotask:
 055DEFINE_SOURCE_SYNC_NODE(ametek_s711_euresys_coaxlink_qsfp_plus, "source", "AmetekS711EuresysCoaxlinkQSFP+",  holotask::s
 056DEFINE_UNARY_SYNC_NODE (fresnel_qin,                            "fresnel_qin",                         "FresnelQin",    
 057DEFINE_UNARY_SYNC_NODE (fresnel_qout,                           "fresnel_qout",                        "FresnelQout",   
 058DEFINE_UNARY_SYNC_NODE (short_time_fresnel_diffraction,         "short_time_fresnel_diffraction",      "ShortTimeFresnel
 059DEFINE_UNARY_SYNC_NODE (unfold2d,                               "unfold2d",                            "Unfold2D",      
 060DEFINE_UNARY_SYNC_NODE (memcpy,                                 "memcpy",                              "Memcpy",        
 061DEFINE_UNARY_SYNC_NODE (convert,                                "conversion",                          "Conversion",    
 062DEFINE_UNARY_SYNC_NODE (pca,                                    "pca",                                 "Pca",           
 063DEFINE_UNARY_SYNC_NODE (flatfield,                              "flatfield",                           "Flatfield",     
 064DEFINE_UNARY_SYNC_NODE (filter_2d,                              "filter_2d",                           "Filter2D",      
 065DEFINE_UNARY_SYNC_NODE (fresnel_diffraction,                    "fresnel_diffraction",                 "FresnelDiffracti
 066DEFINE_UNARY_SYNC_NODE (angular_spectrum,                       "angular_spectrum",                    "AngularSpectrum"
 067DEFINE_UNARY_SYNC_NODE (cuda_stream_synchronize,                "cuda_stream_synchronize",             "CudaStreamSynchr
 068DEFINE_UNARY_SYNC_NODE (causal_slide_avg,                       "causal_slide_avg",                     "CausalSlidingAv
 069DEFINE_UNARY_SYNC_NODE (reshape,                                "reshape",                             "Reshape",       
 070DEFINE_UNARY_SYNC_NODE (convolution,                            "convolution",                         "Convolution",   
 071DEFINE_UNARY_SYNC_NODE (pct_clip,                               "pct_clip",                            "PctClip",       
 072DEFINE_UNARY_SYNC_NODE (registration,                           "registration",                        "Registration",  
 073DEFINE_UNARY_SYNC_NODE (wrap2pi,                                "wrap2pi",                             "Wrap2Pi",       
 074DEFINE_UNARY_SYNC_NODE (zernike_from_slopes,                    "zernike_from_slopes",                 "ZernikeFromSlope
 075DEFINE_UNARY_SYNC_NODE (zernike_phase,                          "zernike_phase",                       "ZernikePhase",  
 076DEFINE_SINK_SYNC_NODE  (zernike_defocus_z_prop,                 "zernike_defocus_z_prop",              "ZernikeDefocusZP
 77
 078DEFINE_SINK_SYNC_NODE  (xy_raw_display,               "xy_raw_display",               "DisplayTensorXYRaw",             
 079DEFINE_SINK_SYNC_NODE  (xy_processed_display,         "xy_processed_display",         "DisplayTensorXY",                
 080DEFINE_SINK_SYNC_NODE  (xz_processed_display,         "xz_processed_display",         "DisplayTensorXZ",                
 081DEFINE_SINK_SYNC_NODE  (yz_processed_display,         "yz_processed_display",         "DisplayTensorYZ",                
 082DEFINE_SINK_SYNC_NODE  (shack_hartmann_display,       "shack_hartmann_display",       "DisplayTensorShackHartmann",     
 083DEFINE_SINK_SYNC_NODE  (shack_hartmann_xcorr_display, "shack_hartmann_xcorr_display", "DisplayTensorShackHartmannXcorr",
 084DEFINE_SINK_SYNC_NODE  (zernike_phase_display,        "zernike_phase_display",        "DisplayTensorZernikePhase",      
 085DEFINE_SINK_SYNC_NODE  (zernike_coefficients_display, "zernike_coefficients_display", "DisplayZernikeCoefficients",     
 086DEFINE_SINK_SYNC_NODE  (zernike_history_display,      "zernike_history_display",      "DisplaySignalHistory",           
 87
 088DEFINE_NARY_SYNC_NODE  (concatenate,                  "concatenate",                  "Concatenate",                    
 089DEFINE_UNARY_SYNC_NODE (transpose,                    "transpose",                    "Transpose",                      
 090DEFINE_UNARY_SYNC_NODE (conj,                         "conj",                         "Conj",                           
 091DEFINE_UNARY_SYNC_NODE (rfft,                         "rfft",                         "RFFT",                           
 092DEFINE_UNARY_SYNC_NODE (rfft2,                        "rfft2",                        "RFFT2",                          
 093DEFINE_UNARY_SYNC_NODE (irfft2,                       "irfft2",                       "IRFFT2",                         
 094DEFINE_UNARY_SYNC_NODE (slice,                        "slice",                        "Slice",                          
 095DEFINE_UNARY_SYNC_NODE (fft,                          "fft",                          "FFT",                            
 096DEFINE_UNARY_SYNC_NODE (fft2,                         "fft2",                         "FFT2",                           
 097DEFINE_UNARY_SYNC_NODE (fftshift,                     "fftshift",                     "FFTShiftNp",                     
 098DEFINE_UNARY_SYNC_NODE (abs,                          "abs",                          "Abs",                            
 099DEFINE_UNARY_SYNC_NODE (mean,                         "mean",                         "Mean",                           
 0100DEFINE_UNARY_SYNC_NODE (mean_abs,                     "mean_abs",                     "MeanAbs",                        
 0101DEFINE_UNARY_SYNC_NODE (min,                          "min",                          "Min",                            
 0102DEFINE_UNARY_SYNC_NODE (max,                          "max",                          "Max",                            
 0103DEFINE_UNARY_SYNC_NODE (normalize,                    "normalize",                    "Normalize",                      
 0104DEFINE_UNARY_ASYNC_NODE(batched_queue,                "batch_queue",                  "BatchQueue",                     
 0105DEFINE_UNARY_ASYNC_NODE(slide_avg,                    "slide_avg",                    "SlidingAverage",                 
 106// clang-format on
 107
 108std::vector<GraphBuilderTasks::TDesc>
 109GraphBuilderTasks::dual_reader_batch_queue(const TDesc                                   &X,
 0110                                           holotask::asyncs::DualReaderBatchQueueSettings s) {
 0111  return make_unary_async_node("dual_reader_batch_queue", "DualReaderBatchQueue",
 112                               "DualReaderBatchQueue", X, s);
 0113}
 114
 115GraphBuilderTasks::TDesc GraphBuilderTasks::slide_avg(const TDesc &X, const TDesc &Valid,
 0116                                                      holotask::asyncs::SlidingAverageSettings s) {
 0117  const std::array<TDesc, 2> inputs{X, Valid};
 0118  return std::move(make_nary_async_node("slide_avg", "SlidingAverage", "SlidingAverage",
 119                                        std::span<const TDesc>{inputs}, s)
 120                       .at(0));
 0121}
 122
 123std::vector<GraphBuilderTasks::TDesc>
 124GraphBuilderTasks::shack_hartmann_slopes(const TDesc                                &X,
 0125                                         holotask::syncs::ShackHartmannSlopeSettings s) {
 0126  return make_unary_sync_node("shack_hartmann_slopes", "ShackHartmannSlopes", "ShackHartmannSlopes",
 127                              X, s);
 0128}
 129
 130GraphBuilderTasks::TDesc GraphBuilderTasks::multiply(const TDesc &A, const TDesc &B,
 0131                                                     holonp::MultiplySettings s) {
 0132  std::array<TDesc, 2> inputs{A, B};
 0133  return std::move(
 134      make_nary_sync_node("multiply", "Multiply", "Multiply", std::span<const TDesc>{inputs}, s)
 135          .at(0));
 0136}
 137
 138GraphBuilderTasks::TDesc
 139GraphBuilderTasks::cross_correlation2(const TDesc &Moving, const TDesc &Reference,
 0140                                      holotask::syncs::CrossCorrelation2Settings s) {
 0141  std::array<TDesc, 2> inputs{Moving, Reference};
 0142  return std::move(make_nary_sync_node("cross_correlation2", "CrossCorrelation2",
 143                                       "CrossCorrelation2", std::span<const TDesc>{inputs}, s)
 144                       .at(0));
 0145}
 146
 147GraphBuilderTasks::TDesc GraphBuilderTasks::add(const TDesc &A, const TDesc &B,
 0148                                                holonp::AddSettings s) {
 0149  std::array<TDesc, 2> inputs{A, B};
 0150  return std::move(
 151      make_nary_sync_node("add", "Add", "Add", std::span<const TDesc>{inputs}, s).at(0));
 0152}
 153
 154GraphBuilderTasks::TDesc GraphBuilderTasks::divide(const TDesc &A, const TDesc &B,
 0155                                                   holonp::DivideSettings s) {
 0156  std::array<TDesc, 2> inputs{A, B};
 0157  return std::move(
 158      make_nary_sync_node("divide", "Divide", "Divide", std::span<const TDesc>{inputs}, s).at(0));
 0159}
 160
 161GraphBuilderTasks::TDesc GraphBuilderTasks::subtract(const TDesc &A, const TDesc &B,
 0162                                                     holonp::SubtractSettings s) {
 0163  std::array<TDesc, 2> inputs{A, B};
 0164  return std::move(
 165      make_nary_sync_node("subtract", "Subtract", "Subtract", std::span<const TDesc>{inputs}, s)
 166          .at(0));
 0167}
 168
 169GraphBuilderTasks::TDesc GraphBuilderTasks::correct_phase(const TDesc &X, const TDesc &PhaseMask,
 0170                                                          holotask::syncs::CorrectPhaseSettings s) {
 0171  std::array<TDesc, 2> inputs{X, PhaseMask};
 0172  return std::move(make_nary_sync_node("correct_phase", "CorrectPhase", "CorrectPhase",
 173                                       std::span<const TDesc>{inputs}, s)
 174                       .at(0));
 0175}
 176
 177GraphBuilderTasks::TDesc GraphBuilderTasks::equal(const TDesc &A, const TDesc &B,
 0178                                                  holonp::EqualSettings s) {
 0179  std::array<TDesc, 2> inputs{A, B};
 0180  return std::move(
 181      make_nary_sync_node("equal", "Equal", "Equal", std::span<const TDesc>{inputs}, s).at(0));
 0182}
 183
 184GraphBuilderTasks::TDesc GraphBuilderTasks::where(const TDesc &Cond, const TDesc &X, const TDesc &Y,
 0185                                                  holonp::WhereSettings s) {
 0186  std::array<TDesc, 3> inputs{Cond, X, Y};
 0187  return std::move(
 188      make_nary_sync_node("where", "Where", "Where", std::span<const TDesc>{inputs}, s).at(0));
 0189}
 190
 0191void GraphBuilderTasks::holofile_write(const TDesc &X, holotask::sinks::HolofileSettings s) {
 0192  auto node_name = "record";
 0193  auto kind      = "HolofileWriter";
 0194  auto reg_key   = "HolofileWriter";
 0195  auto debug     = false;
 196
 0197  HOLOVIBES_CHECK(X.producer.has_value());
 0198  HOLOVIBES_CHECK(reg_.is_sync_registered(std::string{reg_key}));
 199
 200  holoflow::core::NodeSpec node_spec{
 0201      .name     = std::string{node_name},
 0202      .kind     = std::string{kind},
 0203      .settings = nlohmann::json(s),
 0204      .debug    = debug,
 205  };
 206
 0207  auto v = boost::add_vertex(node_spec, g_);
 0208  boost::add_edge(X.producer->vertex, v, {X.producer->out_idx, 0}, g_);
 209
 0210  auto      &factory     = reg_.get_sync(std::string{reg_key});
 0211  const auto core_inputs = to_core_descs(std::span{&X, 1});
 0212  (void)factory.infer(core_inputs, nlohmann::json(s));
 0213}
 214
 215} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tracer.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "graph_builder_tracer.hh"
 16
 17namespace holovibes::pipeline {
 18
 019GraphBuilderTracer::GraphBuilderTracer(holoflow::core::Registry &registry) : reg_(registry) {}
 20
 021holoflow::core::TDesc GraphBuilderTracer::TDesc::as_core() const {
 022  holoflow::core::TDesc t{};
 023  t.shape   = shape;
 024  t.dtype   = dtype;
 025  t.mem_loc = mem_loc;
 026  t.strides = strides;
 027  t.offset  = offset;
 028  return t;
 029}
 30
 031GraphBuilderTracer::TDesc GraphBuilderTracer::TDesc::from_core(const holoflow::core::TDesc &base) {
 032  TDesc t;
 033  static_cast<holoflow::core::TDesc &>(t) = base;
 034  return t;
 035}
 36
 037std::vector<holoflow::core::TDesc> GraphBuilderTracer::to_core_descs(std::span<const TDesc> src) {
 038  std::vector<holoflow::core::TDesc> out;
 039  out.reserve(src.size());
 040  for (const auto &t : src) {
 041    out.push_back(t.as_core());
 042  }
 043  return out;
 044}
 45
 46} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tracer.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <optional>
 18#include <span>
 19#include <stack>
 20#include <string>
 21#include <vector>
 22
 23#include "holoflow/core/graph_spec.hh"
 24#include "holoflow/core/registry.hh"
 25#include "holoflow/core/tasks.hh"
 26
 27namespace holovibes::pipeline {
 28
 29// GraphBuilderTracer is the tensor-descriptor tracing layer used to construct a GraphSpec.
 30//
 31// It maintains a TDesc (tensor descriptor) type that augments the core descriptor with
 32// producer/consumer provenance, and provides generic template helpers that register nodes
 33// in the graph and return traced output descriptors. Subclasses use these helpers to build
 34// strongly-typed wrappers for individual task kinds.
 35class GraphBuilderTracer {
 36public:
 37  explicit GraphBuilderTracer(holoflow::core::Registry &registry);
 38
 39protected:
 40  using V      = holoflow::core::GraphSpec::vertex_descriptor;
 41  using NodeId = std::string;
 42
 43  class TDesc : public holoflow::core::TDesc {
 44  public:
 45    struct Producer {
 46      NodeId node_id;
 47      int    out_idx;
 48      V      vertex;
 49    };
 50
 51    struct Consumer {
 52      NodeId node_id;
 53      int    in_idx;
 54      V      vertex;
 55    };
 56
 57    std::optional<Producer> producer;
 58    std::vector<Consumer>   consumers;
 59
 60    [[nodiscard]] holoflow::core::TDesc as_core() const;
 61    [[nodiscard]] static TDesc          from_core(const holoflow::core::TDesc &base);
 62  };
 63
 64  // Convert a span of TDesc to the core representation.
 65  [[nodiscard]] static std::vector<holoflow::core::TDesc> to_core_descs(std::span<const TDesc> src);
 66
 67  // Wrap the output descriptors returned by a factory's infer() call into traced TDescs.
 68  template <class InferResult>
 69  [[nodiscard]] static std::vector<TDesc> wrap_infer_outputs(std::string_view node_id, V vertex,
 70                                                             const InferResult &infer);
 71
 72  // Add a source (zero-input) sync node and return its output descriptors.
 73  template <typename SettingsT>
 74  std::vector<TDesc> make_source_sync_node(std::string_view node_name, std::string_view kind,
 75                                           std::string_view reg_key, const SettingsT &s,
 76                                           bool debug = true);
 77
 78  // Add a unary sync node (one input) and return its output descriptors.
 79  template <typename SettingsT>
 80  std::vector<TDesc> make_unary_sync_node(std::string_view node_name, std::string_view kind,
 81                                          std::string_view reg_key, const TDesc &X,
 82                                          const SettingsT &s, bool debug = true);
 83
 84  // Add an n-ary sync node (multiple inputs) and return its output descriptors.
 85  template <typename SettingsT>
 86  std::vector<TDesc> make_nary_sync_node(std::string_view node_name, std::string_view kind,
 87                                         std::string_view reg_key, std::span<const TDesc> inputs,
 88                                         const SettingsT &s, bool debug = true);
 89
 90  // Add a unary async node (one input) and return its output descriptors.
 91  template <typename SettingsT>
 92  std::vector<TDesc> make_unary_async_node(std::string_view node_name, std::string_view kind,
 93                                           std::string_view reg_key, const TDesc &X,
 94                                           const SettingsT &s, bool debug = true);
 95
 96  // Add an n-ary async node (multiple inputs) and return its output descriptors.
 97  template <typename SettingsT>
 98  std::vector<TDesc> make_nary_async_node(std::string_view node_name, std::string_view kind,
 99                                          std::string_view reg_key, std::span<const TDesc> inputs,
 100                                          const SettingsT &s, bool debug = true);
 101
 102  holoflow::core::Registry &reg_;
 103  holoflow::core::GraphSpec g_;
 104  std::stack<std::string>   scope_;
 0105  size_t                    unique_id_counter_ = 0;
 106};
 107
 108} // namespace holovibes::pipeline
 109
 110#include "graph_builder_tracer.hxx"

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder_tracer.hxx

#LineLine coverage
 1#pragma once
 2
 3#ifndef HOLOVIBES__GRAPH_BUILDER_TRACER_HXX__INCLUDED
 4#include "graph_builder_tracer.hh"
 5#endif
 6
 7#include "bug.hh"
 8
 9namespace holovibes::pipeline {
 10
 11template <class InferResult>
 12std::vector<GraphBuilderTracer::TDesc>
 13GraphBuilderTracer::wrap_infer_outputs(std::string_view node_id, V vertex,
 014                                       const InferResult &infer) {
 015  std::vector<TDesc> ys;
 016  ys.reserve(infer.output_descs.size());
 017  int out_idx = 0;
 018  for (const auto &base : infer.output_descs) {
 019    auto y     = TDesc::from_core(base);
 020    y.producer = TDesc::Producer{
 21        .node_id = std::string{node_id},
 22        .out_idx = out_idx,
 23        .vertex  = vertex,
 24    };
 025    ys.push_back(std::move(y));
 026    ++out_idx;
 027  }
 028  return ys;
 029}
 30
 31template <typename SettingsT>
 32std::vector<GraphBuilderTracer::TDesc>
 33GraphBuilderTracer::make_source_sync_node(std::string_view node_name, std::string_view kind,
 34                                          std::string_view reg_key, const SettingsT &s,
 035                                          bool debug) {
 036  HOLOVIBES_CHECK(reg_.is_sync_registered(std::string{reg_key}));
 37
 38  holoflow::core::NodeSpec node_spec{
 039      .name     = std::string{node_name} + "_" + std::to_string(unique_id_counter_++),
 040      .kind     = std::string{kind},
 041      .settings = nlohmann::json(s),
 042      .debug    = debug,
 43  };
 44
 045  auto       v       = boost::add_vertex(node_spec, g_);
 046  auto      &factory = reg_.get_sync(std::string{reg_key});
 047  const auto infer   = factory.infer(std::span<const holoflow::core::TDesc>{}, nlohmann::json(s));
 048  return wrap_infer_outputs(node_name, v, infer);
 049}
 50
 51template <typename SettingsT>
 52std::vector<GraphBuilderTracer::TDesc>
 53GraphBuilderTracer::make_unary_sync_node(std::string_view node_name, std::string_view kind,
 54                                         std::string_view reg_key, const TDesc &X,
 055                                         const SettingsT &s, bool debug) {
 056  HOLOVIBES_CHECK(X.producer.has_value());
 057  HOLOVIBES_CHECK(reg_.is_sync_registered(std::string{reg_key}));
 58
 59  holoflow::core::NodeSpec node_spec{
 060      .name     = std::string{node_name} + "_" + std::to_string(unique_id_counter_++),
 061      .kind     = std::string{kind},
 062      .settings = nlohmann::json(s),
 063      .debug    = debug,
 64  };
 65
 066  auto v = boost::add_vertex(node_spec, g_);
 067  boost::add_edge(X.producer->vertex, v, {X.producer->out_idx, 0}, g_);
 68
 069  auto      &factory     = reg_.get_sync(std::string{reg_key});
 070  const auto core_inputs = to_core_descs(std::span{&X, 1});
 071  const auto infer       = factory.infer(core_inputs, nlohmann::json(s));
 072  return wrap_infer_outputs(node_name, v, infer);
 073}
 74
 75template <typename SettingsT>
 76std::vector<GraphBuilderTracer::TDesc>
 77GraphBuilderTracer::make_nary_sync_node(std::string_view node_name, std::string_view kind,
 78                                        std::string_view reg_key, std::span<const TDesc> inputs,
 079                                        const SettingsT &s, bool debug) {
 080  HOLOVIBES_CHECK(!inputs.empty());
 081  for (const auto &X : inputs) {
 082    HOLOVIBES_CHECK(X.producer.has_value());
 83  }
 084  HOLOVIBES_CHECK(reg_.is_sync_registered(std::string{reg_key}));
 85
 86  holoflow::core::NodeSpec node_spec{
 087      .name     = std::string{node_name} + "_" + std::to_string(unique_id_counter_++),
 088      .kind     = std::string{kind},
 089      .settings = nlohmann::json(s),
 090      .debug    = debug,
 91  };
 92
 093  auto v = boost::add_vertex(node_spec, g_);
 094  for (size_t i = 0; i < inputs.size(); ++i) {
 095    const auto &X = inputs[i];
 096    boost::add_edge(X.producer->vertex, v, {X.producer->out_idx, static_cast<int>(i)}, g_);
 097  }
 98
 099  auto      &factory     = reg_.get_sync(std::string{reg_key});
 0100  const auto core_inputs = to_core_descs(inputs);
 0101  const auto infer       = factory.infer(core_inputs, nlohmann::json(s));
 0102  return wrap_infer_outputs(node_name, v, infer);
 0103}
 104
 105template <typename SettingsT>
 106std::vector<GraphBuilderTracer::TDesc>
 107GraphBuilderTracer::make_unary_async_node(std::string_view node_name, std::string_view kind,
 108                                          std::string_view reg_key, const TDesc &X,
 0109                                          const SettingsT &s, bool debug) {
 0110  HOLOVIBES_CHECK(X.producer.has_value());
 0111  HOLOVIBES_CHECK(reg_.is_async_registered(std::string{reg_key}));
 112
 113  holoflow::core::NodeSpec node_spec{
 0114      .name     = std::string{node_name} + "_" + std::to_string(unique_id_counter_++),
 0115      .kind     = std::string{kind},
 0116      .settings = nlohmann::json(s),
 0117      .debug    = debug,
 118  };
 119
 0120  auto v = boost::add_vertex(node_spec, g_);
 0121  boost::add_edge(X.producer->vertex, v, {X.producer->out_idx, 0}, g_);
 122
 0123  auto      &factory     = reg_.get_async(std::string{reg_key});
 0124  const auto core_inputs = to_core_descs(std::span{&X, 1});
 0125  const auto infer       = factory.infer(core_inputs, nlohmann::json(s));
 0126  return wrap_infer_outputs(node_name, v, infer);
 0127}
 128
 129template <typename SettingsT>
 130std::vector<GraphBuilderTracer::TDesc>
 131GraphBuilderTracer::make_nary_async_node(std::string_view node_name, std::string_view kind,
 132                                         std::string_view reg_key, std::span<const TDesc> inputs,
 0133                                         const SettingsT &s, bool debug) {
 0134  HOLOVIBES_CHECK(!inputs.empty());
 0135  for (const auto &X : inputs) {
 0136    HOLOVIBES_CHECK(X.producer.has_value());
 137  }
 0138  HOLOVIBES_CHECK(reg_.is_async_registered(std::string{reg_key}));
 139
 140  holoflow::core::NodeSpec node_spec{
 0141      .name     = std::string{node_name} + "_" + std::to_string(unique_id_counter_++),
 0142      .kind     = std::string{kind},
 0143      .settings = nlohmann::json(s),
 0144      .debug    = debug,
 145  };
 146
 0147  auto v = boost::add_vertex(node_spec, g_);
 0148  for (size_t i = 0; i < inputs.size(); ++i) {
 0149    const auto &X = inputs[i];
 0150    boost::add_edge(X.producer->vertex, v, {X.producer->out_idx, static_cast<int>(i)}, g_);
 0151  }
 152
 0153  auto      &factory     = reg_.get_async(std::string{reg_key});
 0154  const auto core_inputs = to_core_descs(inputs);
 0155  const auto infer       = factory.infer(core_inputs, nlohmann::json(s));
 0156  return wrap_infer_outputs(node_name, v, infer);
 0157}
 158
 159} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\graph_builder.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "graph_builder.hh"
 16
 17#include <algorithm>
 18#include <array>
 19#include <cmath>
 20#include <map>
 21#include <optional>
 22#include <spdlog/fmt/ranges.h>
 23#include <stdexcept>
 24#include <utility>
 25
 26#include "bug.hh"
 27#include "graph_builder_tasks.hh"
 28#include "logger.hh"
 29#include "pipeline/settings.hh"
 30#include "settings_loader.hh"
 31
 32namespace holovibes::pipeline {
 33
 34using PhaseReference = holotask::syncs::STFDPhaseReference;
 35
 36// -------------------------------------------------------------------------------------------------
 37// Implementation
 38// -------------------------------------------------------------------------------------------------
 39
 40class GraphBuilder::Impl : public GraphBuilderTasks {
 41public:
 42  Impl(const Settings &settings, holoflow::core::Registry &registry);
 43
 44  holoflow::core::GraphSpec build();
 45
 46private:
 47  struct ShackHartmannGeometry {
 48    size_t frame_width;
 49    size_t frame_height;
 50    size_t nb_subapertures;
 51    size_t subaperture_width;
 52    size_t subaperture_height;
 53
 54    float wavelength;
 55    float pixel_pitch_x;
 56    float pixel_pitch_y;
 57    float propagation_distance;
 58    float pupil_radius_m;
 59  };
 60
 61  struct ShackHartmannSlopeOutput {
 62    TDesc                slopes;
 63    std::optional<TDesc> xcorr;
 64  };
 65
 66  struct AberrationCorrectionState {
 67    std::optional<TDesc> cumulative_coeffs_gpu;
 68    std::optional<TDesc> cumulative_phase_gpu;
 69  };
 70
 71  // clang-format off
 72  TDesc build_acquisition();
 73  TDesc short_time_fresnel_diffraction(const TDesc &field, size_t win_w, size_t win_h, size_t stride_x, size_t stride_y,
 74  void build_raw_record(const TDesc &H);
 75  bool build_raw_view(const TDesc &H);
 76  TDesc build_preprocessing(TDesc H);
 77  TDesc build_time_frequency_analysis(TDesc H);
 78  TDesc build_aberration_correction(const TDesc &FH_current, const TDesc &FH_delayed);
 79  TDesc build_aberration_correction_pass(const TDesc &FH_current, const TDesc &FH_delayed, bool is_last_pass, Aberration
 80  ShackHartmannGeometry shack_hartmann_geometry(const TDesc &FH) const;
 81  TDesc build_shack_hartmann_sensor(const TDesc &FH, const ShackHartmannGeometry &geometry);
 82  ShackHartmannSlopeOutput build_shack_hartmann_slopes(const TDesc &sensor_images, const ShackHartmannGeometry &geometry
 83  void build_shack_hartmann_view(const TDesc &sensor_images, const ShackHartmannGeometry &geometry);
 84  void build_shack_hartmann_xcorr_view(const TDesc &xcorr, const ShackHartmannGeometry &geometry);
 85  TDesc build_zernike_correction(const TDesc &FH, const TDesc &slopes, const ShackHartmannGeometry &geometry, bool is_la
 86  void build_zernike_outputs(const AberrationCorrectionState &state, const ShackHartmannGeometry &geometry);
 87  TDesc build_spatial_propagation(const TDesc &FH);
 88  TDesc build_spatial_filter(const TDesc &FH_z);
 89  void build_xy_view(const TDesc &FH_z);
 90  void build_3d_cuts(const TDesc &FH_z);
 91  TDesc build_freq_weights();
 92  // clang-format on
 93
 94  Settings s_;
 95
 096  std::map<LoadMethod, holotask::sources::HolofileSettings::LoadKind> load_method_map_{
 97      {LoadMethod::READ_LIVE, holotask::sources::HolofileSettings::LoadKind::Live},
 98      {LoadMethod::LOAD_IN_CPU, holotask::sources::HolofileSettings::LoadKind::CPUCached},
 99      {LoadMethod::LOAD_IN_GPU, holotask::sources::HolofileSettings::LoadKind::GPUCached},
 100  };
 101};
 102
 103// -----------------------------------------------------------------------------
 104// Helpers declarations
 105// -----------------------------------------------------------------------------
 106
 107namespace {
 108
 109holotask::syncs::FlatfieldSettings flatfield_settings_from_cutoff_period(float cutoff_period_m,
 110                                                                         float dy_m, float dx_m);
 111
 112std::pair<float, float> fresnel_1fft_output_pitch(float wavelength_m, float z_m, float dy_in_m,
 113                                                  float dx_in_m, size_t ny, size_t nx);
 114
 115std::pair<float, float> post_propagation_pitch(const Settings              &settings,
 116                                               const holoflow::core::TDesc &desc);
 117
 118} // namespace
 119
 120// -------------------------------------------------------------------------------------------------
 121// Initialization
 122// -------------------------------------------------------------------------------------------------
 123
 124GraphBuilder::GraphBuilder(const Settings &settings, holoflow::core::Registry &registry)
 0125    : impl_(std::make_unique<Impl>(settings, registry)) {}
 126
 0127GraphBuilder::~GraphBuilder() = default;
 128
 0129GraphBuilder::GraphBuilder(GraphBuilder &&) noexcept            = default;
 0130GraphBuilder &GraphBuilder::operator=(GraphBuilder &&) noexcept = default;
 131
 0132holoflow::core::GraphSpec GraphBuilder::build() { return impl_->build(); }
 133
 134GraphBuilder::Impl::Impl(const Settings &settings, holoflow::core::Registry &registry)
 0135    : GraphBuilderTasks(registry), s_(settings) {}
 136
 137// -------------------------------------------------------------------------------------------------
 138// Top-level pipeline
 139// -------------------------------------------------------------------------------------------------
 140
 0141holoflow::core::GraphSpec GraphBuilder::Impl::build() {
 0142  if (!std::isfinite(s_.signal_plot_time_window_seconds) ||
 143      s_.signal_plot_time_window_seconds <= 0.0) {
 0144    throw std::invalid_argument("signal_plot_time_window_seconds must be positive and finite");
 145  }
 0146  const double signal_plot_sample_time_seconds = s_.signal_plot_sample_time_seconds();
 0147  if (!std::isfinite(signal_plot_sample_time_seconds) || signal_plot_sample_time_seconds <= 0.0) {
 0148    throw std::invalid_argument("derived signal plot sample time must be positive and finite");
 149  }
 150
 0151  TDesc H = build_acquisition();
 152
 0153  if (s_.recording_method == RecordingMethod::RAW) {
 0154    build_raw_record(H);
 155  }
 156
 0157  if (s_.raw_view || s_.view_type == ViewType::RAW) {
 0158    bool should_exit = build_raw_view(H);
 0159    if (should_exit) {
 0160      return g_;
 161    }
 162  }
 163
 0164  H = build_preprocessing(H);
 165
 0166  TDesc FH = build_time_frequency_analysis(H);
 167
 0168  bool filter_2d_standalone = s_.spacial_method != SpacialMethod::ANGULAR_SPECTRUM;
 0169  if (s_.filter_2d && filter_2d_standalone) {
 0170    FH = build_spatial_filter(FH);
 171  }
 172
 0173  if (s_.pp_accumulation <= 0) {
 0174    throw std::invalid_argument("pp_accumulation must be positive");
 175  }
 176
 177  // Batch queue with two readers. FH_delayed is used for Shack-Hartmann correction, FH_current is
 178  // used for propagation.
 0179  auto        target_capacity = std::max<size_t>(2, 2 * FH.shape.at(0));
 0180  auto        window_size     = static_cast<size_t>(s_.pp_accumulation);
 0181  const auto  timing_outputs  = dual_reader_batch_queue(FH, {target_capacity, window_size});
 0182  TDesc       FH_current      = timing_outputs.at(0);
 0183  const TDesc FH_delayed      = timing_outputs.at(1);
 184
 0185  if (s_.autofocus_enabled) {
 0186    FH_current = build_aberration_correction(FH_current, FH_delayed);
 187  }
 188
 0189  TDesc FH_z = build_spatial_propagation(FH_current);
 190
 0191  build_xy_view(FH_z);
 192
 0193  if (s_.view_3d_cuts) {
 0194    build_3d_cuts(FH_z);
 195  }
 196
 0197  return g_;
 0198}
 199
 200// -------------------------------------------------------------------------------------------------
 201// Pipeline stages
 202// -------------------------------------------------------------------------------------------------
 203
 0204GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_acquisition() {
 0205  auto cam_path = s_.camera_config_path.string();
 206
 0207  if (s_.import_source == ImportSource::HOLOFILE) {
 0208    return holofile_read({.path        = s_.load_path.string(),
 209                          .load_kind   = load_method_map_.at(s_.load_method),
 210                          .start_frame = s_.load_begin,
 211                          .end_frame   = s_.load_end,
 212                          .batch_size  = s_.load_batch,
 213                          .max_fps     = s_.load_fps_limit,
 214                          .keep_cursor = false});
 215  }
 216
 0217  if (s_.import_source == ImportSource::AMETEK_S710_EURESYS_COAXLINK_OCTO) {
 0218    return ametek_s710_euresys_coaxlink_octo({cam_path});
 219  }
 220
 0221  if (s_.import_source == ImportSource::AMETEK_S711_EURESYS_COAXLINK_QSFP) {
 0222    return ametek_s711_euresys_coaxlink_qsfp_plus({cam_path});
 223  }
 224
 0225  HOLOVIBES_UNREACHABLE();
 0226}
 227
 0228void GraphBuilder::Impl::build_raw_record(const TDesc &H) {
 0229  auto path          = s_.recording_path.string();
 0230  auto count         = s_.recording_count;
 0231  auto settings_json = settings_to_old_json(s_);
 0232  holofile_write(H, {path, count, settings_json, true});
 0233}
 234
 0235bool GraphBuilder::Impl::build_raw_view(const TDesc &H) {
 0236  auto Host = holotask::syncs::MemcpySettings::Target::Host;
 237
 0238  auto H_disp = memcpy(H, {Host});
 0239  auto H_view = batched_queue(H_disp, {s_.cpu_out_size, 1, 1});
 240
 0241  if (s_.raw_view) {
 0242    xy_raw_display(H_view, {});
 243  }
 244
 0245  if (s_.view_type == ViewType::RAW) {
 0246    xy_processed_display(H_view, {});
 0247    return true;
 248  }
 249
 0250  return false;
 0251}
 252
 0253GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_preprocessing(TDesc H) {
 254  using MemLoc = holoflow::core::MemLoc;
 255  using Target = holotask::syncs::ConversionSettings::Target;
 256  using Strat  = holotask::syncs::ConversionSettings::Strategy;
 0257  auto Device  = holotask::syncs::MemcpySettings::Target::Device;
 258
 0259  if (H.mem_loc != MemLoc::Device) {
 0260    H = memcpy(H, {Device});
 0261    H = batched_queue(H, {s_.gpu_in_size, s_.time_window, s_.time_window});
 262  }
 263
 0264  return convert(H, {Target::F32, Strat::Real});
 0265}
 266
 0267GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_time_frequency_analysis(TDesc H) {
 268  // H enters as [T, Hy, Hx] (F32).
 269  // We first accumulate N_pre such windows into a batch, producing [N_pre, T, Hy, Hx].
 270  // Time-frequency analysis then operates along axis 1 (the T dimension).
 271  // The output is [N_pre, Nz, Hy, Hx], which feeds directly into the post-TFA queue.
 272
 0273  int     N_pre = 8;
 0274  int64_t T     = static_cast<int64_t>(H.shape.at(0));
 0275  int64_t Hy    = static_cast<int64_t>(H.shape.at(1));
 0276  int64_t Hx    = static_cast<int64_t>(H.shape.at(2));
 277
 0278  H = reshape(H, {{1, T, Hy, Hx}, false});
 0279  H = batched_queue(H, {N_pre * 2, N_pre, N_pre}); // [N_pre, T, Hy, Hx]
 280
 0281  if (s_.time_method == TimeMethod::RFFT) {
 0282    auto FH = rfft(H, {1}); // axis 1 = T dimension
 283
 0284    if (s_.view_3d_cuts) {
 0285      return FH;
 286    }
 287
 288    // Optimization: slice relevant components early. The rest is not used therefore this saves
 289    // further computations.
 0290    FH = slice(FH, {{{}, holonp::SliceRange{s_.time_z_begin, s_.time_z_end}, {}, {}}});
 0291    FH = copy(FH, {});
 0292    return FH;
 293  }
 294
 0295  if (s_.time_method == TimeMethod::FFT) {
 0296    auto FH = fft(H, {1});
 297
 0298    if (s_.view_3d_cuts) {
 0299      return FH;
 300    }
 301
 302    // Optimization: slice relevant components early, including the symmetric negative band. The
 303    // rest is not used therefore this saves further computations.
 0304    auto pos_range = holonp::SliceRange{s_.time_z_begin, s_.time_z_end};
 0305    auto FH_pos    = slice(FH, {{{}, pos_range, {}, {}}});
 306
 0307    auto neg_range = holonp::SliceRange{T - s_.time_z_end, T - s_.time_z_begin};
 0308    auto FH_neg    = slice(FH, {{{}, neg_range, {}, {}}});
 309
 0310    FH = concatenate(std::array<TDesc, 2>{FH_pos, FH_neg}, {1});
 0311    return FH;
 312  }
 313
 0314  if (s_.time_method == TimeMethod::PRINCIPAL_COMPONENT_ANALYSIS) {
 315    // PCA natively supports arbitrary leading batch dimensions (rank >= 3).
 316    // Input H: [N_pre, T, Hy, Hx] â€” the feature axis is shape[-3] = T.
 317    // Output FH: [N_pre, Nz, Hy, Hx] where Nz = z1 - z0.
 0318    int  z0 = s_.view_3d_cuts ? 0 : s_.time_z_begin;
 0319    int  z1 = s_.view_3d_cuts ? static_cast<int>(T) : s_.time_z_end;
 0320    auto FH = pca(H, {z0, z1});
 0321    return FH;
 322  }
 323
 0324  HOLOVIBES_UNREACHABLE();
 0325}
 326
 327// -------------------------------------------------------------------------------------------------
 328// Aberration correction
 329// -------------------------------------------------------------------------------------------------
 330
 331GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_aberration_correction(const TDesc &FH_current,
 0332                                                                          const TDesc &FH_delayed) {
 0333  if (s_.autofocus_nb_iter <= 0) {
 0334    throw std::invalid_argument("autofocus_nb_iter must be positive");
 335  }
 0336  if (s_.pp_accumulation > 1 && s_.autofocus_nb_iter > 1) {
 0337    throw std::invalid_argument(
 338        "sliding Shack-Hartmann correction only supports one autofocus iteration");
 339  }
 340
 0341  auto                      corrected = FH_current;
 0342  AberrationCorrectionState state;
 343
 0344  for (int pass = 0; pass < s_.autofocus_nb_iter; ++pass) {
 0345    const bool   is_last_pass = pass == s_.autofocus_nb_iter - 1;
 0346    const TDesc &delayed      = pass == 0 ? FH_delayed : corrected;
 0347    corrected = build_aberration_correction_pass(corrected, delayed, is_last_pass, state);
 0348  }
 349
 350  // Decouple the final propagation so it can overlap the next Shack-Hartmann iteration.
 0351  return batched_queue(corrected, {2, 1, 1});
 0352}
 353
 354GraphBuilder::Impl::TDesc
 355GraphBuilder::Impl::build_aberration_correction_pass(const TDesc &FH_current,
 356                                                     const TDesc &FH_delayed, bool is_last_pass,
 0357                                                     AberrationCorrectionState &state) {
 0358  auto geometry      = shack_hartmann_geometry(FH_current);
 0359  auto sensor_images = build_shack_hartmann_sensor(FH_current, geometry);
 360
 0361  bool output_xcorr = is_last_pass && s_.view_shack_hartmann_xcorr;
 0362  auto slope_output = build_shack_hartmann_slopes(sensor_images, geometry, output_xcorr);
 363
 0364  if (is_last_pass && s_.view_shack_hartmann) {
 0365    build_shack_hartmann_view(sensor_images, geometry);
 366  }
 367
 0368  if (slope_output.xcorr.has_value()) {
 0369    build_shack_hartmann_xcorr_view(*slope_output.xcorr, geometry);
 370  }
 371
 0372  return build_zernike_correction(FH_delayed, slope_output.slopes, geometry, is_last_pass, state);
 0373}
 374
 375GraphBuilder::Impl::ShackHartmannGeometry
 0376GraphBuilder::Impl::shack_hartmann_geometry(const TDesc &FH) const {
 0377  if (s_.autofocus_nb_subaps <= 0) {
 0378    throw std::invalid_argument("autofocus_nb_subaps must be positive");
 379  }
 0380  if ((s_.autofocus_nb_subaps % 2) == 0) {
 0381    throw std::invalid_argument("autofocus_nb_subaps must be odd");
 382  }
 383
 0384  const auto frame_width        = FH.shape.at(3);
 0385  const auto frame_height       = FH.shape.at(2);
 0386  const auto nb_subapertures    = static_cast<size_t>(s_.autofocus_nb_subaps);
 0387  const auto subaperture_width  = frame_width / nb_subapertures;
 0388  const auto subaperture_height = frame_height / nb_subapertures;
 389
 0390  if (subaperture_width == 0 || subaperture_height == 0) {
 0391    throw std::invalid_argument("autofocus_nb_subaps is too large for the current frame size");
 392  }
 393
 0394  const auto  wavelength           = s_.spacial_lambda;
 0395  const auto  pixel_pitch_x        = s_.spacial_pixel_size;
 0396  const auto  pixel_pitch_y        = s_.spacial_pixel_size;
 0397  const auto  propagation_distance = s_.spacial_z;
 0398  const auto  width_m  = static_cast<float>(subaperture_width * nb_subapertures) * pixel_pitch_x;
 0399  const auto  height_m = static_cast<float>(subaperture_height * nb_subapertures) * pixel_pitch_y;
 0400  const float pupil_radius_m = 0.5f * std::min(width_m, height_m);
 401
 0402  return {.frame_width          = frame_width,
 403          .frame_height         = frame_height,
 404          .nb_subapertures      = nb_subapertures,
 405          .subaperture_width    = subaperture_width,
 406          .subaperture_height   = subaperture_height,
 407          .wavelength           = wavelength,
 408          .pixel_pitch_x        = pixel_pitch_x,
 409          .pixel_pitch_y        = pixel_pitch_y,
 410          .propagation_distance = propagation_distance,
 411          .pupil_radius_m       = pupil_radius_m};
 0412}
 413
 414GraphBuilder::Impl::TDesc
 415GraphBuilder::Impl::build_shack_hartmann_sensor(const TDesc                 &FH,
 0416                                                const ShackHartmannGeometry &geometry) {
 0417  auto propagated = short_time_fresnel_diffraction(
 418      FH, geometry.subaperture_width, geometry.subaperture_height, geometry.subaperture_width,
 419      geometry.subaperture_height, geometry.wavelength, geometry.pixel_pitch_x,
 420      geometry.pixel_pitch_y, geometry.propagation_distance, PhaseReference::GLOBAL);
 421
 0422  auto sensor_images = mean(propagated, {{1}, false});
 0423  sensor_images      = causal_slide_avg(sensor_images, {static_cast<size_t>(s_.pp_accumulation)});
 0424  sensor_images      = fftshift(sensor_images, {{-2, -1}});
 425
 0426  if (!s_.pp_flatfield) {
 0427    return sensor_images;
 428  }
 429
 0430  const auto [dy, dx] = fresnel_1fft_output_pitch(
 431      geometry.wavelength, geometry.propagation_distance, geometry.pixel_pitch_y,
 432      geometry.pixel_pitch_x, geometry.subaperture_height, geometry.subaperture_width);
 433
 0434  const auto cutoff   = s_.pp_flatfield_cutoff_period_m;
 0435  const auto settings = flatfield_settings_from_cutoff_period(cutoff, dy, dx);
 0436  sensor_images       = flatfield(sensor_images, settings);
 0437  return sensor_images;
 0438}
 439
 440GraphBuilder::Impl::ShackHartmannSlopeOutput GraphBuilder::Impl::build_shack_hartmann_slopes(
 0441    const TDesc &sensor_images, const ShackHartmannGeometry &geometry, bool output_xcorr) {
 442  using SlopeMode      = holotask::syncs::ShackHartmannSlopeMode;
 0443  auto FullPairwise    = SlopeMode::FullPairwise;
 0444  auto SingleReference = SlopeMode::SingleReference;
 445
 0446  const auto mode       = s_.autofocus_use_graph_laplacian ? FullPairwise : SingleReference;
 0447  const bool emit_xcorr = output_xcorr && mode == SingleReference;
 448
 0449  auto outputs = shack_hartmann_slopes(
 450      sensor_images,
 451      {.mode               = mode,
 452       .lambda             = geometry.wavelength,
 453       .dx                 = geometry.pixel_pitch_x,
 454       .dy                 = geometry.pixel_pitch_y,
 455       .z                  = geometry.propagation_distance,
 456       .subaperture_height = geometry.subaperture_height,
 457       .subaperture_width  = geometry.subaperture_width,
 458       .stride_y           = geometry.subaperture_height,
 459       .stride_x           = geometry.subaperture_width,
 460       .correlation_roi    = {0.5f, 0.5f, s_.pp_pctclip_radius, s_.pp_pctclip_radius, 0.0f},
 461       .skip_subapertures_outside_pupil = s_.autofocus_skip_subapertures_outside_pupil,
 462       .output_xcorr_maps               = emit_xcorr});
 463
 0464  ShackHartmannSlopeOutput result{.slopes = outputs.at(0)};
 0465  if (emit_xcorr) {
 0466    result.xcorr = outputs.at(1);
 467  }
 468
 0469  return result;
 0470}
 471
 472void GraphBuilder::Impl::build_shack_hartmann_view(const TDesc                 &sensor_images,
 0473                                                   const ShackHartmannGeometry &geometry) {
 474  using Target = holotask::syncs::ConversionSettings::Target;
 475  using Strat  = holotask::syncs::ConversionSettings::Strategy;
 476
 0477  const auto height = static_cast<int64_t>(geometry.subaperture_height * geometry.nb_subapertures);
 0478  const auto width  = static_cast<int64_t>(geometry.subaperture_width * geometry.nb_subapertures);
 479
 0480  auto display = normalize(sensor_images, {{-2, -1}, 0.0f, 255.0f});
 0481  display      = transpose(display, {{0, 1, 3, 2, 4}});
 0482  display      = reshape(display, {{1, height, width}});
 0483  display      = convert(display, {Target::U8, Strat::Scaled});
 0484  display      = batched_queue(display, {s_.cpu_out_size, 1, 1});
 0485  shack_hartmann_display(display, {});
 0486}
 487
 488void GraphBuilder::Impl::build_shack_hartmann_xcorr_view(const TDesc                 &xcorr,
 0489                                                         const ShackHartmannGeometry &geometry) {
 490  using Target = holotask::syncs::ConversionSettings::Target;
 491  using Strat  = holotask::syncs::ConversionSettings::Strategy;
 492
 0493  auto display = fftshift(xcorr, {{-2, -1}});
 0494  display      = normalize(display, {{-2, -1}, 0.0f, 255.0f});
 495
 0496  const auto height = static_cast<int64_t>(display.shape.at(3) * geometry.nb_subapertures);
 0497  const auto width  = static_cast<int64_t>(display.shape.at(4) * geometry.nb_subapertures);
 498
 0499  display = convert(display, {Target::U8, Strat::Scaled});
 0500  display = transpose(display, {{0, 1, 3, 2, 4}});
 0501  display = reshape(display, {{1, height, width}});
 0502  display = batched_queue(display, {s_.cpu_out_size, 1, 1});
 0503  shack_hartmann_xcorr_display(display, {});
 0504}
 505
 506GraphBuilder::Impl::TDesc
 507GraphBuilder::Impl::build_zernike_correction(const TDesc &FH, const TDesc &slopes,
 508                                             const ShackHartmannGeometry &geometry,
 0509                                             bool is_last_pass, AberrationCorrectionState &state) {
 0510  auto F32    = holoflow::core::DType::F32;
 0511  auto Device = holoflow::core::MemLoc::Device;
 512
 0513  if (s_.autofocus_zernike_orders.empty()) {
 0514    auto phase     = zeros({{1, geometry.frame_height, geometry.frame_width}, F32});
 0515    auto corrected = correct_phase(FH, phase, {});
 516
 0517    if (is_last_pass && s_.view_zernike_phase) {
 0518      zernike_phase_display(phase, {});
 519    }
 520
 0521    return corrected;
 522  }
 523
 524  holotask::syncs::ZernikeFromSlopesSettings settings{
 0525      .indexes                         = s_.autofocus_zernike_orders,
 0526      .lambda                          = geometry.wavelength,
 0527      .dx                              = geometry.pixel_pitch_x,
 0528      .dy                              = geometry.pixel_pitch_y,
 0529      .subaperture_height              = geometry.subaperture_height,
 0530      .subaperture_width               = geometry.subaperture_width,
 0531      .stride_y                        = geometry.subaperture_height,
 0532      .stride_x                        = geometry.subaperture_width,
 0533      .ny                              = 1,
 0534      .nx                              = 1,
 0535      .skip_subapertures_outside_pupil = s_.autofocus_skip_subapertures_outside_pupil};
 536
 0537  auto coeffs    = zernike_from_slopes(slopes, settings);
 0538  coeffs         = slice(coeffs, {{0, 0, {}}});
 0539  auto height    = static_cast<int>(geometry.frame_height);
 0540  auto width     = static_cast<int>(geometry.frame_width);
 0541  auto phase     = zernike_phase(coeffs, {s_.autofocus_zernike_orders, height, width, Device});
 0542  auto corrected = correct_phase(FH, phase, {});
 543
 544  const auto accumulate = [this](auto &cumulative, const auto &value) {
 545    cumulative = cumulative ? add(*cumulative, value, {}) : value;
 0546  };
 547
 0548  accumulate(state.cumulative_coeffs_gpu, coeffs);
 0549  accumulate(state.cumulative_phase_gpu, phase);
 550
 0551  if (is_last_pass) {
 0552    build_zernike_outputs(state, geometry);
 553  }
 554
 0555  return corrected;
 0556}
 557
 558void GraphBuilder::Impl::build_zernike_outputs(const AberrationCorrectionState &state,
 0559                                               const ShackHartmannGeometry     &geometry) {
 0560  auto Host = holotask::syncs::MemcpySettings::Target::Host;
 561
 0562  HOLOVIBES_CHECK(state.cumulative_coeffs_gpu.has_value());
 0563  HOLOVIBES_CHECK(state.cumulative_phase_gpu.has_value());
 564
 0565  const auto &coeffs = *state.cumulative_coeffs_gpu;
 0566  const auto &phase  = *state.cumulative_phase_gpu;
 567
 0568  auto coeffs_reshaped = reshape(coeffs, {{1, static_cast<int64_t>(coeffs.shape.at(0))}});
 0569  auto queue           = batched_queue(coeffs_reshaped, {s_.cpu_out_size, 1, 1});
 0570  coeffs_reshaped      = reshape(queue, {{static_cast<int64_t>(coeffs.shape.at(0))}});
 0571  zernike_coefficients_display(coeffs_reshaped, {s_.autofocus_zernike_orders});
 572
 0573  if (s_.view_zernike_metrics) {
 0574    auto coeffs_host = memcpy(coeffs_reshaped, {Host});
 0575    zernike_history_display(coeffs_host, {
 576                                             s_.autofocus_zernike_orders,
 577                                             s_.signal_plot_time_window_seconds,
 578                                             s_.signal_plot_sample_time_seconds(),
 579                                             static_cast<size_t>(s_.pp_accumulation - 1),
 580                                         });
 0581  }
 582
 0583  const bool defocus_included =
 584      std::ranges::find(s_.autofocus_zernike_orders, 4) != s_.autofocus_zernike_orders.end();
 585
 0586  if (defocus_included) {
 0587    zernike_defocus_z_prop(coeffs, {s_.autofocus_zernike_orders, geometry.wavelength,
 588                                    geometry.propagation_distance, geometry.pupil_radius_m});
 589  }
 590
 0591  if (s_.view_zernike_phase) {
 0592    auto height  = static_cast<int64_t>(geometry.frame_height);
 0593    auto width   = static_cast<int64_t>(geometry.frame_width);
 0594    auto display = copy(phase, {});
 0595    display      = wrap2pi(display, {});
 0596    display      = reshape(display, {{1, height, width}});
 0597    display      = batched_queue(display, {s_.cpu_out_size, 1, 1});
 0598    zernike_phase_display(display, {});
 0599  }
 0600}
 601
 602GraphBuilder::Impl::TDesc GraphBuilder::Impl::short_time_fresnel_diffraction(
 603    const TDesc &field, size_t win_w, size_t win_h, size_t stride_x, size_t stride_y, float lam,
 0604    float dx, float dy, float z_prop, PhaseReference phase_ref, bool skip_phase_shift) {
 0605  return GraphBuilderTasks::short_time_fresnel_diffraction(field,
 606                                                           {.lambda           = lam,
 607                                                            .dx               = dx,
 608                                                            .dy               = dy,
 609                                                            .z                = z_prop,
 610                                                            .win_h            = win_h,
 611                                                            .win_w            = win_w,
 612                                                            .stride_y         = stride_y,
 613                                                            .stride_x         = stride_x,
 614                                                            .phase_ref        = phase_ref,
 615                                                            .skip_phase_shift = skip_phase_shift,
 616                                                            .output_magnitude = true});
 0617}
 618
 0619GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_spatial_propagation(const TDesc &FH) {
 620  using Padding = holotask::syncs::AngularSpectrumSettings::Padding;
 621  using Filter  = holotask::syncs::AngularSpectrumSettings::Filter;
 622
 0623  if (s_.spacial_method == SpacialMethod::FRESNEL_DIFFRACTION) {
 0624    return fresnel_diffraction(FH, {.lambda           = s_.spacial_lambda,
 625                                    .dx               = s_.spacial_pixel_size,
 626                                    .dy               = s_.spacial_pixel_size,
 627                                    .z                = s_.spacial_z,
 628                                    .axes             = {-2, -1},
 629                                    .output_magnitude = true});
 630  }
 631
 0632  if (s_.spacial_method == SpacialMethod::ANGULAR_SPECTRUM) {
 0633    if (s_.autofocus_enabled) {
 0634      throw std::logic_error{"Angular Spectrum is not supported with Shack-Hartmann autofocus"};
 635    }
 636
 0637    std::optional<Padding> padding;
 0638    if (s_.asp_padding_enabled) {
 0639      padding = Padding{.width = s_.asp_padded_width, .height = s_.asp_padded_height};
 640    }
 641
 0642    std::optional<Filter> filter;
 0643    if (s_.filter_2d) {
 0644      filter = Filter{.r_inner = s_.filter_r_inner,
 645                      .r_outer = s_.filter_r_outer,
 646                      .s_inner = s_.filter_smooth_inner,
 647                      .s_outer = s_.filter_smooth_outer};
 648    }
 649
 0650    return angular_spectrum(FH, {.lambda  = s_.spacial_lambda,
 651                                 .dx      = s_.spacial_pixel_size,
 652                                 .dy      = s_.spacial_pixel_size,
 653                                 .z       = s_.spacial_z,
 654                                 .filter  = filter,
 655                                 .padding = padding});
 656  }
 657
 0658  HOLOVIBES_UNREACHABLE();
 0659}
 660
 0661GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_spatial_filter(const TDesc &FH_z) {
 0662  return filter_2d(FH_z, {
 663                             s_.filter_r_inner,
 664                             s_.filter_r_outer,
 665                             s_.filter_smooth_inner,
 666                             s_.filter_smooth_outer,
 667                         });
 0668}
 669
 0670GraphBuilder::Impl::TDesc GraphBuilder::Impl::build_freq_weights() {
 0671  auto N  = static_cast<double>(s_.time_window);
 0672  auto fs = 37e3;
 0673  auto df = fs / N;
 0674  auto f0 = s_.time_z_begin * df;
 0675  auto f1 = s_.time_z_end * df;
 676
 0677  if (s_.view_3d_cuts) {
 0678    throw std::logic_error{"Frequency weights are not supported when 3D cuts are enabled"};
 679  }
 680
 0681  if (s_.time_method == TimeMethod::FFT) {
 0682    auto freqs_pos = arange({f0, f1, df, holoflow::core::DType::F32});
 0683    auto freqs_neg = arange({f0 - fs, f1 - fs, df, holoflow::core::DType::F32});
 0684    auto freqs     = concatenate(std::array<TDesc, 2>{freqs_pos, freqs_neg}, {0});
 0685    return freqs;
 686  }
 687
 0688  if (s_.time_method == TimeMethod::RFFT ||
 689      s_.time_method == TimeMethod::PRINCIPAL_COMPONENT_ANALYSIS) {
 0690    auto freqs = arange({f0, f1, df, holoflow::core::DType::F32});
 0691    return freqs;
 692  }
 693
 0694  HOLOVIBES_UNREACHABLE();
 0695}
 696
 0697void GraphBuilder::Impl::build_xy_view(const TDesc &FH_z) {
 698  using Target                 = holotask::syncs::ConversionSettings::Target;
 699  using Strat                  = holotask::syncs::ConversionSettings::Strategy;
 700  using SlidingAverageSettings = holotask::asyncs::SlidingAverageSettings;
 701  using PctClipSettings        = holotask::syncs::PctClipSettings;
 702  using Ellipse                = PctClipSettings::Ellipse;
 703  using holotask::sinks::HolofileSettings;
 0704  auto Host = holotask::syncs::MemcpySettings::Target::Host;
 705
 0706  TDesc      result;
 0707  const bool is_magnitude = FH_z.dtype == holoflow::core::DType::F32;
 708
 0709  if (s_.moment_type == MomentType::M0) {
 0710    result = is_magnitude ? mean(FH_z, {{-3}, true}) : mean_abs(FH_z, {{-3}, true});
 0711  }
 712
 0713  else if (s_.moment_type == MomentType::M1) {
 0714    auto n_freq   = static_cast<int64_t>(FH_z.shape.at(1));
 0715    auto abs_S    = is_magnitude ? FH_z : abs(FH_z, {});
 0716    auto freqs    = reshape(build_freq_weights(), {{1, n_freq, 1, 1}});
 0717    auto weighted = multiply(freqs, abs_S, {});
 0718    result        = mean(weighted, {{-3}, true});
 0719  }
 720
 0721  else if (s_.moment_type == MomentType::M2) {
 0722    auto n_freq   = static_cast<int64_t>(FH_z.shape.at(1));
 0723    auto abs_S    = is_magnitude ? FH_z : abs(FH_z, {});
 0724    auto freqs    = reshape(build_freq_weights(), {{1, n_freq, 1, 1}});
 0725    freqs         = multiply(freqs, freqs, {});
 0726    auto weighted = multiply(freqs, abs_S, {});
 0727    result        = mean(weighted, {{-3}, true});
 0728  }
 729
 730  else {
 0731    HOLOVIBES_UNREACHABLE();
 732  }
 733
 0734  if (s_.pp_fft_shift) {
 0735    result = fftshift(result, {{-2, -1}});
 736  }
 737
 0738  if (s_.pp_flatfield) {
 0739    auto cutoff         = s_.pp_flatfield_cutoff_period_m;
 0740    const auto [dy, dx] = post_propagation_pitch(s_, FH_z);
 0741    result              = flatfield(result, flatfield_settings_from_cutoff_period(cutoff, dy, dx));
 742  }
 743
 0744  if (s_.pp_registration) {
 0745    throw std::logic_error{"Registration is currently not supported"};
 746  }
 747
 0748  result = mean(result, {{0}, false}); // [1, H, W]
 749
 750  // auto target_capacity = static_cast<size_t>(std::max(1, s_.gpu_out_size));
 0751  auto target_capacity = 8ULL;
 0752  auto window_size     = static_cast<size_t>(s_.pp_accumulation);
 0753  auto discard_first   = s_.autofocus_enabled ? window_size - 1 : 0;
 0754  auto slide_settings  = SlidingAverageSettings{target_capacity, window_size, discard_first};
 0755  result               = slide_avg(result, slide_settings);
 756
 0757  if (s_.pp_convolution) {
 0758    throw std::logic_error{"Convolution is currently not supported"};
 759  }
 760
 0761  if (s_.pp_pctclip) {
 0762    Ellipse         roi{0.5f, 0.5f, s_.pp_pctclip_radius, s_.pp_pctclip_radius, 0.0f};
 0763    PctClipSettings pct_clip_settings{s_.pp_pctclip_lower, s_.pp_pctclip_upper, roi};
 0764    result = pct_clip(result, pct_clip_settings);
 765  }
 766
 0767  result = convert(result, {Target::U8, Strat::Scaled});
 0768  result = batched_queue(result, {s_.gpu_out_size, 1, 1});
 0769  xy_processed_display(result, {});
 770
 0771  if (s_.recording_method == RecordingMethod::PROCESSED) {
 0772    auto result_rec = memcpy(result, {Host});
 0773    result_rec      = batched_queue(result_rec, {s_.cpu_out_size, 1, 1});
 774
 0775    auto path              = s_.recording_path.string();
 0776    auto count             = s_.recording_count;
 0777    auto settings_json     = settings_to_old_json(s_);
 0778    auto holofile_settings = HolofileSettings{path, count, settings_json, true};
 0779    holofile_write(result_rec, holofile_settings);
 0780  }
 0781}
 782
 0783void GraphBuilder::Impl::build_3d_cuts(const TDesc &FH_z) {
 784  (void)FH_z;
 0785  throw std::logic_error{"3D cuts are currently not supported in GraphBuilder"};
 0786}
 787
 788// -----------------------------------------------------------------------------
 789// Helpers definitions
 790// -----------------------------------------------------------------------------
 791
 792namespace {
 793
 794constexpr float kFlatfieldCutoffConstant = 0.187f;
 795
 796holotask::syncs::FlatfieldSettings flatfield_settings_from_cutoff_period(float cutoff_period_m,
 0797                                                                         float dy_m, float dx_m) {
 0798  if (cutoff_period_m <= 0.0f || dy_m <= 0.0f || dx_m <= 0.0f) {
 0799    throw std::invalid_argument("flatfield cutoff period and image pitches must be positive");
 800  }
 801
 802  // The UI exposes a physical cutoff period, not the Gaussian sigma. The 0.187 factor follows
 803  // the 50% amplitude transition convention for the Gaussian high-pass
 804  // H_hp(f) = 1 - exp(-2*pi^2*sigma^2*f^2), so f50 = 0.187 / sigma_px and
 805  // sigma_px = 0.187 * period_px. This is a convention, not a hard cutoff.
 806  // Axis order follows image layout: y uses dy on axis -2, x uses dx on axis -1.
 0807  return {
 808      .sigma_y = kFlatfieldCutoffConstant * cutoff_period_m / dy_m,
 809      .sigma_x = kFlatfieldCutoffConstant * cutoff_period_m / dx_m,
 810  };
 0811}
 812
 813std::pair<float, float> fresnel_1fft_output_pitch(float wavelength_m, float z_m, float dy_in_m,
 0814                                                  float dx_in_m, size_t ny, size_t nx) {
 0815  if (wavelength_m <= 0.0f || dy_in_m <= 0.0f || dx_in_m <= 0.0f || ny == 0 || nx == 0) {
 0816    throw std::invalid_argument(
 817        "Fresnel output pitch requires positive wavelength, pitch, and shape");
 818  }
 819
 0820  const float z_abs = std::abs(z_m);
 0821  if (z_abs <= 0.0f) {
 0822    throw std::invalid_argument("Fresnel output pitch requires non-zero propagation distance");
 823  }
 0824  return {
 825      wavelength_m * z_abs / (static_cast<float>(ny) * dy_in_m),
 826      wavelength_m * z_abs / (static_cast<float>(nx) * dx_in_m),
 827  };
 0828}
 829
 830std::pair<float, float> post_propagation_pitch(const Settings              &settings,
 0831                                               const holoflow::core::TDesc &desc) {
 0832  if (settings.spacial_method == SpacialMethod::FRESNEL_DIFFRACTION) {
 0833    const auto rank = desc.shape.size();
 0834    return fresnel_1fft_output_pitch(settings.spacial_lambda, settings.spacial_z,
 835                                     settings.spacial_pixel_size, settings.spacial_pixel_size,
 836                                     desc.shape.at(rank - 2), desc.shape.at(rank - 1));
 837  }
 838
 0839  return {settings.spacial_pixel_size, settings.spacial_pixel_size};
 0840}
 841
 842} // namespace
 843
 844} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\manager.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "pipeline/manager.hh"
 16
 17#include <QCoreApplication>
 18#include <QMetaObject>
 19#include <QStandardPaths>
 20#include <QThread>
 21#include <QTimer>
 22
 23#include <algorithm>
 24#include <chrono>
 25#include <cmath>
 26#include <filesystem>
 27#include <fstream>
 28#include <limits>
 29#include <map>
 30#include <spdlog/fmt/ranges.h>
 31#include <string>
 32#include <string_view>
 33#include <system_error>
 34
 35#include "bug.hh"
 36#include "graph_builder.hh"
 37#include "holofile/holofile.hh"
 38#include "holonp/abs.hh"
 39#include "holonp/add.hh"
 40#include "holonp/arange.hh"
 41#include "holonp/argmax.hh"
 42#include "holonp/asarray.hh"
 43#include "holonp/ascontiguousarray.hh"
 44#include "holonp/concatenate.hh"
 45#include "holonp/conj.hh"
 46#include "holonp/copy.hh"
 47#include "holonp/divide.hh"
 48#include "holonp/empty.hh"
 49#include "holonp/equal.hh"
 50#include "holonp/fft.hh"
 51#include "holonp/fft2.hh"
 52#include "holonp/fftshift.hh"
 53#include "holonp/irfft2.hh"
 54#include "holonp/max.hh"
 55#include "holonp/mean.hh"
 56#include "holonp/meshgrid.hh"
 57#include "holonp/min.hh"
 58#include "holonp/multiply.hh"
 59#include "holonp/reshape.hh"
 60#include "holonp/rfft.hh"
 61#include "holonp/rfft2.hh"
 62#include "holonp/slice.hh"
 63#include "holonp/subtract.hh"
 64#include "holonp/transpose.hh"
 65#include "holonp/where.hh"
 66#include "holonp/zeros.hh"
 67#include "holotask/asyncs/batch_queue.hh"
 68#include "holotask/asyncs/dual_reader_batch_queue.hh"
 69#include "holotask/asyncs/slide_avg.hh"
 70#include "holotask/sinks/holofile.hh"
 71#include "holotask/sources/ametek_s710_euresys_coaxlink_octo.hh"
 72#include "holotask/sources/ametek_s711_euresys_coaxlink_qsfp+.hh"
 73#include "holotask/sources/fresnel_qin.hh"
 74#include "holotask/sources/fresnel_qout.hh"
 75#include "holotask/sources/holofile.hh"
 76#include "holotask/syncs/angular_spectrum.hh"
 77#include "holotask/syncs/causal_sliding_average.hh"
 78#include "holotask/syncs/conversion.hh"
 79#include "holotask/syncs/convolution.hh"
 80#include "holotask/syncs/correct_phase.hh"
 81#include "holotask/syncs/cross_correlation2.hh"
 82#include "holotask/syncs/cuda_stream_synchronize.hh"
 83#include "holotask/syncs/filter2d.hh"
 84#include "holotask/syncs/flatfield.hh"
 85#include "holotask/syncs/fresnel_diffraction.hh"
 86#include "holotask/syncs/mean_abs.hh"
 87#include "holotask/syncs/memcpy.hh"
 88#include "holotask/syncs/normalize.hh"
 89#include "holotask/syncs/pca.hh"
 90#include "holotask/syncs/pct_clip.hh"
 91#include "holotask/syncs/registration.hh"
 92#include "holotask/syncs/shack_hartmann_slopes.hh"
 93#include "holotask/syncs/short_time_fresnel_diffraction.hh"
 94#include "holotask/syncs/unfold2d.hh"
 95#include "holotask/syncs/wrap2pi.hh"
 96#include "holotask/syncs/zernike_defocus_z_prop.hh"
 97#include "holotask/syncs/zernike_from_slopes.hh"
 98#include "holotask/syncs/zernike_phase.hh"
 99#include "logger.hh"
 100#include "pipeline/validation.hh"
 101#include "settings_loader.hh"
 102#include "tasks/sinks/display_signal_history.hh"
 103#include "tasks/sinks/display_tensor.hh"
 104#include "tasks/sinks/display_zernike_coefficients.hh"
 105#include "ui/widgets/auto_focus_widget.hh"
 106#include "ui/widgets/zernike_history_widget.hh"
 107
 108using namespace holotask;
 109using namespace holonp;
 110
 111namespace holovibes::pipeline {
 112
 113namespace {
 114
 115template <class F, class... Args>
 0116void reg_sync(holoflow::core::Registry &r, std::string_view name, Args &&...args) {
 0117  r.register_sync(std::string{name}, std::make_unique<F>(std::forward<Args>(args)...));
 0118}
 119
 120template <class F, class... Args>
 0121void reg_async(holoflow::core::Registry &r, std::string_view name, Args &&...args) {
 0122  r.register_async(std::string{name}, std::make_unique<F>(std::forward<Args>(args)...));
 0123}
 124
 125} // namespace
 126
 127Manager::Manager(
 128    ui::AutoFocusWidget *autofocus_widget, ui::TensorDisplayWidget *xy_processed_widget,
 129    ui::TensorDisplayWidget *xz_processed_widget, ui::TensorDisplayWidget *yz_processed_widget,
 130    ui::TensorDisplayWidget *xy_raw_widget, ui::TensorDisplayWidget *raw_spectrum_widget,
 131    ui::TensorDisplayWidget *processed_spectrum_widget,
 132    ui::TensorDisplayWidget *shack_hartmann_widget,
 133    ui::TensorDisplayWidget *shack_hartmann_xcorr_widget,
 134    ui::TensorDisplayWidget *zernike_phase_widget, ui::ZernikeHistoryWidget *zernike_history_widget)
 0135    : autofocus_widget_(autofocus_widget), xy_processed_widget_(xy_processed_widget),
 0136      xz_processed_widget_(xz_processed_widget), yz_processed_widget_(yz_processed_widget),
 0137      xy_raw_widget_(xy_raw_widget), raw_spectrum_widget_(raw_spectrum_widget),
 0138      processed_spectrum_widget_(processed_spectrum_widget),
 0139      shack_hartmann_widget_(shack_hartmann_widget),
 0140      shack_hartmann_xcorr_widget_(shack_hartmann_xcorr_widget),
 0141      zernike_phase_widget_(zernike_phase_widget), zernike_history_widget_(zernike_history_widget) {
 142
 0143  register_components();
 144
 145  // Setup UI metric polling (1 FPS)
 0146  metrics_timer_ = new QTimer(this);
 0147  metrics_timer_->setInterval(1000);
 0148  metrics_timer_->setTimerType(Qt::TimerType::CoarseTimer);
 0149  connect(metrics_timer_, &QTimer::timeout, this, &Manager::poll_metrics);
 150
 151  // Setup Event polling (200 FPS for responsive UI feedback)
 0152  events_timer_ = new QTimer(this);
 0153  events_timer_->setInterval(5);
 0154  events_timer_->setTimerType(Qt::TimerType::CoarseTimer);
 0155  connect(events_timer_, &QTimer::timeout, this, &Manager::poll_events);
 0156}
 157
 0158void Manager::register_components() {
 159  // clang-format off
 0160  reg_async<asyncs::BatchQueueFactory>(registry_, "BatchQueue");
 0161  reg_async<asyncs::DualReaderBatchQueueFactory>(registry_, "DualReaderBatchQueue");
 0162  reg_async<asyncs::SlidingAverageFactory>(registry_, "SlidingAverage");
 0163  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorXY", xy_processed_widget_);
 0164  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorXZ", xz_processed_widget_);
 0165  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorYZ", yz_processed_widget_);
 0166  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorXYRaw", xy_raw_widget_);
 0167  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayRawSpectrum", raw_spectrum_widget_);
 0168  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayProcessedSpectrum", processed_spectrum_widg
 0169  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorShackHartmann", shack_hartmann_widget
 0170  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorShackHartmannXcorr", shack_hartmann_x
 0171  reg_sync<holovibes::tasks::sinks::DisplayTensorFactory>(registry_, "DisplayTensorZernikePhase", zernike_phase_widget_)
 0172  reg_sync<holovibes::tasks::sinks::DisplayZernikeCoefficientsFactory>(registry_, "DisplayZernikeCoefficients", autofocu
 0173  reg_sync<holovibes::tasks::sinks::DisplaySignalHistoryFactory>(registry_, "DisplaySignalHistory", zernike_history_widg
 0174  reg_sync<sinks::HolofileFactory>(registry_, "HolofileWriter");
 0175  reg_sync<sources::HolofileFactory>(registry_, "Holofile");
 0176  reg_sync<sources::AmetekS710EuresysCoaxlinkOctoFactory>(registry_, "AmetekS710EuresysCoaxlinkOcto");
 0177  reg_sync<sources::AmetekS711EuresysCoaxlinkQSFPFactory>(registry_, "AmetekS711EuresysCoaxlinkQSFP+");
 0178  reg_sync<sources::FresnelQinFactory>(registry_, "FresnelQin");
 0179  reg_sync<sources::FresnelQoutFactory>(registry_, "FresnelQout");
 0180  reg_sync<syncs::AngularSpectrumFactory>(registry_, "AngularSpectrum");
 0181  reg_sync<syncs::CudaStreamSynchronizeFactory>(registry_, "CudaStreamSynchronize");
 0182  reg_sync<syncs::CausalSlidingAverageFactory>(registry_, "CausalSlidingAverage");
 0183  reg_sync<syncs::ConversionFactory>(registry_, "Conversion");
 0184  reg_sync<syncs::FresnelDiffractionFactory>(registry_, "FresnelDiffraction");
 0185  reg_sync<syncs::MemcpyFactory>(registry_, "Memcpy");
 0186  reg_sync<syncs::PcaFactory>(registry_, "Pca");
 0187  reg_sync<syncs::PctClipFactory>(registry_, "PctClip");
 0188  reg_sync<syncs::CorrectPhaseFactory>(registry_, "CorrectPhase");
 0189  reg_sync<syncs::ConvolutionFactory>(registry_, "Convolution");
 0190  reg_sync<syncs::FlatfieldFactory>(registry_, "Flatfield");
 0191  reg_sync<syncs::Filter2DFactory>(registry_, "Filter2D");
 0192  reg_sync<syncs::RegistrationFactory>(registry_, "Registration");
 0193  reg_sync<syncs::ShortTimeFresnelDiffractionFactory>(registry_, "ShortTimeFresnelDiffraction");
 0194  reg_sync<syncs::Unfold2DFactory>(registry_, "Unfold2D");
 0195  reg_sync<syncs::Wrap2PiFactory>(registry_, "Wrap2Pi");
 0196  reg_sync<syncs::ShackHartmannSlopesFactory>(registry_, "ShackHartmannSlopes");
 0197  reg_sync<syncs::ZernikeDefocusZPropFactory>(registry_, "ZernikeDefocusZProp");
 0198  reg_sync<syncs::ZernikeFromSlopesFactory>(registry_, "ZernikeFromSlopes");
 0199  reg_sync<syncs::ZernikePhaseFactory>(registry_, "ZernikePhase");
 0200  reg_sync<ArangeFactory>(registry_, "Arange");
 0201  reg_sync<AsArrayFactory>(registry_, "AsArray");
 0202  reg_sync<AsContiguousArrayFactory>(registry_, "AsContiguousArray");
 0203  reg_sync<CopyFactory>(registry_, "Copy");
 0204  reg_sync<EmptyFactory>(registry_, "Empty");
 0205  reg_sync<ZerosFactory>(registry_, "Zeros");
 0206  reg_sync<MeshgridFactory>(registry_, "Meshgrid");
 0207  reg_sync<TransposeFactory>(registry_, "Transpose");
 0208  reg_sync<SliceFactory>(registry_, "Slice");
 0209  reg_sync<FFTFactory>(registry_, "FFT");
 0210  reg_sync<FFT2Factory>(registry_, "FFT2");
 0211  reg_sync<FFTShiftFactory>(registry_, "FFTShiftNp");
 0212  reg_sync<AbsFactory>(registry_, "Abs");
 0213  reg_sync<ConjFactory>(registry_, "Conj");
 0214  reg_sync<MeanFactory>(registry_, "Mean");
 0215  reg_sync<syncs::MeanAbsFactory>(registry_, "MeanAbs");
 0216  reg_sync<MinFactory>(registry_, "Min");
 0217  reg_sync<MaxFactory>(registry_, "Max");
 0218  reg_sync<syncs::NormalizeFactory>(registry_, "Normalize");
 0219  reg_sync<ArgmaxFactory>(registry_, "Argmax");
 0220  reg_sync<ConcatenateFactory>(registry_, "Concatenate");
 0221  reg_sync<RFFTFactory>(registry_, "RFFT");
 0222  reg_sync<RFFT2Factory>(registry_, "RFFT2");
 0223  reg_sync<IRFFT2Factory>(registry_, "IRFFT2");
 0224  reg_sync<syncs::CrossCorrelation2Factory>(registry_, "CrossCorrelation2");
 0225  reg_sync<MultiplyFactory>(registry_, "Multiply");
 0226  reg_sync<SubtractFactory>(registry_, "Subtract");
 0227  reg_sync<DivideFactory>(registry_, "Divide");
 0228  reg_sync<AddFactory>(registry_, "Add");
 0229  reg_sync<EqualFactory>(registry_, "Equal");
 0230  reg_sync<WhereFactory>(registry_, "Where");
 0231  reg_sync<ReshapeFactory>(registry_, "Reshape");
 232  // clang-format on
 0233}
 234
 0235void Manager::configure_zernike_history(bool start_run) {
 0236  const double time_window_seconds = s_.signal_plot_time_window_seconds;
 0237  const auto   indexes             = s_.autofocus_zernike_orders;
 0238  if (!std::isfinite(time_window_seconds) || time_window_seconds <= 0.0) {
 0239    throw std::invalid_argument("signal_plot_time_window_seconds must be positive and finite");
 240  }
 0241  const double sample_time_seconds = s_.signal_plot_sample_time_seconds();
 0242  if (!std::isfinite(sample_time_seconds) || sample_time_seconds <= 0.0) {
 0243    throw std::invalid_argument("derived signal plot sample time must be positive and finite");
 244  }
 245
 0246  auto configure = [widget = zernike_history_widget_, indexes, time_window_seconds, start_run]() {
 247    if (start_run) {
 248      widget->start_run(time_window_seconds, indexes);
 249    } else {
 250      widget->set_series(indexes);
 251      widget->set_time_window_seconds(time_window_seconds);
 252    }
 253  };
 254
 0255  if (QThread::currentThread() == zernike_history_widget_->thread()) {
 0256    configure();
 0257    return;
 258  }
 259
 0260  HOLOVIBES_CHECK(QMetaObject::invokeMethod(zernike_history_widget_, std::move(configure),
 261                                            Qt::BlockingQueuedConnection));
 0262}
 263
 0264void Manager::stop_zernike_history() {
 0265  auto stop = [widget = zernike_history_widget_]() { widget->stop_run(); };
 0266  if (QThread::currentThread() == zernike_history_widget_->thread()) {
 0267    stop();
 0268    return;
 269  }
 270
 0271  HOLOVIBES_CHECK(QMetaObject::invokeMethod(zernike_history_widget_, std::move(stop),
 272                                            Qt::BlockingQueuedConnection));
 0273}
 274
 0275void Manager::resume_zernike_history() {
 0276  auto resume = [widget = zernike_history_widget_]() { widget->resume_run(); };
 0277  if (QThread::currentThread() == zernike_history_widget_->thread()) {
 0278    resume();
 0279    return;
 280  }
 281
 0282  HOLOVIBES_CHECK(QMetaObject::invokeMethod(zernike_history_widget_, std::move(resume),
 283                                            Qt::BlockingQueuedConnection));
 0284}
 285
 0286void Manager::start_pipeline() {
 0287  logger()->info("[Manager::start_pipeline] Starting pipeline...");
 0288  std::lock_guard lock(mtx_);
 289
 0290  if (scheduler_ && scheduler_->is_running()) {
 0291    const QString msg = "Pipeline is already running";
 0292    logger()->error("[Manager::start_pipeline] {}", msg.toStdString());
 0293    emit start_pipeline_failure(msg);
 0294    return;
 295  }
 296
 297  try {
 0298    configure_zernike_history(true);
 0299    build_and_run();
 0300    logger()->info("[Manager::start_pipeline] Pipeline started successfully");
 0301    emit start_pipeline_success();
 0302  } catch (const std::exception &e) {
 0303    stop_zernike_history();
 0304    const QString msg = QString("Failed to start pipeline: %1").arg(e.what());
 0305    logger()->error("[Manager::start_pipeline] {}", msg.toStdString());
 0306    emit start_pipeline_failure(msg);
 0307  }
 0308}
 309
 0310void Manager::stop_pipeline() {
 0311  logger()->info("[Manager::stop_pipeline] Stopping pipeline...");
 0312  std::lock_guard lock(mtx_);
 313
 0314  if (!scheduler_ || !scheduler_->is_running()) {
 0315    const QString msg = "Pipeline is not running";
 0316    logger()->error("[Manager::stop_pipeline] {}", msg.toStdString());
 0317    emit stop_pipeline_failure(msg);
 0318    return;
 319  }
 320
 321  try {
 322    // Request an asynchronous stop and block until graph execution concludes safely.
 0323    scheduler_->request_stop();
 0324    scheduler_->wait();
 0325    stop_zernike_history();
 326
 0327    stop_metrics_updates();
 0328    stop_event_polling();
 0329    raw_recording_active_ = false;
 330
 0331    logger()->info("[Manager::stop_pipeline] Pipeline stopped successfully");
 0332    emit stop_pipeline_success();
 0333  } catch (const std::exception &e) {
 0334    stop_zernike_history();
 0335    const QString msg = QString("Failed to stop pipeline: %1").arg(e.what());
 0336    logger()->error("[Manager::stop_pipeline] {}", msg.toStdString());
 0337    emit stop_pipeline_failure(msg);
 0338  }
 0339}
 340
 0341void Manager::resume_pipeline() {
 0342  logger()->info("[Manager::resume_pipeline] Resuming pipeline...");
 0343  std::lock_guard lock(mtx_);
 344
 0345  if (scheduler_ && scheduler_->is_running()) {
 0346    const QString msg = "Pipeline is already running";
 0347    logger()->error("[Manager::resume_pipeline] {}", msg.toStdString());
 0348    emit resume_pipeline_failure(msg);
 0349    return;
 350  }
 0351  if (!compiler_output_ || settings_dirty_) {
 0352    const QString msg = "No unchanged compiled pipeline is available to resume";
 0353    logger()->error("[Manager::resume_pipeline] {}", msg.toStdString());
 0354    emit resume_pipeline_failure(msg);
 0355    return;
 356  }
 357
 358  try {
 0359    resume_zernike_history();
 0360    run_compiled_graph();
 0361    logger()->info("[Manager::resume_pipeline] Pipeline resumed successfully");
 0362    emit resume_pipeline_success();
 0363  } catch (const std::exception &e) {
 0364    stop_zernike_history();
 0365    const QString msg = QString("Failed to resume pipeline: %1").arg(e.what());
 0366    logger()->error("[Manager::resume_pipeline] {}", msg.toStdString());
 0367    emit resume_pipeline_failure(msg);
 0368  }
 0369}
 370
 0371void Manager::update_pipeline(const Settings &settings) {
 0372  logger()->info("[Manager::update_pipeline] Updating pipeline settings...");
 0373  std::lock_guard lock(mtx_);
 0374  s_              = settings;
 0375  settings_dirty_ = true;
 376
 0377  if (!scheduler_ || !scheduler_->is_running()) {
 378    try {
 0379      configure_zernike_history(false);
 0380      logger()->debug(
 381          "[Manager::update_pipeline] Pipeline is not running, updated parameters only.");
 0382      emit update_pipeline_success();
 0383    } catch (const std::exception &e) {
 0384      const QString msg = QString("Failed to update pipeline: %1").arg(e.what());
 0385      logger()->error("[Manager::update_pipeline] {}", msg.toStdString());
 0386      emit update_pipeline_failure(msg);
 0387    }
 0388    return;
 389  }
 390
 391  try {
 392    // Seamless restart mechanism
 0393    scheduler_->request_stop();
 0394    scheduler_->wait();
 0395    raw_recording_active_ = false;
 396
 0397    configure_zernike_history(true);
 0398    build_and_run();
 399
 0400    logger()->info("[Manager::update_pipeline] Pipeline updated successfully");
 0401    emit update_pipeline_success();
 0402  } catch (const std::exception &e) {
 0403    stop_zernike_history();
 0404    const QString msg = QString("Failed to update pipeline: %1").arg(e.what());
 0405    logger()->error("[Manager::update_pipeline] {}", msg.toStdString());
 0406    emit update_pipeline_failure(msg);
 0407  }
 0408}
 409
 0410void Manager::start_raw_record(std::filesystem::path record_path) {
 0411  std::lock_guard lock(mtx_);
 0412  if (!scheduler_ || !scheduler_->is_running()) {
 0413    emit raw_record_started_failure("Pipeline is not running");
 0414    return;
 415  }
 416
 0417  if (raw_recording_active_) {
 0418    emit raw_record_started_failure("Recording already in progress");
 0419    return;
 420  }
 421
 0422  if (const auto error = validate_recording_path(record_path); error.has_value()) {
 0423    logger()->error("[Manager::start_raw_record] Invalid recording path '{}': {}",
 424                    record_path.string(), *error);
 0425    emit raw_record_started_failure(QString::fromStdString(*error));
 0426    return;
 0427  }
 428
 0429  auto payload = nlohmann::json{
 430      {"type", "start_recording"},
 431      {"record_path", record_path},
 432  };
 433
 434  // Send the recording event to the UI message queue inside the scheduler
 0435  if (!scheduler_->ui_try_send("record", std::move(payload))) {
 0436    logger()->error("[Manager::start_raw_record] Failed to enqueue start_recording event");
 0437    emit raw_record_started_failure("Failed to enqueue start_recording event");
 0438    return;
 439  }
 440
 0441  raw_recording_active_ = true;
 0442  logger()->info("[Manager::start_raw_record] Recording request enqueued to path: {}",
 443                 record_path.string());
 0444  emit raw_record_started_success();
 0445}
 446
 0447void Manager::stop_raw_record() {
 0448  std::lock_guard lock(mtx_);
 0449  if (!scheduler_ || !scheduler_->is_running()) {
 0450    emit raw_record_stopped_failure("Pipeline is not running");
 0451    return;
 452  }
 453
 0454  if (!raw_recording_active_) {
 0455    emit raw_record_stopped_failure("No active recording to stop");
 0456    return;
 457  }
 458
 0459  nlohmann::json payload{{"type", "stop_recording"}};
 0460  if (!scheduler_->ui_try_send("record", std::move(payload))) {
 0461    logger()->error("[Manager::stop_raw_record] Failed to enqueue stop_recording event");
 0462    emit raw_record_stopped_failure("Failed to enqueue stop_recording event");
 0463    return;
 464  }
 465
 0466  raw_recording_active_ = false;
 0467  logger()->info("[Manager::stop_raw_record] Stop request enqueued");
 0468  emit raw_record_stopped_success();
 0469}
 470
 471// --- dump graph logs logic ---
 472
 0473void Manager::update_graph_spec_dump_preferences(const GraphSpecDumpPreferences &prefs) {
 0474  graph_spec_dump_prefs_ = prefs;
 0475}
 476
 0477void Manager::update_graph_compiled_dump_preferences(const GraphCompiledDumpPreferences &prefs) {
 0478  graph_compiled_dump_prefs_ = prefs;
 0479}
 480
 0481void Manager::request_compiled_graph_visualization() {
 0482  if (!compiler_output_) {
 0483    emit graph_visualization_failed(
 484        "No compiled pipeline graph is available. Start the pipeline first.");
 0485    return;
 486  }
 487
 0488  emit graph_visualization_ready(QString::fromStdString(holoflow::runtime::to_dot(
 489      *compiler_output_, graph_compiled_dump_prefs_, "compiled_pipeline")));
 0490}
 491
 492// --- Polling logic ---
 0493void Manager::start_metrics_updates() {
 0494  if (metrics_timer_ && !metrics_timer_->isActive())
 0495    metrics_timer_->start();
 0496}
 0497void Manager::stop_metrics_updates() {
 0498  if (metrics_timer_ && metrics_timer_->isActive())
 0499    metrics_timer_->stop();
 0500  emit metrics_updated(0.0);
 0501}
 502
 0503void Manager::poll_metrics() {
 0504  if (!scheduler_ || !scheduler_->is_running()) {
 0505    emit metrics_updated(0.0);
 0506    return;
 507  }
 508
 0509  auto snapshot = scheduler_->metrics();
 0510  if (snapshot.empty()) {
 0511    emit metrics_updated(0.0);
 0512    return;
 513  }
 514
 515  // Calculate FPS based on source execution rate and batch size
 0516  HOLOVIBES_CHECK(snapshot.contains("source_0"), "Missing 'source' node metrics");
 0517  double input_fps = snapshot.at("source_0").runs_per_second;
 0518  input_fps *= s_.load_batch;
 519
 0520  emit metrics_updated(input_fps);
 0521}
 522
 0523void Manager::start_event_polling() {
 0524  if (events_timer_ && !events_timer_->isActive())
 0525    events_timer_->start();
 0526}
 0527void Manager::stop_event_polling() {
 0528  if (events_timer_ && events_timer_->isActive())
 0529    events_timer_->stop();
 0530}
 531
 0532void Manager::poll_events() {
 0533  if (!scheduler_ || !scheduler_->is_running())
 0534    return;
 535
 0536  while (true) {
 0537    auto event = scheduler_->ui_try_receive();
 0538    if (!event.has_value())
 0539      break;
 540
 0541    logger()->info("[Manager::poll_events] Received event from node '{}': {}", event->node_id,
 542                   event->data.dump());
 543
 0544    std::string type = event->data.value("type", "");
 545
 0546    if (type == "recording_finished") {
 0547      std::string path_str    = event->data.value("path", "");
 0548      bool        should_emit = false;
 549
 550      {
 0551        std::lock_guard lock(mtx_);
 0552        if (raw_recording_active_) {
 0553          raw_recording_active_ = false;
 0554          should_emit           = true;
 555        }
 0556      }
 557
 0558      if (should_emit) {
 0559        logger()->info("[Manager::poll_events] Recording finished successfully at {}", path_str);
 0560        emit raw_record_stopped_success();
 561      }
 0562    } else if (type == "recording_failed") {
 0563      const std::string message =
 564          event->data.value("message", std::string{"Recording failed for an unknown reason"});
 0565      const std::string path_str = event->data.value("path", std::string{});
 0566      bool              should_emit{false};
 567
 568      {
 0569        std::lock_guard lock(mtx_);
 0570        if (raw_recording_active_) {
 0571          raw_recording_active_ = false;
 0572          should_emit           = true;
 573        }
 0574      }
 575
 0576      logger()->error("[Manager::poll_events] Recording failed at '{}': {}", path_str, message);
 577
 0578      if (should_emit) {
 0579        emit raw_record_stopped_failure(QString::fromStdString(message));
 580      }
 0581    }
 0582  }
 0583}
 584
 585// --- Graph Building & Execution ---
 0586void Manager::build_and_run() {
 587  using CompilerConfig = holoflow::runtime::Compiler::Config;
 588  using Compiler       = holoflow::runtime::Compiler;
 589
 0590  build_graph_spec();
 591
 0592  std::filesystem::path log_root;
 0593  const auto app_data_dir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
 0594  if (!app_data_dir.isEmpty()) {
 0595    log_root = std::filesystem::path(app_data_dir.toStdString()) /
 596               QCoreApplication::applicationVersion().toStdString() / "logs";
 597
 0598    std::error_code log_ec;
 0599    std::filesystem::create_directories(log_root, log_ec);
 0600    if (log_ec) {
 0601      logger()->warn("[Manager::build_and_run] Failed to create log directory '{}': {}",
 602                     log_root.string(), log_ec.message());
 0603      log_root.clear();
 604    }
 0605  } else {
 0606    logger()->warn("[Manager::build_and_run] No writable application data directory is available");
 607  }
 608
 609  // TODO: What should be done about this verbose logging?
 610  // if (dump_debug_graphs_) {
 611  //   dump_graph_logs(log_root);
 612  // }
 613
 0614  CompilerConfig config;
 0615  config.log_dir             = log_root;
 0616  config.dump_dot_on_failure = dump_debug_graphs_;
 0617  config.verbose_tracing     = dump_debug_graphs_;
 618
 0619  auto     prev_output = std::move(compiler_output_);
 0620  Compiler compiler(registry_, config);
 0621  compiler_output_ = compiler.compile(spec_, std::move(prev_output));
 622
 0623  if (compiler_output_) {
 624    using namespace std::chrono;
 625
 626    // Write original GraphSpec
 0627    const std::filesystem::path log_dir =
 628        QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation).toStdString() + "/" +
 629        QCoreApplication::applicationVersion().toStdString() + "/logs";
 630
 0631    const auto dot_path = log_dir / "compiled.dot";
 632
 0633    std::ofstream(dot_path) << holoflow::runtime::to_dot(
 634        *compiler_output_, graph_compiled_dump_prefs_, "compiled_pipeline");
 0635  }
 636
 0637  run_compiled_graph();
 0638}
 639
 0640void Manager::run_compiled_graph() {
 641  using Scheduler = holoflow::runtime::Scheduler;
 642
 0643  HOLOVIBES_CHECK(compiler_output_ != nullptr, "Compiled pipeline is not available");
 0644  stop_metrics_updates();
 0645  stop_event_polling();
 646
 647  // A fresh scheduler runtime avoids retaining cancelled queue acquisitions while the compiled
 648  // graph, task objects, source cursor, and allocated resources remain intact across a pause.
 0649  scheduler_.reset();
 650
 0651  auto &graph     = compiler_output_->graph;
 0652  auto &sections  = compiler_output_->sections;
 0653  auto &resources = compiler_output_->resources;
 654
 655  const auto metrics_interval = metrics_timer_
 0656                                    ? std::chrono::milliseconds{metrics_timer_->interval()}
 657                                    : std::chrono::milliseconds{1000};
 658
 0659  scheduler_            = std::make_unique<Scheduler>(graph, sections, resources, metrics_interval);
 0660  raw_recording_active_ = false;
 661
 0662  scheduler_->start();
 0663  start_metrics_updates();
 0664  poll_metrics();
 0665  start_event_polling();
 0666}
 667
 0668void Manager::dump_graph_logs(const std::filesystem::path &log_dir) {
 669  // Write original GraphSpec
 0670  const auto json_path = log_dir / "pipeline.json";
 0671  const auto dot_path  = log_dir / "pipeline.dot";
 672
 0673  std::ofstream(dot_path) << holoflow::core::to_dot(spec_, graph_spec_dump_prefs_);
 0674  std::ofstream(json_path) << holoflow::core::to_json(spec_).dump(2);
 675
 0676  logger()->info("[Manager::dump_graph_logs] Pre-compile pipeline graphs saved to {}",
 677                 log_dir.string());
 0678}
 679
 0680void Manager::build_graph_spec() {
 0681  logger()->info("[Manager::build_graph_spec] Building graph spec...");
 0682  HOLOVIBES_CHECK(settings_dirty_, "Settings are not dirty, no need to rebuild graph spec");
 683
 0684  reset_graph_spec();
 0685  guess_optimizations();
 0686  guess_source_dims();
 687
 0688  GraphBuilder builder{s_, registry_};
 0689  spec_ = builder.build();
 690
 0691  settings_dirty_ = false;
 0692  logger()->debug("[Manager::build_graph_spec] Graph spec built successfully");
 0693  dump_graph_logs(
 694      QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation).toStdString() + "/" +
 695      QCoreApplication::applicationVersion().toStdString() + "/logs");
 0696}
 697
 0698void Manager::reset_graph_spec() { spec_ = holoflow::core::GraphSpec{}; }
 699
 0700void Manager::guess_optimizations() {
 0701  bool load_in_gpu     = (s_.load_method == LoadMethod::LOAD_IN_GPU);
 0702  bool stride_multiple = (s_.time_stride % s_.time_window == 0);
 703
 0704  opti_cpu_stride_ = stride_multiple && !load_in_gpu;
 0705  opti_gpu_stride_ = (stride_multiple && load_in_gpu) || opti_cpu_stride_;
 706
 707  // FIXME : disabled for now due to time stride in raw record
 0708  opti_cpu_stride_ = false;
 0709}
 710
 0711void Manager::guess_source_dims() {
 0712  if (s_.import_source == ImportSource::HOLOFILE) {
 0713    auto header = holofile::Reader(s_.load_path.string()).header();
 0714    src_width_  = header.frame_width;
 0715    src_height_ = header.frame_height;
 0716    return;
 717  }
 718
 719  // Refactored slightly to prevent repetition
 0720  if (s_.import_source == ImportSource::AMETEK_S710_EURESYS_COAXLINK_OCTO ||
 721      s_.import_source == ImportSource::AMETEK_S711_EURESYS_COAXLINK_QSFP) {
 722
 0723    std::ifstream cfg_file(s_.camera_config_path);
 0724    if (!cfg_file.is_open()) {
 0725      throw std::runtime_error(
 726          std::format("Could not open camera config file: {}", s_.camera_config_path.string()));
 727    }
 728
 0729    auto        full_cfg = nlohmann::json::parse(cfg_file);
 0730    std::string key =
 731        (s_.import_source == ImportSource::AMETEK_S710_EURESYS_COAXLINK_OCTO) ? "s710" : "s711";
 732
 0733    auto cfg    = full_cfg.at(key);
 0734    src_width_  = cfg.at("Width").get<int>();
 0735    src_height_ = cfg.at("Height").get<int>();
 0736    return;
 737  }
 738
 0739  HOLOVIBES_UNIMPLEMENTED();
 0740}
 741
 742} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\manager.hh

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QObject>
 18#include <concepts>
 19#include <filesystem>
 20#include <memory>
 21#include <mutex>
 22#include <nlohmann/json.hpp>
 23#include <optional>
 24
 25#include "holoflow/core/graph_spec.hh"
 26#include "holoflow/core/registry.hh"
 27#include "holoflow/runtime/compiler.hh"
 28#include "holoflow/runtime/graph_display.hh"
 29#include "holoflow/runtime/graph_exec.hh"
 30#include "pipeline/settings.hh"
 31
 32namespace holovibes::ui {
 33class AutoFocusWidget;
 34class TensorDisplayWidget;
 35class ZernikeHistoryWidget;
 36} // namespace holovibes::ui
 37
 38class QTimer;
 39
 40namespace holovibes::pipeline {
 41
 42/// @brief Manages the lifecycle, execution, and updating of the Holoflow pipeline.
 43class Manager : public QObject {
 44  Q_OBJECT
 45
 46public:
 47  Manager(ui::AutoFocusWidget *autofocus_widget, ui::TensorDisplayWidget *xy_processed_widget,
 48          ui::TensorDisplayWidget *xz_processed_widget,
 49          ui::TensorDisplayWidget *yz_processed_widget, ui::TensorDisplayWidget *xy_raw_widget,
 50          ui::TensorDisplayWidget  *raw_spectrum_widget,
 51          ui::TensorDisplayWidget  *processed_spectrum_widget,
 52          ui::TensorDisplayWidget  *shack_hartmann_widget,
 53          ui::TensorDisplayWidget  *shack_hartmann_xcorr_widget,
 54          ui::TensorDisplayWidget  *zernike_phase_widget,
 55          ui::ZernikeHistoryWidget *zernike_history_widget);
 56
 057  ~Manager() override = default;
 58
 59  /// @brief Compiles and starts the pipeline graph.
 60  void start_pipeline();
 61
 62  /// @brief Requests a stop and waits for the pipeline to halt cleanly.
 63  void stop_pipeline();
 64
 65  /// @brief Restarts execution from the retained compiled graph and task state.
 66  void resume_pipeline();
 67
 68  /// @brief Updates settings and dynamically rebuilds/restarts the pipeline if currently running.
 69  void update_pipeline(const Settings &settings);
 70
 71  /// @brief Sends an event to the pipeline to start writing raw data to disk.
 72  void start_raw_record(std::filesystem::path record_path);
 73
 74  /// @brief Sends an event to the pipeline to stop writing raw data.
 75  void stop_raw_record();
 76
 77  using GraphSpecDumpPreferences     = holoflow::core::GraphSpecDumpPreferences;
 78  using GraphCompiledDumpPreferences = holoflow::runtime::GraphCompiledDumpPreferences;
 79
 80  /// @brief Returns the current graph spec dump preferences
 081  const GraphSpecDumpPreferences &get_graph_spec_dump_preferences() const {
 082    return graph_spec_dump_prefs_;
 083  }
 84  void update_graph_spec_dump_preferences(const GraphSpecDumpPreferences &prefs);
 85
 086  const GraphCompiledDumpPreferences &get_graph_compiled_dump_preferences() const {
 087    return graph_compiled_dump_prefs_;
 088  }
 89  void update_graph_compiled_dump_preferences(const GraphCompiledDumpPreferences &prefs);
 90
 91  /// @brief Emits Graphviz DOT for the current compiled pipeline graph.
 92  void request_compiled_graph_visualization();
 93
 94signals:
 95  // Lifecycle signals
 96  void start_pipeline_success();
 97  void start_pipeline_failure(const QString &error);
 98  void stop_pipeline_success();
 99  void stop_pipeline_failure(const QString &error);
 100  void resume_pipeline_success();
 101  void resume_pipeline_failure(const QString &error);
 102  void update_pipeline_success();
 103  void update_pipeline_failure(const QString &error);
 104
 105  // Metric signals
 106  void metrics_updated(double input_fps);
 107
 108  // Recording signals
 109  void raw_record_started_success();
 110  void raw_record_started_failure(const QString &error);
 111  void raw_record_stopped_success();
 112  void raw_record_stopped_failure(const QString &error);
 113
 114  // Graph visualization signals
 115  void graph_visualization_ready(const QString &dot);
 116  void graph_visualization_failed(const QString &error);
 117
 118private:
 119  using V = holoflow::core::GraphSpec::vertex_descriptor;
 120
 121  // --- Initialization Helpers ---
 122  void register_components();
 123
 124  // --- Polling Helpers ---
 125  void start_metrics_updates();
 126  void stop_metrics_updates();
 127  void poll_metrics();
 128
 129  void start_event_polling();
 130  void stop_event_polling();
 131  void poll_events();
 132
 133  // --- Graph Management ---
 134  void build_and_run();
 135  void run_compiled_graph();
 136  void build_graph_spec();
 137  void reset_graph_spec();
 138  void guess_optimizations();
 139  void guess_source_dims();
 140  void configure_zernike_history(bool start_run);
 141  void resume_zernike_history();
 142  void stop_zernike_history();
 143
 144  // --- Logging Helpers ---
 145  void dump_graph_logs(const std::filesystem::path &log_dir);
 146
 147  // --- UI Elements ---
 148  ui::AutoFocusWidget      *autofocus_widget_;
 149  ui::TensorDisplayWidget  *xy_processed_widget_;
 150  ui::TensorDisplayWidget  *xz_processed_widget_;
 151  ui::TensorDisplayWidget  *yz_processed_widget_;
 152  ui::TensorDisplayWidget  *xy_raw_widget_;
 153  ui::TensorDisplayWidget  *raw_spectrum_widget_;
 154  ui::TensorDisplayWidget  *processed_spectrum_widget_;
 155  ui::TensorDisplayWidget  *shack_hartmann_widget_;
 156  ui::TensorDisplayWidget  *shack_hartmann_xcorr_widget_;
 157  ui::TensorDisplayWidget  *zernike_phase_widget_;
 158  ui::ZernikeHistoryWidget *zernike_history_widget_;
 159
 160  // --- State & Configuration ---
 161  Settings s_;
 0162  int      src_width_  = 0;
 0163  int      src_height_ = 0;
 164
 0165  GraphSpecDumpPreferences     graph_spec_dump_prefs_     = {};
 0166  GraphCompiledDumpPreferences graph_compiled_dump_prefs_ = {};
 167
 168  /// @brief Toggles debug dumps of the pipeline (.dot, .json) to disk.
 0169  bool dump_debug_graphs_ = true;
 170
 171  /// @brief Mutex to protect state shared between the main UI thread and pipeline callbacks.
 172  std::mutex mtx_;
 0173  bool       settings_dirty_       = false;
 0174  bool       raw_recording_active_ = false;
 175
 176  // Optimizations
 0177  bool opti_cpu_stride_ = false;
 0178  bool opti_gpu_stride_ = false;
 179
 180  // --- Holoflow Core Components ---
 181  holoflow::core::Registry                           registry_;
 182  holoflow::core::GraphSpec                          spec_;
 183  std::unique_ptr<holoflow::runtime::CompilerOutput> compiler_output_;
 184  std::unique_ptr<holoflow::runtime::Scheduler>      scheduler_;
 185
 186  // Timers for regular UI updates
 0187  QTimer *metrics_timer_ = nullptr;
 0188  QTimer *events_timer_  = nullptr;
 189};
 190
 191} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\settings.hh

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <filesystem>
 18#include <optional>
 19#include <variant>
 20
 21namespace holovibes::pipeline {
 22
 23enum class ImportSource {
 24  AMETEK_S710_EURESYS_COAXLINK_OCTO,
 25  AMETEK_S711_EURESYS_COAXLINK_QSFP,
 26  HOLOFILE,
 27};
 28
 29enum class LoadMethod {
 30  READ_LIVE,
 31  LOAD_IN_CPU,
 32  LOAD_IN_GPU,
 33};
 34
 35enum class SpacialMethod {
 36  NONE,
 37  FRESNEL_DIFFRACTION,
 38  ANGULAR_SPECTRUM,
 39};
 40
 41enum class TimeMethod {
 42  NONE,
 43  PRINCIPAL_COMPONENT_ANALYSIS,
 44  RFFT,
 45  FFT,
 46};
 47
 48enum class RecordingMethod {
 49  NONE,
 50  RAW,
 51  PROCESSED,
 52};
 53
 54enum class ViewType {
 55  PROCESSED,
 56  RAW,
 57};
 58
 59enum class MomentType {
 60  M0,
 61  M1,
 62  M2,
 63};
 64
 65struct Settings {
 66  // Advanced
 67  int cpu_in_size;
 68  int gpu_in_size;
 69  int cpu_rec_size;
 70  int cpu_out_size;
 71  int gpu_out_size;
 72  int time_accumulation;
 73
 74  // Import
 75  ImportSource          import_source;
 76  std::filesystem::path load_path;
 77  LoadMethod            load_method;
 78  int                   load_begin;
 79  int                   load_end;
 80  int                   load_batch;
 81  std::optional<int>    load_fps_limit;
 82  double                input_sampling_frequency_hz = 1.0e6 / 27.0;
 83  std::filesystem::path camera_config_path;
 84
 85  // Spacial Propagation
 86  SpacialMethod spacial_method;
 87  float         spacial_lambda;
 88  float         spacial_z;
 89  float         spacial_pixel_size;
 90  bool          asp_padding_enabled = false;
 91  int           asp_padded_width    = 1024;
 92  int           asp_padded_height   = 1024;
 93
 94  // Spatial Filter
 95  bool filter_2d;
 96  int  filter_r_inner;
 97  int  filter_r_outer;
 98  int  filter_smooth_inner;
 99  int  filter_smooth_outer;
 100
 101  // Temporal Filter
 102  TimeMethod time_method;
 103  int        time_window;
 104  int        time_stride;
 105
 106  // Cuts views
 107  int time_x_begin;
 108  int time_x_end;
 109  int time_y_begin;
 110  int time_y_end;
 111  int time_z_begin;
 112  int time_z_end;
 113
 114  // View
 115  bool       view_3d_cuts;
 116  bool       raw_view;
 117  ViewType   view_type;
 118  MomentType moment_type;
 119  bool       view_raw_spectrum;
 120  bool       view_processed_spectrum;
 121  bool       view_zernike_metrics;
 122  bool       view_zernike_phase;
 123  bool       view_shack_hartmann;
 124  bool       view_shack_hartmann_xcorr;
 125
 126  // Post-processing
 127  int         pp_fps;
 128  bool        pp_fft_shift;
 129  bool        pp_flatfield;
 130  float       pp_flatfield_cutoff_period_m;
 131  int         pp_accumulation;
 132  bool        pp_convolution;
 133  std::string pp_convolution_path;
 134  bool        pp_convolution_divide;
 135  bool        pp_pctclip;
 136  float       pp_pctclip_lower;
 137  float       pp_pctclip_upper;
 138  float       pp_pctclip_radius;
 139  bool        pp_registration;
 140  float       pp_registration_radius;
 141
 142  // Recording
 143  RecordingMethod       recording_method;
 144  std::filesystem::path recording_path;
 145  int                   recording_count;
 146
 147  // Auto-focus
 148  bool             autofocus_enabled;
 149  int              autofocus_nb_subaps;
 150  int              autofocus_nb_iter;
 151  std::vector<int> autofocus_zernike_orders;
 152  bool             autofocus_skip_subapertures_outside_pupil = true;
 153  bool             autofocus_use_graph_laplacian             = false;
 154
 155  // Zernike coefficient history.
 156  double signal_plot_time_window_seconds = 8.0;
 157
 1158  [[nodiscard]] double signal_plot_sample_time_seconds() const {
 1159    return static_cast<double>(time_stride) / input_sampling_frequency_hz;
 1160  }
 161};
 162
 163} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\pipeline\validation.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "pipeline/validation.hh"
 16
 17#include <cerrno>
 18#include <cmath>
 19#include <cstdio>
 20#include <filesystem>
 21#include <format>
 22#include <system_error>
 23
 24namespace holovibes::pipeline {
 25
 26namespace {
 27
 28void add_issue(ValidationResult &result, ValidationSeverity severity, std::string code,
 129               std::string message, std::vector<SettingsField> fields) {
 130  result.issues.push_back(ValidationIssue{
 31      .severity = severity,
 32      .code     = std::move(code),
 33      .message  = std::move(message),
 34      .fields   = std::move(fields),
 35  });
 136}
 37
 138bool requires_processed_pipeline(const Settings &settings) {
 139  return settings.view_type != ViewType::RAW;
 140}
 41
 042int recording_batch_size(const Settings &settings) {
 043  return settings.recording_method == RecordingMethod::RAW ? settings.load_batch
 44                                                           : settings.cpu_out_size;
 045}
 46
 147int max_time_z_end(const Settings &settings) {
 148  if (settings.time_method == TimeMethod::RFFT) {
 149    return settings.time_window / 2 + 1;
 50  }
 51
 052  return settings.time_window;
 153}
 54
 55} // namespace
 56
 057bool ValidationResult::ok() const { return !has_errors(); }
 58
 059bool ValidationResult::has_errors() const {
 060  for (const auto &issue : issues) {
 061    if (issue.severity == ValidationSeverity::Error) {
 062      return true;
 63    }
 064  }
 65
 066  return false;
 067}
 68
 069std::vector<const ValidationIssue *> ValidationResult::issues_for(SettingsField field) const {
 070  std::vector<const ValidationIssue *> matches;
 71
 072  for (const auto &issue : issues) {
 073    for (const auto issue_field : issue.fields) {
 074      if (issue_field == field) {
 075        matches.push_back(&issue);
 076        break;
 77      }
 078    }
 079  }
 80
 081  return matches;
 082}
 83
 084std::optional<std::string> validate_recording_path(const std::filesystem::path &record_path) {
 085  if (record_path.empty()) {
 086    return std::string{"Recording path is empty."};
 87  }
 88
 089  std::error_code       ec;
 090  std::filesystem::path directory = record_path.parent_path();
 091  const std::string     path_str  = record_path.string();
 92
 093  if (directory.empty()) {
 094    directory = std::filesystem::current_path(ec);
 095    if (ec) {
 096      return std::format("Failed to resolve the current recording directory: {}.", ec.message());
 97    }
 98  }
 99
 0100  if (!std::filesystem::exists(directory, ec)) {
 0101    if (ec) {
 0102      return std::format("Failed to inspect recording directory \"{}\": {}.", directory.string(),
 103                         ec.message());
 104    }
 105
 0106    return std::format("Recording directory does not exist: {}.", directory.string());
 107  }
 108
 0109  if (!std::filesystem::is_directory(directory, ec)) {
 0110    if (ec) {
 0111      return std::format("Failed to inspect recording directory \"{}\": {}.", directory.string(),
 112                         ec.message());
 113    }
 114
 0115    return std::format("Recording destination is not a directory: {}.", directory.string());
 116  }
 117
 0118  if (record_path.filename().empty()) {
 0119    return std::string{"Recording path must include a file name."};
 120  }
 121
 0122  FILE *fp = nullptr;
 0123  if (fopen_s(&fp, path_str.c_str(), "wb") != 0 || !fp) {
 0124    std::error_code open_ec(errno, std::generic_category());
 0125    return std::format("Cannot write recording file \"{}\": {}.", record_path.string(),
 126                       open_ec.message());
 127  }
 128
 0129  std::fclose(fp);
 130
 0131  if (std::remove(path_str.c_str()) != 0) {
 0132    std::error_code remove_ec(errno, std::generic_category());
 0133    return std::format("Recording file probe succeeded, but cleanup failed for \"{}\": {}.",
 134                       record_path.string(), remove_ec.message());
 135  }
 136
 0137  return std::nullopt;
 0138}
 139
 1140ValidationResult validate_settings(const Settings &settings, const ValidationContext &context) {
 1141  ValidationResult result;
 142
 1143  if (settings.import_source == ImportSource::HOLOFILE) {
 0144    if (settings.load_path.empty()) {
 0145      add_issue(result, ValidationSeverity::Error, "load_path_empty",
 146                "An input holofile must be selected.", {SettingsField::LoadPath});
 0147    } else if (!context.load_path_exists) {
 0148      add_issue(result, ValidationSeverity::Error, "load_path_missing",
 149                std::format("Input holofile does not exist: {}", settings.load_path.string()),
 150                {SettingsField::LoadPath});
 151    }
 152
 0153    if (settings.load_end <= settings.load_begin) {
 0154      add_issue(result, ValidationSeverity::Error, "load_range_invalid",
 155                "End frame must be strictly greater than start frame.",
 156                {SettingsField::LoadBegin, SettingsField::LoadEnd});
 157    }
 158
 0159    if (context.source_frame_count.has_value() && settings.load_end > *context.source_frame_count) {
 0160      add_issue(result, ValidationSeverity::Error, "load_end_out_of_bounds",
 161                std::format("End frame ({}) exceeds the number of frames in the holofile ({}).",
 162                            settings.load_end, *context.source_frame_count),
 163                {SettingsField::LoadEnd});
 164    }
 0165  } else {
 1166    if (settings.camera_config_path.empty()) {
 1167      add_issue(result, ValidationSeverity::Error, "camera_config_empty",
 168                "A camera configuration file must be selected.", {SettingsField::CameraConfigPath});
 0169    } else if (!context.camera_config_exists) {
 0170      add_issue(result, ValidationSeverity::Error, "camera_config_missing",
 171                std::format("Camera configuration file does not exist: {}",
 172                            settings.camera_config_path.string()),
 173                {SettingsField::CameraConfigPath});
 0174    } else if (!context.camera_config_valid) {
 0175      add_issue(result, ValidationSeverity::Error, "camera_config_invalid",
 176                "The selected camera configuration file is invalid or missing required fields.",
 177                {SettingsField::CameraConfigPath});
 178    }
 179  }
 180
 1181  if (settings.load_batch <= 0) {
 1182    add_issue(result, ValidationSeverity::Error, "load_batch_non_positive",
 183              "Input batch size must be strictly positive.", {SettingsField::LoadBatch});
 184  }
 185
 1186  if (!std::isfinite(settings.input_sampling_frequency_hz) ||
 187      settings.input_sampling_frequency_hz <= 0.0) {
 1188    add_issue(result, ValidationSeverity::Error, "input_sampling_frequency_non_positive",
 189              "Input sampling frequency must be strictly positive and finite.",
 190              {SettingsField::InputSamplingFrequency});
 191  }
 192
 1193  if (settings.time_window <= 0) {
 1194    add_issue(result, ValidationSeverity::Error, "time_window_non_positive",
 195              "Time window must be strictly positive.", {SettingsField::TimeWindow});
 196  }
 197
 1198  if (settings.time_stride <= 0) {
 1199    add_issue(result, ValidationSeverity::Error, "time_stride_non_positive",
 200              "Time stride must be strictly positive.", {SettingsField::TimeStride});
 201  }
 202
 1203  if (!std::isfinite(settings.signal_plot_time_window_seconds) ||
 204      settings.signal_plot_time_window_seconds <= 0.0) {
 0205    add_issue(result, ValidationSeverity::Error, "signal_plot_time_window_non_positive",
 206              "Signal plot time window must be strictly positive and finite.",
 207              {SettingsField::SignalPlotTimeWindow});
 208  }
 209
 1210  if (settings.time_window > 0 && settings.time_stride > 0 &&
 211      (settings.time_stride % settings.time_window != 0)) {
 0212    add_issue(result, ValidationSeverity::Error, "time_stride_not_multiple",
 213              "Time stride must be a multiple of the time window.",
 214              {SettingsField::TimeWindow, SettingsField::TimeStride});
 215  }
 216
 1217  if (settings.filter_2d) {
 0218    if (settings.filter_r_inner < 0) {
 0219      add_issue(result, ValidationSeverity::Error, "filter_2d_inner_negative",
 220                "Filter 2D inner radius must be non-negative.",
 221                {SettingsField::Filter2DInnerRadius});
 222    }
 223
 0224    if (settings.filter_r_outer < 0) {
 0225      add_issue(result, ValidationSeverity::Error, "filter_2d_outer_negative",
 226                "Filter 2D outer radius must be non-negative.",
 227                {SettingsField::Filter2DOuterRadius});
 228    }
 229
 0230    if (settings.filter_r_inner >= 0 && settings.filter_r_outer >= 0 &&
 231        settings.filter_r_outer < settings.filter_r_inner) {
 0232      add_issue(result, ValidationSeverity::Error, "filter_2d_radius_order",
 233                "Filter 2D outer radius must be greater than or equal to the inner radius.",
 234                {SettingsField::Filter2DInnerRadius, SettingsField::Filter2DOuterRadius});
 235    }
 236  }
 237
 1238  if (settings.import_source == ImportSource::HOLOFILE && settings.load_end > settings.load_begin &&
 239      settings.time_stride > (settings.load_end - settings.load_begin)) {
 0240    add_issue(result, ValidationSeverity::Error, "time_stride_exceeds_range",
 241              "Time stride cannot exceed the selected input frame range.",
 242              {SettingsField::LoadBegin, SettingsField::LoadEnd, SettingsField::TimeStride});
 243  }
 244
 1245  if (requires_processed_pipeline(settings)) {
 1246    if (settings.pp_accumulation <= 0) {
 1247      add_issue(result, ValidationSeverity::Error, "pp_accumulation_non_positive",
 248                "Accumulation must be strictly positive.", {SettingsField::PpAccumulation});
 249    }
 250
 1251    if (settings.time_method == TimeMethod::NONE) {
 1252      add_issue(result, ValidationSeverity::Error, "time_method_unsupported",
 253                "A time transform must be selected for processed view mode.",
 254                {SettingsField::TimeMethod});
 255    }
 256
 1257    const bool supported_spacial_method =
 258        settings.spacial_method == SpacialMethod::FRESNEL_DIFFRACTION ||
 259        settings.spacial_method == SpacialMethod::ANGULAR_SPECTRUM;
 1260    if (!supported_spacial_method) {
 1261      add_issue(result, ValidationSeverity::Error, "spacial_method_unsupported",
 262                "Fresnel Diffraction or Angular Spectrum must be selected for processed view mode.",
 263                {SettingsField::SpacialMethod});
 264    }
 265
 1266    if (settings.autofocus_enabled && settings.spacial_method == SpacialMethod::ANGULAR_SPECTRUM) {
 0267      add_issue(result, ValidationSeverity::Error, "spacial_method_autofocus_unsupported",
 268                "Angular Spectrum is not supported when Auto Focus is enabled.",
 269                {SettingsField::SpacialMethod});
 270    }
 271
 1272    if (settings.spacial_method == SpacialMethod::ANGULAR_SPECTRUM &&
 273        settings.asp_padding_enabled) {
 1274      if (settings.asp_padded_width <= 0 || settings.asp_padded_height <= 0) {
 0275        add_issue(result, ValidationSeverity::Error, "asp_padding_non_positive",
 276                  "Angular Spectrum padded width and height must be strictly positive.",
 277                  {SettingsField::AspPadding});
 278      }
 279
 1280      if (context.source_width.has_value() && context.source_height.has_value()) {
 1281        const int width_padding  = settings.asp_padded_width - *context.source_width;
 1282        const int height_padding = settings.asp_padded_height - *context.source_height;
 1283        if (width_padding < 0 || height_padding < 0) {
 1284          add_issue(result, ValidationSeverity::Error, "asp_padding_smaller_than_source",
 285                    "Angular Spectrum padded resolution must be at least the source resolution.",
 286                    {SettingsField::AspPadding});
 1287        } else if (width_padding % 2 != 0 || height_padding % 2 != 0) {
 1288          add_issue(result, ValidationSeverity::Error, "asp_padding_not_even",
 289                    "Angular Spectrum must add an even number of pixels on each axis.",
 290                    {SettingsField::AspPadding});
 291        }
 292      }
 293    }
 294
 1295    if (settings.view_3d_cuts) {
 0296      add_issue(result, ValidationSeverity::Error, "view_3d_cuts_unsupported",
 297                "3D cuts are not currently supported by the pipeline.",
 298                {SettingsField::View3DCuts});
 299    }
 300
 1301    if (settings.pp_registration) {
 0302      add_issue(result, ValidationSeverity::Error, "pp_registration_unsupported",
 303                "Registration is not currently supported by the pipeline.",
 304                {SettingsField::PpRegistration});
 305    }
 306
 1307    if (settings.pp_flatfield && settings.pp_flatfield_cutoff_period_m <= 0.0f) {
 0308      add_issue(result, ValidationSeverity::Error, "pp_flatfield_cutoff_period_non_positive",
 309                "Flatfield cutoff period must be strictly positive.",
 310                {SettingsField::PpFlatfieldCutoffPeriod});
 311    }
 312
 1313    if (settings.pp_convolution) {
 0314      add_issue(result, ValidationSeverity::Error, "pp_convolution_unsupported",
 315                "Convolution is not currently supported by the pipeline.",
 316                {SettingsField::PpConvolution});
 317    }
 318  }
 319
 1320  if (requires_processed_pipeline(settings) && settings.time_method != TimeMethod::NONE) {
 1321    if (settings.time_z_end <= settings.time_z_begin) {
 0322      add_issue(result, ValidationSeverity::Error, "time_z_range_invalid",
 323                "Z range width must be strictly positive.",
 324                {SettingsField::TimeZBegin, SettingsField::TimeZEnd});
 1325    } else if (settings.time_z_end > max_time_z_end(settings)) {
 0326      add_issue(result, ValidationSeverity::Error, "time_z_range_out_of_bounds",
 327                std::format("Z range end ({}) exceeds the maximum supported value ({}).",
 328                            settings.time_z_end, max_time_z_end(settings)),
 329                {SettingsField::TimeWindow, SettingsField::TimeZBegin, SettingsField::TimeZEnd});
 330    }
 331  }
 332
 1333  if (settings.recording_method != RecordingMethod::NONE) {
 0334    if (settings.recording_path.empty()) {
 0335      add_issue(result, ValidationSeverity::Error, "recording_path_empty",
 336                "A recording output path must be selected.", {SettingsField::RecordingPath});
 0337    } else if (context.recording_path_error.has_value()) {
 0338      add_issue(result, ValidationSeverity::Error, "recording_path_invalid",
 339                *context.recording_path_error, {SettingsField::RecordingPath});
 340    }
 341
 0342    if (settings.recording_count <= 0) {
 0343      add_issue(result, ValidationSeverity::Error, "recording_count_non_positive",
 344                "Recording frame count must be strictly positive.",
 345                {SettingsField::RecordingCount});
 0346    } else {
 0347      const int batch_size = recording_batch_size(settings);
 0348      if (batch_size > 0 && (settings.recording_count % batch_size != 0)) {
 0349        add_issue(
 350            result, ValidationSeverity::Error, "recording_count_batch_mismatch",
 351            std::format(
 352                "Recording frame count ({}) must be divisible by the pipeline batch size ({}).",
 353                settings.recording_count, batch_size),
 354            {SettingsField::RecordingCount});
 355      }
 356    }
 357  }
 358
 1359  if (settings.autofocus_enabled) {
 1360    if (settings.autofocus_nb_subaps <= 0) {
 0361      add_issue(result, ValidationSeverity::Error, "autofocus_nb_subaps_non_positive",
 362                "Auto-focus sub-aperture count must be strictly positive.",
 363                {SettingsField::AutofocusNbSubaps});
 1364    } else if ((settings.autofocus_nb_subaps % 2) == 0) {
 0365      add_issue(result, ValidationSeverity::Error, "autofocus_nb_subaps_even",
 366                "Auto-focus sub-aperture count must be odd.", {SettingsField::AutofocusNbSubaps});
 367    }
 368
 1369    if (context.source_width.has_value() && context.source_height.has_value() &&
 370        (settings.autofocus_nb_subaps > *context.source_width ||
 371         settings.autofocus_nb_subaps > *context.source_height)) {
 0372      add_issue(
 373          result, ValidationSeverity::Error, "autofocus_nb_subaps_too_large",
 374          std::format("Auto-focus sub-aperture count ({}) exceeds the source dimensions ({}x{}).",
 375                      settings.autofocus_nb_subaps, *context.source_width, *context.source_height),
 376          {SettingsField::AutofocusNbSubaps});
 377    }
 378
 1379    if (settings.autofocus_nb_iter <= 0) {
 0380      add_issue(result, ValidationSeverity::Error, "autofocus_nb_iter_non_positive",
 381                "Auto-focus iteration count must be strictly positive.",
 382                {SettingsField::AutofocusNbIter});
 1383    } else if (settings.pp_accumulation > 1 && settings.autofocus_nb_iter > 1) {
 1384      add_issue(result, ValidationSeverity::Error, "autofocus_sliding_average_multiple_iterations",
 385                "Sliding Shack-Hartmann correction supports one auto-focus iteration.",
 386                {SettingsField::PpAccumulation, SettingsField::AutofocusNbIter});
 387    }
 388  }
 389
 1390  return result;
 1391}
 392
 393} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\settings_loader.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "settings_loader.hh"
 16
 17namespace holovibes::pipeline {
 18namespace {
 19
 20using json = nlohmann::json;
 21
 122[[nodiscard]] const json &child_or_empty(const json &j, const char *key) {
 123  static const json kEmpty = json::object();
 124  if (!j.is_object() || !j.contains(key) || !j.at(key).is_object()) {
 125    return kEmpty;
 26  }
 127  return j.at(key);
 128}
 29
 130[[nodiscard]] int read_range_end(const json &node, int begin, int default_end) {
 131  constexpr int kMissingWidth = -1;
 132  const int     width         = val(node, "width", kMissingWidth);
 133  return width == kMissingWidth ? default_end : begin + width;
 134}
 35
 136[[nodiscard]] std::string to_legacy_space_transform(SpacialMethod method) {
 137  switch (method) {
 38  case SpacialMethod::FRESNEL_DIFFRACTION:
 039    return "FRESNELTR";
 40  case SpacialMethod::ANGULAR_SPECTRUM:
 041    return "ANGULAR";
 42  case SpacialMethod::NONE:
 43  default:
 144    return "NONE";
 45  }
 146}
 47
 148[[nodiscard]] SpacialMethod from_legacy_space_transform(const std::string &value) {
 149  if (value == "FRESNELTR") {
 050    return SpacialMethod::FRESNEL_DIFFRACTION;
 51  }
 152  if (value == "ANGULAR") {
 053    return SpacialMethod::ANGULAR_SPECTRUM;
 54  }
 155  return SpacialMethod::NONE;
 156}
 57
 158[[nodiscard]] std::string to_legacy_time_transform(TimeMethod method) {
 159  switch (method) {
 60  case TimeMethod::PRINCIPAL_COMPONENT_ANALYSIS:
 061    return "PCA";
 62  case TimeMethod::RFFT:
 063    return "RFFT";
 64  case TimeMethod::FFT:
 065    return "FFT";
 66  case TimeMethod::NONE:
 67  default:
 168    return "NONE";
 69  }
 170}
 71
 172[[nodiscard]] TimeMethod from_legacy_time_transform(const std::string &value) {
 173  if (value == "PCA") {
 074    return TimeMethod::PRINCIPAL_COMPONENT_ANALYSIS;
 75  }
 176  if (value == "RFFT" || value == "STFT") { // "STFT" kept for backward compatibility
 077    return TimeMethod::RFFT;
 78  }
 179  if (value == "FFT") {
 080    return TimeMethod::FFT;
 81  }
 182  return TimeMethod::NONE;
 183}
 84
 185[[nodiscard]] std::string autofocus_slope_mode_name(bool use_graph_laplacian) {
 186  return use_graph_laplacian ? "full_pairwise" : "single_reference";
 187}
 88
 189[[nodiscard]] bool use_graph_laplacian(const std::string &value, bool default_value) {
 190  if (value == "full_pairwise") {
 191    return true;
 92  }
 193  if (value == "single_reference") {
 194    return false;
 95  }
 196  return default_value;
 197}
 98
 199void write_advanced(json &j, const Settings &s) {
 1100  auto &advanced = j["compute_settings"]["advanced"];
 101
 1102  advanced["buffer_size"]["input"]  = s.gpu_in_size;
 1103  advanced["buffer_size"]["output"] = s.gpu_out_size;
 1104  advanced["buffer_size"]["record"] = s.cpu_rec_size;
 105
 1106  advanced["filter2d_smooth"]["low"]  = s.filter_smooth_inner;
 1107  advanced["filter2d_smooth"]["high"] = s.filter_smooth_outer;
 108
 1109  advanced["nb_frames_to_record"] = s.recording_count;
 110
 1111  if (s.load_fps_limit.has_value()) {
 0112    advanced["input_fps_limit"] = *s.load_fps_limit;
 113  }
 1114  advanced["input_sampling_frequency_hz"] = s.input_sampling_frequency_hz;
 1115}
 116
 1117void write_image_rendering(json &j, const Settings &s) {
 1118  auto &rendering = j["compute_settings"]["image_rendering"];
 119
 1120  rendering["batch_size"] = s.load_batch;
 121
 1122  rendering["convolution"]["divide"] = s.pp_convolution_divide;
 1123  rendering["convolution"]["type"]   = s.pp_convolution ? s.pp_convolution_path : "";
 124
 1125  rendering["filter2d"]["enabled"]      = s.filter_2d;
 1126  rendering["filter2d"]["inner_radius"] = s.filter_r_inner;
 1127  rendering["filter2d"]["outer_radius"] = s.filter_r_outer;
 128
 1129  rendering["autofocus"]["enabled"]   = s.autofocus_enabled;
 1130  rendering["autofocus"]["nb_subaps"] = s.autofocus_nb_subaps;
 1131  rendering["autofocus"]["nb_iter"]   = s.autofocus_nb_iter;
 1132  rendering["autofocus"]["skip_subapertures_outside_pupil"] =
 133      s.autofocus_skip_subapertures_outside_pupil;
 1134  rendering["autofocus"]["slope_mode"] = autofocus_slope_mode_name(s.autofocus_use_graph_laplacian);
 1135  rendering["autofocus"]["a4_history"]["time_window_seconds"] = s.signal_plot_time_window_seconds;
 136
 137  // Legacy format expects this field even if space transformation is NONE.
 1138  rendering["image_mode"] = "HOLOGRAM";
 139
 1140  rendering["lambda"]               = s.spacial_lambda;
 1141  rendering["propagation_distance"] = s.spacial_z;
 142
 1143  rendering["space_transformation"] = to_legacy_space_transform(s.spacial_method);
 1144  rendering["angular_spectrum"]["padding"]["enabled"] = s.asp_padding_enabled;
 1145  rendering["angular_spectrum"]["padding"]["width"]   = s.asp_padded_width;
 1146  rendering["angular_spectrum"]["padding"]["height"]  = s.asp_padded_height;
 1147  rendering["time_transformation"]        = to_legacy_time_transform(s.time_method);
 1148  rendering["time_transformation_size"]   = s.time_window;
 1149  rendering["time_transformation_stride"] = s.time_stride;
 1150}
 151
 1152void write_view(json &j, const Settings &s) {
 1153  auto &view = j["compute_settings"]["view"];
 154
 1155  view["fft_shift"]                    = s.pp_fft_shift;
 1156  view["flatfield"]["enabled"]         = s.pp_flatfield;
 1157  view["flatfield"]["cutoff_period_m"] = s.pp_flatfield_cutoff_period_m;
 158
 1159  view["registration"]["registration_enabled"] = s.pp_registration;
 1160  view["registration"]["registration_zone"]    = s.pp_registration_radius;
 161
 1162  view["window"]["filter2d"]["contrast"]["enabled"] = s.pp_pctclip;
 163
 1164  view["window"]["xy"]["contrast"]["enabled"]       = s.pp_pctclip;
 1165  view["window"]["xy"]["contrast"]["min"]           = s.pp_pctclip_lower;
 1166  view["window"]["xy"]["contrast"]["max"]           = s.pp_pctclip_upper;
 1167  view["window"]["xy"]["output_image_accumulation"] = s.pp_accumulation;
 168
 1169  view["window"]["xz"]["enabled"] = s.view_3d_cuts;
 1170  view["window"]["yz"]["enabled"] = s.view_3d_cuts;
 171
 1172  view["x"]["start"] = s.time_x_begin;
 1173  view["x"]["width"] = s.time_x_end - s.time_x_begin;
 174
 1175  view["y"]["start"] = s.time_y_begin;
 1176  view["y"]["width"] = s.time_y_end - s.time_y_begin;
 177
 1178  view["z"]["start"] = s.time_z_begin;
 1179  view["z"]["width"] = s.time_z_end - s.time_z_begin;
 1180}
 181
 1182void write_info(json &j, const Settings &s) {
 1183  auto &info = j["info"];
 184
 1185  info["contiguous"]       = s.cpu_rec_size;
 1186  info["pixel_pitch"]["x"] = s.spacial_pixel_size;
 1187  info["pixel_pitch"]["y"] = s.spacial_pixel_size;
 1188}
 189
 1190void read_advanced(Settings &s, const json &advanced) {
 1191  const auto &buffer_size = child_or_empty(advanced, "buffer_size");
 1192  const auto &smooth      = child_or_empty(advanced, "filter2d_smooth");
 193
 1194  s.gpu_in_size  = val(buffer_size, "input", s.gpu_in_size);
 1195  s.gpu_out_size = val(buffer_size, "output", s.gpu_out_size);
 1196  s.cpu_rec_size = val(buffer_size, "record", s.cpu_rec_size);
 197
 1198  s.filter_smooth_inner = val(smooth, "low", s.filter_smooth_inner);
 1199  s.filter_smooth_outer = val(smooth, "high", s.filter_smooth_outer);
 200
 1201  s.recording_count = val(advanced, "nb_frames_to_record", s.recording_count);
 202
 1203  const int input_fps_limit = val(advanced, "input_fps_limit", 0);
 1204  s.load_fps_limit = input_fps_limit > 0 ? std::optional<int>{input_fps_limit} : std::nullopt;
 1205  s.input_sampling_frequency_hz =
 206      val(advanced, "input_sampling_frequency_hz", s.input_sampling_frequency_hz);
 1207}
 208
 1209void read_image_rendering(Settings &s, const json &rendering) {
 1210  s.load_batch = val(rendering, "batch_size", s.load_batch);
 211
 1212  s.spacial_method = from_legacy_space_transform(val(rendering, "space_transformation", "NONE"));
 1213  s.spacial_lambda = val(rendering, "lambda", s.spacial_lambda);
 1214  s.spacial_z      = val(rendering, "propagation_distance", s.spacial_z);
 215
 1216  const auto &angular_spectrum = child_or_empty(rendering, "angular_spectrum");
 1217  const auto &padding          = child_or_empty(angular_spectrum, "padding");
 1218  s.asp_padding_enabled        = val(padding, "enabled", s.asp_padding_enabled);
 1219  s.asp_padded_width           = val(padding, "width", s.asp_padded_width);
 1220  s.asp_padded_height          = val(padding, "height", s.asp_padded_height);
 221
 1222  s.time_method = from_legacy_time_transform(val(rendering, "time_transformation", "NONE"));
 1223  s.time_window = val(rendering, "time_transformation_size", s.time_window);
 1224  s.time_stride = val(rendering, "time_transformation_stride", s.time_stride);
 225
 1226  const auto &filter2d = child_or_empty(rendering, "filter2d");
 1227  s.filter_2d          = val(filter2d, "enabled", s.filter_2d);
 1228  s.filter_r_inner     = val(filter2d, "inner_radius", s.filter_r_inner);
 1229  s.filter_r_outer     = val(filter2d, "outer_radius", s.filter_r_outer);
 230
 1231  const auto &autofocus                       = child_or_empty(rendering, "autofocus");
 1232  s.autofocus_enabled                         = val(autofocus, "enabled", s.autofocus_enabled);
 1233  s.autofocus_nb_subaps                       = val(autofocus, "nb_subaps", s.autofocus_nb_subaps);
 1234  s.autofocus_nb_iter                         = val(autofocus, "nb_iter", s.autofocus_nb_iter);
 1235  s.autofocus_skip_subapertures_outside_pupil = val(autofocus, "skip_subapertures_outside_pupil",
 236                                                    s.autofocus_skip_subapertures_outside_pupil);
 1237  s.autofocus_use_graph_laplacian             = use_graph_laplacian(
 238      val(autofocus, "slope_mode", autofocus_slope_mode_name(s.autofocus_use_graph_laplacian)),
 239      s.autofocus_use_graph_laplacian);
 1240  const auto &a4_history = child_or_empty(autofocus, "a4_history");
 1241  s.signal_plot_time_window_seconds =
 242      val(a4_history, "time_window_seconds", s.signal_plot_time_window_seconds);
 243
 1244  const auto &convolution = child_or_empty(rendering, "convolution");
 1245  s.pp_convolution_path   = val(convolution, "type", s.pp_convolution_path);
 1246  s.pp_convolution        = !s.pp_convolution_path.empty();
 1247  s.pp_convolution_divide = val(convolution, "divide", s.pp_convolution_divide);
 1248}
 249
 1250void read_view_ranges(Settings &s, const json &view) {
 1251  const auto &x = child_or_empty(view, "x");
 1252  const auto &y = child_or_empty(view, "y");
 1253  const auto &z = child_or_empty(view, "z");
 254
 1255  s.time_x_begin = val(x, "start", s.time_x_begin);
 1256  s.time_y_begin = val(y, "start", s.time_y_begin);
 1257  s.time_z_begin = val(z, "start", s.time_z_begin);
 258
 1259  s.time_x_end = read_range_end(x, s.time_x_begin, s.time_x_end);
 1260  s.time_y_end = read_range_end(y, s.time_y_begin, s.time_y_end);
 1261  s.time_z_end = read_range_end(z, s.time_z_begin, s.time_z_end);
 1262}
 263
 1264void read_view(Settings &s, const json &view) {
 1265  s.pp_fft_shift        = val(view, "fft_shift", s.pp_fft_shift);
 1266  const auto &flatfield = child_or_empty(view, "flatfield");
 1267  s.pp_flatfield        = val(flatfield, "enabled", s.pp_flatfield);
 1268  s.pp_flatfield_cutoff_period_m =
 269      val(flatfield, "cutoff_period_m", s.pp_flatfield_cutoff_period_m);
 270
 1271  const auto &window = child_or_empty(view, "window");
 1272  const auto &xy     = child_or_empty(window, "xy");
 1273  const auto &xz     = child_or_empty(window, "xz");
 1274  const auto &yz     = child_or_empty(window, "yz");
 275
 1276  s.view_3d_cuts = val(xz, "enabled", s.view_3d_cuts) || val(yz, "enabled", s.view_3d_cuts);
 277
 1278  s.pp_accumulation = val(xy, "output_image_accumulation", s.pp_accumulation);
 279
 1280  const auto &contrast = child_or_empty(xy, "contrast");
 1281  s.pp_pctclip         = val(contrast, "enabled", s.pp_pctclip);
 1282  s.pp_pctclip_lower   = val(contrast, "min", s.pp_pctclip_lower);
 1283  s.pp_pctclip_upper   = val(contrast, "max", s.pp_pctclip_upper);
 284
 1285  const auto &registration = child_or_empty(view, "registration");
 1286  s.pp_registration        = val(registration, "registration_enabled", s.pp_registration);
 1287  s.pp_registration_radius = val(registration, "registration_zone", s.pp_registration_radius);
 288
 1289  const auto &reticle = child_or_empty(view, "reticle");
 1290  s.pp_pctclip_radius = val(reticle, "scale", s.pp_pctclip_radius);
 291
 1292  read_view_ranges(s, view);
 1293}
 294
 1295void read_info(Settings &s, const json &info) {
 1296  const auto &pixel_pitch = child_or_empty(info, "pixel_pitch");
 1297  s.spacial_pixel_size    = val(pixel_pitch, "x", s.spacial_pixel_size);
 1298}
 299
 300} // namespace
 301
 1302nlohmann::json settings_to_old_json(const Settings &settings) {
 1303  json j = json::object();
 304
 1305  write_advanced(j, settings);
 1306  write_image_rendering(j, settings);
 1307  write_view(j, settings);
 1308  write_info(j, settings);
 309
 1310  return j;
 1311}
 312
 1313Settings old_json_to_settings(const nlohmann::json &j, const Settings &default_settings) {
 1314  Settings settings = default_settings;
 315
 1316  const auto &compute   = child_or_empty(j, "compute_settings");
 1317  const auto &advanced  = child_or_empty(compute, "advanced");
 1318  const auto &rendering = child_or_empty(compute, "image_rendering");
 1319  const auto &view      = child_or_empty(compute, "view");
 1320  const auto &info      = child_or_empty(j, "info");
 321
 1322  read_advanced(settings, advanced);
 1323  read_image_rendering(settings, rendering);
 1324  read_view(settings, view);
 1325  read_info(settings, info);
 326
 1327  return settings;
 1328}
 329
 330} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\settings_loader.hh

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include "pipeline/settings.hh"
 18
 19#include <nlohmann/json.hpp>
 20#include <string>
 21#include <type_traits>
 22
 23namespace holovibes::pipeline {
 24
 25/**
 26 * @brief Convert the new Settings structure to the legacy JSON format.
 27 */
 28nlohmann::json settings_to_old_json(const Settings &settings);
 29
 30/**
 31 * @brief Safely get a value from JSON with a default fallback.
 32 *
 33 * Returns @p def if:
 34 * - the key does not exist
 35 * - the value is null
 36 * - conversion to T throws
 37 */
 138template <typename T> inline T val(const nlohmann::json &j, const char *key, const T &def) {
 39  try {
 140    if (!j.is_object() || !j.contains(key) || j.at(key).is_null()) {
 141      return def;
 42    }
 143    return j.at(key).get<T>();
 044  } catch (...) {
 045    return def;
 046  }
 147}
 48
 149inline std::string val(const nlohmann::json &j, const char *key, const char *def) {
 150  return val<std::string>(j, key, std::string(def));
 151}
 52
 53/**
 54 * @brief Convert legacy JSON format to the new Settings structure.
 55 *
 56 * Any field that does not exist in the legacy format falls back to
 57 * @p default_settings.
 58 */
 59Settings old_json_to_settings(const nlohmann::json &j, const Settings &default_settings);
 60
 61} // namespace holovibes::pipeline

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\tasks\sinks\display_signal_history.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "display_signal_history.hh"
 16
 17#include <QMetaObject>
 18#include <QPointer>
 19
 20#include <algorithm>
 21#include <cmath>
 22#include <cstdint>
 23#include <cstring>
 24#include <iterator>
 25#include <memory>
 26#include <mutex>
 27#include <stdexcept>
 28#include <utility>
 29#include <vector>
 30
 31#include "bug.hh"
 32#include "cuda_runtime_api.h"
 33#include "logger.hh"
 34#include "ui/widgets/signal_history.hh"
 35#include "ui/widgets/zernike_history_widget.hh"
 36
 37namespace holovibes::tasks::sinks {
 38
 39namespace {
 40
 41// -------------------------------------------------------------------------------------------------
 42// Helpers
 43// -------------------------------------------------------------------------------------------------
 44
 045void check(bool condition, const std::string &msg) {
 046  if (!condition) {
 047    logger()->error("[DisplaySignalHistoryFactory] {}", msg);
 048    throw std::invalid_argument("DisplaySignalHistoryFactory: " + msg);
 49  }
 050}
 51
 52class SignalHistoryDispatcher : public std::enable_shared_from_this<SignalHistoryDispatcher> {
 53public:
 054  explicit SignalHistoryDispatcher(QPointer<holovibes::ui::ZernikeHistoryWidget> widget)
 055      : widget_(std::move(widget)) {}
 56
 057  void enqueue(std::vector<holovibes::ui::ZernikeHistorySample> samples) {
 058    bool should_schedule = false;
 59    {
 060      std::lock_guard lock(mutex_);
 061      pending_.insert(pending_.end(), std::make_move_iterator(samples.begin()),
 62                      std::make_move_iterator(samples.end()));
 063      if (!scheduled_) {
 064        scheduled_      = true;
 065        should_schedule = true;
 66      }
 067    }
 68
 069    if (!should_schedule || widget_.isNull()) {
 070      return;
 71    }
 72
 073    auto self = shared_from_this();
 074    if (!QMetaObject::invokeMethod(
 75            widget_.data(), [self]() { self->drain(); }, Qt::QueuedConnection)) {
 076      std::lock_guard lock(mutex_);
 077      scheduled_ = false;
 078    }
 079  }
 80
 081  void configure(std::vector<int> indexes, double time_window_seconds) {
 082    if (widget_.isNull()) {
 083      return;
 84    }
 85
 086    QPointer<holovibes::ui::ZernikeHistoryWidget> safe_widget = widget_;
 087    QMetaObject::invokeMethod(
 88        widget_.data(),
 89        [safe_widget, indexes = std::move(indexes), time_window_seconds]() {
 90          if (!safe_widget.isNull()) {
 91            safe_widget->set_series(indexes);
 92            safe_widget->set_time_window_seconds(time_window_seconds);
 93          }
 94        },
 95        Qt::QueuedConnection);
 096  }
 97
 98private:
 099  void drain() {
 0100    std::vector<holovibes::ui::ZernikeHistorySample> samples;
 101    {
 0102      std::lock_guard lock(mutex_);
 0103      samples.swap(pending_);
 0104      scheduled_ = false;
 0105    }
 106
 0107    if (!widget_.isNull() && !samples.empty()) {
 0108      widget_->append_samples(std::move(samples));
 109    }
 0110  }
 111
 112  QPointer<holovibes::ui::ZernikeHistoryWidget>    widget_;
 113  std::mutex                                       mutex_;
 114  std::vector<holovibes::ui::ZernikeHistorySample> pending_;
 0115  bool                                             scheduled_ = false;
 116};
 117
 118// -------------------------------------------------------------------------------------------------
 119// DisplaySignalHistoryTask
 120// -------------------------------------------------------------------------------------------------
 121
 122class DisplaySignalHistoryTask : public holoflow::core::ISyncTask {
 123public:
 124  DisplaySignalHistoryTask(DisplaySignalHistorySettings settings, holoflow::core::TDesc idesc,
 125                           QPointer<holovibes::ui::ZernikeHistoryWidget> widget,
 126                           cudaStream_t                                  stream)
 0127      : settings_(std::move(settings)), idesc_(std::move(idesc)),
 0128        dispatcher_(std::make_shared<SignalHistoryDispatcher>(std::move(widget))), stream_(stream) {
 0129    dispatcher_->configure(settings_.indexes, settings_.time_window_seconds);
 0130  }
 131
 0132  holoflow::core::OpResult execute(holoflow::core::SyncCtx &ctx) override {
 0133    if (received_sample_index_++ < settings_.discard_first) {
 0134      return holoflow::core::OpResult::Ok;
 135    }
 136
 0137    auto              &input = ctx.inputs[0];
 0138    const auto        &desc  = input.desc;
 0139    const auto         count = settings_.indexes.size();
 0140    std::vector<float> values(count);
 0141    auto              *src        = reinterpret_cast<const float *>(input.data());
 0142    const auto         byte_count = count * sizeof(float);
 143
 0144    switch (desc.mem_loc) {
 145    case holoflow::core::MemLoc::Host:
 0146      std::memcpy(values.data(), src, byte_count);
 0147      break;
 148    case holoflow::core::MemLoc::Device:
 0149      CUDA_CHECK(cudaMemcpyAsync(values.data(), src, byte_count, cudaMemcpyDeviceToHost, stream_));
 0150      CUDA_CHECK(cudaStreamSynchronize(stream_));
 0151      break;
 152    default:
 0153      throw std::logic_error("Unsupported memory location for Zernike signal history");
 154    }
 155
 0156    if (std::ranges::none_of(values, [](float value) { return std::isfinite(value); })) {
 0157      return holoflow::core::OpResult::Ok;
 158    }
 159
 160    // Logical sample time prevents variable processing latency and GUI refresh cadence from
 161    // distorting the acquisition/pipeline timeline represented by consecutive coefficient sets.
 0162    const double sample_time =
 163        static_cast<double>(valid_sample_index_++) * settings_.sample_time_seconds;
 0164    std::vector<holovibes::ui::ZernikeHistorySample> samples;
 0165    samples.reserve(count);
 0166    for (size_t i = 0; i < count; ++i) {
 0167      if (std::isfinite(values[i])) {
 0168        samples.push_back({settings_.indexes[i], {sample_time, static_cast<double>(values[i])}});
 169      }
 0170    }
 0171    dispatcher_->enqueue(std::move(samples));
 0172    return holoflow::core::OpResult::Ok;
 0173  }
 174
 175private:
 176  DisplaySignalHistorySettings             settings_;
 177  holoflow::core::TDesc                    idesc_;
 178  std::shared_ptr<SignalHistoryDispatcher> dispatcher_;
 179  cudaStream_t                             stream_;
 0180  uint64_t                                 received_sample_index_ = 0;
 0181  uint64_t                                 valid_sample_index_    = 0;
 182};
 183
 184} // namespace
 185
 0186void to_json(nlohmann::json &j, const DisplaySignalHistorySettings &settings) {
 0187  j = nlohmann::json{
 188      {"indexes", settings.indexes},
 189      {"time_window_seconds", settings.time_window_seconds},
 190      {"sample_time_seconds", settings.sample_time_seconds},
 191      {"discard_first", settings.discard_first},
 192  };
 0193}
 194
 0195void from_json(const nlohmann::json &j, DisplaySignalHistorySettings &settings) {
 0196  j.at("indexes").get_to(settings.indexes);
 0197  j.at("time_window_seconds").get_to(settings.time_window_seconds);
 0198  j.at("sample_time_seconds").get_to(settings.sample_time_seconds);
 0199  settings.discard_first = j.value("discard_first", size_t{0});
 0200}
 201
 202DisplaySignalHistoryFactory::DisplaySignalHistoryFactory(
 203    holovibes::ui::ZernikeHistoryWidget *widget)
 0204    : widget_(widget) {
 0205  HOLOVIBES_CHECK(widget_ != nullptr,
 206                  "DisplaySignalHistoryFactory requires a valid widget pointer");
 0207}
 208
 209holoflow::core::InferResult
 210DisplaySignalHistoryFactory::infer(std::span<const holoflow::core::TDesc> input_descs,
 0211                                   const nlohmann::json                  &jsettings) const {
 0212  const auto settings = jsettings.get<DisplaySignalHistorySettings>();
 0213  check(std::isfinite(settings.time_window_seconds) && settings.time_window_seconds > 0.0,
 214        "time_window_seconds must be positive and finite");
 0215  check(std::isfinite(settings.sample_time_seconds) && settings.sample_time_seconds > 0.0,
 216        "sample_time_seconds must be positive and finite");
 0217  check(!settings.indexes.empty(), "at least one Zernike index is required");
 0218  check(input_descs.size() == 1, "expected exactly one input descriptor");
 219
 0220  const auto &desc = input_descs[0];
 0221  check(desc.rank() == 1, "supports only rank-1 tensors");
 0222  check(desc.shape[0] == settings.indexes.size(),
 223        "input coefficient count must match configured indexes size");
 0224  check(desc.dtype == holoflow::core::DType::F32, "supports only F32 tensors");
 225
 0226  return holoflow::core::InferResult{
 227      .input_descs   = {desc},
 228      .output_descs  = {},
 229      .in_place      = {},
 230      .owned_inputs  = {false},
 231      .owned_outputs = {},
 232      .kind          = holoflow::core::TaskKind::Sync,
 233  };
 0234}
 235
 236std::unique_ptr<holoflow::core::ISyncTask>
 237DisplaySignalHistoryFactory::create(std::span<const holoflow::core::TDesc> input_descs,
 238                                    const nlohmann::json                  &jsettings,
 0239                                    const holoflow::core::SyncCreateCtx   &ctx) const {
 0240  auto settings = jsettings.get<DisplaySignalHistorySettings>();
 0241  infer(input_descs, jsettings);
 0242  return std::make_unique<DisplaySignalHistoryTask>(std::move(settings), input_descs[0], widget_,
 243                                                    ctx.stream);
 0244}
 245
 246std::unique_ptr<holoflow::core::ISyncTask> DisplaySignalHistoryFactory::update(
 247    std::unique_ptr<holoflow::core::ISyncTask>, std::span<const holoflow::core::TDesc> input_descs,
 0248    const nlohmann::json &jsettings, const holoflow::core::SyncCreateCtx &ctx) const {
 249  // A rebuilt graph is a new computation run, so a fresh task resets logical sample index to zero.
 0250  return create(input_descs, jsettings, ctx);
 0251}
 252
 253} // namespace holovibes::tasks::sinks

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\tasks\sinks\display_tensor.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "display_tensor.hh"
 16
 17#include <QMetaObject>
 18#include <QPointer>
 19#include <QtGlobal>
 20
 21#include <chrono>
 22#include <cstring>
 23#include <limits>
 24#include <stdexcept>
 25#include <utility>
 26
 27#include "bug.hh"
 28#include "cuda_runtime_api.h"
 29#include "curaii/cuda.hh"
 30#include "driver_types.h"
 31#include "holoflow/core/tensor.hh"
 32#include "logger.hh"
 33#include "ui/widgets/tensor_display_widget.hh"
 34
 35#include <cuda_runtime.h>
 36
 37namespace holovibes::tasks::sinks {
 38
 39namespace {
 40
 41// -------------------------------------------------------------------------------------------------
 42// Helpers
 43// -------------------------------------------------------------------------------------------------
 44
 045void check(bool condition, const std::string &msg) {
 046  if (!condition) {
 047    logger()->error("[DisplayTensorFactory] {}", msg);
 048    throw std::invalid_argument("DisplayTensorFactory: " + msg);
 49  }
 050}
 51
 052bool is_c_contiguous(const holoflow::core::TDesc &desc) {
 053  size_t expected = size_of(desc.dtype);
 054  for (size_t i = desc.rank(); i-- > 0;) {
 055    if (desc.strides[i] != expected)
 056      return false;
 057    expected *= desc.shape[i];
 058  }
 059  return true;
 060}
 61
 062bool same_desc(const holoflow::core::TDesc &a, const holoflow::core::TDesc &b) {
 063  return a.shape == b.shape && a.strides == b.strides && a.dtype == b.dtype &&
 64         a.mem_loc == b.mem_loc;
 065}
 66
 67// -------------------------------------------------------------------------------------------------
 68// DisplayTensorTask
 69// -------------------------------------------------------------------------------------------------
 70
 71class DisplayTensorTask : public holoflow::core::ISyncTask {
 72public:
 73  DisplayTensorTask(DisplayTensorSettings settings, holoflow::core::TDesc idesc,
 74                    QPointer<holovibes::ui::TensorDisplayWidget> widget, cudaStream_t stream);
 75
 76  holoflow::core::OpResult execute(holoflow::core::SyncCtx &ctx) override;
 77
 78  const holoflow::core::TDesc &idesc() const;
 79  const DisplayTensorSettings &settings() const;
 80  void                         update_stream(cudaStream_t stream);
 81
 82private:
 83  void dispatch_to_ui(QByteArray payload, int width, int height, holoflow::core::DType dtype);
 84
 85  DisplayTensorSettings                        settings_;
 86  holoflow::core::TDesc                        idesc_;
 87  std::chrono::steady_clock::time_point        next_refresh_;
 88  QPointer<holovibes::ui::TensorDisplayWidget> widget_;
 89  cudaStream_t                                 stream_;
 90};
 91
 92} // namespace
 93
 094void to_json(nlohmann::json &j, const DisplayTensorSettings &ds) {
 095  j = nlohmann::json{
 96      {"refresh_rate_hz", ds.refresh_rate_hz},
 97  };
 098}
 99
 0100void from_json(const nlohmann::json &j, DisplayTensorSettings &ds) {
 0101  j.at("refresh_rate_hz").get_to(ds.refresh_rate_hz);
 0102}
 103
 104DisplayTensorTask::DisplayTensorTask(DisplayTensorSettings settings, holoflow::core::TDesc idesc,
 105                                     QPointer<holovibes::ui::TensorDisplayWidget> widget,
 106                                     cudaStream_t                                 stream)
 0107    : settings_(std::move(settings)), idesc_(std::move(idesc)),
 0108      next_refresh_(std::chrono::steady_clock::now()), widget_(std::move(widget)), stream_(stream) {
 0109}
 110
 0111const holoflow::core::TDesc &DisplayTensorTask::idesc() const { return idesc_; }
 0112const DisplayTensorSettings &DisplayTensorTask::settings() const { return settings_; }
 0113void DisplayTensorTask::update_stream(cudaStream_t stream) { stream_ = stream; }
 114
 0115holoflow::core::OpResult DisplayTensorTask::execute(holoflow::core::SyncCtx &ctx) {
 0116  if (widget_.isNull()) {
 0117    logger()->warn("[DisplayTensorTask::execute] target widget is not available");
 0118    return holoflow::core::OpResult::Ok;
 119  }
 120
 0121  auto now = std::chrono::steady_clock::now();
 0122  if (now < next_refresh_) {
 0123    return holoflow::core::OpResult::Ok;
 124  }
 0125  auto delta_ms = static_cast<int>(1000 / settings_.refresh_rate_hz);
 0126  next_refresh_ = now + std::chrono::milliseconds(delta_ms);
 127
 0128  auto        &input     = ctx.inputs[0];
 0129  const auto  &desc      = input.desc;
 0130  const size_t height    = desc.rank() == 2 ? desc.shape[0] : desc.shape[1];
 0131  const size_t width     = desc.rank() == 2 ? desc.shape[1] : desc.shape[2];
 0132  const size_t elt_count = width * height;
 133  size_t       byte_count;
 134
 0135  switch (desc.dtype) {
 136  case holoflow::core::DType::U16: {
 0137    byte_count = 2;
 0138  } break;
 139
 140  case holoflow::core::DType::U8: {
 0141    byte_count = 1;
 0142  } break;
 143
 144  case holoflow::core::DType::F32: {
 0145    byte_count = 4;
 0146  } break;
 147
 148  default:
 0149    byte_count = 1;
 150  }
 151
 0152  QByteArray payload;
 0153  payload.resize(static_cast<qsizetype>(byte_count * elt_count));
 0154  void *dst = payload.data();
 155
 0156  switch (desc.mem_loc) {
 157  case holoflow::core::MemLoc::Host: {
 0158    void *src = static_cast<void *>(input.data());
 0159    CUDA_CHECK(cudaMemcpyAsync(dst, src, byte_count * elt_count, cudaMemcpyHostToHost, stream_));
 0160  } break;
 161
 162  case holoflow::core::MemLoc::Device: {
 0163    void *src = static_cast<void *>(input.data());
 0164    CUDA_CHECK(cudaMemcpyAsync(dst, src, byte_count * elt_count, cudaMemcpyDeviceToHost, stream_));
 165  } break;
 166  }
 167
 0168  CUDA_CHECK(cudaStreamSynchronize(stream_));
 0169  dispatch_to_ui(std::move(payload), static_cast<int>(width), static_cast<int>(height), desc.dtype);
 0170  return holoflow::core::OpResult::Ok;
 0171}
 172
 173void DisplayTensorTask::dispatch_to_ui(QByteArray payload, int width, int height,
 0174                                       holoflow::core::DType dtype) {
 0175  if (widget_.isNull()) {
 0176    logger()->warn("[DisplayTensorTask::dispatchToUi] target widget is not available");
 0177    return;
 178  }
 179
 0180  QPointer<holovibes::ui::TensorDisplayWidget> safe_widget = widget_;
 0181  QMetaObject::invokeMethod(
 182      widget_.data(),
 183      [this, safe_widget, payload = std::move(payload), width, height, dtype]() mutable {
 184        if (safe_widget.isNull()) {
 185          logger()->warn("[DisplayTensorTask::dispatchToUi] target widget is not available");
 186          return;
 187        }
 188        safe_widget->presentTensor(payload, width, height, dtype);
 189      },
 190      Qt::QueuedConnection);
 0191}
 192
 193DisplayTensorFactory::DisplayTensorFactory(holovibes::ui::TensorDisplayWidget *widget)
 0194    : widget_(widget) {
 0195  HOLOVIBES_CHECK(widget_ != nullptr, "DisplayTensorFactory requires a valid widget pointer");
 0196}
 197
 198holoflow::core::InferResult
 199DisplayTensorFactory::infer(std::span<const holoflow::core::TDesc> input_descs,
 0200                            const nlohmann::json                  &jsettings) const {
 0201  auto settings = jsettings.get<DisplayTensorSettings>();
 0202  check(settings.refresh_rate_hz > 0.0f, "refresh_rate_hz must be positive");
 0203  check(input_descs.size() == 1, "expected exactly one input descriptor");
 0204  const auto &desc         = input_descs[0];
 0205  const bool  is_2d_tensor = desc.rank() == 2 || (desc.rank() == 3 && desc.shape[0] == 1);
 0206  check(is_2d_tensor, "supports only rank-2 tensors or rank-3 tensors with batch size 1");
 0207  check(desc.dtype == holoflow::core::DType::U8 || desc.dtype == holoflow::core::DType::U16 ||
 208            desc.dtype == holoflow::core::DType::F32,
 209        "supports only U8, U16, or F32 tensors");
 0210  check(is_c_contiguous(desc), "input must be C-contiguous");
 211
 0212  return holoflow::core::InferResult{
 213      .input_descs   = {desc},
 214      .output_descs  = {},
 215      .in_place      = {},
 216      .owned_inputs  = {false},
 217      .owned_outputs = {},
 218      .kind          = holoflow::core::TaskKind::Sync,
 219  };
 0220}
 221
 222std::unique_ptr<holoflow::core::ISyncTask>
 223DisplayTensorFactory::create(std::span<const holoflow::core::TDesc> input_descs,
 224                             const nlohmann::json                  &jsettings,
 0225                             const holoflow::core::SyncCreateCtx   &ctx) const {
 0226  auto settings = jsettings.get<DisplayTensorSettings>();
 0227  infer(input_descs, jsettings);
 0228  return std::make_unique<DisplayTensorTask>(std::move(settings), input_descs[0], widget_,
 229                                             ctx.stream);
 0230}
 231
 232std::unique_ptr<holoflow::core::ISyncTask>
 233DisplayTensorFactory::update(std::unique_ptr<holoflow::core::ISyncTask> old_task,
 234                             std::span<const holoflow::core::TDesc>     input_descs,
 235                             const nlohmann::json                      &jsettings,
 0236                             const holoflow::core::SyncCreateCtx       &ctx) const {
 0237  auto *old = dynamic_cast<DisplayTensorTask *>(old_task.get());
 0238  if (old == nullptr)
 0239    return create(input_descs, jsettings, ctx);
 240
 0241  infer(input_descs, jsettings);
 0242  auto settings = jsettings.get<DisplayTensorSettings>();
 0243  if (settings == old->settings() && same_desc(input_descs[0], old->idesc())) {
 0244    old->update_stream(ctx.stream);
 0245    return old_task;
 246  }
 247
 0248  return create(input_descs, jsettings, ctx);
 0249}
 250
 0251} // namespace holovibes::tasks::sinks

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\tasks\sinks\display_zernike_coefficients.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "display_zernike_coefficients.hh"
 16
 17#include <QMetaObject>
 18#include <QPointer>
 19
 20#include <chrono>
 21#include <cstring>
 22#include <stdexcept>
 23#include <utility>
 24
 25#include "bug.hh"
 26#include "cuda_runtime_api.h"
 27#include "logger.hh"
 28#include "ui/widgets/auto_focus_widget.hh"
 29
 30namespace holovibes::tasks::sinks {
 31
 32namespace {
 33
 34// -------------------------------------------------------------------------------------------------
 35// Helpers
 36// -------------------------------------------------------------------------------------------------
 37
 038void check(bool condition, const std::string &msg) {
 039  if (!condition) {
 040    logger()->error("[DisplayZernikeCoefficientsFactory] {}", msg);
 041    throw std::invalid_argument("DisplayZernikeCoefficientsFactory: " + msg);
 42  }
 043}
 44
 045bool is_c_contiguous(const holoflow::core::TDesc &desc) {
 046  size_t expected = size_of(desc.dtype);
 047  for (size_t i = desc.rank(); i-- > 0;) {
 048    if (desc.strides[i] != expected)
 049      return false;
 050    expected *= desc.shape[i];
 051  }
 052  return true;
 053}
 54
 055bool same_desc(const holoflow::core::TDesc &a, const holoflow::core::TDesc &b) {
 056  return a.shape == b.shape && a.strides == b.strides && a.dtype == b.dtype &&
 57         a.mem_loc == b.mem_loc;
 058}
 59
 60// -------------------------------------------------------------------------------------------------
 61// DisplayZernikeCoefficientsTask
 62// -------------------------------------------------------------------------------------------------
 63
 64class DisplayZernikeCoefficientsTask : public holoflow::core::ISyncTask {
 65public:
 66  DisplayZernikeCoefficientsTask(DisplayZernikeCoefficientsSettings       settings,
 67                                 holoflow::core::TDesc                    idesc,
 68                                 QPointer<holovibes::ui::AutoFocusWidget> widget,
 69                                 cudaStream_t                             stream);
 70
 71  holoflow::core::OpResult execute(holoflow::core::SyncCtx &ctx) override;
 72
 73  const holoflow::core::TDesc              &idesc() const;
 74  const DisplayZernikeCoefficientsSettings &settings() const;
 75  void                                      update_stream(cudaStream_t stream);
 76
 77private:
 78  void dispatch_to_ui(std::vector<float> values);
 79
 80  DisplayZernikeCoefficientsSettings       settings_;
 81  holoflow::core::TDesc                    idesc_;
 82  std::chrono::steady_clock::time_point    next_refresh_;
 83  QPointer<holovibes::ui::AutoFocusWidget> widget_;
 84  cudaStream_t                             stream_;
 85};
 86
 87} // namespace
 88
 089void to_json(nlohmann::json &j, const DisplayZernikeCoefficientsSettings &settings) {
 090  j = nlohmann::json{
 91      {"indexes", settings.indexes},
 92      {"refresh_rate_hz", settings.refresh_rate_hz},
 93  };
 094}
 95
 096void from_json(const nlohmann::json &j, DisplayZernikeCoefficientsSettings &settings) {
 097  j.at("indexes").get_to(settings.indexes);
 098  j.at("refresh_rate_hz").get_to(settings.refresh_rate_hz);
 099}
 100
 101DisplayZernikeCoefficientsTask::DisplayZernikeCoefficientsTask(
 102    DisplayZernikeCoefficientsSettings settings, holoflow::core::TDesc idesc,
 103    QPointer<holovibes::ui::AutoFocusWidget> widget, cudaStream_t stream)
 0104    : settings_(std::move(settings)), idesc_(std::move(idesc)),
 0105      next_refresh_(std::chrono::steady_clock::now()), widget_(std::move(widget)), stream_(stream) {
 0106}
 107
 0108const holoflow::core::TDesc &DisplayZernikeCoefficientsTask::idesc() const { return idesc_; }
 0109const DisplayZernikeCoefficientsSettings &DisplayZernikeCoefficientsTask::settings() const {
 0110  return settings_;
 0111}
 0112void DisplayZernikeCoefficientsTask::update_stream(cudaStream_t stream) { stream_ = stream; }
 113
 0114holoflow::core::OpResult DisplayZernikeCoefficientsTask::execute(holoflow::core::SyncCtx &ctx) {
 0115  if (widget_.isNull()) {
 0116    logger()->warn("[DisplayZernikeCoefficientsTask::execute] target widget is not available");
 0117    return holoflow::core::OpResult::Ok;
 118  }
 119
 0120  auto now = std::chrono::steady_clock::now();
 0121  if (now < next_refresh_) {
 0122    return holoflow::core::OpResult::Ok;
 123  }
 0124  auto delta_ms = static_cast<int>(1000 / settings_.refresh_rate_hz);
 0125  next_refresh_ = now + std::chrono::milliseconds(delta_ms);
 126
 0127  auto       &input = ctx.inputs[0];
 0128  const auto &desc  = input.desc;
 0129  const auto  count = static_cast<size_t>(desc.shape[0]);
 130
 0131  std::vector<float> values(count);
 0132  auto              *dst        = values.data();
 0133  auto              *src        = reinterpret_cast<float *>(input.data());
 0134  const auto         byte_count = count * sizeof(float);
 135
 0136  switch (desc.mem_loc) {
 137  case holoflow::core::MemLoc::Host:
 0138    std::memcpy(dst, src, byte_count);
 0139    break;
 140  case holoflow::core::MemLoc::Device:
 0141    CUDA_CHECK(cudaMemcpyAsync(dst, src, byte_count, cudaMemcpyDeviceToHost, stream_));
 0142    CUDA_CHECK(cudaStreamSynchronize(stream_));
 0143    break;
 144  default:
 0145    throw std::logic_error("Unsupported memory location for zernike coefficient display");
 146  }
 147
 0148  dispatch_to_ui(std::move(values));
 0149  return holoflow::core::OpResult::Ok;
 0150}
 151
 0152void DisplayZernikeCoefficientsTask::dispatch_to_ui(std::vector<float> values) {
 0153  if (widget_.isNull()) {
 0154    logger()->warn("[DisplayZernikeCoefficientsTask::dispatchToUi] target widget is not available");
 0155    return;
 156  }
 157
 0158  QPointer<holovibes::ui::AutoFocusWidget> safe_widget = widget_;
 0159  auto                                     indexes     = settings_.indexes;
 0160  QMetaObject::invokeMethod(
 161      widget_.data(),
 162      [safe_widget, indexes = std::move(indexes), values = std::move(values)]() mutable {
 163        if (safe_widget.isNull()) {
 164          return;
 165        }
 166        safe_widget->set_zernike_values(indexes, values);
 167      },
 168      Qt::QueuedConnection);
 0169}
 170
 171DisplayZernikeCoefficientsFactory::DisplayZernikeCoefficientsFactory(
 172    holovibes::ui::AutoFocusWidget *widget)
 0173    : widget_(widget) {
 0174  HOLOVIBES_CHECK(widget_ != nullptr,
 175                  "DisplayZernikeCoefficientsFactory requires a valid widget pointer");
 0176}
 177
 178holoflow::core::InferResult
 179DisplayZernikeCoefficientsFactory::infer(std::span<const holoflow::core::TDesc> input_descs,
 0180                                         const nlohmann::json                  &jsettings) const {
 0181  const auto settings = jsettings.get<DisplayZernikeCoefficientsSettings>();
 0182  check(settings.refresh_rate_hz > 0.0f, "refresh_rate_hz must be positive");
 183
 0184  check(input_descs.size() == 1, "expected exactly one input descriptor");
 185
 0186  const auto &desc = input_descs[0];
 0187  check(desc.rank() == 1, "supports only rank-1 tensors");
 0188  check(desc.dtype == holoflow::core::DType::F32, "supports only F32 tensors");
 0189  check(is_c_contiguous(desc), "input must be C-contiguous");
 0190  check(desc.shape[0] == settings.indexes.size(),
 191        "Input coefficient count must match configured indexes size");
 192
 0193  return holoflow::core::InferResult{
 194      .input_descs   = {desc},
 195      .output_descs  = {},
 196      .in_place      = {},
 197      .owned_inputs  = {false},
 198      .owned_outputs = {},
 199      .kind          = holoflow::core::TaskKind::Sync,
 200  };
 0201}
 202
 203std::unique_ptr<holoflow::core::ISyncTask>
 204DisplayZernikeCoefficientsFactory::create(std::span<const holoflow::core::TDesc> input_descs,
 205                                          const nlohmann::json                  &jsettings,
 0206                                          const holoflow::core::SyncCreateCtx   &ctx) const {
 0207  auto settings = jsettings.get<DisplayZernikeCoefficientsSettings>();
 0208  infer(input_descs, jsettings);
 0209  return std::make_unique<DisplayZernikeCoefficientsTask>(std::move(settings), input_descs[0],
 210                                                          widget_, ctx.stream);
 0211}
 212
 213std::unique_ptr<holoflow::core::ISyncTask>
 214DisplayZernikeCoefficientsFactory::update(std::unique_ptr<holoflow::core::ISyncTask> old_task,
 215                                          std::span<const holoflow::core::TDesc>     input_descs,
 216                                          const nlohmann::json                      &jsettings,
 0217                                          const holoflow::core::SyncCreateCtx       &ctx) const {
 0218  auto *old = dynamic_cast<DisplayZernikeCoefficientsTask *>(old_task.get());
 0219  if (old == nullptr)
 0220    return create(input_descs, jsettings, ctx);
 221
 0222  infer(input_descs, jsettings);
 0223  auto settings = jsettings.get<DisplayZernikeCoefficientsSettings>();
 0224  if (settings == old->settings() && same_desc(input_descs[0], old->idesc())) {
 0225    old->update_stream(ctx.stream);
 0226    return old_task;
 227  }
 228
 0229  return create(input_descs, jsettings, ctx);
 0230}
 231
 0232} // namespace holovibes::tasks::sinks

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\graph_visualizer_widget.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/graph_visualizer_widget.hh"
 16
 17#include <QGraphicsScene>
 18#include <QGraphicsSvgItem>
 19#include <QGraphicsView>
 20#include <QHBoxLayout>
 21#include <QLabel>
 22#include <QPainter>
 23#include <QStandardPaths>
 24#include <QSvgRenderer>
 25#include <QToolButton>
 26#include <QVBoxLayout>
 27#include <QWheelEvent>
 28
 29namespace holovibes::ui {
 30
 31namespace {
 32
 33class GraphView : public QGraphicsView {
 34public:
 035  explicit GraphView(QGraphicsScene *scene, QWidget *parent = nullptr)
 036      : QGraphicsView(scene, parent) {
 037    setDragMode(QGraphicsView::ScrollHandDrag);
 038    setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
 039    setResizeAnchor(QGraphicsView::AnchorViewCenter);
 040  }
 41
 42protected:
 043  void wheelEvent(QWheelEvent *event) override {
 044    const qreal factor = event->angleDelta().y() > 0 ? 1.15 : 1.0 / 1.15;
 045    scale(factor, factor);
 046    event->accept();
 047  }
 48};
 49
 50} // namespace
 51
 052GraphVisualizerWidget::GraphVisualizerWidget(QWidget *parent) : QWidget(parent) {
 053  auto *layout = new QVBoxLayout(this);
 054  layout->setContentsMargins(8, 8, 8, 8);
 55
 056  auto *toolbar    = new QHBoxLayout();
 057  auto *fit_button = new QToolButton(this);
 058  fit_button->setText(tr("Fit"));
 059  fit_button->setToolTip(tr("Fit the complete graph in the panel."));
 060  toolbar->addWidget(fit_button);
 061  reload_button_ = new QToolButton(this);
 062  reload_button_->setText(tr("Reload"));
 063  reload_button_->setToolTip(tr("Regenerate the pipeline graph with the current preferences."));
 064  reload_button_->setEnabled(false);
 065  toolbar->addWidget(reload_button_);
 066  toolbar->addStretch();
 067  status_ = new QLabel(tr("Select a pipeline graph or open a DOT file."), this);
 068  toolbar->addWidget(status_);
 069  layout->addLayout(toolbar);
 70
 071  scene_ = new QGraphicsScene(this);
 072  view_  = new GraphView(scene_, this);
 073  view_->setRenderHint(QPainter::Antialiasing);
 074  layout->addWidget(view_, 1);
 75
 076  process_ = new QProcess(this);
 077  connect(process_, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
 78          &GraphVisualizerWidget::render_finished);
 079  connect(process_, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
 80    show_error(tr("Graphviz could not be started: %1").arg(process_->errorString()));
 81  });
 082  connect(fit_button, &QToolButton::clicked, this, &GraphVisualizerWidget::fit_graph);
 083  connect(reload_button_, &QToolButton::clicked, this, [this]() {
 84    reload_button_->setEnabled(false);
 85    status_->setText(tr("Reloading pipeline graph…"));
 86    emit reload_requested();
 87  });
 088}
 89
 090void GraphVisualizerWidget::render_dot(const QString &dot) {
 091  reload_button_->setEnabled(false);
 092  const QString executable = QStandardPaths::findExecutable(QStringLiteral("dot"));
 093  if (executable.isEmpty()) {
 094    show_error(tr("Graphviz was not found. Install Graphviz and ensure its 'dot' executable is "
 95                  "available on PATH."));
 096    return;
 97  }
 98
 099  status_->setText(tr("Rendering pipeline graph…"));
 0100  process_->start(executable, {QStringLiteral("-Tsvg")});
 0101  if (!process_->waitForStarted()) {
 0102    show_error(tr("Graphviz could not be started: %1").arg(process_->errorString()));
 0103    return;
 104  }
 105
 0106  process_->write(dot.toUtf8());
 0107  process_->closeWriteChannel();
 0108}
 109
 0110void GraphVisualizerWidget::set_reload_enabled(bool enabled) {
 0111  reload_enabled_ = enabled;
 0112  reload_button_->setEnabled(enabled && process_->state() == QProcess::NotRunning);
 0113}
 114
 0115void GraphVisualizerWidget::show_error(const QString &message) {
 0116  status_->setText(message);
 0117  status_->setWordWrap(true);
 0118  reload_button_->setEnabled(reload_enabled_);
 0119}
 120
 0121void GraphVisualizerWidget::fit_graph() {
 0122  if (!scene_->items().isEmpty()) {
 0123    view_->fitInView(scene_->itemsBoundingRect(), Qt::KeepAspectRatio);
 124  }
 0125}
 126
 0127void GraphVisualizerWidget::render_finished(int exit_code, QProcess::ExitStatus exit_status) {
 0128  reload_button_->setEnabled(reload_enabled_);
 0129  if (exit_status != QProcess::NormalExit || exit_code != 0) {
 0130    const QString details = QString::fromUtf8(process_->readAllStandardError()).trimmed();
 0131    show_error(details.isEmpty() ? tr("Graphviz failed to render the pipeline graph.") : details);
 0132    return;
 133  }
 134
 0135  const QByteArray svg = process_->readAllStandardOutput();
 0136  if (svg.isEmpty()) {
 0137    show_error(tr("Graphviz returned an empty graph."));
 0138    return;
 139  }
 140
 0141  auto *renderer = new QSvgRenderer(svg, this);
 0142  if (!renderer->isValid()) {
 0143    renderer->deleteLater();
 0144    show_error(tr("Graphviz returned an invalid SVG graph."));
 0145    return;
 146  }
 147
 0148  scene_->clear();
 0149  if (renderer_ != nullptr) {
 0150    renderer_->deleteLater();
 151  }
 0152  renderer_ = renderer;
 153
 0154  auto *item = new QGraphicsSvgItem();
 0155  item->setSharedRenderer(renderer_);
 0156  scene_->addItem(item);
 0157  scene_->setSceneRect(item->boundingRect());
 0158  status_->setText(tr("Scroll to zoom; drag to pan."));
 0159  status_->setWordWrap(false);
 0160  fit_graph();
 0161}
 162
 163} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\graph_visualizer_widget.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QProcess>
 18#include <QWidget>
 19
 20class QGraphicsScene;
 21class QGraphicsView;
 22class QLabel;
 23class QSvgRenderer;
 24class QToolButton;
 25
 26namespace holovibes::ui {
 27
 28class GraphVisualizerWidget : public QWidget {
 029  Q_OBJECT
 30
 31public:
 32  explicit GraphVisualizerWidget(QWidget *parent = nullptr);
 33
 34  void render_dot(const QString &dot);
 35  void set_reload_enabled(bool enabled);
 36  void show_error(const QString &message);
 37
 38signals:
 39  void reload_requested();
 40
 41private:
 42  void fit_graph();
 43  void render_finished(int exit_code, QProcess::ExitStatus exit_status);
 44
 045  QGraphicsView  *view_     = nullptr;
 046  QGraphicsScene *scene_    = nullptr;
 047  QLabel         *status_   = nullptr;
 048  QProcess       *process_  = nullptr;
 049  QSvgRenderer   *renderer_ = nullptr;
 050  QToolButton    *reload_button_ = nullptr;
 051  bool            reload_enabled_ = false;
 52};
 53
 54} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\main_window.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/main_window.hh"
 16
 17#include <QAction>
 18#include <QCheckBox>
 19#include <QCloseEvent>
 20#include <QComboBox>
 21#include <QDateTime>
 22#include <QDesktopServices>
 23#include <QDialog>
 24#include <QDialogButtonBox>
 25#include <QDir>
 26#include <QDirIterator>
 27#include <QDoubleSpinBox>
 28#include <QFile>
 29#include <QFileDialog>
 30#include <QFileInfoList>
 31#include <QFormLayout>
 32#include <QFrame>
 33#include <QHBoxLayout>
 34#include <QKeyEvent>
 35#include <QKeySequence>
 36#include <QLabel>
 37#include <QLineEdit>
 38#include <QMenuBar>
 39#include <QMessageBox>
 40#include <QProgressBar>
 41#include <QPushButton>
 42#include <QRegularExpression>
 43#include <QScrollArea>
 44#include <QScrollBar>
 45#include <QSettings>
 46#include <QSignalBlocker>
 47#include <QSizePolicy>
 48#include <QSlider>
 49#include <QSpinBox>
 50#include <QSplitter>
 51#include <QStandardPaths>
 52#include <QStringList>
 53#include <QStyle>
 54#include <QThread>
 55#include <QTimer>
 56#include <QToolButton>
 57#include <QVBoxLayout>
 58#include <algorithm>
 59#include <array>
 60#include <cmath>
 61#include <filesystem>
 62#include <fstream>
 63#include <functional>
 64#include <limits>
 65#include <optional>
 66
 67#include "bug.hh"
 68#include "holofile/holofile.hh"
 69#include "logger.hh"
 70#include "settings_loader.hh"
 71#include "ui/update_checker.hh"
 72#include "ui/graph_visualizer_widget.hh"
 73#include "ui/visualization_workspace.hh"
 74#include "ui/widgets/selected_widget_settings_panel.hh"
 75#include "ui/widgets/tensor_display_widget.hh"
 76#include "ui/widgets/zernike_history_widget.hh"
 77
 78namespace {
 79
 80constexpr int  kLargeSpinMax             = 1024 * 1024;
 81constexpr int  kTopBarHeight             = 30;
 82constexpr auto kDefaultSamplingFrequency = 37'000.0;
 83
 084QSpinBox *create_spin_box(QWidget *parent, int minimum, int maximum, int value) {
 085  auto *spin_box = new QSpinBox(parent);
 086  spin_box->setRange(minimum, maximum);
 087  spin_box->setValue(value);
 088  return spin_box;
 089}
 90
 91QDoubleSpinBox *create_double_spin_box(QWidget *parent, double minimum, double maximum, double step,
 092                                       double value, int decimals = 2) {
 093  auto *spin_box = new QDoubleSpinBox(parent);
 094  spin_box->setRange(minimum, maximum);
 095  spin_box->setSingleStep(step);
 096  spin_box->setDecimals(decimals);
 097  spin_box->setValue(value);
 098  return spin_box;
 099}
 100
 0101QComboBox *create_combo_box(QWidget *parent, const QStringList &items) {
 0102  auto *combo_box = new QComboBox(parent);
 0103  combo_box->addItems(items);
 0104  return combo_box;
 0105}
 106
 0107QLabel *create_result_label(QWidget *parent) {
 0108  auto *label = new QLabel("-", parent);
 0109  label->setTextInteractionFlags(Qt::TextSelectableByMouse);
 0110  return label;
 0111}
 112
 0113QString format_decimal(double value, int decimals = 6) {
 0114  QString text = QString::number(value, 'f', decimals);
 0115  while (text.contains('.') && text.endsWith('0')) {
 0116    text.chop(1);
 0117  }
 0118  if (text.endsWith('.')) {
 0119    text.chop(1);
 120  }
 0121  return text;
 0122}
 123
 0124QString format_frequency(double value) { return QString("%1 Hz").arg(format_decimal(value, 3)); }
 125
 126struct FieldBinding {
 127  holovibes::pipeline::SettingsField field;
 128  QWidget                           *widget;
 129};
 130
 0131QString to_qstring(holovibes::pipeline::ValidationSeverity severity) {
 0132  switch (severity) {
 133  case holovibes::pipeline::ValidationSeverity::Warning:
 0134    return "Warning";
 135  case holovibes::pipeline::ValidationSeverity::Error:
 0136    return "Error";
 137  }
 138
 0139  HOLOVIBES_UNREACHABLE();
 0140}
 141
 142QString build_field_tooltip(const holovibes::pipeline::FieldHelp                        &help,
 0143                            std::span<const holovibes::pipeline::ValidationIssue *const> issues) {
 0144  QStringList lines;
 0145  lines << QString::fromStdString(help.title);
 0146  lines << "";
 0147  lines << QString::fromStdString(help.description);
 148
 0149  if (!help.constraints.empty()) {
 0150    lines << "";
 0151    lines << "Constraints:";
 0152    for (const auto *constraint : help.constraints) {
 0153      lines << QString("- %1").arg(constraint);
 0154    }
 155  }
 156
 0157  if (!issues.empty()) {
 0158    lines << "";
 0159    lines << "Current issues:";
 0160    for (const auto *issue : issues) {
 0161      lines << QString("- %1: %2")
 162                   .arg(to_qstring(issue->severity), QString::fromStdString(issue->message));
 0163    }
 164  }
 165
 0166  return lines.join('\n');
 0167}
 168
 0169void restore_combo_text(QSettings &settings, const char *key, QComboBox *combo_box) {
 0170  const QString text = settings.value(key).toString();
 0171  if (text.isEmpty()) {
 0172    return;
 173  }
 174
 0175  const int index = combo_box->findText(text);
 0176  if (index >= 0) {
 0177    combo_box->setCurrentIndex(index);
 178  }
 0179}
 180
 181class FftFrequencyToolDialog : public QDialog {
 182public:
 183  using ApplyHandler = std::function<void(int, int)>;
 184
 0185  FftFrequencyToolDialog(double sampling_frequency_hz, int window_size, double start_frequency_hz,
 186                         double end_frequency_hz, const QString &time_transform,
 187                         ApplyHandler apply_handler, QWidget *parent)
 0188      : QDialog(parent), apply_handler_(std::move(apply_handler)) {
 0189    setWindowTitle(tr("FFT Frequency Range to Bins"));
 0190    setMinimumWidth(420);
 191
 0192    auto *dialog_layout = new QVBoxLayout(this);
 193
 0194    auto *input_form = new QFormLayout();
 0195    input_form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
 196
 0197    sampling_frequency_spin_ =
 198        create_double_spin_box(this, 0.000001, 1.0e12, 100.0, sampling_frequency_hz, 6);
 0199    sampling_frequency_spin_->setSuffix(" Hz");
 200
 0201    sampling_frequency_spin_->setToolTip(tr("The sampling frequency of the input signal. This is "
 202                                            "always set to the input sampling frequency."));
 0203    sampling_frequency_spin_->setEnabled(false);
 204
 0205    input_form->addRow(tr("Sampling frequency"), sampling_frequency_spin_);
 206
 0207    window_size_spin_ = create_spin_box(this, 1, std::numeric_limits<int>::max(), window_size);
 0208    input_form->addRow(tr("Window size"), window_size_spin_);
 0209    window_size_spin_->setToolTip(
 210        tr("The number of samples in the FFT window. This is always set to the "
 211           "time window."));
 0212    window_size_spin_->setEnabled(false);
 213
 0214    start_frequency_spin_ =
 215        create_double_spin_box(this, -1.0e12, 1.0e12, 1.0, start_frequency_hz, 2);
 0216    start_frequency_spin_->setSuffix(" Hz");
 0217    input_form->addRow(tr("F0"), start_frequency_spin_);
 218
 0219    end_frequency_spin_ = create_double_spin_box(this, -1.0e12, 1.0e12, 1.0, end_frequency_hz, 2);
 0220    end_frequency_spin_->setSuffix(" Hz");
 0221    input_form->addRow(tr("F1"), end_frequency_spin_);
 222
 0223    transform_combo_ = create_combo_box(this, QStringList{"RFFT", "FFT"});
 0224    if (time_transform == "FFT") {
 0225      transform_combo_->setCurrentText("FFT");
 226    }
 0227    input_form->addRow(tr("Transform"), transform_combo_);
 228
 0229    dialog_layout->addLayout(input_form);
 230
 0231    auto *result_form = new QFormLayout();
 0232    result_form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
 233
 0234    bin_spacing_value_     = create_result_label(this);
 0235    fractional_bin_value_  = create_result_label(this);
 0236    pipeline_start_value_  = create_result_label(this);
 0237    pipeline_width_value_  = create_result_label(this);
 0238    pipeline_end_value_    = create_result_label(this);
 0239    full_fft_bin_value_    = create_result_label(this);
 0240    bin_frequency_value_   = create_result_label(this);
 0241    frequency_error_value_ = create_result_label(this);
 242
 0243    result_form->addRow(tr("Bin spacing"), bin_spacing_value_);
 0244    result_form->addRow(tr("Fractional bins"), fractional_bin_value_);
 0245    result_form->addRow(tr("Pipeline Z start"), pipeline_start_value_);
 0246    result_form->addRow(tr("Pipeline Z width"), pipeline_width_value_);
 0247    result_form->addRow(tr("Pipeline Z end"), pipeline_end_value_);
 0248    result_form->addRow(tr("Full FFT bins"), full_fft_bin_value_);
 0249    result_form->addRow(tr("Selected frequency range"), bin_frequency_value_);
 0250    result_form->addRow(tr("Endpoint error"), frequency_error_value_);
 0251    dialog_layout->addLayout(result_form);
 252
 0253    message_label_ = new QLabel(this);
 0254    message_label_->setWordWrap(true);
 0255    dialog_layout->addWidget(message_label_);
 256
 0257    auto_apply_check_ = new QCheckBox(tr("Auto write Z range to settings"), this);
 0258    auto_apply_check_->setToolTip(
 259        tr("When enabled, valid calculator results are written to View Z and Width immediately."));
 0260    dialog_layout->addWidget(auto_apply_check_);
 261
 0262    auto *button_box = new QDialogButtonBox(QDialogButtonBox::Close, this);
 0263    apply_button_    = button_box->addButton(tr("Set Z Range"), QDialogButtonBox::ActionRole);
 0264    apply_button_->setToolTip(tr("Writes the calculated Z start and width to the View settings."));
 0265    dialog_layout->addWidget(button_box);
 266
 0267    connect(sampling_frequency_spin_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 268            [this](double) { update_result(); });
 0269    connect(window_size_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 270            [this](int) { update_result(); });
 0271    connect(start_frequency_spin_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 272            [this](double) { update_result(); });
 0273    connect(end_frequency_spin_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 274            [this](double) { update_result(); });
 0275    connect(transform_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 276            [this](int) { update_result(); });
 0277    connect(auto_apply_check_, &QCheckBox::toggled, this, [this](bool checked) {
 278      if (checked && result_valid_) {
 279        apply_result();
 280      }
 281    });
 0282    connect(apply_button_, &QPushButton::clicked, this, [this]() { apply_result(); });
 0283    connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
 284
 0285    update_result();
 0286  }
 287
 288private:
 0289  bool is_fft_transform() const { return transform_combo_->currentText() == "FFT"; }
 290
 0291  void apply_result() {
 0292    if (result_valid_ && apply_handler_) {
 0293      apply_handler_(pipeline_bin_start_, pipeline_bin_width_);
 294    }
 0295  }
 296
 0297  void update_result() {
 0298    const double sampling_frequency_hz = sampling_frequency_spin_->value();
 0299    const int    window_size           = window_size_spin_->value();
 0300    const double start_frequency_hz    = start_frequency_spin_->value();
 0301    const double end_frequency_hz      = end_frequency_spin_->value();
 0302    const double bin_spacing_hz        = sampling_frequency_hz / window_size;
 0303    const double nyquist_hz            = sampling_frequency_hz / 2.0;
 0304    const double range_start_hz =
 305        std::min(std::abs(start_frequency_hz), std::abs(end_frequency_hz));
 0306    const double range_end_hz = std::max(std::abs(start_frequency_hz), std::abs(end_frequency_hz));
 0307    const int    max_positive_bin = window_size / 2;
 0308    const int    max_pipeline_end = max_positive_bin + 1;
 309
 0310    const bool endpoints_in_range = range_end_hz <= nyquist_hz;
 0311    const auto rounded_start      = std::llround(range_start_hz / bin_spacing_hz);
 0312    const auto rounded_end        = std::llround(range_end_hz / bin_spacing_hz);
 313
 0314    pipeline_bin_start_ =
 315        static_cast<int>(std::clamp(rounded_start, 0LL, static_cast<long long>(max_pipeline_end)));
 0316    pipeline_bin_end_ =
 317        static_cast<int>(std::clamp(rounded_end, 0LL, static_cast<long long>(max_pipeline_end)));
 0318    if (pipeline_bin_end_ <= pipeline_bin_start_) {
 0319      pipeline_bin_end_ = std::min(max_pipeline_end, pipeline_bin_start_ + 1);
 320    }
 0321    pipeline_bin_width_ = pipeline_bin_end_ - pipeline_bin_start_;
 322
 0323    result_valid_ = endpoints_in_range && pipeline_bin_width_ > 0;
 0324    apply_button_->setEnabled(result_valid_);
 325
 0326    const bool    fft_transform      = is_fft_transform();
 0327    const double  selected_start_hz  = pipeline_bin_start_ * bin_spacing_hz;
 0328    const double  selected_end_hz    = pipeline_bin_end_ * bin_spacing_hz;
 0329    const double  start_endpoint_err = selected_start_hz - range_start_hz;
 0330    const double  end_endpoint_err   = selected_end_hz - range_end_hz;
 0331    const QString full_fft_bins_text = fft_transform && result_valid_
 332                                           ? QString("[%1, %2) and [%3, %4)")
 333                                                 .arg(pipeline_bin_start_)
 334                                                 .arg(pipeline_bin_end_)
 335                                                 .arg(window_size - pipeline_bin_end_)
 336                                                 .arg(window_size - pipeline_bin_start_)
 337                                           : tr("N/A");
 338
 0339    bin_spacing_value_->setText(format_frequency(bin_spacing_hz));
 0340    fractional_bin_value_->setText(QString("%1 to %2")
 341                                       .arg(format_decimal(range_start_hz / bin_spacing_hz, 6),
 342                                            format_decimal(range_end_hz / bin_spacing_hz, 6)));
 0343    pipeline_start_value_->setText(result_valid_ ? QString::number(pipeline_bin_start_) : "-");
 0344    pipeline_width_value_->setText(result_valid_ ? QString::number(pipeline_bin_width_) : "-");
 0345    pipeline_end_value_->setText(result_valid_ ? QString::number(pipeline_bin_end_) : "-");
 0346    full_fft_bin_value_->setText(full_fft_bins_text);
 0347    bin_frequency_value_->setText(result_valid_ ? QString("[%1, %2)")
 348                                                      .arg(format_frequency(selected_start_hz),
 349                                                           format_frequency(selected_end_hz))
 350                                                : "-");
 0351    frequency_error_value_->setText(
 352        result_valid_ ? QString("%1, %2").arg(format_frequency(start_endpoint_err),
 353                                              format_frequency(end_endpoint_err))
 354                      : "-");
 355
 0356    if (!endpoints_in_range) {
 0357      message_label_->setText(tr("Frequency range is outside the Nyquist range of +/- %1.")
 358                                  .arg(format_frequency(nyquist_hz)));
 0359    } else if (!fft_transform && (start_frequency_hz < 0.0 || end_frequency_hz < 0.0)) {
 0360      message_label_->setText(tr(
 361          "RFFT exposes non-negative frequencies; negative inputs are converted to magnitudes."));
 0362    } else if (fft_transform) {
 0363      message_label_->setText(
 364          tr("The pipeline stores the positive range and mirrors the negative FFT range."));
 0365    } else {
 0366      message_label_->clear();
 367    }
 368
 0369    if (auto_apply_check_->isChecked() && result_valid_) {
 0370      apply_result();
 371    }
 0372  }
 373
 0374  QDoubleSpinBox *sampling_frequency_spin_ = nullptr;
 0375  QSpinBox       *window_size_spin_        = nullptr;
 0376  QDoubleSpinBox *start_frequency_spin_    = nullptr;
 0377  QDoubleSpinBox *end_frequency_spin_      = nullptr;
 0378  QComboBox      *transform_combo_         = nullptr;
 0379  QLabel         *bin_spacing_value_       = nullptr;
 0380  QLabel         *fractional_bin_value_    = nullptr;
 0381  QLabel         *pipeline_start_value_    = nullptr;
 0382  QLabel         *pipeline_width_value_    = nullptr;
 0383  QLabel         *pipeline_end_value_      = nullptr;
 0384  QLabel         *full_fft_bin_value_      = nullptr;
 0385  QLabel         *bin_frequency_value_     = nullptr;
 0386  QLabel         *frequency_error_value_   = nullptr;
 0387  QLabel         *message_label_           = nullptr;
 0388  QCheckBox      *auto_apply_check_        = nullptr;
 0389  QPushButton    *apply_button_            = nullptr;
 390  ApplyHandler    apply_handler_;
 0391  int             pipeline_bin_start_ = 0;
 0392  int             pipeline_bin_width_ = 1;
 0393  int             pipeline_bin_end_   = 1;
 0394  bool            result_valid_       = false;
 395};
 396
 397class PreferencesDialog : public QDialog {
 398public:
 399  using GraphSpecDumpPreferences     = holoflow::core::GraphSpecDumpPreferences;
 400  using GraphCompiledDumpPreferences = holoflow::runtime::GraphCompiledDumpPreferences;
 401  // TODO : find the right import
 402
 0403  PreferencesDialog(QWidget *parent, holovibes::pipeline::Manager &manager)
 0404      : QDialog(parent), manager_(manager) {
 0405    setWindowTitle(tr("Preferences"));
 0406    setMinimumWidth(400);
 407
 0408    auto *dialog_layout = new QVBoxLayout(this);
 409
 0410    auto *splitter = new QSplitter(Qt::Horizontal, this);
 411
 412    // Dump preferences
 0413    auto *graph_spec_dump_group_box =
 414        setup_graph_spec_dump_preferences(manager_.get_graph_spec_dump_preferences());
 0415    splitter->addWidget(graph_spec_dump_group_box);
 416
 0417    auto *graph_compiled_dump_group_box =
 418        setup_graph_compiled_dump_preferences(manager_.get_graph_compiled_dump_preferences());
 0419    splitter->addWidget(graph_compiled_dump_group_box);
 420
 0421    dialog_layout->addWidget(splitter);
 422    // Apply / Close
 0423    auto *button_box = new QDialogButtonBox(QDialogButtonBox::Close, this);
 0424    apply_button_    = button_box->addButton(tr("Apply"), QDialogButtonBox::ActionRole);
 0425    apply_button_->setDisabled(true);
 0426    dialog_layout->addWidget(button_box);
 427
 0428    connect(apply_button_, &QPushButton::clicked, this, [this]() { update_preferences(); });
 0429    connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
 0430    connect_signals();
 0431  }
 432
 433private:
 434  QGroupBox *
 0435  setup_graph_spec_dump_preferences(const GraphSpecDumpPreferences &graph_spec_dump_preferences) {
 0436    auto *group_box  = new QGroupBox(tr("Graph Dump Spec"), this);
 0437    auto *input_form = new QFormLayout();
 0438    input_form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
 439
 0440    graph_spec_dump_preferences_widgets_.rankdir_combo_ =
 441        create_combo_box(this, QStringList{"LR", "TB"});
 0442    if (graph_spec_dump_preferences.rankdir == GraphSpecDumpPreferences::Rankdir::TopToBottom) {
 0443      graph_spec_dump_preferences_widgets_.rankdir_combo_->setCurrentText("TB");
 444    }
 445
 0446    input_form->addRow(tr("Graph rankdir"), graph_spec_dump_preferences_widgets_.rankdir_combo_);
 447
 0448    graph_spec_dump_preferences_widgets_.floating_point_precision_spin_ =
 449        create_spin_box(this, 0, 17, graph_spec_dump_preferences.floating_point_precision);
 0450    graph_spec_dump_preferences_widgets_.floating_point_precision_spin_->setToolTip(
 451        tr("Number of digits after the decimal point in scientific notation."));
 0452    input_form->addRow(tr("Floating-point precision"),
 453                       graph_spec_dump_preferences_widgets_.floating_point_precision_spin_);
 454
 0455    graph_spec_dump_preferences_widgets_.node_name_checkbox_ = new QCheckBox(this);
 0456    graph_spec_dump_preferences_widgets_.node_name_checkbox_->setChecked(
 457        graph_spec_dump_preferences.dump_node_name);
 0458    graph_spec_dump_preferences_widgets_.node_name_checkbox_->setToolTip(
 459        tr("Include node names in the graph dump."));
 0460    input_form->addRow(tr("Node names"), graph_spec_dump_preferences_widgets_.node_name_checkbox_);
 461
 0462    graph_spec_dump_preferences_widgets_.node_kind_checkbox_ = new QCheckBox(this);
 0463    graph_spec_dump_preferences_widgets_.node_kind_checkbox_->setChecked(
 464        graph_spec_dump_preferences.dump_node_kind);
 0465    graph_spec_dump_preferences_widgets_.node_kind_checkbox_->setToolTip(
 466        tr("Include node kinds in the graph dump."));
 0467    input_form->addRow(tr("Node kinds"), graph_spec_dump_preferences_widgets_.node_kind_checkbox_);
 468
 0469    graph_spec_dump_preferences_widgets_.node_settings_checkbox_ = new QCheckBox(this);
 0470    graph_spec_dump_preferences_widgets_.node_settings_checkbox_->setChecked(
 471        graph_spec_dump_preferences.dump_node_settings);
 0472    graph_spec_dump_preferences_widgets_.node_settings_checkbox_->setToolTip(
 473        tr("Include node settings in the graph dump."));
 0474    input_form->addRow(tr("Node settings"),
 475                       graph_spec_dump_preferences_widgets_.node_settings_checkbox_);
 476
 0477    graph_spec_dump_preferences_widgets_.edge_indices_checkbox_ = new QCheckBox(this);
 0478    graph_spec_dump_preferences_widgets_.edge_indices_checkbox_->setChecked(
 479        graph_spec_dump_preferences.dump_edge_indices);
 0480    graph_spec_dump_preferences_widgets_.edge_indices_checkbox_->setToolTip(
 481        tr("Include edge indices in the graph dump."));
 0482    input_form->addRow(tr("Edge indices"),
 483                       graph_spec_dump_preferences_widgets_.edge_indices_checkbox_);
 484
 0485    group_box->setLayout(input_form);
 0486    return group_box;
 0487  }
 488
 489  // TODO add compiled graph preferences args
 490  QGroupBox *setup_graph_compiled_dump_preferences(
 0491      const GraphCompiledDumpPreferences &graph_compiled_dump_preferences) {
 0492    auto *group_box  = new QGroupBox(tr("Graph Compiled Spec"), this);
 0493    auto *input_form = new QFormLayout();
 0494    input_form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
 495
 0496    graph_compiled_dump_preferences_widgets_.rankdir_combo_ =
 497        create_combo_box(this, QStringList{"LR", "TB"});
 0498    if (graph_compiled_dump_preferences.rankdir ==
 499        GraphCompiledDumpPreferences::Rankdir::TopToBottom) {
 0500      graph_compiled_dump_preferences_widgets_.rankdir_combo_->setCurrentText("TB");
 501    }
 502
 0503    graph_compiled_dump_preferences_widgets_.rankdir_combo_->setToolTip(
 504        tr("Direction used by the normal compiled-graph layout."));
 0505    input_form->addRow(tr("Rank direction"),
 506                       graph_compiled_dump_preferences_widgets_.rankdir_combo_);
 507
 0508    graph_compiled_dump_preferences_widgets_.layout_combo_ =
 509        create_combo_box(this,
 510                         QStringList{tr("Normal"), tr("Stairs"), tr("Block"), tr("Snake")});
 0511    graph_compiled_dump_preferences_widgets_.layout_combo_->setCurrentIndex(
 512        static_cast<int>(graph_compiled_dump_preferences.layout));
 0513    graph_compiled_dump_preferences_widgets_.layout_combo_->setToolTip(
 514        tr("Arrange sections normally, as horizontal stairs, as a left-aligned block, or in "
 515           "alternating snake rows."));
 0516    graph_compiled_dump_preferences_widgets_.rankdir_combo_->setEnabled(
 517        graph_compiled_dump_preferences.layout == GraphCompiledDumpPreferences::Layout::Normal);
 0518    input_form->addRow(tr("Section layout"),
 519                       graph_compiled_dump_preferences_widgets_.layout_combo_);
 520
 0521    graph_compiled_dump_preferences_widgets_.floating_point_precision_spin_ =
 522        create_spin_box(this, 0, 17, graph_compiled_dump_preferences.floating_point_precision);
 0523    graph_compiled_dump_preferences_widgets_.floating_point_precision_spin_->setToolTip(
 524        tr("Number of digits after the decimal point in scientific notation."));
 0525    input_form->addRow(tr("Floating-point precision"),
 526                       graph_compiled_dump_preferences_widgets_.floating_point_precision_spin_);
 527
 0528    graph_compiled_dump_preferences_widgets_.node_name_checkbox_ = new QCheckBox(this);
 0529    graph_compiled_dump_preferences_widgets_.node_name_checkbox_->setChecked(
 530        graph_compiled_dump_preferences.dump_node_name);
 0531    graph_compiled_dump_preferences_widgets_.node_name_checkbox_->setToolTip(
 532        tr("Include node names in the compiled graph."));
 0533    input_form->addRow(tr("Node names"),
 534                       graph_compiled_dump_preferences_widgets_.node_name_checkbox_);
 535
 0536    graph_compiled_dump_preferences_widgets_.node_kind_checkbox_ = new QCheckBox(this);
 0537    graph_compiled_dump_preferences_widgets_.node_kind_checkbox_->setChecked(
 538        graph_compiled_dump_preferences.dump_node_kind);
 0539    graph_compiled_dump_preferences_widgets_.node_kind_checkbox_->setToolTip(
 540        tr("Include node kinds in the compiled graph."));
 0541    input_form->addRow(tr("Node kinds"),
 542                       graph_compiled_dump_preferences_widgets_.node_kind_checkbox_);
 543
 0544    graph_compiled_dump_preferences_widgets_.node_settings_checkbox_ = new QCheckBox(this);
 0545    graph_compiled_dump_preferences_widgets_.node_settings_checkbox_->setChecked(
 546        graph_compiled_dump_preferences.dump_node_settings);
 0547    graph_compiled_dump_preferences_widgets_.node_settings_checkbox_->setToolTip(
 548        tr("Include node settings in the compiled graph."));
 0549    input_form->addRow(tr("Node settings"),
 550                       graph_compiled_dump_preferences_widgets_.node_settings_checkbox_);
 551
 0552    graph_compiled_dump_preferences_widgets_.node_in_out_tids_ = new QCheckBox(this);
 0553    graph_compiled_dump_preferences_widgets_.node_in_out_tids_->setChecked(
 554        graph_compiled_dump_preferences.dump_node_in_out_tids);
 0555    graph_compiled_dump_preferences_widgets_.node_in_out_tids_->setToolTip(
 556        tr("Include node input and output TIDs in the compiled graph."));
 0557    input_form->addRow(tr("Node I/O TIDs"),
 558                       graph_compiled_dump_preferences_widgets_.node_in_out_tids_);
 559
 0560    graph_compiled_dump_preferences_widgets_.edge_indices_checkbox_ = new QCheckBox(this);
 0561    graph_compiled_dump_preferences_widgets_.edge_indices_checkbox_->setChecked(
 562        graph_compiled_dump_preferences.dump_edge_indices);
 0563    graph_compiled_dump_preferences_widgets_.edge_indices_checkbox_->setToolTip(
 564        tr("Include edge indices in the compiled graph."));
 0565    input_form->addRow(tr("Edge indices"),
 566                       graph_compiled_dump_preferences_widgets_.edge_indices_checkbox_);
 567
 0568    graph_compiled_dump_preferences_widgets_.edge_desc_checkbox_ = new QCheckBox(this);
 0569    graph_compiled_dump_preferences_widgets_.edge_desc_checkbox_->setChecked(
 570        graph_compiled_dump_preferences.dump_edge_descriptions);
 0571    graph_compiled_dump_preferences_widgets_.edge_desc_checkbox_->setToolTip(
 572        tr("Include edge descriptions in the compiled graph."));
 0573    input_form->addRow(tr("Edge descriptions"),
 574                       graph_compiled_dump_preferences_widgets_.edge_desc_checkbox_);
 575
 0576    graph_compiled_dump_preferences_widgets_.section_toggle_checkbox_ = new QCheckBox(this);
 0577    graph_compiled_dump_preferences_widgets_.section_toggle_checkbox_->setChecked(
 578        graph_compiled_dump_preferences.dump_section_info);
 0579    graph_compiled_dump_preferences_widgets_.section_toggle_checkbox_->setToolTip(
 580        tr("Include section information in the compiled graph."));
 0581    input_form->addRow(tr("Section info"),
 582                       graph_compiled_dump_preferences_widgets_.section_toggle_checkbox_);
 583
 0584    graph_compiled_dump_preferences_widgets_.section_stream_addr_checkbox_ = new QCheckBox(this);
 0585    graph_compiled_dump_preferences_widgets_.section_stream_addr_checkbox_->setChecked(
 586        graph_compiled_dump_preferences.dump_section_stream_addr);
 0587    graph_compiled_dump_preferences_widgets_.section_stream_addr_checkbox_->setToolTip(
 588        tr("Include section stream addresses in the compiled graph."));
 0589    input_form->addRow(tr("Section stream addresses"),
 590                       graph_compiled_dump_preferences_widgets_.section_stream_addr_checkbox_);
 591
 0592    graph_compiled_dump_preferences_widgets_.resources_toggle_checkbox_ = new QCheckBox(this);
 0593    graph_compiled_dump_preferences_widgets_.resources_toggle_checkbox_->setChecked(
 594        graph_compiled_dump_preferences.dump_resource_info);
 0595    graph_compiled_dump_preferences_widgets_.resources_toggle_checkbox_->setToolTip(
 596        tr("Include resource information in the compiled graph."));
 0597    input_form->addRow(tr("Resource info"),
 598                       graph_compiled_dump_preferences_widgets_.resources_toggle_checkbox_);
 599
 0600    group_box->setLayout(input_form);
 0601    return group_box;
 0602  }
 603
 0604  void update_preferences() {
 605    // const auto &specs_ = nullptr;
 606    // holoflow::core::to_dot(specs_);
 0607    apply_button_->setEnabled(false);
 0608    auto graph_spec_dump_preferences = GraphSpecDumpPreferences{
 609        .rankdir = graph_spec_dump_preferences_widgets_.rankdir_combo_->currentText() == "LR"
 610                       ? GraphSpecDumpPreferences::Rankdir::LeftToRight
 611                       : GraphSpecDumpPreferences::Rankdir::TopToBottom,
 612
 613        .floating_point_precision =
 614            graph_spec_dump_preferences_widgets_.floating_point_precision_spin_->value(),
 615
 616        .dump_node_name = graph_spec_dump_preferences_widgets_.node_name_checkbox_->isChecked(),
 617        .dump_node_kind = graph_spec_dump_preferences_widgets_.node_kind_checkbox_->isChecked(),
 618        .dump_node_settings =
 619            graph_spec_dump_preferences_widgets_.node_settings_checkbox_->isChecked(),
 620        .dump_edge_indices =
 621            graph_spec_dump_preferences_widgets_.edge_indices_checkbox_->isChecked()};
 622
 0623    auto graph_compiled_dump_preferences = GraphCompiledDumpPreferences{
 624        .rankdir = graph_compiled_dump_preferences_widgets_.rankdir_combo_->currentText() == "LR"
 625                       ? GraphCompiledDumpPreferences::Rankdir::LeftToRight
 626                       : GraphCompiledDumpPreferences::Rankdir::TopToBottom,
 627        .layout = static_cast<GraphCompiledDumpPreferences::Layout>(
 628            graph_compiled_dump_preferences_widgets_.layout_combo_->currentIndex()),
 629
 630        .floating_point_precision =
 631            graph_compiled_dump_preferences_widgets_.floating_point_precision_spin_->value(),
 632
 633        .dump_node_name = graph_compiled_dump_preferences_widgets_.node_name_checkbox_->isChecked(),
 634        .dump_node_kind = graph_compiled_dump_preferences_widgets_.node_kind_checkbox_->isChecked(),
 635        .dump_node_settings =
 636            graph_compiled_dump_preferences_widgets_.node_settings_checkbox_->isChecked(),
 637        .dump_node_in_out_tids =
 638            graph_compiled_dump_preferences_widgets_.node_in_out_tids_->isChecked(),
 639        .dump_edge_indices =
 640            graph_compiled_dump_preferences_widgets_.edge_indices_checkbox_->isChecked(),
 641        .dump_edge_descriptions =
 642            graph_compiled_dump_preferences_widgets_.edge_desc_checkbox_->isChecked(),
 643        .dump_section_info =
 644            graph_compiled_dump_preferences_widgets_.section_toggle_checkbox_->isChecked(),
 645        .dump_section_stream_addr =
 646            graph_compiled_dump_preferences_widgets_.section_stream_addr_checkbox_->isChecked(),
 647        .dump_resource_info =
 648            graph_compiled_dump_preferences_widgets_.resources_toggle_checkbox_->isChecked()};
 649
 0650    manager_.update_graph_spec_dump_preferences(graph_spec_dump_preferences);
 0651    manager_.update_graph_compiled_dump_preferences(graph_compiled_dump_preferences);
 0652  }
 653
 0654  void connect_signals() {
 0655    connect(graph_spec_dump_preferences_widgets_.rankdir_combo_,
 656            qOverload<int>(&QComboBox::currentIndexChanged), this,
 657            [this](int) { apply_button_->setEnabled(true); });
 0658    connect(graph_spec_dump_preferences_widgets_.floating_point_precision_spin_,
 659            qOverload<int>(&QSpinBox::valueChanged), this,
 660            [this](int) { apply_button_->setEnabled(true); });
 0661    connect(graph_spec_dump_preferences_widgets_.node_name_checkbox_, &QCheckBox::toggled, this,
 662            [this](bool) { apply_button_->setEnabled(true); });
 0663    connect(graph_spec_dump_preferences_widgets_.node_kind_checkbox_, &QCheckBox::toggled, this,
 664            [this](bool) { apply_button_->setEnabled(true); });
 0665    connect(graph_spec_dump_preferences_widgets_.node_settings_checkbox_, &QCheckBox::toggled, this,
 666            [this](bool) { apply_button_->setEnabled(true); });
 0667    connect(graph_spec_dump_preferences_widgets_.edge_indices_checkbox_, &QCheckBox::toggled, this,
 668            [this](bool) { apply_button_->setEnabled(true); });
 669
 0670    connect(graph_compiled_dump_preferences_widgets_.rankdir_combo_,
 671            qOverload<int>(&QComboBox::currentIndexChanged), this,
 672            [this](int) { apply_button_->setEnabled(true); });
 0673    connect(graph_compiled_dump_preferences_widgets_.layout_combo_,
 674            qOverload<int>(&QComboBox::currentIndexChanged), this, [this](int index) {
 675              graph_compiled_dump_preferences_widgets_.rankdir_combo_->setEnabled(
 676                  index == static_cast<int>(GraphCompiledDumpPreferences::Layout::Normal));
 677              apply_button_->setEnabled(true);
 678            });
 0679    connect(graph_compiled_dump_preferences_widgets_.floating_point_precision_spin_,
 680            qOverload<int>(&QSpinBox::valueChanged), this,
 681            [this](int) { apply_button_->setEnabled(true); });
 0682    connect(graph_compiled_dump_preferences_widgets_.node_name_checkbox_, &QCheckBox::toggled, this,
 683            [this](bool) { apply_button_->setEnabled(true); });
 0684    connect(graph_compiled_dump_preferences_widgets_.node_kind_checkbox_, &QCheckBox::toggled, this,
 685            [this](bool) { apply_button_->setEnabled(true); });
 0686    connect(graph_compiled_dump_preferences_widgets_.node_settings_checkbox_, &QCheckBox::toggled,
 687            this, [this](bool) { apply_button_->setEnabled(true); });
 0688    connect(graph_compiled_dump_preferences_widgets_.node_in_out_tids_, &QCheckBox::toggled, this,
 689            [this](bool) { apply_button_->setEnabled(true); });
 0690    connect(graph_compiled_dump_preferences_widgets_.edge_indices_checkbox_, &QCheckBox::toggled,
 691            this, [this](bool) { apply_button_->setEnabled(true); });
 0692    connect(graph_compiled_dump_preferences_widgets_.edge_desc_checkbox_, &QCheckBox::toggled, this,
 693            [this](bool) { apply_button_->setEnabled(true); });
 0694    connect(graph_compiled_dump_preferences_widgets_.section_toggle_checkbox_, &QCheckBox::toggled,
 695            this, [this](bool) { apply_button_->setEnabled(true); });
 0696    connect(graph_compiled_dump_preferences_widgets_.section_stream_addr_checkbox_,
 697            &QCheckBox::toggled, this, [this](bool) { apply_button_->setEnabled(true); });
 0698    connect(graph_compiled_dump_preferences_widgets_.resources_toggle_checkbox_,
 699            &QCheckBox::toggled, this, [this](bool) { apply_button_->setEnabled(true); });
 0700  }
 701
 702  holovibes::pipeline::Manager &manager_;
 703
 0704  QPushButton *apply_button_ = nullptr;
 705
 706  struct GraphSpecDumpPreferencesWidgets {
 707    // dump preferences
 708    // rankdir: LR | TB
 709    QComboBox *rankdir_combo_                  = nullptr;
 710    QSpinBox  *floating_point_precision_spin_ = nullptr;
 711    // Nodes
 712    QCheckBox *node_name_checkbox_     = nullptr;
 713    QCheckBox *node_kind_checkbox_     = nullptr;
 714    QCheckBox *node_settings_checkbox_ = nullptr;
 715    // Edges
 716    QCheckBox *edge_indices_checkbox_ = nullptr;
 717  };
 718  GraphSpecDumpPreferencesWidgets graph_spec_dump_preferences_widgets_;
 719
 720  struct GraphCompiledDumpPreferencesWidgets {
 721    // dump preferences
 722    // rankdir: LR | TB
 723    QComboBox *rankdir_combo_                  = nullptr;
 724    QComboBox *layout_combo_                   = nullptr;
 725    QSpinBox  *floating_point_precision_spin_ = nullptr;
 726    QCheckBox *node_name_checkbox_             = nullptr;
 727    QCheckBox *node_kind_checkbox_             = nullptr;
 728    QCheckBox *node_settings_checkbox_         = nullptr;
 729    QCheckBox *node_in_out_tids_               = nullptr;
 730    // Edges
 731    QCheckBox *edge_indices_checkbox_ = nullptr;
 732    QCheckBox *edge_desc_checkbox_    = nullptr;
 733
 734    // Section
 735    QCheckBox *section_toggle_checkbox_      = nullptr;
 736    QCheckBox *section_stream_addr_checkbox_ = nullptr;
 737
 738    // Resources
 739    QCheckBox *resources_toggle_checkbox_ = nullptr;
 740  };
 741  GraphCompiledDumpPreferencesWidgets graph_compiled_dump_preferences_widgets_;
 742};
 743
 744} // namespace
 745
 746namespace holovibes::ui {
 747
 748MainWindow::MainWindow(QWidget *parent)
 0749    : QMainWindow(parent), session_id_(QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss")) {
 0750  setup_main_layout();
 0751  initialize_display_widgets();
 0752  restore_persistent_state();
 0753  configure_unsupported_features();
 0754  initialize_pipeline_manager();
 755
 0756  connect_manager_signals();
 0757  connect_import_controls();
 0758  connect_export_controls();
 0759  setup_validation_connections();
 0760  setup_update_connections();
 761
 0762  validate_inputs();
 0763  refresh_command_bar();
 0764  configure_window();
 0765  check_for_updates();
 0766}
 767
 0768void MainWindow::setup_main_layout() {
 0769  auto *central_widget = new QWidget(this);
 0770  setCentralWidget(central_widget);
 771
 0772  auto *main_layout = new QVBoxLayout(central_widget);
 0773  main_layout->setContentsMargins(8, 6, 8, 8);
 0774  main_layout->setSpacing(4);
 775
 0776  auto *session_bar = new QFrame(central_widget);
 0777  session_bar->setObjectName("sessionBar");
 0778  session_bar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
 0779  session_bar->setFixedHeight(kTopBarHeight);
 0780  auto *session_layout = new QHBoxLayout(session_bar);
 0781  session_layout->setContentsMargins(8, 2, 8, 2);
 0782  session_layout->setSpacing(6);
 783
 0784  auto *patient_label = new QLabel("Patient", session_bar);
 0785  patient_line_edit_  = new QLineEdit(session_bar);
 0786  patient_line_edit_->setObjectName("patientField");
 0787  patient_line_edit_->setText("PATIENT");
 0788  patient_line_edit_->setPlaceholderText("PATIENT");
 0789  patient_line_edit_->setMinimumWidth(180);
 0790  patient_line_edit_->setMaximumWidth(260);
 791
 0792  auto *eye_label = new QLabel("Eye", session_bar);
 0793  eye_side_combo_ = create_combo_box(session_bar, QStringList{"NA", "OD", "OS", "OU"});
 0794  eye_side_combo_->setObjectName("eyeSideField");
 0795  eye_side_combo_->setToolTip(
 796      "Eye side used in recording file names. OD: right eye, OS: left eye, OU: both eyes, NA: not "
 797      "specified.");
 798
 0799  auto *session_label  = new QLabel("Session", session_bar);
 0800  session_value_label_ = new QLabel(session_id_, session_bar);
 0801  session_value_label_->setObjectName("sessionValue");
 802
 0803  auto *next_acquisition_label = new QLabel("Next Acquisition", session_bar);
 0804  acquisition_value_label_     = new QLabel(acquisition_label(next_acquisition_id_), session_bar);
 0805  acquisition_value_label_->setObjectName("sessionValue");
 806
 0807  session_layout->addWidget(patient_label);
 0808  session_layout->addWidget(patient_line_edit_);
 0809  session_layout->addSpacing(8);
 0810  session_layout->addWidget(eye_label);
 0811  session_layout->addWidget(eye_side_combo_);
 0812  session_layout->addSpacing(16);
 0813  session_layout->addWidget(session_label);
 0814  session_layout->addWidget(session_value_label_);
 0815  session_layout->addSpacing(16);
 0816  session_layout->addWidget(next_acquisition_label);
 0817  session_layout->addWidget(acquisition_value_label_);
 0818  session_layout->addStretch(1);
 819
 0820  update_indicator_ = new QToolButton(session_bar);
 0821  update_indicator_->setObjectName("updateIndicator");
 0822  update_indicator_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
 0823  update_indicator_->setIcon(
 824      QIcon(QStringLiteral(":/resources/holovibes/assets/download_update.svg")));
 0825  update_indicator_->setIconSize(QSize(14, 14));
 0826  update_indicator_->setCursor(Qt::PointingHandCursor);
 0827  update_indicator_->setVisible(false);
 0828  update_indicator_->setToolTip(tr("Open the Holoflow release page"));
 0829  session_layout->addWidget(update_indicator_);
 0830  main_layout->addWidget(session_bar, 0);
 831
 0832  render_widget_ = new ImageRenderingWidget(this);
 0833  view_widget_   = new ViewWidget(this);
 0834  import_widget_ = new ImportWidget(this);
 0835  export_widget_ = new ExportWidget(this);
 836
 0837  auto *command_bar = new QFrame(central_widget);
 0838  command_bar->setObjectName("commandBar");
 0839  command_bar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
 0840  command_bar->setFixedHeight(kTopBarHeight);
 0841  auto *command_layout = new QHBoxLayout(command_bar);
 0842  command_layout->setContentsMargins(8, 2, 8, 2);
 0843  command_layout->setSpacing(6);
 844
 0845  start_command_button_       = new QPushButton("Start", command_bar);
 0846  stop_command_button_        = new QPushButton("Stop", command_bar);
 0847  record_command_button_      = new QPushButton("Record", command_bar);
 0848  stop_record_command_button_ = new QPushButton("Stop Rec", command_bar);
 0849  start_command_button_->setObjectName("primaryCommand");
 0850  record_command_button_->setObjectName("recordCommand");
 851
 0852  command_layout->addWidget(start_command_button_);
 0853  command_layout->addWidget(stop_command_button_);
 0854  command_layout->addSpacing(8);
 0855  command_layout->addWidget(record_command_button_);
 0856  command_layout->addWidget(stop_record_command_button_);
 0857  command_layout->addSpacing(16);
 858
 859  auto add_status_field = [&](const QString &label, QLabel *&value_label) {
 860    auto *name_label = new QLabel(label, command_bar);
 861    name_label->setObjectName("commandLabel");
 862    value_label = new QLabel(command_bar);
 863    value_label->setObjectName("commandValue");
 864    command_layout->addWidget(name_label);
 865    command_layout->addWidget(value_label);
 866    command_layout->addSpacing(12);
 0867  };
 868
 0869  add_status_field("Status", pipeline_status_label_);
 0870  add_status_field("Source", source_status_label_);
 0871  add_status_field("View", view_status_label_);
 0872  add_status_field("FPS", fps_status_label_);
 0873  add_status_field("Recording", recording_status_label_);
 0874  command_layout->addStretch(1);
 0875  main_layout->addWidget(command_bar, 0);
 876
 0877  connect(start_command_button_, &QPushButton::clicked, this, &MainWindow::on_import_start_clicked);
 0878  connect(stop_command_button_, &QPushButton::clicked, this, &MainWindow::on_import_stop_clicked);
 0879  connect(record_command_button_, &QPushButton::clicked, this,
 880          &MainWindow::on_export_record_clicked);
 0881  connect(stop_record_command_button_, &QPushButton::clicked, this,
 882          &MainWindow::on_export_stop_clicked);
 0883  connect(update_indicator_, &QToolButton::clicked, this, [this]() {
 884    if (available_update_url_.isValid()) {
 885      QDesktopServices::openUrl(available_update_url_);
 886    }
 887  });
 888
 0889  auto *content_row    = new QWidget(central_widget);
 0890  auto *content_layout = new QHBoxLayout(content_row);
 0891  content_layout->setContentsMargins(0, 0, 0, 0);
 0892  content_layout->setSpacing(8);
 0893  main_layout->addWidget(content_row, 1);
 894
 0895  auto *controls_content = new QWidget(content_row);
 0896  controls_content->setObjectName("controlsContent");
 0897  controls_content->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
 0898  auto *controls_layout = new QHBoxLayout(controls_content);
 0899  controls_layout->setContentsMargins(0, 0, 0, 0);
 0900  controls_layout->setSpacing(12);
 901
 0902  auto *acquisition_column = new QWidget(controls_content);
 0903  acquisition_column->setObjectName("controlsColumn");
 0904  auto *acquisition_layout = new QVBoxLayout(acquisition_column);
 0905  acquisition_layout->setContentsMargins(0, 0, 0, 0);
 0906  acquisition_layout->setSpacing(12);
 0907  acquisition_layout->addWidget(import_widget_);
 0908  acquisition_layout->addWidget(export_widget_);
 0909  acquisition_layout->addWidget(view_widget_);
 0910  acquisition_layout->addWidget(view_widget_->post_processing_group());
 0911  acquisition_layout->addStretch(1);
 912
 0913  auto *processing_column = new QWidget(controls_content);
 0914  processing_column->setObjectName("controlsColumn");
 0915  auto *processing_layout = new QVBoxLayout(processing_column);
 0916  processing_layout->setContentsMargins(0, 0, 0, 0);
 0917  processing_layout->setSpacing(12);
 0918  processing_layout->addWidget(render_widget_);
 0919  processing_layout->addWidget(render_widget_->autofocus_widget());
 0920  processing_layout->addStretch(1);
 921
 0922  auto *controls_divider = new QFrame(controls_content);
 0923  controls_divider->setObjectName("controlsColumnDivider");
 0924  controls_divider->setFixedWidth(1);
 0925  controls_divider->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
 926
 0927  controls_layout->addWidget(acquisition_column);
 0928  controls_layout->addWidget(controls_divider);
 0929  controls_layout->addWidget(processing_column);
 930
 0931  auto *controls_scroll = new QScrollArea(content_row);
 0932  controls_scroll->setObjectName("controlsScrollArea");
 0933  controls_scroll->viewport()->setObjectName("controlsScrollViewport");
 0934  controls_scroll->setWidgetResizable(true);
 0935  controls_scroll->setFrameShape(QFrame::NoFrame);
 0936  controls_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
 0937  controls_scroll->setWidget(controls_content);
 0938  const int controls_width =
 939      std::max(controls_content->sizeHint().width() +
 940                   controls_scroll->verticalScrollBar()->sizeHint().width() + 12,
 941               430);
 0942  controls_scroll->setFixedWidth(controls_width);
 0943  controls_scroll->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
 944
 0945  display_workspace_ = new VisualizationWorkspace(content_row);
 946
 0947  right_sidebar_ = new QWidget(content_row);
 0948  right_sidebar_->setObjectName("rightSidebar");
 0949  right_sidebar_->setMinimumWidth(280);
 0950  right_sidebar_->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);
 0951  auto *right_sidebar_layout = new QVBoxLayout(right_sidebar_);
 0952  right_sidebar_layout->setContentsMargins(0, 0, 0, 0);
 0953  right_sidebar_layout->setSpacing(8);
 954
 0955  monitor_widget_ = new SystemMonitorWidget(right_sidebar_);
 0956  monitor_widget_->setObjectName("systemMonitorPanel");
 0957  monitor_widget_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum);
 0958  selected_widget_settings_panel_ = new SelectedWidgetSettingsPanel(right_sidebar_);
 0959  right_sidebar_layout->addWidget(monitor_widget_);
 0960  right_sidebar_layout->addWidget(selected_widget_settings_panel_, 1);
 961
 0962  const int right_sidebar_width = std::max(280, right_sidebar_->sizeHint().width());
 0963  right_sidebar_->setFixedWidth(right_sidebar_width);
 964
 0965  content_layout->addWidget(controls_scroll, 0);
 0966  content_layout->addWidget(display_workspace_, 1);
 0967  content_layout->addWidget(right_sidebar_, 0);
 968
 0969  connect(patient_line_edit_, &QLineEdit::textChanged, this,
 970          &MainWindow::update_recording_path_preview);
 0971  connect(eye_side_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 972          [this](int) { update_recording_path_preview(); });
 0973}
 974
 0975void MainWindow::refresh_visualization_availability() {
 0976  if (display_workspace_ == nullptr || view_widget_ == nullptr || render_widget_ == nullptr) {
 0977    return;
 978  }
 979
 0980  auto      *autofocus         = render_widget_->autofocus_widget();
 0981  const bool autofocus_enabled = autofocus->is_enabled();
 0982  const bool has_zernike_order =
 983      autofocus->is_z2_enabled() || autofocus->is_z3_enabled() || autofocus->is_z4_enabled() ||
 984      autofocus->is_z5_enabled() || autofocus->is_z6_enabled() || autofocus->is_z7_enabled() ||
 985      autofocus->is_z8_enabled() || autofocus->is_z9_enabled() || autofocus->is_z10_enabled();
 0986  const bool single_reference_slopes = !autofocus->use_graph_laplacian();
 987
 0988  const auto autofocus_message = tr("Enable Auto Focus to generate this visualization.");
 0989  const auto zernike_order_message =
 990      tr("Select at least one Zernike coefficient to generate the metrics plot.");
 0991  const auto xcorr_message =
 992      tr("Cross-correlation is available only in Single reference slope recovery mode.");
 0993  const auto spectrum_message =
 994      tr("Spectrum visualizations are not generated by the current pipeline.");
 0995  const auto cuts_message =
 996      tr("3D cut visualizations are not currently supported by the pipeline.");
 997
 0998  display_workspace_->set_visualization_content_expectations({
 999      {"xy_processed", {true, {}}},
 1000      {"zernike_a4",
 1001       {autofocus_enabled && has_zernike_order,
 1002        autofocus_enabled ? zernike_order_message : autofocus_message}},
 1003      {"zernike_phase", {autofocus_enabled, autofocus_message}},
 1004      {"shack_hartmann", {autofocus_enabled, autofocus_message}},
 1005      {"shack_hartmann_xcorr",
 1006       {autofocus_enabled && single_reference_slopes,
 1007        autofocus_enabled ? xcorr_message : autofocus_message}},
 1008      {"xy_raw", {true, {}}},
 1009      {"raw_spectrum", {false, spectrum_message}},
 1010      {"processed_spectrum", {false, spectrum_message}},
 1011      {"xz_processed", {false, cuts_message}},
 1012      {"yz_processed", {false, cuts_message}},
 1013      {"pipeline_graph", {true, {}}},
 1014  });
 01015}
 1016
 01017void MainWindow::select_configurable_widget(ZernikeHistoryWidget *widget) {
 01018  if (widget == nullptr) {
 01019    selected_widget_settings_panel_->clear_selection();
 01020    return;
 1021  }
 1022
 01023  display_workspace_->select_visualization("zernike_a4");
 01024}
 1025
 01026void MainWindow::on_selected_visualization_changed(const QString &visualization_id) {
 01027  if (visualization_id == "zernike_a4") {
 01028    selected_widget_settings_panel_->set_selected_widget(zernike_history_widget_);
 01029  } else if (visualization_id.isEmpty()) {
 01030    selected_widget_settings_panel_->clear_selection();
 01031  } else {
 01032    selected_widget_settings_panel_->show_no_configurable_settings();
 1033  }
 01034}
 1035
 01036QString MainWindow::sanitize_recording_token(const QString &value) const {
 01037  QString token = value.trimmed();
 01038  if (token.isEmpty()) {
 01039    token = "PATIENT";
 1040  }
 1041
 01042  token.replace(QRegularExpression(R"(\s+)"), "_");
 01043  token.remove(QRegularExpression(R"([^A-Za-z0-9_-])"));
 01044  token = token.left(64);
 1045
 01046  if (token.isEmpty()) {
 01047    token = "PATIENT";
 1048  }
 1049
 01050  return token;
 01051}
 1052
 01053QString MainWindow::recording_file_name(int acquisition_id) const {
 01054  const QString patient = sanitize_recording_token(patient_line_edit_->text());
 01055  const QString eye     = sanitize_recording_token(eye_side_combo_->currentText());
 01056  return QString("%1_%2_%3_%4.holo")
 1057      .arg(patient, eye, session_id_, acquisition_label(acquisition_id));
 01058}
 1059
 01060QString MainWindow::acquisition_label(int acquisition_id) const {
 01061  return QString("AQ%1").arg(std::max(acquisition_id, 1), 3, 10, QChar('0'));
 01062}
 1063
 01064void MainWindow::update_acquisition_label() {
 01065  if (acquisition_value_label_ != nullptr) {
 01066    acquisition_value_label_->setText(acquisition_label(next_acquisition_id_));
 1067  }
 01068}
 1069
 01070void MainWindow::update_recording_path_preview() {
 01071  if (export_widget_ == nullptr) {
 01072    return;
 1073  }
 1074
 01075  QFileInfo info(export_widget_->get_file_path());
 01076  QString   directory = info.absolutePath();
 01077  if (directory.isEmpty() || directory == ".") {
 01078    directory = QDir::currentPath();
 1079  }
 1080
 01081  export_widget_->set_file_path(
 1082      QDir(directory).filePath(recording_file_name(next_acquisition_id_)));
 01083  update_acquisition_label();
 01084  refresh_command_bar();
 01085}
 1086
 01087void MainWindow::set_status_label(QLabel *label, const QString &text, const char *tone) {
 01088  if (label == nullptr) {
 01089    return;
 1090  }
 1091
 01092  label->setText(text);
 01093  label->setProperty("statusTone", QString::fromLatin1(tone));
 01094  label->style()->unpolish(label);
 01095  label->style()->polish(label);
 01096  label->update();
 01097}
 1098
 01099void MainWindow::configure_unsupported_features() {
 01100  if (view_widget_ == nullptr || render_widget_ == nullptr) {
 01101    return;
 1102  }
 1103
 01104  const QString registration_tooltip =
 1105      tr("Visible for planned support. The current pipeline does not support registration yet.");
 01106  const QString convolution_tooltip =
 1107      tr("Visible for planned support. The current pipeline does not support convolution kernels "
 1108         "yet.");
 1109
 1110  {
 01111    QSignalBlocker blocker(view_widget_->registration_check());
 01112    view_widget_->set_registration_enabled(false);
 01113  }
 01114  view_widget_->registration_check()->setEnabled(false);
 01115  view_widget_->registration_check()->setToolTip(registration_tooltip);
 01116  view_widget_->registration_radius()->setEnabled(false);
 01117  view_widget_->registration_radius()->setToolTip(registration_tooltip);
 1118
 1119  {
 01120    QSignalBlocker blocker(render_widget_->convolution_combo());
 01121    render_widget_->set_convolution(QStringLiteral("None"));
 01122  }
 1123  {
 01124    QSignalBlocker blocker(render_widget_->convolution_divide_check());
 01125    render_widget_->set_convolution_divide(false);
 01126  }
 01127  render_widget_->convolution_combo()->setEnabled(false);
 01128  render_widget_->convolution_combo()->setToolTip(convolution_tooltip);
 01129  render_widget_->convolution_divide_check()->setEnabled(false);
 01130  render_widget_->convolution_divide_check()->setToolTip(convolution_tooltip);
 01131  refresh_visualization_availability();
 01132}
 1133
 01134void MainWindow::refresh_command_bar() {
 1135  if (start_command_button_ == nullptr || import_widget_ == nullptr || render_widget_ == nullptr ||
 01136      view_widget_ == nullptr || export_widget_ == nullptr) {
 01137    return;
 1138  }
 1139
 01140  const QString primary_action = pipeline_running_  ? QStringLiteral("Pause")
 1141                                 : pipeline_paused_ ? QStringLiteral("Resume")
 1142                                                    : QStringLiteral("Start");
 01143  import_widget_->start_button()->setText(primary_action);
 01144  start_command_button_->setText(primary_action);
 01145  start_command_button_->setEnabled(import_widget_->start_button()->isEnabled());
 01146  stop_command_button_->setEnabled(import_widget_->stop_button()->isEnabled());
 01147  record_command_button_->setEnabled(export_widget_->record_button()->isEnabled());
 01148  stop_record_command_button_->setEnabled(export_widget_->stop_button()->isEnabled());
 1149
 01150  if (pause_requested_) {
 01151    set_status_label(pipeline_status_label_, "Pausing", "warning");
 01152  } else if (resume_in_progress_) {
 01153    set_status_label(pipeline_status_label_, "Resuming", "warning");
 01154  } else if (update_in_progress_) {
 01155    set_status_label(pipeline_status_label_, "Updating", "warning");
 01156  } else if (pipeline_running_) {
 01157    set_status_label(pipeline_status_label_, "Live", "success");
 01158  } else if (pipeline_paused_) {
 01159    set_status_label(pipeline_status_label_, "Paused", "warning");
 01160  } else {
 01161    set_status_label(pipeline_status_label_, "Idle", "muted");
 1162  }
 1163
 01164  QString source_text;
 01165  if (import_widget_->is_camera_mode()) {
 01166    source_text = import_widget_->get_camera_type();
 01167  } else {
 01168    const QFileInfo file_info(import_widget_->get_file_path());
 01169    source_text = file_info.fileName().isEmpty() ? QStringLiteral("File") : file_info.fileName();
 01170  }
 01171  source_status_label_->setText(source_text);
 1172
 01173  view_status_label_->setText(
 1174      QString("%1 %2").arg(render_widget_->get_image_mode(), view_widget_->get_image_type()));
 1175
 01176  if (!pipeline_running_) {
 01177    fps_status_label_->setText("--");
 1178  }
 1179
 01180  if (export_in_progress_) {
 01181    set_status_label(recording_status_label_, "Recording", "danger");
 01182  } else if (export_widget_->isChecked()) {
 01183    set_status_label(recording_status_label_, "Armed", "warning");
 01184  } else {
 01185    set_status_label(recording_status_label_, "Off", "muted");
 1186  }
 01187}
 1188
 01189void MainWindow::save_persistent_state() {
 01190  QSettings settings;
 1191
 01192  settings.beginGroup("main_window");
 01193  settings.setValue("geometry", saveGeometry());
 01194  settings.endGroup();
 1195
 01196  settings.beginGroup("session");
 01197  settings.setValue("patient", patient_line_edit_->text());
 01198  settings.setValue("eye_side", eye_side_combo_->currentText());
 01199  settings.endGroup();
 1200
 01201  settings.beginGroup("import");
 01202  settings.setValue("camera_mode", import_widget_->is_camera_mode());
 01203  settings.setValue("file_path", import_widget_->get_file_path());
 01204  settings.setValue("fps_limit", import_widget_->get_fps_limit().value_or(0));
 01205  settings.setValue("sampling_frequency_hz", import_widget_->get_sampling_frequency_hz());
 01206  settings.setValue("start_index", import_widget_->get_start_index());
 01207  settings.setValue("end_index", import_widget_->get_end_index());
 01208  settings.setValue("load_method", import_widget_->get_load_method());
 01209  settings.setValue("camera_type", import_widget_->get_camera_type());
 01210  settings.setValue("camera_config", import_widget_->get_camera_config());
 01211  settings.endGroup();
 1212
 01213  settings.beginGroup("rendering");
 01214  settings.setValue("image_mode", render_widget_->get_image_mode());
 01215  settings.setValue("batch_size", render_widget_->get_batch_size());
 01216  settings.setValue("time_stride", render_widget_->get_time_stride());
 01217  settings.setValue("filter_2d", render_widget_->is_filter_2d_enabled());
 01218  settings.setValue("filter_inner", render_widget_->get_filter_inner());
 01219  settings.setValue("filter_outer", render_widget_->get_filter_outer());
 01220  settings.setValue("space_transform", render_widget_->get_space_transform());
 01221  settings.setValue("time_transform", render_widget_->get_time_transform());
 01222  settings.setValue("time_window", render_widget_->get_time_window());
 01223  settings.setValue("lambda_nm", render_widget_->get_lambda());
 01224  settings.setValue("focus_mm", render_widget_->get_focus());
 01225  settings.setValue("asp_padding_enabled", render_widget_->is_asp_padding_enabled());
 01226  settings.setValue("asp_padded_width", render_widget_->get_asp_padded_width());
 01227  settings.setValue("asp_padded_height", render_widget_->get_asp_padded_height());
 01228  settings.setValue("convolution", render_widget_->get_convolution());
 01229  settings.setValue("convolution_divide", render_widget_->is_convolution_divide());
 01230  settings.endGroup();
 1231
 01232  settings.beginGroup("view");
 01233  settings.setValue("image_type", view_widget_->get_image_type());
 01234  settings.setValue("z_origin", view_widget_->get_z_origin());
 01235  settings.setValue("z_width", view_widget_->get_z_width());
 01236  settings.setValue("accumulation", view_widget_->get_accumulation());
 01237  settings.setValue("flatfield", view_widget_->is_flatfield_enabled());
 01238  settings.setValue("flatfield_cutoff_period_um", view_widget_->get_flatfield_cutoff_period_um());
 01239  settings.setValue("range_start", view_widget_->get_range_start());
 01240  settings.setValue("range_end", view_widget_->get_range_end());
 01241  settings.setValue("registration", view_widget_->is_registration_enabled());
 01242  settings.setValue("registration_radius", view_widget_->get_registration_radius());
 01243  settings.setValue("reticle", view_widget_->is_reticle_enabled());
 01244  settings.setValue("reticle_radius", view_widget_->get_reticle_radius());
 01245  settings.setValue("pct", view_widget_->is_pct_enabled());
 01246  settings.setValue("pct_radius", view_widget_->get_pct_radius());
 01247  settings.endGroup();
 1248
 01249  settings.beginGroup("export");
 01250  settings.setValue("enabled", export_widget_->isChecked());
 01251  settings.setValue("image_type", export_widget_->get_image_type());
 01252  settings.setValue("file_path", export_widget_->get_file_path());
 01253  settings.setValue("tag", export_widget_->get_tag());
 01254  settings.setValue("frame_count_enabled", export_widget_->is_frame_count_enabled());
 01255  settings.setValue("frame_count", export_widget_->get_frame_count());
 01256  settings.endGroup();
 1257
 01258  auto *autofocus = render_widget_->autofocus_widget();
 01259  settings.beginGroup("autofocus");
 01260  settings.setValue("enabled", autofocus->is_enabled());
 01261  settings.setValue("nb_subaps", autofocus->get_nb_subaps());
 01262  settings.setValue("nb_iter", autofocus->get_nb_iter());
 01263  settings.setValue("skip_subapertures_outside_pupil",
 1264                    autofocus->skip_subapertures_outside_pupil());
 01265  settings.setValue("use_graph_laplacian", autofocus->use_graph_laplacian());
 01266  settings.setValue("z2_enabled", autofocus->is_z2_enabled());
 01267  settings.setValue("z3_enabled", autofocus->is_z3_enabled());
 01268  settings.setValue("z4_enabled", autofocus->is_z4_enabled());
 01269  settings.setValue("z5_enabled", autofocus->is_z5_enabled());
 01270  settings.setValue("z6_enabled", autofocus->is_z6_enabled());
 01271  settings.setValue("z7_enabled", autofocus->is_z7_enabled());
 01272  settings.setValue("z8_enabled", autofocus->is_z8_enabled());
 01273  settings.setValue("z9_enabled", autofocus->is_z9_enabled());
 01274  settings.setValue("z10_enabled", autofocus->is_z10_enabled());
 01275  settings.endGroup();
 1276
 01277  settings.beginGroup("zernike_history_display");
 01278  const auto display_settings = zernike_history_widget_->display_settings();
 01279  settings.setValue("time_window_seconds", display_settings.time_window_seconds);
 01280  settings.setValue("y_scaling_mode", static_cast<int>(display_settings.y_scaling_mode));
 01281  settings.setValue("manual_y_minimum", display_settings.manual_y_minimum);
 01282  settings.setValue("manual_y_maximum", display_settings.manual_y_maximum);
 01283  settings.setValue("show_statistics", display_settings.show_statistics);
 01284  settings.endGroup();
 1285
 01286  display_workspace_->save_persistent_state(settings);
 1287
 01288  settings.sync();
 01289}
 1290
 01291void MainWindow::restore_persistent_state() {
 01292  QSettings settings;
 1293
 01294  settings.beginGroup("main_window");
 01295  if (settings.contains("geometry")) {
 01296    geometry_restored_ = restoreGeometry(settings.value("geometry").toByteArray());
 1297  }
 01298  settings.endGroup();
 1299
 01300  display_workspace_->restore_persistent_state(settings);
 01301  std::optional<bool> legacy_raw_view;
 01302  std::optional<bool> legacy_raw_spectrum;
 01303  std::optional<bool> legacy_processed_spectrum;
 01304  std::optional<bool> legacy_3d_cuts;
 01305  std::optional<bool> legacy_zernike_metrics;
 01306  std::optional<bool> legacy_zernike_phase;
 01307  std::optional<bool> legacy_shack_hartmann;
 01308  std::optional<bool> legacy_shack_hartmann_xcorr;
 1309
 01310  settings.beginGroup("session");
 01311  patient_line_edit_->setText(settings.value("patient", patient_line_edit_->text()).toString());
 01312  restore_combo_text(settings, "eye_side", eye_side_combo_);
 01313  settings.endGroup();
 1314
 01315  settings.beginGroup("import");
 01316  import_widget_->set_camera_mode(
 1317      settings.value("camera_mode", import_widget_->is_camera_mode()).toBool());
 01318  import_widget_->set_file_path(
 1319      settings.value("file_path", import_widget_->get_file_path()).toString());
 01320  import_widget_->set_fps_limit(settings.value("fps_limit", 0).toInt());
 01321  import_widget_->set_start_index(
 1322      settings.value("start_index", import_widget_->get_start_index()).toInt());
 01323  import_widget_->set_end_index(
 1324      settings.value("end_index", import_widget_->get_end_index()).toInt());
 01325  restore_combo_text(settings, "load_method", import_widget_->load_method_combo());
 01326  restore_combo_text(settings, "camera_type", import_widget_->camera_combo());
 01327  restore_combo_text(settings, "camera_config", import_widget_->camera_config_combo());
 01328  import_widget_->set_sampling_frequency_hz(
 1329      settings.value("sampling_frequency_hz", import_widget_->get_sampling_frequency_hz())
 1330          .toDouble());
 01331  settings.endGroup();
 1332
 01333  settings.beginGroup("rendering");
 01334  restore_combo_text(settings, "image_mode", render_widget_->image_combo());
 01335  render_widget_->set_batch_size(
 1336      settings.value("batch_size", render_widget_->get_batch_size()).toInt());
 01337  render_widget_->set_time_stride(
 1338      settings.value("time_stride", render_widget_->get_time_stride()).toInt());
 01339  render_widget_->set_filter_2d_enabled(
 1340      settings.value("filter_2d", render_widget_->is_filter_2d_enabled()).toBool());
 01341  render_widget_->set_filter_inner(
 1342      settings.value("filter_inner", render_widget_->get_filter_inner()).toInt());
 01343  render_widget_->set_filter_outer(
 1344      settings.value("filter_outer", render_widget_->get_filter_outer()).toInt());
 01345  restore_combo_text(settings, "space_transform", render_widget_->space_transform_combo());
 01346  restore_combo_text(settings, "time_transform", render_widget_->time_transform_combo());
 01347  render_widget_->set_time_window(
 1348      settings.value("time_window", render_widget_->get_time_window()).toInt());
 01349  render_widget_->set_lambda(settings.value("lambda_nm", render_widget_->get_lambda()).toInt());
 01350  render_widget_->set_focus(settings.value("focus_mm", render_widget_->get_focus()).toInt());
 01351  render_widget_->set_asp_padding_enabled(
 1352      settings.value("asp_padding_enabled", render_widget_->is_asp_padding_enabled()).toBool());
 01353  render_widget_->set_asp_padded_width(
 1354      settings.value("asp_padded_width", render_widget_->get_asp_padded_width()).toInt());
 01355  render_widget_->set_asp_padded_height(
 1356      settings.value("asp_padded_height", render_widget_->get_asp_padded_height()).toInt());
 01357  restore_combo_text(settings, "convolution", render_widget_->convolution_combo());
 01358  render_widget_->set_convolution_divide(
 1359      settings.value("convolution_divide", render_widget_->is_convolution_divide()).toBool());
 01360  settings.endGroup();
 1361
 01362  settings.beginGroup("view");
 01363  restore_combo_text(settings, "image_type", view_widget_->image_type_combo());
 01364  if (settings.contains("cuts_3d")) {
 01365    legacy_3d_cuts = settings.value("cuts_3d").toBool();
 1366  }
 01367  if (settings.contains("raw_view")) {
 01368    legacy_raw_view = settings.value("raw_view").toBool();
 1369  }
 01370  if (settings.contains("raw_spectrum")) {
 01371    legacy_raw_spectrum = settings.value("raw_spectrum").toBool();
 1372  }
 01373  if (settings.contains("processed_spectrum")) {
 01374    legacy_processed_spectrum = settings.value("processed_spectrum").toBool();
 1375  }
 01376  view_widget_->set_z_origin(settings.value("z_origin", view_widget_->get_z_origin()).toInt());
 01377  view_widget_->set_z_width(settings.value("z_width", view_widget_->get_z_width()).toInt());
 01378  view_widget_->set_accumulation(
 1379      settings.value("accumulation", view_widget_->get_accumulation()).toInt());
 01380  view_widget_->set_flatfield_enabled(
 1381      settings.value("flatfield", view_widget_->is_flatfield_enabled()).toBool());
 01382  view_widget_->set_flatfield_cutoff_period_um(
 1383      settings.value("flatfield_cutoff_period_um", view_widget_->get_flatfield_cutoff_period_um())
 1384          .toDouble());
 01385  view_widget_->range_start_spin()->setValue(
 1386      settings.value("range_start", view_widget_->get_range_start()).toInt());
 01387  view_widget_->range_end_spin()->setValue(
 1388      settings.value("range_end", view_widget_->get_range_end()).toInt());
 01389  view_widget_->set_registration_enabled(
 1390      settings.value("registration", view_widget_->is_registration_enabled()).toBool());
 01391  view_widget_->registration_radius()->setValue(
 1392      settings.value("registration_radius", view_widget_->get_registration_radius()).toDouble());
 01393  view_widget_->reticle_check()->setChecked(
 1394      settings.value("reticle", view_widget_->is_reticle_enabled()).toBool());
 01395  view_widget_->reticle_radius()->setValue(
 1396      settings.value("reticle_radius", view_widget_->get_reticle_radius()).toDouble());
 01397  view_widget_->set_pct_enabled(settings.value("pct", view_widget_->is_pct_enabled()).toBool());
 01398  view_widget_->set_pct_radius(
 1399      settings.value("pct_radius", view_widget_->get_pct_radius()).toDouble());
 01400  settings.endGroup();
 1401
 01402  settings.beginGroup("export");
 01403  export_widget_->setChecked(settings.value("enabled", export_widget_->isChecked()).toBool());
 01404  restore_combo_text(settings, "image_type", export_widget_->image_type_combo());
 01405  export_widget_->set_file_path(
 1406      settings.value("file_path", export_widget_->get_file_path()).toString());
 01407  restore_combo_text(settings, "tag", export_widget_->tag_combo());
 01408  export_widget_->frames_check()->setChecked(
 1409      settings.value("frame_count_enabled", export_widget_->is_frame_count_enabled()).toBool());
 01410  export_widget_->set_frame_count(
 1411      settings.value("frame_count", export_widget_->get_frame_count()).toInt());
 01412  settings.endGroup();
 1413
 01414  auto *autofocus = render_widget_->autofocus_widget();
 01415  settings.beginGroup("autofocus");
 01416  autofocus->nb_subaps_spin()->setValue(
 1417      settings.value("nb_subaps", autofocus->get_nb_subaps()).toInt());
 01418  autofocus->nb_iter_spin()->setValue(settings.value("nb_iter", autofocus->get_nb_iter()).toInt());
 01419  autofocus->set_skip_subapertures_outside_pupil(
 1420      settings
 1421          .value("skip_subapertures_outside_pupil", autofocus->skip_subapertures_outside_pupil())
 1422          .toBool());
 01423  autofocus->set_use_graph_laplacian(
 1424      settings.value("use_graph_laplacian", autofocus->use_graph_laplacian()).toBool());
 01425  autofocus->set_z2_enabled(settings.value("z2_enabled", autofocus->is_z2_enabled()).toBool());
 01426  autofocus->set_z3_enabled(settings.value("z3_enabled", autofocus->is_z3_enabled()).toBool());
 01427  autofocus->set_z4_enabled(settings.value("z4_enabled", autofocus->is_z4_enabled()).toBool());
 01428  autofocus->set_z5_enabled(settings.value("z5_enabled", autofocus->is_z5_enabled()).toBool());
 01429  autofocus->set_z6_enabled(settings.value("z6_enabled", autofocus->is_z6_enabled()).toBool());
 01430  autofocus->set_z7_enabled(settings.value("z7_enabled", autofocus->is_z7_enabled()).toBool());
 01431  autofocus->set_z8_enabled(settings.value("z8_enabled", autofocus->is_z8_enabled()).toBool());
 01432  autofocus->set_z9_enabled(settings.value("z9_enabled", autofocus->is_z9_enabled()).toBool());
 01433  autofocus->set_z10_enabled(settings.value("z10_enabled", autofocus->is_z10_enabled()).toBool());
 01434  if (settings.contains("show_zernike_metrics")) {
 01435    legacy_zernike_metrics = settings.value("show_zernike_metrics").toBool();
 1436  }
 01437  if (settings.contains("show_phase")) {
 01438    legacy_zernike_phase = settings.value("show_phase").toBool();
 1439  }
 01440  if (settings.contains("show_shack_hartmann")) {
 01441    legacy_shack_hartmann = settings.value("show_shack_hartmann").toBool();
 1442  }
 01443  if (settings.contains("show_xcorr")) {
 01444    legacy_shack_hartmann_xcorr = settings.value("show_xcorr").toBool();
 1445  }
 01446  autofocus->set_enabled(settings.value("enabled", autofocus->is_enabled()).toBool());
 01447  settings.endGroup();
 1448
 01449  settings.beginGroup("zernike_history_display");
 01450  auto display_settings = zernike_history_widget_->display_settings();
 01451  display_settings.time_window_seconds =
 1452      settings.value("time_window_seconds", display_settings.time_window_seconds).toDouble();
 01453  const int scaling_mode =
 1454      settings.value("y_scaling_mode", static_cast<int>(display_settings.y_scaling_mode)).toInt();
 01455  if (scaling_mode >= static_cast<int>(YAxisScalingMode::VisibleWindow) &&
 1456      scaling_mode <= static_cast<int>(YAxisScalingMode::Manual)) {
 01457    display_settings.y_scaling_mode = static_cast<YAxisScalingMode>(scaling_mode);
 1458  }
 01459  display_settings.manual_y_minimum =
 1460      settings.value("manual_y_minimum", display_settings.manual_y_minimum).toDouble();
 01461  display_settings.manual_y_maximum =
 1462      settings.value("manual_y_maximum", display_settings.manual_y_maximum).toDouble();
 01463  display_settings.show_statistics =
 1464      settings.value("show_statistics", display_settings.show_statistics).toBool();
 01465  zernike_history_widget_->set_display_settings(display_settings);
 01466  signal_plot_time_window_seconds_ =
 1467      zernike_history_widget_->display_settings().time_window_seconds;
 01468  settings.endGroup();
 1469
 1470  const auto migrate_legacy_view_preference = [this, &settings](const QString      &id,
 1471                                                                std::optional<bool> legacy_value) {
 1472    if (!legacy_value.has_value()) {
 1473      return;
 1474    }
 1475
 1476    const QString workspace_key =
 1477        QStringLiteral("visualization_workspace/view_preferences/%1").arg(id);
 1478    if (!settings.contains(workspace_key)) {
 1479      display_workspace_->set_visualization_enabled(id, *legacy_value);
 1480    }
 01481  };
 01482  migrate_legacy_view_preference(QStringLiteral("xy_raw"), legacy_raw_view);
 01483  migrate_legacy_view_preference(QStringLiteral("raw_spectrum"), legacy_raw_spectrum);
 01484  migrate_legacy_view_preference(QStringLiteral("processed_spectrum"), legacy_processed_spectrum);
 01485  migrate_legacy_view_preference(QStringLiteral("xz_processed"), legacy_3d_cuts);
 01486  migrate_legacy_view_preference(QStringLiteral("yz_processed"), legacy_3d_cuts);
 01487  migrate_legacy_view_preference(QStringLiteral("zernike_a4"), legacy_zernike_metrics);
 01488  migrate_legacy_view_preference(QStringLiteral("zernike_phase"), legacy_zernike_phase);
 01489  migrate_legacy_view_preference(QStringLiteral("shack_hartmann"), legacy_shack_hartmann);
 01490  migrate_legacy_view_preference(QStringLiteral("shack_hartmann_xcorr"),
 1491                                 legacy_shack_hartmann_xcorr);
 1492
 01493  update_recording_path_preview();
 01494  refresh_visualization_availability();
 01495  display_workspace_->set_pipeline_running(false);
 01496}
 1497
 01498void MainWindow::initialize_display_widgets() {
 01499  xy_raw_widget_               = new TensorDisplayWidget(display_workspace_);
 01500  xy_processed_widget_         = new TensorDisplayWidget(display_workspace_);
 01501  xz_processed_widget_         = new TensorDisplayWidget(display_workspace_);
 01502  yz_processed_widget_         = new TensorDisplayWidget(display_workspace_);
 01503  raw_spectrum_widget_         = new TensorDisplayWidget(display_workspace_);
 01504  processed_spectrum_widget_   = new TensorDisplayWidget(display_workspace_);
 01505  shack_hartmann_widget_       = new TensorDisplayWidget(display_workspace_);
 01506  shack_hartmann_xcorr_widget_ = new TensorDisplayWidget(display_workspace_);
 01507  zernike_phase_widget_        = new TensorDisplayWidget(display_workspace_);
 01508  zernike_history_widget_      = new ZernikeHistoryWidget(display_workspace_);
 01509  graph_visualizer_widget_     = new GraphVisualizerWidget(display_workspace_);
 1510
 01511  zernike_phase_widget_->set_colormap(Colormap::Twilight);
 01512  zernike_phase_widget_->set_value_range(0.0f, 2 * static_cast<float>(M_PI));
 1513
 01514  display_workspace_->register_visualization(
 1515      {"xy_processed", "XY Processed", xy_processed_widget_, {}, DockPlacement::Root});
 01516  display_workspace_->register_visualization({"zernike_a4", "Zernike Metrics",
 1517                                              zernike_history_widget_, "xy_processed",
 1518                                              DockPlacement::Right});
 01519  display_workspace_->register_visualization({"zernike_phase", "Zernike Phase",
 1520                                              zernike_phase_widget_, "xy_processed",
 1521                                              DockPlacement::Below, false});
 01522  display_workspace_->register_visualization({"shack_hartmann", "Shack Hartmann",
 1523                                              shack_hartmann_widget_, "zernike_phase",
 1524                                              DockPlacement::Right, false});
 01525  display_workspace_->register_visualization({"shack_hartmann_xcorr", "Shack Hartmann XCorr",
 1526                                              shack_hartmann_xcorr_widget_, "shack_hartmann",
 1527                                              DockPlacement::Right, false});
 1528
 1529  // Optional current displays use the same registry and visibility model. They default to tabs so
 1530  // enabling one does not disturb the requested five-display hierarchy.
 01531  display_workspace_->register_visualization(
 1532      {"xy_raw", "XY Raw", xy_raw_widget_, "xy_processed", DockPlacement::Tab, false});
 01533  display_workspace_->register_visualization({"raw_spectrum", "Raw Spectrum", raw_spectrum_widget_,
 1534                                              "zernike_phase", DockPlacement::Tab, false});
 01535  display_workspace_->register_visualization({"processed_spectrum", "Processed Spectrum",
 1536                                              processed_spectrum_widget_, "raw_spectrum",
 1537                                              DockPlacement::Tab, false});
 01538  display_workspace_->register_visualization({"xz_processed", "XZ Processed", xz_processed_widget_,
 1539                                              "xy_processed", DockPlacement::Tab, false});
 01540  display_workspace_->register_visualization({"yz_processed", "YZ Processed", yz_processed_widget_,
 1541                                              "xy_processed", DockPlacement::Tab, false});
 01542  display_workspace_->register_visualization({"pipeline_graph", "Pipeline Graph",
 1543                                              graph_visualizer_widget_, "xy_processed",
 1544                                              DockPlacement::Tab, false});
 01545  display_workspace_->finalize_registration();
 1546
 01547  connect(display_workspace_, &VisualizationWorkspace::selected_visualization_changed, this,
 1548          &MainWindow::on_selected_visualization_changed);
 01549  connect(zernike_history_widget_, &ZernikeHistoryWidget::configuration_requested, this,
 1550          &MainWindow::select_configurable_widget);
 01551  connect(zernike_history_widget_, &ZernikeHistoryWidget::display_settings_changed, this,
 1552          [this](const ZernikeHistoryDisplaySettings &settings) {
 1553            // Axis policy is GUI-only. Keeping this out of update_if_running avoids rebuilding or
 1554            // restarting the processing pipeline for display changes.
 1555            signal_plot_time_window_seconds_ = settings.time_window_seconds;
 1556          });
 01557  connect(graph_visualizer_widget_, &GraphVisualizerWidget::reload_requested, this,
 1558          &MainWindow::show_pipeline_graph);
 01559  connect(display_workspace_, &VisualizationWorkspace::visualization_preferences_changed, this,
 1560          &MainWindow::refresh_visualization_availability);
 1561
 01562  connect(render_widget_, &ImageRenderingWidget::settings_changed, this,
 1563          &MainWindow::refresh_visualization_availability);
 1564
 01565  connect(view_widget_, &ViewWidget::reticle_toggled, this, [this](bool checked) {
 1566    xy_processed_widget_->set_reticle_enabled(checked);
 1567    if (checked) {
 1568      xy_processed_widget_->set_reticle_radius(view_widget_->get_reticle_radius());
 1569    }
 1570  });
 01571  connect(view_widget_, &ViewWidget::reticle_radius_changed, this, [this](double value) {
 1572    if (view_widget_->is_reticle_enabled()) {
 1573      xy_processed_widget_->set_reticle_radius(value);
 1574    }
 1575  });
 01576}
 1577
 01578void MainWindow::initialize_pipeline_manager() {
 01579  pipeline_manager_ = new pipeline::Manager(
 1580      render_widget_->autofocus_widget(), xy_processed_widget_, xz_processed_widget_,
 1581      yz_processed_widget_, xy_raw_widget_, raw_spectrum_widget_, processed_spectrum_widget_,
 1582      shack_hartmann_widget_, shack_hartmann_xcorr_widget_, zernike_phase_widget_,
 1583      zernike_history_widget_);
 01584  pipeline_manager_thread_ = new QThread(this);
 01585  pipeline_manager_->moveToThread(pipeline_manager_thread_);
 01586  pipeline_manager_thread_->start();
 01587}
 1588
 01589void MainWindow::connect_manager_signals() {
 01590  connect(pipeline_manager_, &pipeline::Manager::start_pipeline_success, this,
 1591          &MainWindow::on_start_pipeline_success);
 1592
 01593  connect(pipeline_manager_, &pipeline::Manager::start_pipeline_failure, this,
 1594          &MainWindow::on_start_pipeline_failure);
 1595
 01596  connect(pipeline_manager_, &pipeline::Manager::stop_pipeline_success, this,
 1597          &MainWindow::on_stop_pipeline_success);
 1598
 01599  connect(pipeline_manager_, &pipeline::Manager::stop_pipeline_failure, this,
 1600          &MainWindow::on_stop_pipeline_failure);
 1601
 01602  connect(pipeline_manager_, &pipeline::Manager::resume_pipeline_success, this,
 1603          &MainWindow::on_resume_pipeline_success);
 1604
 01605  connect(pipeline_manager_, &pipeline::Manager::resume_pipeline_failure, this,
 1606          &MainWindow::on_resume_pipeline_failure);
 1607
 01608  connect(pipeline_manager_, &pipeline::Manager::update_pipeline_success, this,
 1609          &MainWindow::on_update_pipeline_success);
 1610
 01611  connect(pipeline_manager_, &pipeline::Manager::update_pipeline_failure, this,
 1612          &MainWindow::on_update_pipeline_failure);
 1613
 01614  connect(pipeline_manager_, &pipeline::Manager::metrics_updated, this,
 1615          &MainWindow::on_metrics_updated, Qt::QueuedConnection);
 1616
 01617  connect(pipeline_manager_, &pipeline::Manager::raw_record_started_success, this,
 1618          &MainWindow::on_raw_record_started_success, Qt::QueuedConnection);
 1619
 01620  connect(pipeline_manager_, &pipeline::Manager::raw_record_started_failure, this,
 1621          &MainWindow::on_raw_record_started_failure, Qt::QueuedConnection);
 1622
 01623  connect(pipeline_manager_, &pipeline::Manager::raw_record_stopped_success, this,
 1624          &MainWindow::on_raw_record_stopped_success, Qt::QueuedConnection);
 1625
 01626  connect(pipeline_manager_, &pipeline::Manager::raw_record_stopped_failure, this,
 1627          &MainWindow::on_raw_record_stopped_failure, Qt::QueuedConnection);
 1628
 01629  connect(pipeline_manager_, &pipeline::Manager::graph_visualization_ready, this,
 1630          [this](const QString &dot) {
 1631            if (graph_visualizer_widget_ != nullptr) {
 1632              graph_visualizer_widget_->render_dot(dot);
 1633            }
 1634          });
 01635  connect(pipeline_manager_, &pipeline::Manager::graph_visualization_failed, this,
 1636          [this](const QString &error) {
 1637            if (graph_visualizer_widget_ != nullptr) {
 1638              graph_visualizer_widget_->show_error(error);
 1639            }
 1640          });
 1641
 01642}
 1643
 01644void MainWindow::connect_import_controls() {
 01645  connect(import_widget_, &ImportWidget::start_clicked, this, &MainWindow::on_import_start_clicked);
 01646  connect(import_widget_, &ImportWidget::stop_clicked, this, &MainWindow::on_import_stop_clicked);
 01647  connect(import_widget_, &ImportWidget::browse_clicked, this, [=]() {
 1648    QString file = QFileDialog::getOpenFileName(this, tr("Select File"));
 1649    if (file.isEmpty()) {
 1650      return;
 1651    }
 1652    try {
 1653      auto reader      = holofile::Reader(file.toStdString());
 1654      auto frame_count = reader.header().frame_count;
 1655
 1656      import_widget_->set_file_path(file);
 1657      import_widget_->set_end_index_range(0, static_cast<int>(frame_count));
 1658      import_widget_->set_end_index(static_cast<int>(frame_count));
 1659      import_widget_->set_start_index(0);
 1660
 1661      if (reader.footer().has_value()) {
 1662        auto               footer        = reader.footer().value();
 1663        pipeline::Settings prev_settings = get_pipeline_settings();
 1664        prev_settings.view_type          = pipeline::ViewType::PROCESSED;
 1665        pipeline::Settings new_settings =
 1666            pipeline::old_json_to_settings(footer.pipeline_settings, prev_settings);
 1667        set_pipeline_settings(new_settings);
 1668      }
 1669    } catch (std::exception &e) {
 1670      logger()->error("failed to open \"{}\": \"{}\"", file.toStdString(), e.what());
 1671    }
 1672  });
 01673}
 1674
 01675void MainWindow::connect_export_controls() {
 1676
 01677  connect(export_widget_, &ExportWidget::record_clicked, this,
 1678          &MainWindow::on_export_record_clicked);
 01679  connect(export_widget_, &ExportWidget::stop_clicked, this, &MainWindow::on_export_stop_clicked);
 01680  connect(export_widget_, &ExportWidget::browse_clicked, this, [=]() {
 1681    QFileInfo current_info(export_widget_->get_file_path());
 1682    QString   initial_directory = current_info.absolutePath();
 1683    if (initial_directory.isEmpty() || initial_directory == ".") {
 1684      initial_directory = QDir::currentPath();
 1685    }
 1686
 1687    QString directory = QFileDialog::getExistingDirectory(this, tr("Select Recording Directory"),
 1688                                                          initial_directory);
 1689    if (!directory.isEmpty()) {
 1690      export_widget_->set_file_path(
 1691          QDir(directory).filePath(recording_file_name(next_acquisition_id_)));
 1692    }
 1693  });
 01694}
 1695
 01696void MainWindow::configure_window() {
 01697  setWindowTitle("Holovibes");
 1698
 01699  auto *file_menu   = menuBar()->addMenu(tr("&File"));
 01700  auto *preferences = file_menu->addAction(tr("Preferences"));
 01701  preferences->setShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+P")));
 01702  preferences->setShortcutContext(Qt::ApplicationShortcut);
 01703  connect(preferences, &QAction::triggered, this, &MainWindow::show_preferences);
 1704
 01705  auto *view_menu = menuBar()->addMenu(tr("&View"));
 01706  display_workspace_->populate_view_menu(view_menu);
 01707  view_menu->addSeparator();
 01708  auto *debug_menu              = view_menu->addMenu(tr("Debug"));
 01709  auto *graph_visualizer_action = debug_menu->addAction(tr("Open Pipeline Graph..."));
 01710  connect(graph_visualizer_action, &QAction::triggered, this, &MainWindow::show_pipeline_graph);
 01711  auto *open_dot_action = debug_menu->addAction(tr("Open DOT File..."));
 01712  connect(open_dot_action, &QAction::triggered, this, &MainWindow::open_dot_file);
 1713
 01714  auto *tools_menu      = menuBar()->addMenu(tr("&Tools"));
 01715  auto *fft_tool_action = tools_menu->addAction(tr("FFT Frequency Range to Bins..."));
 01716  fft_tool_action->setShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+F")));
 01717  fft_tool_action->setShortcutContext(Qt::ApplicationShortcut);
 01718  connect(fft_tool_action, &QAction::triggered, this, &MainWindow::show_fft_frequency_tool);
 1719
 01720  const QSize minimum_size(960, 640);
 01721  const QSize default_size = minimum_size.expandedTo(QSize(1280, 800));
 01722  setMinimumSize(minimum_size);
 01723  if (!geometry_restored_) {
 01724    resize(default_size);
 1725  }
 01726}
 1727
 01728void MainWindow::check_for_updates() {
 01729  update_checker_ = new UpdateChecker(QCoreApplication::applicationVersion(), this);
 01730  connect(update_checker_, &UpdateChecker::update_available, this,
 1731          [this](const QString &version, const QUrl &page_url) {
 1732            available_update_url_ = page_url;
 1733            update_indicator_->setText(tr("Update available  v%1").arg(version));
 1734            update_indicator_->setVisible(true);
 1735          });
 01736  QTimer::singleShot(0, update_checker_, &UpdateChecker::start);
 01737}
 1738
 01739void MainWindow::show_pipeline_graph() {
 01740  graph_visualizer_widget_->set_reload_enabled(true);
 01741  display_workspace_->set_visualization_title(QStringLiteral("pipeline_graph"),
 1742                                              tr("Pipeline Graph"));
 01743  display_workspace_->set_visualization_enabled(QStringLiteral("pipeline_graph"), true);
 01744  display_workspace_->select_visualization(QStringLiteral("pipeline_graph"));
 1745
 01746  auto request = [manager = pipeline_manager_]() { manager->request_compiled_graph_visualization(); };
 01747  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, std::move(request),
 1748                                             Qt::QueuedConnection));
 01749}
 1750
 01751void MainWindow::open_dot_file() {
 01752  const QString path = QFileDialog::getOpenFileName(
 1753      this, tr("Open Graphviz DOT File"), {}, tr("Graphviz DOT Files (*.dot);;All Files (*)"));
 01754  if (path.isEmpty()) {
 01755    return;
 1756  }
 1757
 01758  QFile dot_file(path);
 01759  if (!dot_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
 01760    QMessageBox::warning(this, tr("Open Graphviz DOT File"),
 1761                         tr("Could not open '%1': %2").arg(path, dot_file.errorString()));
 01762    return;
 1763  }
 1764
 01765  display_workspace_->set_visualization_title(
 1766      QStringLiteral("pipeline_graph"), tr("Pipeline Graph â€” %1").arg(QFileInfo(path).fileName()));
 01767  graph_visualizer_widget_->set_reload_enabled(false);
 01768  display_workspace_->set_visualization_enabled(QStringLiteral("pipeline_graph"), true);
 01769  display_workspace_->select_visualization(QStringLiteral("pipeline_graph"));
 01770  graph_visualizer_widget_->render_dot(QString::fromUtf8(dot_file.readAll()));
 01771}
 1772
 01773void MainWindow::show_fft_frequency_tool() {
 01774  const double sampling_frequency_hz = import_widget_->get_sampling_frequency_hz();
 01775  const int    window_size           = std::max(1, render_widget_->get_time_window());
 01776  const double bin_spacing_hz        = sampling_frequency_hz / static_cast<double>(window_size);
 01777  const double start_frequency_hz    = view_widget_->get_z_origin() * bin_spacing_hz;
 01778  const double end_frequency_hz =
 1779      (view_widget_->get_z_origin() + view_widget_->get_z_width()) * bin_spacing_hz;
 1780
 01781  FftFrequencyToolDialog dialog(
 1782      sampling_frequency_hz, window_size, start_frequency_hz, end_frequency_hz,
 1783      render_widget_->get_time_transform(),
 1784      [this](int z_start, int z_width) {
 1785        if (view_widget_ != nullptr) {
 1786          view_widget_->set_z_origin(z_start);
 1787          view_widget_->set_z_width(z_width);
 1788        }
 1789      },
 1790      this);
 01791  dialog.exec();
 01792}
 1793
 01794void MainWindow::show_preferences() {
 01795  PreferencesDialog dialog(this, *pipeline_manager_);
 01796  dialog.exec();
 01797}
 1798
 01799std::filesystem::path MainWindow::makeRecordingPath(const QString &userText) {
 1800  namespace fs = std::filesystem;
 1801
 01802  QFileInfo info(userText);
 1803
 01804  fs::path dir = info.absolutePath().isEmpty() ? fs::current_path()
 1805                                               : fs::path(info.absolutePath().toStdString());
 1806
 01807  int      acquisition_id = next_acquisition_id_;
 01808  QString  finalFileName  = recording_file_name(acquisition_id);
 01809  fs::path candidate      = dir / finalFileName.toStdString();
 1810
 1811  // Add suffix if needed
 01812  while (fs::exists(candidate)) {
 01813    ++acquisition_id;
 01814    finalFileName = recording_file_name(acquisition_id);
 01815    candidate     = dir / finalFileName.toStdString();
 01816  }
 1817
 01818  pending_recording_acquisition_id_ = acquisition_id;
 01819  QSignalBlocker blocker(export_widget_->file_line_edit());
 01820  export_widget_->set_file_path(QString::fromStdString(candidate.string()));
 1821
 01822  logger()->info("[MainWindow::makeRecordingPath] Generated recording path: {}",
 1823                 candidate.string());
 1824
 01825  return candidate;
 01826}
 1827
 01828void MainWindow::show_pipeline_error_popup(const QString &message) {
 01829  QMessageBox msgBox(this);
 01830  msgBox.setIcon(QMessageBox::Critical);
 01831  msgBox.setWindowTitle(tr("Pipeline Error"));
 01832  msgBox.setText(message);
 01833  msgBox.setStandardButtons(QMessageBox::Ok);
 01834  msgBox.setDefaultButton(QMessageBox::Ok);
 01835  msgBox.exec();
 01836}
 1837
 01838void MainWindow::on_start_pipeline_success() {
 01839  logger()->info("[MainWindow::on_start_pipeline_success]");
 01840  pipeline_running_      = true;
 01841  pipeline_paused_       = false;
 01842  pause_requested_       = false;
 01843  resume_in_progress_    = false;
 01844  paused_settings_dirty_ = false;
 01845  import_widget_->set_start_enabled(true);
 01846  import_widget_->set_stop_enabled(true);
 01847  export_widget_->set_record_enabled(!export_in_progress_ && export_widget_->isChecked());
 01848  export_widget_->set_stop_enabled(export_in_progress_);
 1849
 01850  auto dims = guess_source_dims();
 01851  xy_raw_widget_->set_fixed_aspect(dims);
 01852  dims = processed_display_dims(dims);
 01853  view_widget_->set_xy_extent(dims.width(), dims.height());
 01854  xy_processed_widget_->set_fixed_aspect(dims);
 01855  xy_raw_widget_->set_fixed_aspect(guess_source_dims());
 01856  shack_hartmann_widget_->set_fixed_aspect(dims);
 01857  shack_hartmann_xcorr_widget_->set_fixed_aspect(dims);
 01858  zernike_phase_widget_->set_fixed_aspect(guess_source_dims());
 1859
 01860  auto *autofocus_widget = render_widget_->autofocus_widget();
 01861  if (autofocus_widget->is_enabled()) {
 01862    const bool has_enabled_zernike =
 1863        autofocus_widget->is_z2_enabled() || autofocus_widget->is_z3_enabled() ||
 1864        autofocus_widget->is_z4_enabled() || autofocus_widget->is_z5_enabled() ||
 1865        autofocus_widget->is_z6_enabled() || autofocus_widget->is_z7_enabled() ||
 1866        autofocus_widget->is_z8_enabled() || autofocus_widget->is_z9_enabled() ||
 1867        autofocus_widget->is_z10_enabled();
 01868    if (!has_enabled_zernike) {
 01869      autofocus_widget->reset_zernike_values();
 1870    }
 01871  } else {
 01872    autofocus_widget->reset_zernike_values();
 1873  }
 01874  refresh_visualization_availability();
 01875  display_workspace_->set_pipeline_running(true);
 01876  refresh_command_bar();
 01877}
 1878
 01879void MainWindow::on_start_pipeline_failure(const QString &error) {
 01880  logger()->error("[MainWindow::on_start_pipeline_failure]");
 01881  pipeline_running_      = false;
 01882  pipeline_paused_       = false;
 01883  pause_requested_       = false;
 01884  resume_in_progress_    = false;
 01885  paused_settings_dirty_ = false;
 01886  export_in_progress_    = false;
 01887  import_widget_->set_start_enabled(true);
 01888  import_widget_->set_stop_enabled(false);
 01889  export_widget_->set_record_enabled(false);
 01890  export_widget_->set_stop_enabled(false);
 01891  display_workspace_->set_pipeline_running(false);
 01892  refresh_command_bar();
 1893
 01894  show_pipeline_error_popup(tr("An error occurred while starting the pipeline:\n%1").arg(error));
 01895}
 1896
 01897void MainWindow::on_stop_pipeline_success() {
 01898  logger()->info("[MainWindow::on_stop_pipeline_success]");
 01899  if (pause_requested_) {
 01900    pipeline_running_   = false;
 01901    pipeline_paused_    = true;
 01902    pause_requested_    = false;
 01903    export_in_progress_ = false;
 01904    import_widget_->set_start_enabled(true);
 01905    import_widget_->set_stop_enabled(true);
 01906    export_widget_->set_record_enabled(false);
 01907    export_widget_->set_stop_enabled(false);
 01908    on_metrics_updated(0.0);
 01909    refresh_command_bar();
 01910    return;
 1911  }
 1912
 01913  reset_pipeline_ui_after_stop();
 01914}
 1915
 01916void MainWindow::on_stop_pipeline_failure(const QString &error) {
 01917  logger()->error("[MainWindow::on_stop_pipeline_failure]");
 01918  if (pause_requested_) {
 01919    pipeline_running_   = false;
 01920    pipeline_paused_    = true;
 01921    pause_requested_    = false;
 01922    export_in_progress_ = false;
 01923    import_widget_->set_start_enabled(true);
 01924    import_widget_->set_stop_enabled(true);
 01925    export_widget_->set_record_enabled(false);
 01926    export_widget_->set_stop_enabled(false);
 01927    on_metrics_updated(0.0);
 01928    refresh_command_bar();
 01929  } else {
 01930    reset_pipeline_ui_after_stop();
 1931  }
 1932
 01933  show_pipeline_error_popup(error);
 01934}
 1935
 01936void MainWindow::on_resume_pipeline_success() {
 01937  logger()->info("[MainWindow::on_resume_pipeline_success]");
 01938  pipeline_running_   = true;
 01939  pipeline_paused_    = false;
 01940  resume_in_progress_ = false;
 01941  import_widget_->set_start_enabled(true);
 01942  import_widget_->set_stop_enabled(true);
 01943  export_widget_->set_record_enabled(!export_in_progress_ && export_widget_->isChecked());
 01944  export_widget_->set_stop_enabled(export_in_progress_);
 01945  refresh_visualization_availability();
 01946  display_workspace_->set_pipeline_running(true);
 01947  refresh_command_bar();
 01948}
 1949
 01950void MainWindow::on_resume_pipeline_failure(const QString &error) {
 01951  logger()->error("[MainWindow::on_resume_pipeline_failure]");
 01952  pipeline_running_   = false;
 01953  pipeline_paused_    = true;
 01954  resume_in_progress_ = false;
 01955  export_in_progress_ = false;
 01956  import_widget_->set_start_enabled(true);
 01957  import_widget_->set_stop_enabled(true);
 01958  export_widget_->set_record_enabled(false);
 01959  export_widget_->set_stop_enabled(false);
 01960  refresh_command_bar();
 1961
 01962  show_pipeline_error_popup(tr("An error occurred while resuming the pipeline:\n%1").arg(error));
 01963}
 1964
 01965void MainWindow::reset_pipeline_ui_after_stop() {
 01966  pipeline_running_      = false;
 01967  pipeline_paused_       = false;
 01968  pause_requested_       = false;
 01969  resume_in_progress_    = false;
 01970  paused_settings_dirty_ = false;
 01971  export_in_progress_    = false;
 01972  import_widget_->set_start_enabled(true);
 01973  import_widget_->set_stop_enabled(false);
 01974  export_widget_->set_record_enabled(false);
 01975  export_widget_->set_stop_enabled(false);
 01976  on_metrics_updated(0.0);
 1977
 01978  display_workspace_->set_pipeline_running(false);
 01979  render_widget_->autofocus_widget()->reset_zernike_values();
 01980  refresh_command_bar();
 01981}
 1982
 01983void MainWindow::on_metrics_updated(double input_fps) {
 01984  if (input_fps < 0.0) {
 01985    input_fps = 0.0;
 1986  }
 01987  last_input_fps_ = input_fps;
 1988
 01989  int fps = static_cast<int>(input_fps);
 1990
 01991  const QString text = QString("%1 fps").arg(fps, 6, 10, QChar('0'));
 1992
 01993  if (fps_status_label_ != nullptr) {
 01994    fps_status_label_->setText(text);
 1995  }
 01996  monitor_widget_->set_input_throughput_fps(text);
 1997
 01998  monitor_widget_->set_gpu_load("N/A");
 01999  monitor_widget_->set_cpu_load("N/A");
 02000  monitor_widget_->set_input_throughput_bytes("N/A");
 02001  monitor_widget_->set_cpu_throughput("N/A");
 02002  monitor_widget_->set_gpu_throughput("N/A");
 02003  monitor_widget_->set_ram_usage("N/A");
 02004  monitor_widget_->set_vram_usage("N/A");
 02005  monitor_widget_->set_dropped_frames("N/A");
 02006  monitor_widget_->set_pipeline_latency("N/A");
 02007  refresh_command_bar();
 02008}
 2009
 02010void MainWindow::on_update_pipeline_success() {
 02011  logger()->info("[MainWindow::on_update_pipeline_success]");
 02012  update_in_progress_ = false;
 02013  import_widget_->set_start_enabled(pipeline_running_);
 02014  export_widget_->set_record_enabled(!export_in_progress_ && export_widget_->isChecked());
 02015  export_widget_->set_stop_enabled(export_in_progress_);
 2016
 02017  auto dims = guess_source_dims();
 02018  xy_raw_widget_->set_fixed_aspect(dims);
 02019  dims = processed_display_dims(dims);
 02020  view_widget_->set_xy_extent(dims.width(), dims.height());
 02021  xy_processed_widget_->set_fixed_aspect(dims);
 2022
 02023  shack_hartmann_widget_->set_fixed_aspect(dims);
 02024  shack_hartmann_xcorr_widget_->set_fixed_aspect(dims);
 02025  zernike_phase_widget_->set_fixed_aspect(guess_source_dims());
 2026
 02027  auto *autofocus_widget = render_widget_->autofocus_widget();
 02028  if (autofocus_widget->is_enabled()) {
 02029    const bool has_enabled_zernike =
 2030        autofocus_widget->is_z2_enabled() || autofocus_widget->is_z3_enabled() ||
 2031        autofocus_widget->is_z4_enabled() || autofocus_widget->is_z5_enabled() ||
 2032        autofocus_widget->is_z6_enabled() || autofocus_widget->is_z7_enabled() ||
 2033        autofocus_widget->is_z8_enabled() || autofocus_widget->is_z9_enabled() ||
 2034        autofocus_widget->is_z10_enabled();
 02035    if (!has_enabled_zernike) {
 02036      autofocus_widget->reset_zernike_values();
 2037    }
 02038  } else {
 02039    autofocus_widget->reset_zernike_values();
 2040  }
 02041  refresh_visualization_availability();
 02042  display_workspace_->set_pipeline_running(true);
 02043  refresh_command_bar();
 02044}
 2045
 02046void MainWindow::on_update_pipeline_failure(const QString &error) {
 02047  logger()->error("[MainWindow::on_update_pipeline_failure]");
 02048  pipeline_running_      = false;
 02049  pipeline_paused_       = false;
 02050  pause_requested_       = false;
 02051  resume_in_progress_    = false;
 02052  update_in_progress_    = false;
 02053  paused_settings_dirty_ = false;
 02054  import_widget_->set_start_enabled(true);
 02055  import_widget_->set_stop_enabled(false);
 02056  export_in_progress_ = false;
 02057  export_widget_->set_record_enabled(false);
 02058  export_widget_->set_stop_enabled(false);
 02059  display_workspace_->set_pipeline_running(false);
 02060  refresh_command_bar();
 2061
 02062  show_pipeline_error_popup(tr("An error occurred while updating the pipeline:\n%1").arg(error));
 02063}
 2064
 02065void MainWindow::closeEvent(QCloseEvent *event) {
 02066  save_persistent_state();
 2067
 02068  if (pipeline_manager_ && pipeline_running_) {
 02069    pipeline_manager_->stop_pipeline();
 2070  }
 2071
 02072  if (pipeline_manager_thread_) {
 02073    pipeline_manager_thread_->quit();
 02074    pipeline_manager_thread_->wait();
 2075  }
 2076
 02077  QMainWindow::closeEvent(event);
 02078  QCoreApplication::quit();
 02079}
 2080
 02081void MainWindow::on_import_start_clicked() {
 02082  if (pipeline_running_) {
 02083    pause_requested_ = true;
 02084    import_widget_->set_start_enabled(false);
 02085    import_widget_->set_stop_enabled(false);
 02086    refresh_command_bar();
 02087    auto pause = [manager = pipeline_manager_]() { manager->stop_pipeline(); };
 02088    HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, pause, Qt::QueuedConnection));
 02089    return;
 2090  }
 2091
 02092  if (pipeline_paused_ && !paused_settings_dirty_) {
 02093    resume_in_progress_ = true;
 02094    import_widget_->set_start_enabled(false);
 02095    import_widget_->set_stop_enabled(false);
 02096    refresh_command_bar();
 02097    auto resume = [manager = pipeline_manager_]() { manager->resume_pipeline(); };
 02098    HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, resume, Qt::QueuedConnection));
 02099    return;
 2100  }
 2101
 02102  if (!validate_inputs()) {
 02103    return;
 2104  }
 2105
 02106  pipeline_paused_       = false;
 02107  paused_settings_dirty_ = false;
 02108  import_widget_->set_start_enabled(false);
 02109  import_widget_->set_stop_enabled(false);
 02110  display_workspace_->reset_visualization_content();
 02111  refresh_command_bar();
 02112  pipeline::Settings settings = get_pipeline_settings();
 02113  auto               update   = [=]() { pipeline_manager_->update_pipeline(settings); };
 02114  auto               start    = [=]() { pipeline_manager_->start_pipeline(); };
 02115  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, update, Qt::QueuedConnection));
 02116  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, start, Qt::QueuedConnection));
 02117}
 2118
 02119void MainWindow::on_import_stop_clicked() {
 02120  if (pipeline_paused_) {
 02121    reset_pipeline_ui_after_stop();
 02122    return;
 2123  }
 2124
 02125  HOLOVIBES_CHECK(pipeline_running_);
 02126  pause_requested_ = false;
 02127  import_widget_->set_start_enabled(false);
 02128  import_widget_->set_stop_enabled(false);
 02129  refresh_command_bar();
 02130  auto stop = [=]() { pipeline_manager_->stop_pipeline(); };
 02131  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, stop, Qt::QueuedConnection));
 02132}
 2133
 02134void MainWindow::on_export_record_clicked() {
 02135  if (export_in_progress_) {
 02136    logger()->warn("[MainWindow::on_export_record_clicked] Recording already in progress");
 02137    return;
 2138  }
 02139  if (!pipeline_running_) {
 02140    logger()->warn("[MainWindow::on_export_record_clicked] Pipeline is not running");
 02141    return;
 2142  }
 2143
 02144  if (!validate_inputs()) {
 02145    logger()->warn("[MainWindow::on_export_record_clicked] Validation failed");
 02146    export_widget_->set_stop_enabled(pipeline_running_);
 02147    return;
 2148  }
 2149
 02150  export_widget_->set_record_enabled(false);
 02151  export_widget_->set_stop_enabled(false);
 02152  refresh_command_bar();
 2153
 02154  std::filesystem::path record_path = makeRecordingPath(export_widget_->get_file_path());
 02155  std::optional<size_t> frame_count;
 02156  if (export_widget_->is_frame_count_enabled()) {
 02157    frame_count = static_cast<size_t>(export_widget_->get_frame_count());
 2158  }
 2159
 02160  auto start = [mgr = pipeline_manager_, record_path]() { mgr->start_raw_record(record_path); };
 02161  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, start, Qt::QueuedConnection));
 02162}
 2163
 02164void MainWindow::on_export_stop_clicked() {
 02165  if (!export_in_progress_) {
 02166    logger()->warn("[MainWindow::on_export_stop_clicked] No active recording to stop");
 02167    return;
 2168  }
 2169
 02170  export_widget_->set_record_enabled(false);
 02171  refresh_command_bar();
 02172  auto stop = [mgr = pipeline_manager_]() { mgr->stop_raw_record(); };
 02173  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, stop, Qt::QueuedConnection));
 02174}
 2175
 02176void MainWindow::on_raw_record_started_success() {
 02177  logger()->info("[MainWindow::on_raw_record_started_success]");
 02178  export_in_progress_ = true;
 02179  export_widget_->set_stop_enabled(true);
 02180  export_widget_->set_record_enabled(false);
 02181  refresh_command_bar();
 02182}
 2183
 02184void MainWindow::on_raw_record_started_failure(const QString &error) {
 02185  logger()->error("[MainWindow::on_raw_record_started_failure]");
 02186  export_in_progress_ = false;
 02187  pending_recording_acquisition_id_.reset();
 02188  export_widget_->set_record_enabled(pipeline_running_ && export_widget_->isChecked());
 02189  export_widget_->set_stop_enabled(false);
 02190  refresh_command_bar();
 2191
 02192  show_pipeline_error_popup(tr("An error occurred while starting raw recording:\n%1").arg(error));
 02193}
 2194
 02195void MainWindow::on_raw_record_stopped_success() {
 02196  logger()->info("[MainWindow::on_raw_record_stopped_success]");
 02197  export_in_progress_ = false;
 02198  if (pending_recording_acquisition_id_.has_value()) {
 02199    next_acquisition_id_ = std::max(next_acquisition_id_, *pending_recording_acquisition_id_ + 1);
 02200    pending_recording_acquisition_id_.reset();
 02201    update_recording_path_preview();
 2202  }
 02203  export_widget_->set_record_enabled(pipeline_running_ && export_widget_->isChecked());
 02204  export_widget_->set_stop_enabled(false);
 02205  refresh_command_bar();
 02206}
 2207
 02208void MainWindow::on_raw_record_stopped_failure(const QString &error) {
 02209  logger()->error("[MainWindow::on_raw_record_stopped_failure]");
 02210  export_widget_->set_stop_enabled(pipeline_running_);
 02211  refresh_command_bar();
 02212  show_pipeline_error_popup(tr("An error occurred while stopping raw recording:\n%1").arg(error));
 02213}
 2214
 02215bool MainWindow::validate_inputs() {
 02216  import_widget_->clear_validation_styles();
 02217  export_widget_->clear_validation_styles();
 02218  render_widget_->clear_validation_styles();
 02219  view_widget_->clear_validation_styles();
 02220  render_widget_->autofocus_widget()->clear_validation_styles();
 02221  configure_unsupported_features();
 2222
 02223  pipeline::Settings settings = get_pipeline_settings();
 02224  const auto         context  = build_validation_context(settings);
 02225  QSize display_dims(context.source_width.value_or(1), context.source_height.value_or(1));
 2226  if (settings.view_type != pipeline::ViewType::RAW &&
 02227      settings.spacial_method == pipeline::SpacialMethod::ANGULAR_SPECTRUM &&
 2228      settings.asp_padding_enabled) {
 02229    display_dims = QSize(settings.asp_padded_width, settings.asp_padded_height);
 2230  }
 02231  view_widget_->set_xy_extent(display_dims.width(), display_dims.height());
 02232  const auto result = pipeline::validate_settings(settings, context);
 02233  refresh_validation_tooltips(result);
 02234  apply_validation_result(result);
 02235  configure_unsupported_features();
 02236  return result.ok();
 02237}
 2238
 02239void MainWindow::apply_validation_result(const pipeline::ValidationResult &result) {
 2240  using pipeline::SettingsField;
 2241
 02242  for (const auto &issue : result.issues) {
 02243    for (const auto field : issue.fields) {
 02244      switch (field) {
 2245      case SettingsField::LoadPath:
 02246        import_widget_->mark_file_invalid();
 02247        break;
 2248      case SettingsField::CameraConfigPath:
 02249        import_widget_->mark_camera_config_invalid();
 02250        break;
 2251      case SettingsField::InputSamplingFrequency:
 02252        import_widget_->mark_sampling_frequency_invalid();
 02253        break;
 2254      case SettingsField::LoadBegin:
 02255        import_widget_->mark_start_index_invalid();
 02256        break;
 2257      case SettingsField::LoadEnd:
 02258        import_widget_->mark_end_index_invalid();
 02259        break;
 2260      case SettingsField::LoadBatch:
 02261        render_widget_->mark_batch_size_invalid();
 02262        break;
 2263      case SettingsField::Filter2D:
 02264        render_widget_->mark_filter_2d_invalid();
 02265        break;
 2266      case SettingsField::Filter2DInnerRadius:
 02267        render_widget_->mark_filter_inner_invalid();
 02268        break;
 2269      case SettingsField::Filter2DOuterRadius:
 02270        render_widget_->mark_filter_outer_invalid();
 02271        break;
 2272      case SettingsField::SpacialMethod:
 02273        render_widget_->mark_space_transform_invalid();
 02274        break;
 2275      case SettingsField::AspPadding:
 02276        render_widget_->mark_asp_padding_invalid();
 02277        break;
 2278      case SettingsField::TimeMethod:
 02279        render_widget_->mark_time_transform_invalid();
 02280        break;
 2281      case SettingsField::TimeWindow:
 02282        render_widget_->mark_time_window_invalid();
 02283        break;
 2284      case SettingsField::TimeStride:
 02285        render_widget_->mark_time_stride_invalid();
 02286        break;
 2287      case SettingsField::TimeZBegin:
 02288        view_widget_->mark_z_invalid();
 02289        break;
 2290      case SettingsField::TimeZEnd:
 02291        view_widget_->mark_z_width_invalid();
 02292        break;
 2293      case SettingsField::View3DCuts:
 2294        break;
 2295      case SettingsField::PpAccumulation:
 02296        view_widget_->mark_accumulation_invalid();
 02297        break;
 2298      case SettingsField::PpFlatfieldCutoffPeriod:
 02299        view_widget_->mark_flatfield_cutoff_period_invalid();
 02300        break;
 2301      case SettingsField::PpConvolution:
 02302        render_widget_->mark_convolution_invalid();
 02303        break;
 2304      case SettingsField::PpRegistration:
 02305        view_widget_->mark_registration_invalid();
 02306        break;
 2307      case SettingsField::RecordingPath:
 02308        export_widget_->mark_file_invalid();
 02309        break;
 2310      case SettingsField::RecordingCount:
 02311        export_widget_->mark_frames_invalid();
 02312        break;
 2313      case SettingsField::AutofocusNbSubaps:
 02314        render_widget_->autofocus_widget()->mark_nb_subaps_invalid();
 02315        break;
 2316      case SettingsField::AutofocusNbIter:
 02317        render_widget_->autofocus_widget()->mark_nb_iter_invalid();
 2318        break;
 2319      }
 02320    }
 02321  }
 02322}
 2323
 02324void MainWindow::refresh_validation_tooltips(const pipeline::ValidationResult &result) {
 2325  using pipeline::SettingsField;
 2326
 02327  const std::array bindings = {
 2328      FieldBinding{SettingsField::LoadPath, import_widget_->file_line_edit()},
 2329      FieldBinding{SettingsField::CameraConfigPath, import_widget_->camera_config_combo()},
 2330      FieldBinding{SettingsField::InputSamplingFrequency,
 2331                   import_widget_->sampling_frequency_spin()},
 2332      FieldBinding{SettingsField::LoadBegin, import_widget_->start_index_spin()},
 2333      FieldBinding{SettingsField::LoadEnd, import_widget_->end_index_spin()},
 2334      FieldBinding{SettingsField::LoadBatch, render_widget_->batch_size_spin()},
 2335      FieldBinding{SettingsField::Filter2D, render_widget_->filter_2d_check()},
 2336      FieldBinding{SettingsField::Filter2DInnerRadius, render_widget_->filter_2d_inner_spin()},
 2337      FieldBinding{SettingsField::Filter2DOuterRadius, render_widget_->filter_2d_outer_spin()},
 2338      FieldBinding{SettingsField::SpacialMethod, render_widget_->space_transform_combo()},
 2339      FieldBinding{SettingsField::AspPadding, render_widget_->asp_padding_check()},
 2340      FieldBinding{SettingsField::AspPadding, render_widget_->asp_padded_width_spin()},
 2341      FieldBinding{SettingsField::AspPadding, render_widget_->asp_padded_height_spin()},
 2342      FieldBinding{SettingsField::TimeMethod, render_widget_->time_transform_combo()},
 2343      FieldBinding{SettingsField::TimeWindow, render_widget_->time_window_spin()},
 2344      FieldBinding{SettingsField::TimeStride, render_widget_->time_stride_spin()},
 2345      FieldBinding{SettingsField::TimeZBegin, view_widget_->z_spin()},
 2346      FieldBinding{SettingsField::TimeZEnd, view_widget_->z_width_spin()},
 2347      FieldBinding{SettingsField::PpAccumulation, view_widget_->accumulation_spin()},
 2348      FieldBinding{SettingsField::PpFlatfieldCutoffPeriod,
 2349                   view_widget_->flatfield_cutoff_period_um()},
 2350      FieldBinding{SettingsField::PpConvolution, render_widget_->convolution_combo()},
 2351      FieldBinding{SettingsField::PpRegistration, view_widget_->registration_check()},
 2352      FieldBinding{SettingsField::RecordingPath, export_widget_->file_line_edit()},
 2353      FieldBinding{SettingsField::RecordingCount, export_widget_->frames_spin()},
 2354      FieldBinding{SettingsField::AutofocusNbSubaps,
 2355                   render_widget_->autofocus_widget()->nb_subaps_spin()},
 2356      FieldBinding{SettingsField::AutofocusNbIter,
 2357                   render_widget_->autofocus_widget()->nb_iter_spin()},
 2358  };
 2359
 02360  for (const auto &binding : bindings) {
 02361    const auto  issues  = result.issues_for(binding.field);
 02362    const auto &help    = pipeline::get_field_help(binding.field);
 02363    const auto  tooltip = build_field_tooltip(help, std::span{issues});
 02364    binding.widget->setToolTip(tooltip);
 02365  }
 02366}
 2367
 2368pipeline::ValidationContext
 02369MainWindow::build_validation_context(const pipeline::Settings &settings) const {
 02370  pipeline::ValidationContext context;
 2371
 02372  if (settings.import_source == pipeline::ImportSource::HOLOFILE) {
 02373    context.load_path_exists =
 2374        !settings.load_path.empty() && std::filesystem::exists(settings.load_path);
 2375
 02376    if (context.load_path_exists) {
 2377      try {
 02378        auto header                = holofile::Reader(settings.load_path.string()).header();
 02379        context.source_width       = static_cast<int>(header.frame_width);
 02380        context.source_height      = static_cast<int>(header.frame_height);
 02381        context.source_frame_count = static_cast<int>(header.frame_count);
 02382      } catch (...) {
 02383        context.load_path_exists = false;
 02384      }
 2385    }
 02386  } else {
 02387    context.camera_config_exists = !settings.camera_config_path.empty() &&
 2388                                   std::filesystem::exists(settings.camera_config_path);
 2389
 02390    if (context.camera_config_exists) {
 2391      try {
 02392        std::ifstream cfg_file(settings.camera_config_path);
 02393        auto          cfg_json = nlohmann::json::parse(cfg_file);
 02394        const auto   &cfg = cfg_json.contains("s711") ? cfg_json.at("s711") : cfg_json.at("s710");
 2395
 02396        context.source_width        = cfg.at("Width").get<int>();
 02397        context.source_height       = cfg.at("Height").get<int>();
 02398        context.camera_config_valid = true;
 02399      } catch (...) {
 02400        context.camera_config_valid = false;
 02401      }
 2402    }
 2403  }
 2404
 02405  if (settings.recording_method != pipeline::RecordingMethod::NONE) {
 02406    context.recording_path_error = pipeline::validate_recording_path(settings.recording_path);
 2407  }
 2408
 02409  return context;
 02410}
 2411
 02412void MainWindow::setup_validation_connections() {
 02413  bool (MainWindow::*cb)() = &MainWindow::validate_inputs;
 2414
 2415  // Connect to widget signals
 02416  connect(import_widget_, &ImportWidget::settings_changed, this, cb);
 02417  connect(export_widget_, &ExportWidget::settings_changed, this, cb);
 02418  connect(render_widget_, &ImageRenderingWidget::settings_changed, this, cb);
 02419  connect(view_widget_, &ViewWidget::settings_changed, this, cb);
 02420  connect(display_workspace_, &VisualizationWorkspace::visualization_preferences_changed, this, cb);
 2421
 02422  connect(import_widget_, &ImportWidget::settings_changed, this, &MainWindow::refresh_command_bar);
 02423  connect(export_widget_, &ExportWidget::settings_changed, this, &MainWindow::refresh_command_bar);
 02424  connect(render_widget_, &ImageRenderingWidget::settings_changed, this,
 2425          &MainWindow::refresh_command_bar);
 02426  connect(view_widget_, &ViewWidget::settings_changed, this, &MainWindow::refresh_command_bar);
 02427  connect(display_workspace_, &VisualizationWorkspace::visualization_preferences_changed, this,
 2428          &MainWindow::refresh_command_bar);
 02429}
 2430
 02431void MainWindow::setup_update_connections() {
 02432  void (MainWindow::*cb)() = &MainWindow::update_if_running;
 2433
 02434  connect(import_widget_, &ImportWidget::settings_changed, this, cb);
 02435  connect(export_widget_, &ExportWidget::settings_changed, this, cb);
 02436  connect(render_widget_, &ImageRenderingWidget::settings_changed, this, cb);
 02437  connect(view_widget_, &ViewWidget::settings_changed, this, cb);
 02438  connect(display_workspace_, &VisualizationWorkspace::visualization_preferences_changed, this, cb);
 02439}
 2440
 02441void MainWindow::update_if_running() {
 02442  if (pipeline_paused_) {
 02443    paused_settings_dirty_ = true;
 02444    return;
 2445  }
 2446
 02447  if (!pipeline_manager_ || !pipeline_running_) {
 02448    return;
 2449  }
 2450
 02451  if (update_in_progress_) {
 02452    return;
 2453  }
 2454
 02455  if (!validate_inputs()) {
 02456    return;
 2457  }
 2458
 02459  display_workspace_->reset_visualization_content();
 02460  update_in_progress_ = true;
 02461  import_widget_->set_start_enabled(false);
 02462  refresh_command_bar();
 02463  pipeline::Settings settings = get_pipeline_settings();
 02464  auto               update   = [=]() { pipeline_manager_->update_pipeline(settings); };
 02465  HOLOVIBES_CHECK(QMetaObject::invokeMethod(pipeline_manager_, update, Qt::QueuedConnection));
 02466}
 2467
 02468QSize MainWindow::guess_source_dims() {
 02469  if (!import_widget_->is_camera_mode()) {
 02470    auto header     = holofile::Reader(import_widget_->get_file_path().toStdString()).header();
 02471    int  src_width  = header.frame_width;
 02472    int  src_height = header.frame_height;
 02473    return QSize(src_width, src_height);
 2474  }
 2475
 02476  else if (import_widget_->is_camera_mode()) {
 02477    auto path     = get_selected_camera_config_path();
 02478    auto cfg_file = std::ifstream(path);
 02479    if (!cfg_file.is_open()) {
 02480      throw std::runtime_error(std::format("Could not open camera config file: {}", path));
 2481    }
 2482
 2483    // FIXME: This needs to be properly handeled for more camera support
 02484    auto cfg_json   = nlohmann::json::parse(cfg_file);
 02485    auto cfg        = cfg_json.contains("s711") ? cfg_json.at("s711") : cfg_json.at("s710");
 02486    int  src_width  = cfg.at("Width").get<int>();
 02487    int  src_height = cfg.at("Height").get<int>();
 02488    return QSize(src_width, src_height);
 2489  }
 2490
 02491  HOLOVIBES_UNIMPLEMENTED();
 02492}
 2493
 02494QSize MainWindow::processed_display_dims(QSize source_dims) const {
 02495  if (render_widget_->get_image_mode() == "Raw") {
 02496    return source_dims;
 2497  }
 02498  if (render_widget_->get_space_transform() == "Fresnel Diffraction") {
 02499    return QSize(source_dims.width(), source_dims.width());
 2500  }
 02501  if (render_widget_->get_space_transform() == "Angular Spectrum" &&
 2502      render_widget_->is_asp_padding_enabled()) {
 02503    return QSize(render_widget_->get_asp_padded_width(), render_widget_->get_asp_padded_height());
 2504  }
 02505  return source_dims;
 02506}
 2507
 02508pipeline::Settings MainWindow::get_pipeline_settings() {
 2509  using namespace holovibes::pipeline;
 2510
 02511  Settings s;
 2512
 2513  // Advanced Settings
 2514  {
 02515    s.cpu_in_size  = 4096;
 02516    s.gpu_in_size  = 1024;
 02517    s.cpu_rec_size = 4096;
 02518    s.cpu_out_size = 64;
 02519    s.gpu_out_size = 64;
 2520  }
 2521
 2522  // Import Settings
 2523  {
 02524    std::map<std::string, LoadMethod> method_from_str{
 2525        {"Read Live", LoadMethod::READ_LIVE},
 2526        {"Load in CPU RAM", LoadMethod::LOAD_IN_CPU},
 2527        {"Load in GPU RAM", LoadMethod::LOAD_IN_GPU},
 2528    };
 02529    std::map<std::string, ImportSource> source_from_str{
 2530        {"Ametek S710 Euresys Coaxlink Octo", ImportSource::AMETEK_S710_EURESYS_COAXLINK_OCTO},
 2531        {"Ametek S711 Euresys Coaxlink QSFP+", ImportSource::AMETEK_S711_EURESYS_COAXLINK_QSFP},
 2532    };
 02533    s.input_sampling_frequency_hz = import_widget_->get_sampling_frequency_hz();
 2534
 02535    if (!import_widget_->is_camera_mode()) {
 02536      s.import_source  = ImportSource::HOLOFILE;
 02537      s.load_path      = import_widget_->get_file_path().toStdString();
 02538      s.load_begin     = static_cast<size_t>(import_widget_->get_start_index());
 02539      s.load_end       = static_cast<size_t>(import_widget_->get_end_index());
 02540      s.load_fps_limit = import_widget_->get_fps_limit();
 02541      QString method   = import_widget_->get_load_method();
 02542      s.load_method    = method_from_str.at(method.toStdString());
 02543      if (render_widget_->get_time_transform() == "Principal Component Analysis") {
 02544        s.load_batch = 32;
 02545      } else {
 02546        s.load_batch = render_widget_->get_batch_size();
 2547      }
 02548    } else {
 02549      QString source       = import_widget_->get_camera_type();
 02550      s.import_source      = source_from_str.at(source.toStdString());
 02551      s.camera_config_path = get_selected_camera_config_path();
 02552      s.load_batch         = 1;
 02553      s.load_fps_limit     = std::nullopt;
 2554
 02555      std::ifstream cfg_file(s.camera_config_path);
 02556      if (cfg_file.is_open()) {
 2557        try {
 02558          auto cfg_json = nlohmann::json::parse(cfg_file);
 02559          s.load_batch  = cfg_json.contains("s711")
 2560                              ? cfg_json.at("s711").at("BufferPartCount").get<int>()
 2561                              : cfg_json.at("s710").at("BufferPartCount").get<int>();
 02562        } catch (...) {
 02563        }
 2564      }
 02565    }
 02566  }
 2567
 2568  // Image Rendering Settings
 2569  {
 02570    s.view_type = render_widget_->get_image_mode() == "Raw" ? ViewType::RAW : ViewType::PROCESSED;
 2571
 02572    std::map<std::string, SpacialMethod> method_from_str{
 2573        {"None", SpacialMethod::NONE},
 2574        {"Fresnel Diffraction", SpacialMethod::FRESNEL_DIFFRACTION},
 2575        {"Angular Spectrum", SpacialMethod::ANGULAR_SPECTRUM},
 2576    };
 02577    QString method        = render_widget_->get_space_transform();
 02578    s.spacial_method      = method_from_str.at(method.toStdString());
 02579    s.spacial_lambda      = static_cast<float>(render_widget_->get_lambda()) * 1e-9f;
 02580    s.spacial_z           = static_cast<float>(render_widget_->get_focus()) * 1e-3f;
 02581    s.spacial_pixel_size  = 20e-6f; // TODO: get from camera
 02582    s.asp_padding_enabled = render_widget_->is_asp_padding_enabled();
 02583    s.asp_padded_width    = render_widget_->get_asp_padded_width();
 02584    s.asp_padded_height   = render_widget_->get_asp_padded_height();
 02585  }
 2586  {
 02587    s.filter_2d           = render_widget_->is_filter_2d_enabled();
 02588    s.filter_r_inner      = render_widget_->get_filter_inner();
 02589    s.filter_r_outer      = render_widget_->get_filter_outer();
 02590    s.filter_smooth_inner = 0;
 02591    s.filter_smooth_outer = 1;
 2592  }
 2593  {
 02594    std::map<std::string, TimeMethod> method_from_str{
 2595        {"None", TimeMethod::NONE},
 2596        {"Principal Component Analysis", TimeMethod::PRINCIPAL_COMPONENT_ANALYSIS},
 2597        {"RFFT", TimeMethod::RFFT},
 2598        {"FFT", TimeMethod::FFT},
 2599    };
 02600    s.time_window       = render_widget_->get_time_window();
 02601    s.time_stride       = render_widget_->get_time_stride();
 02602    s.time_accumulation = 4; // TODO: expose in UI
 02603    QString method      = render_widget_->get_time_transform();
 02604    s.time_method       = method_from_str.at(method.toStdString());
 02605    s.time_x_begin      = view_widget_->get_x_origin();
 02606    s.time_x_end        = s.time_x_begin + view_widget_->get_x_width();
 02607    s.time_y_begin      = view_widget_->get_y_origin();
 02608    s.time_y_end        = s.time_y_begin + view_widget_->get_y_width();
 02609    s.time_z_begin      = view_widget_->get_z_origin();
 02610    s.time_z_end        = s.time_z_begin + view_widget_->get_z_width();
 02611  }
 2612
 2613  // View Settings
 2614  {
 02615    std::map<std::string, MomentType> moment_from_str{
 2616        {"M0", MomentType::M0},
 2617        {"M1", MomentType::M1},
 2618        {"M2", MomentType::M2},
 2619    };
 2620    const auto view_enabled = [this](const QString &id, bool fallback) {
 2621      return display_workspace_ == nullptr ? fallback
 2622                                           : display_workspace_->is_visualization_enabled(id);
 02623    };
 2624
 02625    s.view_3d_cuts              = false;
 02626    s.raw_view                  = view_enabled(QStringLiteral("xy_raw"), false);
 02627    s.moment_type               = moment_from_str.at(view_widget_->get_image_type().toStdString());
 02628    s.view_raw_spectrum         = view_enabled(QStringLiteral("raw_spectrum"), false);
 02629    s.view_processed_spectrum   = view_enabled(QStringLiteral("processed_spectrum"), false);
 02630    s.view_zernike_metrics      = view_enabled(QStringLiteral("zernike_a4"), true);
 02631    s.view_zernike_phase        = view_enabled(QStringLiteral("zernike_phase"), false);
 02632    s.view_shack_hartmann       = view_enabled(QStringLiteral("shack_hartmann"), false);
 02633    s.view_shack_hartmann_xcorr = view_enabled(QStringLiteral("shack_hartmann_xcorr"), false);
 02634  }
 2635
 2636  // Post-processing Settings
 2637  {
 02638    QString     appDataBase = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 02639    QString     appDataPath = appDataBase + "/" + QCoreApplication::applicationVersion();
 02640    QString     convolutionsKernelsPath = appDataPath + "/" + "convolution_kernels/";
 02641    std::string kernel_path             = convolutionsKernelsPath.toStdString() +
 2642                                          render_widget_->get_convolution().toStdString() + ".json";
 2643
 02644    s.pp_fps       = 60;
 02645    s.pp_fft_shift = s.spacial_method == SpacialMethod::FRESNEL_DIFFRACTION;
 02646    s.pp_flatfield = view_widget_->is_flatfield_enabled();
 02647    s.pp_flatfield_cutoff_period_m =
 2648        static_cast<float>(view_widget_->get_flatfield_cutoff_period_um() * 1e-6);
 02649    s.pp_accumulation        = static_cast<size_t>(view_widget_->get_accumulation());
 02650    s.pp_convolution         = render_widget_->get_convolution() != "None";
 02651    s.pp_convolution_path    = kernel_path;
 02652    s.pp_convolution_divide  = render_widget_->is_convolution_divide();
 02653    s.pp_pctclip             = view_widget_->is_pct_enabled();
 02654    s.pp_pctclip_lower       = 0.02f;
 02655    s.pp_pctclip_upper       = 99.98f;
 02656    s.pp_pctclip_radius      = view_widget_->get_pct_radius();
 02657    s.pp_registration        = view_widget_->is_registration_enabled();
 02658    s.pp_registration_radius = view_widget_->get_registration_radius();
 02659  }
 2660
 2661  // Recording Settings
 2662  {
 02663    if (export_widget_->isChecked()) {
 2664
 02665      s.recording_method = export_widget_->get_image_type() == "Raw Image"
 2666                               ? RecordingMethod::RAW
 2667                               : RecordingMethod::PROCESSED;
 02668    } else {
 02669      s.recording_method = RecordingMethod::NONE;
 2670    }
 02671    s.recording_path  = export_widget_->get_file_path().toStdString();
 02672    s.recording_count = export_widget_->get_frame_count();
 2673  }
 2674
 2675  // Auto-Focus Settings
 2676  {
 02677    s.autofocus_enabled        = render_widget_->autofocus_widget()->is_enabled();
 02678    s.autofocus_nb_subaps      = render_widget_->autofocus_widget()->get_nb_subaps();
 02679    s.autofocus_nb_iter        = render_widget_->autofocus_widget()->get_nb_iter();
 02680    s.autofocus_zernike_orders = std::vector<int>();
 02681    s.autofocus_skip_subapertures_outside_pupil =
 2682        render_widget_->autofocus_widget()->skip_subapertures_outside_pupil();
 02683    s.autofocus_use_graph_laplacian = render_widget_->autofocus_widget()->use_graph_laplacian();
 2684
 02685    if (render_widget_->autofocus_widget()->is_z2_enabled()) {
 02686      s.autofocus_zernike_orders.push_back(2);
 2687    }
 2688
 02689    if (render_widget_->autofocus_widget()->is_z3_enabled()) {
 02690      s.autofocus_zernike_orders.push_back(3);
 2691    }
 2692
 02693    if (render_widget_->autofocus_widget()->is_z4_enabled()) {
 02694      s.autofocus_zernike_orders.push_back(4);
 2695    }
 2696
 02697    if (render_widget_->autofocus_widget()->is_z5_enabled()) {
 02698      s.autofocus_zernike_orders.push_back(5);
 2699    }
 2700
 02701    if (render_widget_->autofocus_widget()->is_z6_enabled()) {
 02702      s.autofocus_zernike_orders.push_back(6);
 2703    }
 2704
 02705    if (render_widget_->autofocus_widget()->is_z7_enabled()) {
 02706      s.autofocus_zernike_orders.push_back(7);
 2707    }
 2708
 02709    if (render_widget_->autofocus_widget()->is_z8_enabled()) {
 02710      s.autofocus_zernike_orders.push_back(8);
 2711    }
 2712
 02713    if (render_widget_->autofocus_widget()->is_z9_enabled()) {
 02714      s.autofocus_zernike_orders.push_back(9);
 2715    }
 2716
 02717    if (render_widget_->autofocus_widget()->is_z10_enabled()) {
 02718      s.autofocus_zernike_orders.push_back(10);
 2719    }
 2720
 02721    s.signal_plot_time_window_seconds = signal_plot_time_window_seconds_;
 2722  }
 2723
 02724  return s;
 02725}
 2726
 02727void MainWindow::set_pipeline_settings(const pipeline::Settings &s) {
 2728  using namespace holovibes::pipeline;
 2729
 02730  signal_plot_time_window_seconds_ = s.signal_plot_time_window_seconds;
 2731
 2732  // --- Advanced Settings ---
 2733  {
 2734    // (none exposed in UI)
 2735  }
 2736
 2737  // --- Import Settings ---
 2738  {
 2739    // Source: camera or file
 02740    if (s.import_source == ImportSource::HOLOFILE) {
 02741      import_widget_->set_camera_mode(false);
 02742      import_widget_->set_file_path(QString::fromStdString(s.load_path.string()));
 02743      import_widget_->set_fps_limit(s.load_fps_limit);
 02744      import_widget_->set_start_index(static_cast<int>(s.load_begin));
 02745      import_widget_->set_end_index(static_cast<int>(s.load_end));
 2746
 02747      QString method;
 02748      switch (s.load_method) {
 2749      case LoadMethod::READ_LIVE:
 02750        method = "Read Live";
 02751        break;
 2752      case LoadMethod::LOAD_IN_CPU:
 02753        method = "Load in CPU RAM";
 02754        break;
 2755      case LoadMethod::LOAD_IN_GPU:
 02756        method = "Load in GPU RAM";
 02757        break;
 2758      default:
 02759        method = "Read Live";
 2760        break;
 2761      }
 02762      import_widget_->set_load_method(method);
 02763    } else {
 02764      import_widget_->set_camera_mode(true);
 02765      import_widget_->set_fps_limit(std::nullopt);
 2766
 02767      QString source;
 02768      switch (s.import_source) {
 2769      case ImportSource::AMETEK_S710_EURESYS_COAXLINK_OCTO:
 02770        source = "Ametek S710 Euresys Coaxlink Octo";
 02771        break;
 2772      case ImportSource::AMETEK_S711_EURESYS_COAXLINK_QSFP:
 02773        source = "Ametek S711 Euresys Coaxlink QSFP+";
 02774        break;
 2775      default:
 02776        source = "Ametek S710 Euresys Coaxlink Octo";
 2777        break;
 2778      }
 02779      import_widget_->set_camera_type(source);
 02780    }
 02781    import_widget_->set_sampling_frequency_hz(s.input_sampling_frequency_hz);
 2782  }
 2783
 2784  // --- Image Rendering Settings ---
 2785  {
 02786    QString method;
 02787    switch (s.spacial_method) {
 2788    case SpacialMethod::FRESNEL_DIFFRACTION:
 02789      method = "Fresnel Diffraction";
 02790      break;
 2791    case SpacialMethod::ANGULAR_SPECTRUM:
 02792      method = "Angular Spectrum";
 02793      break;
 2794    default:
 02795      method = "None";
 2796      break;
 2797    }
 02798    render_widget_->set_batch_size(s.load_batch);
 02799    render_widget_->set_space_transform(method);
 02800    render_widget_->set_lambda(s.spacial_lambda * 1e9); // nm
 02801    render_widget_->set_focus(s.spacial_z * 1e3);       // mm
 02802    render_widget_->set_asp_padding_enabled(s.asp_padding_enabled);
 02803    render_widget_->set_asp_padded_width(s.asp_padded_width);
 02804    render_widget_->set_asp_padded_height(s.asp_padded_height);
 02805  }
 2806  {
 02807    render_widget_->set_filter_2d_enabled(s.filter_2d);
 02808    render_widget_->set_filter_inner(s.filter_r_inner);
 02809    render_widget_->set_filter_outer(s.filter_r_outer);
 2810    // smooth_inner/smooth_outer omitted since not exposed in UI
 2811  }
 2812  {
 02813    QString method;
 02814    switch (s.time_method) {
 2815    case TimeMethod::PRINCIPAL_COMPONENT_ANALYSIS:
 02816      method = "Principal Component Analysis";
 02817      break;
 2818    case TimeMethod::RFFT:
 02819      method = "RFFT";
 02820      break;
 2821    case TimeMethod::FFT:
 02822      method = "FFT";
 02823      break;
 2824    default:
 02825      method = "None";
 2826      break;
 2827    }
 02828    render_widget_->set_time_transform(method);
 02829    render_widget_->set_time_window(static_cast<int>(s.time_window));
 02830    render_widget_->set_time_stride(static_cast<int>(s.time_stride));
 2831
 02832    view_widget_->set_z_origin(static_cast<int>(s.time_z_begin));
 02833    view_widget_->set_z_width(static_cast<int>(s.time_z_end - s.time_z_begin));
 02834  }
 2835
 2836  // --- View Settings ---
 2837  {
 02838    if (display_workspace_ != nullptr) {
 02839      display_workspace_->set_visualization_enabled(QStringLiteral("xy_raw"), s.raw_view);
 02840      display_workspace_->set_visualization_enabled(QStringLiteral("raw_spectrum"),
 2841                                                    s.view_raw_spectrum);
 02842      display_workspace_->set_visualization_enabled(QStringLiteral("processed_spectrum"),
 2843                                                    s.view_processed_spectrum);
 02844      display_workspace_->set_visualization_enabled(QStringLiteral("zernike_a4"),
 2845                                                    s.view_zernike_metrics);
 02846      display_workspace_->set_visualization_enabled(QStringLiteral("zernike_phase"),
 2847                                                    s.view_zernike_phase);
 02848      display_workspace_->set_visualization_enabled(QStringLiteral("shack_hartmann"),
 2849                                                    s.view_shack_hartmann);
 02850      display_workspace_->set_visualization_enabled(QStringLiteral("shack_hartmann_xcorr"),
 2851                                                    s.view_shack_hartmann_xcorr);
 02852      display_workspace_->set_visualization_enabled(QStringLiteral("xz_processed"), s.view_3d_cuts);
 02853      display_workspace_->set_visualization_enabled(QStringLiteral("yz_processed"), s.view_3d_cuts);
 2854    }
 2855  }
 2856
 2857  // --- Post-processing Settings ---
 2858  {
 02859    view_widget_->set_flatfield_enabled(s.pp_flatfield);
 02860    view_widget_->set_flatfield_cutoff_period_um(s.pp_flatfield_cutoff_period_m * 1e6);
 02861    view_widget_->set_accumulation(static_cast<int>(s.pp_accumulation));
 02862    render_widget_->set_convolution_divide(s.pp_convolution_divide);
 2863
 2864    // Select convolution kernel name from path
 02865    QString convName = "None";
 02866    if (s.pp_convolution && !s.pp_convolution_path.empty()) {
 02867      auto fileName = QFileInfo(QString::fromStdString(s.pp_convolution_path)).baseName();
 02868      convName      = fileName;
 02869    }
 02870    render_widget_->set_convolution(convName);
 2871
 02872    view_widget_->set_pct_radius(s.pp_pctclip_radius);
 02873    view_widget_->set_pct_enabled(s.pp_pctclip);
 02874    view_widget_->set_registration_enabled(s.pp_registration);
 02875    view_widget_->set_registration_radius(s.pp_registration_radius);
 02876  }
 2877
 2878  // --- Recording Settings ---
 2879  {
 02880    export_widget_->set_file_path(QString::fromStdString(s.recording_path.string()));
 02881    export_widget_->set_frame_count(static_cast<int>(s.recording_count));
 2882    // recording_method not exposed (always RAW in get_pipeline_settings)
 2883  }
 2884
 2885  // --- Auto-Focus Settings ---
 2886  {
 02887    auto *autofocus = render_widget_->autofocus_widget();
 02888    autofocus->set_enabled(s.autofocus_enabled);
 02889    autofocus->nb_subaps_spin()->setValue(s.autofocus_nb_subaps);
 02890    autofocus->nb_iter_spin()->setValue(s.autofocus_nb_iter);
 02891    autofocus->set_skip_subapertures_outside_pupil(s.autofocus_skip_subapertures_outside_pupil);
 02892    autofocus->set_use_graph_laplacian(s.autofocus_use_graph_laplacian);
 2893  }
 2894
 02895  configure_unsupported_features();
 02896}
 2897
 02898std::string MainWindow::get_selected_camera_config_path() {
 02899  QString     appDataBase      = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 02900  QString     appDataPath      = appDataBase + "/" + QCoreApplication::applicationVersion();
 02901  QString     cameraConfigPath = appDataPath + "/" + "camera_configs/";
 02902  std::string config_path =
 2903      cameraConfigPath.toStdString() + import_widget_->get_camera_config().toStdString() + ".json";
 02904  return config_path;
 02905}
 2906
 2907} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\main_window.hh

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QCheckBox>
 18#include <QCloseEvent>
 19#include <QComboBox>
 20#include <QDoubleSpinBox>
 21#include <QLabel>
 22#include <QLineEdit>
 23#include <QMainWindow>
 24#include <QProgressBar>
 25#include <QPushButton>
 26#include <QSlider>
 27#include <QSpinBox>
 28#include <QToolButton>
 29#include <QUrl>
 30#include <QWidget>
 31#include <optional>
 32
 33#include "pipeline/field_help.hh"
 34#include "pipeline/manager.hh"
 35#include "pipeline/settings.hh"
 36#include "pipeline/validation.hh"
 37#include "ui/widgets/export_widget.hh"
 38#include "ui/widgets/image_rendering_widget.hh"
 39#include "ui/widgets/import_widget.hh"
 40#include "ui/widgets/system_monitor_widget.hh"
 41#include "ui/widgets/view_widget.hh"
 42
 43namespace holovibes::ui {
 44
 45class ZernikeHistoryWidget;
 46class SelectedWidgetSettingsPanel;
 47class VisualizationWorkspace;
 48class UpdateChecker;
 49class GraphVisualizerWidget;
 50
 51class MainWindow : public QMainWindow {
 052  Q_OBJECT
 53public:
 54  explicit MainWindow(QWidget *parent = nullptr);
 055  ~MainWindow() override = default;
 56
 57protected:
 58  void closeEvent(QCloseEvent *event) override;
 59
 60private slots:
 61  void on_import_start_clicked();
 62  void on_import_stop_clicked();
 63  void on_metrics_updated(double input_fps);
 64  void on_export_record_clicked();
 65  void on_export_stop_clicked();
 66  void on_raw_record_started_success();
 67  void on_raw_record_started_failure(const QString &error);
 68  void on_raw_record_stopped_success();
 69  void on_raw_record_stopped_failure(const QString &error);
 70
 71private:
 72  ImportWidget                *import_widget_;
 73  ExportWidget                *export_widget_;
 74  ImageRenderingWidget        *render_widget_;
 75  ViewWidget                  *view_widget_;
 76  SystemMonitorWidget         *monitor_widget_;
 77  SelectedWidgetSettingsPanel *selected_widget_settings_panel_;
 78
 79  void show_pipeline_error_popup(const QString &message);
 80  void on_start_pipeline_success();
 81  void on_start_pipeline_failure(const QString &error);
 82  void on_stop_pipeline_success();
 83  void on_stop_pipeline_failure(const QString &error);
 84  void on_resume_pipeline_success();
 85  void on_resume_pipeline_failure(const QString &error);
 86  void on_update_pipeline_success();
 87  void on_update_pipeline_failure(const QString &error);
 88
 89  bool                        validate_inputs();
 90  void                        apply_validation_result(const pipeline::ValidationResult &result);
 91  void                        refresh_validation_tooltips(const pipeline::ValidationResult &result);
 92  pipeline::ValidationContext build_validation_context(const pipeline::Settings &settings) const;
 93  void                        setup_validation_connections();
 94  void                        setup_update_connections();
 95  void                        update_if_running();
 96
 97  QSize              guess_source_dims();
 98  QSize              processed_display_dims(QSize source_dims) const;
 99  pipeline::Settings get_pipeline_settings();
 100  void               set_pipeline_settings(const pipeline::Settings &s);
 101
 102  void setup_main_layout();
 103  void initialize_display_widgets();
 104  void initialize_pipeline_manager();
 105  void connect_manager_signals();
 106  void connect_import_controls();
 107  void connect_export_controls();
 108  void configure_window();
 109  void check_for_updates();
 110  void show_fft_frequency_tool();
 111  void show_preferences();
 112  void show_pipeline_graph();
 113  void open_dot_file();
 114
 115  void    refresh_visualization_availability();
 116  void    select_configurable_widget(ZernikeHistoryWidget *widget);
 117  void    on_selected_visualization_changed(const QString &visualization_id);
 118  void    reset_pipeline_ui_after_stop();
 119  void    configure_unsupported_features();
 120  void    save_persistent_state();
 121  void    restore_persistent_state();
 122  QString sanitize_recording_token(const QString &value) const;
 123  QString recording_file_name(int acquisition_id) const;
 124  QString acquisition_label(int acquisition_id) const;
 125  void    update_acquisition_label();
 126  void    update_recording_path_preview();
 127  void    refresh_command_bar();
 128  void    set_status_label(QLabel *label, const QString &text, const char *tone);
 129
 130  std::filesystem::path makeRecordingPath(const QString &userText);
 131  QStringList           load_available_camera_configs();
 132  std::string           get_selected_camera_config_path();
 133
 134  // Current state
 0135  bool               update_in_progress_              = false;
 0136  bool               pipeline_running_                = false;
 0137  bool               pipeline_paused_                 = false;
 0138  bool               pause_requested_                 = false;
 0139  bool               resume_in_progress_              = false;
 0140  bool               paused_settings_dirty_           = false;
 0141  bool               export_in_progress_              = false;
 0142  bool               geometry_restored_               = false;
 0143  double             last_input_fps_                  = 0.0;
 0144  double             signal_plot_time_window_seconds_ = 8.0;
 145  QString            session_id_;
 0146  int                next_acquisition_id_ = 1;
 147  std::optional<int> pending_recording_acquisition_id_;
 148
 149  // Workers
 150  pipeline::Manager *pipeline_manager_;
 151  QThread           *pipeline_manager_thread_;
 152
 153  // Display widgets
 154  TensorDisplayWidget  *xy_processed_widget_;
 155  TensorDisplayWidget  *xz_processed_widget_;
 156  TensorDisplayWidget  *yz_processed_widget_;
 157  TensorDisplayWidget  *xy_raw_widget_;
 158  TensorDisplayWidget  *raw_spectrum_widget_;
 159  TensorDisplayWidget  *processed_spectrum_widget_;
 160  TensorDisplayWidget  *shack_hartmann_widget_;
 161  TensorDisplayWidget  *shack_hartmann_xcorr_widget_;
 162  TensorDisplayWidget  *zernike_phase_widget_;
 163  ZernikeHistoryWidget *zernike_history_widget_;
 164  // Application columns and central visualization workspace
 0165  QLineEdit              *patient_line_edit_          = nullptr;
 0166  QComboBox              *eye_side_combo_             = nullptr;
 0167  QLabel                 *session_value_label_        = nullptr;
 0168  QLabel                 *acquisition_value_label_    = nullptr;
 0169  QPushButton            *start_command_button_       = nullptr;
 0170  QPushButton            *stop_command_button_        = nullptr;
 0171  QPushButton            *record_command_button_      = nullptr;
 0172  QPushButton            *stop_record_command_button_ = nullptr;
 0173  QLabel                 *pipeline_status_label_      = nullptr;
 0174  QLabel                 *source_status_label_        = nullptr;
 0175  QLabel                 *view_status_label_          = nullptr;
 0176  QLabel                 *fps_status_label_           = nullptr;
 0177  QLabel                 *recording_status_label_     = nullptr;
 0178  VisualizationWorkspace *display_workspace_          = nullptr;
 0179  QWidget                *right_sidebar_              = nullptr;
 0180  QToolButton            *update_indicator_           = nullptr;
 0181  UpdateChecker          *update_checker_             = nullptr;
 0182  GraphVisualizerWidget  *graph_visualizer_widget_    = nullptr;
 183  QUrl                    available_update_url_;
 184};
 185
 186} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\theme.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/theme.hh"
 16
 17#include <QApplication>
 18#include <QColor>
 19#include <QFile>
 20#include <QIODevice>
 21#include <QPalette>
 22#include <QString>
 23#include <QStyleFactory>
 24#include <QtLogging>
 25
 26namespace holovibes::ui {
 27
 28namespace {
 29
 30struct ClinicalThemeColors {
 31  QString window           = QStringLiteral("#101215");
 32  QString panel            = QStringLiteral("#13181E");
 33  QString panel_dark       = QStringLiteral("#11161B");
 34  QString surface          = QStringLiteral("#171A1F");
 35  QString surface_alt      = QStringLiteral("#1F2329");
 36  QString field            = QStringLiteral("#0F1216");
 37  QString field_readonly   = QStringLiteral("#15191E");
 38  QString display          = QStringLiteral("#050608");
 39  QString border           = QStringLiteral("#2C333A");
 40  QString border_strong    = QStringLiteral("#3A424B");
 41  QString border_hover     = QStringLiteral("#4A5561");
 42  QString button_hover     = QStringLiteral("#252B32");
 43  QString disabled         = QStringLiteral("#6F7A86");
 44  QString text             = QStringLiteral("#E6EAF0");
 45  QString text_muted       = QStringLiteral("#A7B0BA");
 46  QString text_setting     = QStringLiteral("#A4ACB6");
 47  QString text_header      = QStringLiteral("#D8DEE6");
 48  QString accent           = QStringLiteral("#2DA6A1");
 49  QString accent_hover     = QStringLiteral("#38BDB7");
 50  QString accent_dark      = QStringLiteral("#1C3B3A");
 51  QString status_success   = QStringLiteral("#3FB950");
 52  QString status_warning   = QStringLiteral("#D29922");
 53  QString danger           = QStringLiteral("#F85149");
 54  QString danger_border    = QStringLiteral("#DA3633");
 55  QString danger_bg        = QStringLiteral("#3A1D20");
 56  QString error_text       = QStringLiteral("#F4D6D2");
 57  QString error_bg         = QStringLiteral("#241416");
 58  QString error_focus      = QStringLiteral("#FF7B72");
 59  QString error_check      = QStringLiteral("#FCA5A5");
 60  QString link             = QStringLiteral("#58A6FF");
 61  QString scroll           = QStringLiteral("#30363D");
 62  QString highlighted_text = QStringLiteral("#050608");
 63};
 64
 065void set_palette_color(QPalette &palette, QPalette::ColorRole role, const QString &color) {
 066  palette.setColor(role, QColor(color));
 067}
 68
 069void set_disabled_palette_color(QPalette &palette, QPalette::ColorRole role, const QString &color) {
 070  palette.setColor(QPalette::Disabled, role, QColor(color));
 071}
 72
 073QPalette build_palette(const ClinicalThemeColors &colors) {
 074  QPalette palette;
 075  set_palette_color(palette, QPalette::Window, colors.window);
 076  set_palette_color(palette, QPalette::WindowText, colors.text);
 077  set_palette_color(palette, QPalette::Base, colors.surface);
 078  set_palette_color(palette, QPalette::AlternateBase, colors.surface_alt);
 079  set_palette_color(palette, QPalette::ToolTipBase, colors.surface_alt);
 080  set_palette_color(palette, QPalette::ToolTipText, colors.text);
 081  set_palette_color(palette, QPalette::Text, colors.text);
 082  set_palette_color(palette, QPalette::Button, colors.surface_alt);
 083  set_palette_color(palette, QPalette::ButtonText, colors.text);
 084  set_palette_color(palette, QPalette::BrightText, colors.danger);
 085  set_palette_color(palette, QPalette::Highlight, colors.accent);
 086  set_palette_color(palette, QPalette::HighlightedText, colors.highlighted_text);
 087  set_palette_color(palette, QPalette::Link, colors.link);
 088  set_disabled_palette_color(palette, QPalette::WindowText, colors.disabled);
 089  set_disabled_palette_color(palette, QPalette::Text, colors.disabled);
 090  set_disabled_palette_color(palette, QPalette::ButtonText, colors.disabled);
 091  set_disabled_palette_color(palette, QPalette::Button, colors.scroll);
 092  set_disabled_palette_color(palette, QPalette::Base, colors.surface);
 093  return palette;
 094}
 95
 096void replace_token(QString &style_sheet, const char *token, const QString &value) {
 097  style_sheet.replace(QStringLiteral("@%1@").arg(QString::fromLatin1(token)), value);
 098}
 99
 0100QString load_clinical_qss(const ClinicalThemeColors &colors) {
 0101  QFile file(QStringLiteral(":/resources/holovibes/styles/clinical_dark.qss"));
 0102  if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
 0103    qWarning() << "Unable to load clinical theme stylesheet:" << file.errorString();
 0104    return {};
 105  }
 106
 0107  QString style_sheet = QString::fromUtf8(file.readAll());
 0108  replace_token(style_sheet, "WINDOW", colors.window);
 0109  replace_token(style_sheet, "PANEL", colors.panel);
 0110  replace_token(style_sheet, "PANEL_DARK", colors.panel_dark);
 0111  replace_token(style_sheet, "SURFACE", colors.surface);
 0112  replace_token(style_sheet, "SURFACE_ALT", colors.surface_alt);
 0113  replace_token(style_sheet, "FIELD", colors.field);
 0114  replace_token(style_sheet, "FIELD_READONLY", colors.field_readonly);
 0115  replace_token(style_sheet, "DISPLAY", colors.display);
 0116  replace_token(style_sheet, "BORDER", colors.border);
 0117  replace_token(style_sheet, "BORDER_STRONG", colors.border_strong);
 0118  replace_token(style_sheet, "BORDER_HOVER", colors.border_hover);
 0119  replace_token(style_sheet, "BUTTON_HOVER", colors.button_hover);
 0120  replace_token(style_sheet, "DISABLED", colors.disabled);
 0121  replace_token(style_sheet, "TEXT", colors.text);
 0122  replace_token(style_sheet, "TEXT_MUTED", colors.text_muted);
 0123  replace_token(style_sheet, "TEXT_SETTING", colors.text_setting);
 0124  replace_token(style_sheet, "TEXT_HEADER", colors.text_header);
 0125  replace_token(style_sheet, "ACCENT", colors.accent);
 0126  replace_token(style_sheet, "ACCENT_HOVER", colors.accent_hover);
 0127  replace_token(style_sheet, "ACCENT_DARK", colors.accent_dark);
 0128  replace_token(style_sheet, "STATUS_SUCCESS", colors.status_success);
 0129  replace_token(style_sheet, "STATUS_WARNING", colors.status_warning);
 0130  replace_token(style_sheet, "DANGER", colors.danger);
 0131  replace_token(style_sheet, "DANGER_BORDER", colors.danger_border);
 0132  replace_token(style_sheet, "DANGER_BG", colors.danger_bg);
 0133  replace_token(style_sheet, "ERROR_TEXT", colors.error_text);
 0134  replace_token(style_sheet, "ERROR_BG", colors.error_bg);
 0135  replace_token(style_sheet, "ERROR_FOCUS", colors.error_focus);
 0136  replace_token(style_sheet, "ERROR_CHECK", colors.error_check);
 0137  replace_token(style_sheet, "SCROLL", colors.scroll);
 0138  replace_token(style_sheet, "HIGHLIGHTED_TEXT", colors.highlighted_text);
 0139  return style_sheet;
 0140}
 141
 142} // namespace
 143
 0144void apply_dark_clinical_theme(QApplication &app) {
 0145  const ClinicalThemeColors colors;
 146
 0147  app.setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
 0148  app.setPalette(build_palette(colors));
 0149  app.setStyleSheet(load_clinical_qss(colors));
 0150}
 151
 152} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\update_checker.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/update_checker.hh"
 16
 17#include <QJsonArray>
 18#include <QJsonDocument>
 19#include <QJsonObject>
 20#include <QNetworkAccessManager>
 21#include <QNetworkReply>
 22#include <QNetworkRequest>
 23#include <QRegularExpression>
 24#include <QStringList>
 25#include <algorithm>
 26#include <utility>
 27
 28namespace holovibes::ui {
 29
 30namespace {
 31
 32constexpr auto kReleasesApiUrl =
 33    "https://api.github.com/repos/DigitalHolography/Holoflow/releases?per_page=100";
 34
 35struct SemanticVersion {
 36  quint64     major;
 37  quint64     minor;
 38  quint64     patch;
 39  QStringList prerelease;
 40};
 41
 142std::optional<SemanticVersion> parse_version(const QString &text) {
 143  static const QRegularExpression expression(
 44      QStringLiteral(R"(^v?([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z.-]+))?$)"));
 45
 146  const auto match = expression.match(text);
 147  if (!match.hasMatch()) {
 148    return std::nullopt;
 49  }
 50
 151  bool          major_ok = false;
 152  bool          minor_ok = false;
 153  bool          patch_ok = false;
 154  const quint64 major    = match.captured(1).toULongLong(&major_ok);
 155  const quint64 minor    = match.captured(2).toULongLong(&minor_ok);
 156  const quint64 patch    = match.captured(3).toULongLong(&patch_ok);
 157  if (!major_ok || !minor_ok || !patch_ok) {
 058    return std::nullopt;
 59  }
 60
 161  QStringList prerelease;
 162  if (!match.captured(4).isEmpty()) {
 163    prerelease = match.captured(4).split('.');
 164    if (prerelease.contains(QString{})) {
 065      return std::nullopt;
 66    }
 67  }
 68
 169  return SemanticVersion{major, minor, patch, std::move(prerelease)};
 170}
 71
 172int compare_identifier(const QString &left, const QString &right) {
 173  bool          left_numeric  = false;
 174  bool          right_numeric = false;
 175  const quint64 left_value    = left.toULongLong(&left_numeric);
 176  const quint64 right_value   = right.toULongLong(&right_numeric);
 77
 178  if (left_numeric && right_numeric) {
 179    return left_value < right_value ? -1 : left_value > right_value ? 1 : 0;
 80  }
 181  if (left_numeric != right_numeric) {
 082    return left_numeric ? -1 : 1;
 83  }
 184  return QString::compare(left, right, Qt::CaseSensitive);
 185}
 86
 187int compare_versions(const SemanticVersion &left, const SemanticVersion &right) {
 188  if (left.major != right.major) {
 189    return left.major < right.major ? -1 : 1;
 90  }
 191  if (left.minor != right.minor) {
 192    return left.minor < right.minor ? -1 : 1;
 93  }
 194  if (left.patch != right.patch) {
 095    return left.patch < right.patch ? -1 : 1;
 96  }
 97
 198  if (left.prerelease.isEmpty() != right.prerelease.isEmpty()) {
 199    return left.prerelease.isEmpty() ? 1 : -1;
 100  }
 101
 1102  const qsizetype common_size = std::min(left.prerelease.size(), right.prerelease.size());
 1103  for (qsizetype index = 0; index < common_size; ++index) {
 1104    const int comparison = compare_identifier(left.prerelease[index], right.prerelease[index]);
 1105    if (comparison != 0) {
 1106      return comparison;
 107    }
 1108  }
 1109  if (left.prerelease.size() == right.prerelease.size()) {
 1110    return 0;
 111  }
 0112  return left.prerelease.size() < right.prerelease.size() ? -1 : 1;
 1113}
 114
 1115bool is_trusted_release_url(const QUrl &url) {
 1116  return url.scheme() == QStringLiteral("https") &&
 117         url.host().compare(QStringLiteral("github.com"), Qt::CaseInsensitive) == 0 &&
 118         url.path().startsWith(QStringLiteral("/DigitalHolography/Holoflow/releases/"));
 1119}
 120
 121} // namespace
 122
 123std::optional<AvailableRelease> find_available_release(const QByteArray &response,
 1124                                                       const QString    &current_version) {
 1125  const auto current = parse_version(current_version);
 1126  if (!current.has_value()) {
 1127    return std::nullopt;
 128  }
 129
 1130  QJsonParseError error;
 1131  const auto      document = QJsonDocument::fromJson(response, &error);
 1132  if (error.error != QJsonParseError::NoError || !document.isArray()) {
 1133    return std::nullopt;
 134  }
 135
 1136  std::optional<SemanticVersion>  best_version;
 1137  std::optional<AvailableRelease> best_release;
 1138  for (const auto &value : document.array()) {
 1139    const auto release = value.toObject();
 1140    if (release.isEmpty() || release.value(QStringLiteral("draft")).toBool()) {
 1141      continue;
 142    }
 143
 1144    const QString tag_name = release.value(QStringLiteral("tag_name")).toString();
 1145    const auto    version  = parse_version(tag_name);
 1146    const QUrl    page_url(release.value(QStringLiteral("html_url")).toString());
 1147    if (!version.has_value() || compare_versions(*version, *current) <= 0 ||
 148        !is_trusted_release_url(page_url)) {
 1149      continue;
 150    }
 151
 1152    if (!best_version.has_value() || compare_versions(*version, *best_version) > 0) {
 1153      best_version = version;
 1154      best_release = AvailableRelease{
 155          tag_name.startsWith('v') ? tag_name.sliced(1) : tag_name,
 156          page_url,
 157      };
 158    }
 1159  }
 160
 1161  return best_release;
 1162}
 163
 164UpdateChecker::UpdateChecker(QString current_version, QObject *parent)
 0165    : QObject(parent), current_version_(std::move(current_version)),
 0166      network_manager_(new QNetworkAccessManager(this)) {}
 167
 0168void UpdateChecker::start() {
 0169  if (started_) {
 0170    return;
 171  }
 0172  started_ = true;
 173
 0174  QNetworkRequest request{QUrl(QString::fromLatin1(kReleasesApiUrl))};
 0175  request.setHeader(QNetworkRequest::UserAgentHeader,
 176                    QStringLiteral("Holovibes/%1").arg(current_version_));
 0177  request.setRawHeader("Accept", "application/vnd.github+json");
 0178  request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
 0179  request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
 180                       QNetworkRequest::NoLessSafeRedirectPolicy);
 0181  request.setTransferTimeout(5000);
 182
 0183  auto *reply = network_manager_->get(request);
 0184  connect(reply, &QNetworkReply::finished, this, [this, reply]() {
 185    const auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
 186    if (reply->error() == QNetworkReply::NoError && status == 200) {
 187      const auto release = find_available_release(reply->readAll(), current_version_);
 188      if (release.has_value()) {
 189        emit update_available(release->version, release->page_url);
 190      }
 191    }
 192    reply->deleteLater();
 193  });
 0194}
 195
 196} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\update_checker.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QByteArray>
 18#include <QObject>
 19#include <QString>
 20#include <QUrl>
 21#include <optional>
 22
 23class QNetworkAccessManager;
 24
 25namespace holovibes::ui {
 26
 27struct AvailableRelease {
 28  QString version;
 29  QUrl    page_url;
 30};
 31
 32std::optional<AvailableRelease> find_available_release(const QByteArray &response,
 33                                                       const QString    &current_version);
 34
 35class UpdateChecker : public QObject {
 36  Q_OBJECT
 37
 38public:
 39  explicit UpdateChecker(QString current_version, QObject *parent = nullptr);
 40
 41  void start();
 42
 43signals:
 44  void update_available(const QString &version, const QUrl &page_url);
 45
 46private:
 47  QString                current_version_;
 48  QNetworkAccessManager *network_manager_;
 049  bool                   started_ = false;
 50};
 51
 52} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\visualization_workspace.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/visualization_workspace.hh"
 16
 17#include <DockAreaWidget.h>
 18#include <DockManager.h>
 19#include <DockWidget.h>
 20
 21#include <QAction>
 22#include <QLabel>
 23#include <QMenu>
 24#include <QSettings>
 25#include <QSignalBlocker>
 26#include <QStackedLayout>
 27#include <QStackedWidget>
 28#include <QVBoxLayout>
 29
 30#include <algorithm>
 31#include <utility>
 32
 33#include "ui/widgets/tensor_display_widget.hh"
 34#include "ui/widgets/zernike_history_widget.hh"
 35
 36namespace {
 37
 38constexpr int kLayoutStateVersion = 1;
 39
 040QWidget *create_placeholder(const QString &text, QWidget *parent) {
 041  auto *page   = new QWidget(parent);
 042  auto *layout = new QVBoxLayout(page);
 043  layout->setContentsMargins(24, 24, 24, 24);
 44
 045  auto *label = new QLabel(text, page);
 046  label->setObjectName("visualizationWorkspacePlaceholder");
 047  label->setAlignment(Qt::AlignCenter);
 048  label->setWordWrap(true);
 049  layout->addWidget(label, 1);
 050  return page;
 051}
 52
 053void show_waiting_placeholder(QWidget *widget) {
 054  if (auto *tensor_widget = qobject_cast<holovibes::ui::TensorDisplayWidget *>(widget);
 055      tensor_widget != nullptr) {
 056    tensor_widget->show_waiting_placeholder();
 057    return;
 58  }
 59
 060  if (auto *history_widget = qobject_cast<holovibes::ui::ZernikeHistoryWidget *>(widget);
 061      history_widget != nullptr) {
 062    history_widget->show_waiting_placeholder();
 63  }
 064}
 65
 066ads::DockWidgetArea dock_area_for(holovibes::ui::DockPlacement placement) {
 67  using holovibes::ui::DockPlacement;
 068  switch (placement) {
 69  case DockPlacement::Left:
 070    return ads::LeftDockWidgetArea;
 71  case DockPlacement::Right:
 072    return ads::RightDockWidgetArea;
 73  case DockPlacement::Above:
 074    return ads::TopDockWidgetArea;
 75  case DockPlacement::Below:
 076    return ads::BottomDockWidgetArea;
 77  case DockPlacement::Root:
 78  case DockPlacement::Tab:
 079    return ads::CenterDockWidgetArea;
 80  }
 81
 082  return ads::CenterDockWidgetArea;
 083}
 84
 85} // namespace
 86
 87namespace holovibes::ui {
 88
 89struct VisualizationWorkspace::Entry {
 90  VisualizationDescriptor descriptor;
 91  ads::CDockWidget       *dock_widget      = nullptr;
 92  QStackedWidget         *content_stack    = nullptr;
 93  QWidget                *placeholder      = nullptr;
 94  QLabel                 *placeholder_text = nullptr;
 95  QAction                *view_action      = nullptr;
 96  bool                    content_expected = false;
 97  bool                    content_received = false;
 98  QString                 unavailable_message;
 99  bool                    view_preference = true;
 100};
 101
 0102VisualizationWorkspace::VisualizationWorkspace(QWidget *parent) : QWidget(parent) {
 0103  setObjectName("displayWorkspace");
 0104  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 0105  setMinimumSize(0, 0);
 106
 0107  ads::CDockManager::setConfigFlag(ads::CDockManager::FocusHighlighting, true);
 0108  ads::CDockManager::setConfigFlag(ads::CDockManager::EqualSplitOnInsertion, true);
 0109  ads::CDockManager::setConfigFlag(ads::CDockManager::DockAreaCloseButtonClosesTab, true);
 0110  ads::CDockManager::setConfigFlag(ads::CDockManager::TabCloseButtonIsToolButton, true);
 111
 0112  auto *stack = new QStackedLayout(this);
 0113  stack->setContentsMargins(0, 0, 0, 0);
 0114  stack->setSpacing(0);
 115
 0116  pipeline_stopped_page_ =
 117      create_placeholder(tr("Start the pipeline to display visualizations"), this);
 0118  no_displays_page_ = create_placeholder(
 119      tr("No displays are enabled\n\nEnable a display in the processing settings"), this);
 0120  all_hidden_page_ = create_placeholder(
 121      tr("No visualizations are currently shown\n\nUse View > Visualizations to show one"), this);
 122
 0123  dock_manager_ = new ads::CDockManager(this);
 0124  dock_manager_->setObjectName("visualizationDockManager");
 0125  dock_manager_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 0126  dock_manager_->setMinimumSize(0, 0);
 127
 0128  stack->addWidget(pipeline_stopped_page_);
 0129  stack->addWidget(no_displays_page_);
 0130  stack->addWidget(all_hidden_page_);
 0131  stack->addWidget(dock_manager_);
 0132  stack->setCurrentWidget(pipeline_stopped_page_);
 133
 0134  connect(dock_manager_, &ads::CDockManager::focusedDockWidgetChanged, this,
 135          [this](ads::CDockWidget *, ads::CDockWidget *current) {
 136            const auto *entry = entry_for_dock(current);
 137            emit        selected_visualization_changed(entry == nullptr ? QString{}
 138                                                                        : entry->descriptor.id);
 139          });
 0140}
 141
 0142VisualizationWorkspace::~VisualizationWorkspace() = default;
 143
 0144bool VisualizationWorkspace::register_visualization(VisualizationDescriptor descriptor) {
 145  if (registration_finalized_ || descriptor.id.isEmpty() || descriptor.title.isEmpty() ||
 0146      descriptor.widget == nullptr || entry_for_id(descriptor.id) != nullptr) {
 0147    return false;
 148  }
 149
 0150  auto entry             = std::make_unique<Entry>();
 0151  entry->descriptor      = std::move(descriptor);
 0152  entry->view_preference = entry->descriptor.default_enabled;
 0153  entry->dock_widget     = new ads::CDockWidget(dock_manager_, entry->descriptor.title);
 0154  entry->dock_widget->setObjectName(entry->descriptor.id);
 0155  entry->dock_widget->setFeatures(
 156      ads::CDockWidget::DockWidgetClosable | ads::CDockWidget::DockWidgetMovable |
 157      ads::CDockWidget::DockWidgetFloatable | ads::CDockWidget::DockWidgetFocusable);
 0158  entry->dock_widget->setMinimumSizeHintMode(ads::CDockWidget::MinimumSizeHintFromDockWidget);
 159
 0160  entry->content_stack = new QStackedWidget(entry->dock_widget);
 0161  entry->content_stack->setContentsMargins(0, 0, 0, 0);
 0162  entry->content_stack->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
 0163  entry->content_stack->setMinimumSize(0, 0);
 164
 0165  entry->descriptor.widget->setMinimumSize(0, 0);
 0166  entry->descriptor.widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
 0167  entry->content_stack->addWidget(entry->descriptor.widget);
 168
 0169  entry->placeholder = create_placeholder({}, entry->content_stack);
 0170  entry->placeholder_text =
 171      entry->placeholder->findChild<QLabel *>(QStringLiteral("visualizationWorkspacePlaceholder"));
 0172  entry->content_stack->addWidget(entry->placeholder);
 173
 0174  entry->dock_widget->setWidget(entry->content_stack, ads::CDockWidget::ForceNoScrollArea);
 175
 0176  entry->view_action = new QAction(entry->descriptor.title, this);
 0177  entry->view_action->setCheckable(true);
 0178  entry->view_action->setChecked(entry->view_preference);
 0179  entry->view_action->setEnabled(false);
 180
 0181  auto *entry_pointer = entry.get();
 0182  if (auto *tensor_widget = qobject_cast<TensorDisplayWidget *>(entry_pointer->descriptor.widget);
 0183      tensor_widget != nullptr) {
 0184    connect(tensor_widget, &TensorDisplayWidget::tensorDisplayed, this, [this, entry_pointer]() {
 185      entry_pointer->content_received = true;
 186      update_entry_content(*entry_pointer);
 187    });
 0188  } else if (auto *history_widget =
 189                 qobject_cast<ZernikeHistoryWidget *>(entry_pointer->descriptor.widget);
 0190             history_widget != nullptr) {
 0191    connect(history_widget, &ZernikeHistoryWidget::samplesDisplayed, this, [this, entry_pointer]() {
 192      entry_pointer->content_received = true;
 193      update_entry_content(*entry_pointer);
 194    });
 195  }
 196
 0197  connect(entry->view_action, &QAction::toggled, this, [this, entry_pointer](bool checked) {
 198    if (applying_state_) {
 199      return;
 200    }
 201    entry_pointer->view_preference = checked;
 202    apply_state();
 203    emit visualization_preferences_changed();
 204  });
 0205  connect(entry->dock_widget, &ads::CDockWidget::closed, this, [this, entry_pointer]() {
 206    if (applying_state_) {
 207      return;
 208    }
 209    entry_pointer->view_preference = false;
 210    const QSignalBlocker blocker(entry_pointer->view_action);
 211    entry_pointer->view_action->setChecked(false);
 212    cache_active_layout();
 213    update_central_page();
 214    emit visualization_preferences_changed();
 215  });
 216
 0217  entries_.push_back(std::move(entry));
 0218  add_entry_at_default_location(*entries_.back());
 0219  return true;
 0220}
 221
 0222void VisualizationWorkspace::finalize_registration() {
 0223  if (registration_finalized_) {
 0224    return;
 225  }
 226
 0227  registration_finalized_   = true;
 0228  default_layout_state_     = dock_manager_->saveState(kLayoutStateVersion);
 0229  last_active_layout_state_ = default_layout_state_;
 0230  apply_state();
 0231}
 232
 0233void VisualizationWorkspace::populate_view_menu(QMenu *view_menu) {
 0234  if (view_menu == nullptr) {
 0235    return;
 236  }
 237
 0238  auto *visualizations_menu = view_menu->addMenu(tr("Visualizations"));
 0239  for (const auto &entry : entries_) {
 0240    visualizations_menu->addAction(entry->view_action);
 0241  }
 242
 0243  view_menu->addSeparator();
 0244  auto *show_all_action = view_menu->addAction(tr("Show All Visualizations"));
 0245  auto *hide_all_action = view_menu->addAction(tr("Hide All Visualizations"));
 0246  view_menu->addSeparator();
 0247  auto *dock_all_action = view_menu->addAction(tr("Dock All Visualizations"));
 0248  auto *reset_action    = view_menu->addAction(tr("Reset Visualization Layout"));
 249
 0250  connect(show_all_action, &QAction::triggered, this,
 251          &VisualizationWorkspace::show_all_visualizations);
 0252  connect(hide_all_action, &QAction::triggered, this,
 253          &VisualizationWorkspace::hide_all_visualizations);
 0254  connect(dock_all_action, &QAction::triggered, this,
 255          &VisualizationWorkspace::dock_all_visualizations);
 0256  connect(reset_action, &QAction::triggered, this,
 257          &VisualizationWorkspace::reset_visualization_layout);
 0258}
 259
 0260void VisualizationWorkspace::set_pipeline_running(bool running) {
 0261  if (pipeline_running_ == running) {
 0262    apply_state();
 0263    return;
 264  }
 265
 0266  if (pipeline_running_) {
 0267    cache_active_layout();
 268  }
 0269  if (!pipeline_running_ && running) {
 0270    reset_visualization_content();
 271  }
 0272  pipeline_running_ = running;
 0273  apply_state();
 0274}
 275
 276void VisualizationWorkspace::set_visualization_content_expectations(
 0277    const QHash<QString, VisualizationContentExpectation> &expectations) {
 0278  for (auto &entry : entries_) {
 0279    const auto it = expectations.constFind(entry->descriptor.id);
 0280    if (it != expectations.cend()) {
 0281      const bool expected_changed = entry->content_expected != it->expected;
 0282      entry->content_expected     = it->expected;
 0283      entry->unavailable_message  = it->unavailable_message;
 0284      if (expected_changed || !entry->content_expected) {
 0285        entry->content_received = false;
 286      }
 287    }
 0288  }
 0289  apply_state();
 0290}
 291
 0292void VisualizationWorkspace::set_visualization_title(const QString &id, const QString &title) {
 0293  auto *entry = entry_for_id(id);
 0294  if (entry == nullptr || title.isEmpty()) {
 0295    return;
 296  }
 297
 0298  entry->descriptor.title = title;
 0299  entry->dock_widget->setWindowTitle(title);
 0300  entry->view_action->setText(title);
 0301}
 302
 0303void VisualizationWorkspace::set_visualization_enabled(const QString &id, bool enabled) {
 0304  auto *entry = entry_for_id(id);
 0305  if (entry == nullptr || entry->view_preference == enabled) {
 0306    return;
 307  }
 308
 0309  entry->view_preference = enabled;
 0310  apply_state();
 0311  emit visualization_preferences_changed();
 0312}
 313
 0314bool VisualizationWorkspace::is_visualization_enabled(const QString &id) const {
 0315  const auto *entry = entry_for_id(id);
 0316  return entry != nullptr && entry->view_preference;
 0317}
 318
 0319void VisualizationWorkspace::reset_visualization_content() {
 0320  for (auto &entry : entries_) {
 0321    entry->content_received = false;
 0322  }
 0323  apply_state();
 0324}
 325
 0326void VisualizationWorkspace::select_visualization(const QString &id) {
 0327  auto *entry = entry_for_id(id);
 0328  if (entry == nullptr || entry->dock_widget->isClosed()) {
 0329    emit selected_visualization_changed({});
 0330    return;
 331  }
 332
 0333  entry->dock_widget->setAsCurrentTab();
 0334  entry->dock_widget->raise();
 0335  dock_manager_->setDockWidgetFocused(entry->dock_widget);
 0336  emit selected_visualization_changed(id);
 0337}
 338
 0339void VisualizationWorkspace::save_persistent_state(QSettings &settings) {
 0340  if (!registration_finalized_ || default_layout_state_.isEmpty()) {
 0341    return;
 342  }
 343
 0344  if (pipeline_running_) {
 0345    cache_active_layout();
 346  }
 347
 0348  settings.beginGroup("visualization_workspace");
 0349  settings.setValue("layout_state", last_active_layout_state_);
 0350  for (const auto &entry : entries_) {
 0351    settings.setValue(QString("view_preferences/%1").arg(entry->descriptor.id),
 352                      entry->view_preference);
 0353  }
 0354  settings.endGroup();
 0355}
 356
 0357bool VisualizationWorkspace::restore_persistent_state(QSettings &settings) {
 0358  if (!registration_finalized_) {
 0359    return false;
 360  }
 361
 0362  settings.beginGroup("visualization_workspace");
 0363  for (auto &entry : entries_) {
 0364    entry->view_preference =
 365        settings
 366            .value(QString("view_preferences/%1").arg(entry->descriptor.id), entry->view_preference)
 367            .toBool();
 0368  }
 369
 0370  const QByteArray saved_state = settings.value("layout_state").toByteArray();
 0371  bool             restored    = false;
 0372  if (!saved_state.isEmpty()) {
 0373    applying_state_ = true;
 0374    restored        = dock_manager_->restoreState(saved_state, kLayoutStateVersion);
 0375    if (restored) {
 0376      for (auto &entry : entries_) {
 0377        if (entry->dock_widget->dockAreaWidget() == nullptr) {
 0378          add_entry_at_default_location(*entry);
 379        }
 0380      }
 381    }
 0382    applying_state_ = false;
 383  }
 0384  settings.endGroup();
 385
 0386  if (!restored) {
 0387    applying_state_               = true;
 0388    const bool has_default_layout = !default_layout_state_.isEmpty();
 0389    if (has_default_layout) {
 0390      restored = dock_manager_->restoreState(default_layout_state_, kLayoutStateVersion);
 391    }
 0392    applying_state_ = false;
 393  }
 394
 0395  last_active_layout_state_ = dock_manager_->saveState(kLayoutStateVersion);
 0396  apply_state();
 0397  return restored;
 0398}
 399
 0400VisualizationWorkspace::Entry *VisualizationWorkspace::entry_for_id(const QString &id) const {
 0401  const auto it = std::find_if(entries_.cbegin(), entries_.cend(),
 402                               [&id](const auto &entry) { return entry->descriptor.id == id; });
 0403  return it == entries_.cend() ? nullptr : it->get();
 0404}
 405
 406VisualizationWorkspace::Entry *
 0407VisualizationWorkspace::entry_for_dock(ads::CDockWidget *dock_widget) const {
 408  const auto it =
 0409      std::find_if(entries_.cbegin(), entries_.cend(),
 410                   [dock_widget](const auto &entry) { return entry->dock_widget == dock_widget; });
 0411  return it == entries_.cend() ? nullptr : it->get();
 0412}
 413
 0414void VisualizationWorkspace::add_entry_at_default_location(Entry &entry) {
 0415  if (entry.descriptor.default_placement == DockPlacement::Root ||
 416      entry.descriptor.default_anchor_id.isEmpty()) {
 0417    dock_manager_->addDockWidget(ads::CenterDockWidgetArea, entry.dock_widget);
 0418    return;
 419  }
 420
 0421  auto *anchor = entry_for_id(entry.descriptor.default_anchor_id);
 0422  if (anchor == nullptr || anchor->dock_widget->dockAreaWidget() == nullptr) {
 0423    dock_manager_->addDockWidget(ads::CenterDockWidgetArea, entry.dock_widget);
 0424    return;
 425  }
 426
 0427  dock_manager_->addDockWidget(dock_area_for(entry.descriptor.default_placement), entry.dock_widget,
 428                               anchor->dock_widget->dockAreaWidget());
 0429}
 430
 0431void VisualizationWorkspace::apply_state() {
 0432  if (!registration_finalized_ || applying_state_) {
 0433    return;
 434  }
 435
 0436  applying_state_ = true;
 0437  for (auto &entry : entries_) {
 438    {
 0439      const QSignalBlocker blocker(entry->view_action);
 0440      entry->view_action->setChecked(entry->view_preference);
 0441      entry->view_action->setEnabled(true);
 0442    }
 443
 0444    const bool should_be_open = entry->view_preference;
 0445    if (should_be_open == entry->dock_widget->isClosed()) {
 0446      entry->dock_widget->toggleView(should_be_open);
 447    }
 0448    update_entry_content(*entry);
 0449  }
 0450  applying_state_ = false;
 0451  update_central_page();
 0452}
 453
 0454void VisualizationWorkspace::update_central_page() {
 0455  auto *stack = qobject_cast<QStackedLayout *>(layout());
 0456  if (stack == nullptr) {
 0457    return;
 458  }
 459
 0460  const auto visible_count =
 461      std::count_if(entries_.cbegin(), entries_.cend(), [](const auto &entry) {
 462        return entry->view_preference && !entry->dock_widget->isClosed();
 463      });
 0464  stack->setCurrentWidget(visible_count == 0 ? all_hidden_page_ : dock_manager_);
 0465}
 466
 0467void VisualizationWorkspace::update_entry_content(Entry &entry) {
 0468  if (entry.content_stack == nullptr || entry.placeholder == nullptr) {
 0469    return;
 470  }
 471
 0472  if (!entry.content_expected) {
 0473    const QString message =
 474        entry.unavailable_message.isEmpty()
 475            ? tr("This visualization is unavailable in the current configuration.")
 476            : entry.unavailable_message;
 0477    if (entry.placeholder_text != nullptr) {
 0478      entry.placeholder_text->setText(message);
 479    }
 0480    entry.content_stack->setCurrentWidget(entry.placeholder);
 0481    return;
 482  }
 483
 0484  entry.content_stack->setCurrentWidget(entry.descriptor.widget);
 0485  if (!entry.content_received) {
 0486    show_waiting_placeholder(entry.descriptor.widget);
 487  }
 0488}
 489
 0490void VisualizationWorkspace::cache_active_layout() {
 0491  if (!registration_finalized_ || dock_manager_ == nullptr) {
 0492    return;
 493  }
 494
 0495  const QByteArray state = dock_manager_->saveState(kLayoutStateVersion);
 0496  if (!state.isEmpty()) {
 0497    last_active_layout_state_ = state;
 498  }
 0499}
 500
 0501void VisualizationWorkspace::show_all_visualizations() {
 0502  for (auto &entry : entries_) {
 0503    entry->view_preference = true;
 0504  }
 0505  apply_state();
 0506  emit visualization_preferences_changed();
 0507}
 508
 0509void VisualizationWorkspace::hide_all_visualizations() {
 0510  for (auto &entry : entries_) {
 0511    entry->view_preference = false;
 0512  }
 0513  apply_state();
 0514  emit visualization_preferences_changed();
 0515}
 516
 0517void VisualizationWorkspace::dock_all_visualizations() {
 0518  ads::CDockAreaWidget *target_area = nullptr;
 0519  for (const auto &entry : entries_) {
 0520    if (!entry->dock_widget->isClosed() && !entry->dock_widget->isInFloatingContainer()) {
 0521      target_area = entry->dock_widget->dockAreaWidget();
 0522      break;
 523    }
 0524  }
 525
 0526  for (const auto &entry : entries_) {
 0527    if (entry->dock_widget->isClosed() || !entry->dock_widget->isInFloatingContainer()) {
 0528      continue;
 529    }
 530
 0531    target_area =
 532        dock_manager_->addDockWidget(ads::CenterDockWidgetArea, entry->dock_widget, target_area);
 0533  }
 0534  cache_active_layout();
 0535}
 536
 0537void VisualizationWorkspace::reset_visualization_layout() {
 0538  if (default_layout_state_.isEmpty()) {
 0539    return;
 540  }
 541
 0542  applying_state_     = true;
 0543  const bool restored = dock_manager_->restoreState(default_layout_state_, kLayoutStateVersion);
 0544  applying_state_     = false;
 0545  if (!restored) {
 0546    return;
 547  }
 548
 0549  last_active_layout_state_ = default_layout_state_;
 0550  apply_state();
 0551  save_workspace_settings_now();
 0552}
 553
 0554void VisualizationWorkspace::save_workspace_settings_now() {
 0555  QSettings settings;
 0556  save_persistent_state(settings);
 0557  settings.sync();
 0558}
 559
 560} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\visualization_workspace.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QByteArray>
 18#include <QHash>
 19#include <QString>
 20#include <QWidget>
 21#include <memory>
 22#include <vector>
 23
 24class QMenu;
 25class QSettings;
 26
 27namespace ads {
 28class CDockManager;
 29class CDockWidget;
 30} // namespace ads
 31
 32namespace holovibes::ui {
 33
 34enum class DockPlacement { Root, Left, Right, Above, Below, Tab };
 35
 36struct VisualizationDescriptor {
 37  QString       id;
 38  QString       title;
 39  QWidget      *widget = nullptr;
 40  QString       default_anchor_id;
 41  DockPlacement default_placement = DockPlacement::Tab;
 42  bool          default_enabled   = true;
 43};
 44
 45struct VisualizationContentExpectation {
 46  bool    expected = false;
 47  QString unavailable_message;
 48};
 49
 50class VisualizationWorkspace : public QWidget {
 051  Q_OBJECT
 52
 53public:
 54  explicit VisualizationWorkspace(QWidget *parent = nullptr);
 55  ~VisualizationWorkspace() override;
 56
 57  bool register_visualization(VisualizationDescriptor descriptor);
 58  void finalize_registration();
 59  void populate_view_menu(QMenu *view_menu);
 60
 61  void set_pipeline_running(bool running);
 62  void set_visualization_content_expectations(
 63      const QHash<QString, VisualizationContentExpectation> &expectations);
 64  void set_visualization_title(const QString &id, const QString &title);
 65  void set_visualization_enabled(const QString &id, bool enabled);
 66  bool is_visualization_enabled(const QString &id) const;
 67  void reset_visualization_content();
 68  void select_visualization(const QString &id);
 69
 70  void save_persistent_state(QSettings &settings);
 71  bool restore_persistent_state(QSettings &settings);
 72
 73signals:
 74  void selected_visualization_changed(const QString &visualization_id);
 75  void visualization_preferences_changed();
 76
 77private:
 78  struct Entry;
 79
 80  Entry *entry_for_id(const QString &id) const;
 81  Entry *entry_for_dock(ads::CDockWidget *dock_widget) const;
 82  void   add_entry_at_default_location(Entry &entry);
 83  void   apply_state();
 84  void   update_central_page();
 85  void   update_entry_content(Entry &entry);
 86  void   cache_active_layout();
 87  void   show_all_visualizations();
 88  void   hide_all_visualizations();
 89  void   dock_all_visualizations();
 90  void   reset_visualization_layout();
 91  void   save_workspace_settings_now();
 92
 093  ads::CDockManager                  *dock_manager_          = nullptr;
 094  QWidget                            *pipeline_stopped_page_ = nullptr;
 095  QWidget                            *no_displays_page_      = nullptr;
 096  QWidget                            *all_hidden_page_       = nullptr;
 97  std::vector<std::unique_ptr<Entry>> entries_;
 98  QByteArray                          default_layout_state_;
 99  QByteArray                          last_active_layout_state_;
 0100  bool                                registration_finalized_ = false;
 0101  bool                                pipeline_running_       = false;
 0102  bool                                applying_state_         = false;
 103};
 104
 105} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\auto_focus_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/auto_focus_widget.hh"
 16#include "ui/widgets/validation_style.hh"
 17
 18#include <algorithm>
 19
 20#include <QGridLayout>
 21#include <QLabel>
 22#include <QSignalBlocker>
 23#include <QSizePolicy>
 24#include <QSpacerItem>
 25#include <QVBoxLayout>
 26
 27namespace holovibes::ui {
 28
 29namespace {
 30
 31constexpr int kDefaultNbSubaps      = 5;
 32constexpr int kMinNbSubaps          = 3;
 33constexpr int kMaxNbSubaps          = 99;
 34constexpr int kDefaultNbIter        = 1;
 35constexpr int kMinNbIter            = 1;
 36constexpr int kMaxNbIter            = 8;
 37constexpr int kSingleReferenceIndex = 0;
 38constexpr int kGraphLaplacianIndex  = 1;
 39
 40QSpinBox *create_spin_box(QWidget *parent, int value, int min_value, int max_value,
 041                          const QString &tooltip) {
 042  auto *spin_box = new QSpinBox(parent);
 043  spin_box->setRange(min_value, max_value);
 044  spin_box->setValue(value);
 045  spin_box->setToolTip(tooltip);
 046  return spin_box;
 047}
 48
 49QSpinBox *create_odd_spin_box(QWidget *parent, int value, int min_value, int max_value,
 050                              const QString &tooltip) {
 051  auto *spin_box = new QSpinBox(parent);
 052  spin_box->setRange(min_value, max_value);
 053  spin_box->setSingleStep(2);
 054  spin_box->setValue(value);
 055  spin_box->setToolTip(tooltip);
 056  return spin_box;
 057}
 58
 059QDoubleSpinBox *create_display_double_spin_box(QWidget *parent, double value, int decimals = 6) {
 060  auto *spin_box = new QDoubleSpinBox(parent);
 061  spin_box->setRange(-1e9, 1e9);
 062  spin_box->setDecimals(decimals);
 063  spin_box->setButtonSymbols(QAbstractSpinBox::NoButtons);
 064  spin_box->setReadOnly(true);
 065  spin_box->setValue(value);
 066  spin_box->setSuffix(" rad");
 067  return spin_box;
 068}
 69
 070QCheckBox *create_checkbox(QWidget *parent, bool checked = false, const QString &text = {}) {
 071  auto *checkbox = new QCheckBox(text, parent);
 072  checkbox->setChecked(checked);
 073  return checkbox;
 074}
 75
 76} // namespace
 77
 078AutoFocusWidget::AutoFocusWidget(QWidget *parent) : QGroupBox("AUTO FOCUS", parent) {
 079  setup_ui();
 080  connect_signals();
 081  set_enabled(false);
 082}
 83
 84// Configuration
 085int  AutoFocusWidget::get_nb_subaps() const { return nb_subaps_spin_->value(); }
 086int  AutoFocusWidget::get_nb_iter() const { return nb_iter_spin_->value(); }
 087bool AutoFocusWidget::is_enabled() const { return enable_check_->isChecked(); }
 088bool AutoFocusWidget::skip_subapertures_outside_pupil() const {
 089  return skip_subapertures_outside_pupil_checkbox_->isChecked();
 090}
 091bool AutoFocusWidget::use_graph_laplacian() const {
 092  return slope_recovery_combo_->currentIndex() == kGraphLaplacianIndex;
 093}
 94
 95// Zernike values
 096double AutoFocusWidget::get_z2() const { return z2_spin_->value(); }
 097double AutoFocusWidget::get_z3() const { return z3_spin_->value(); }
 098double AutoFocusWidget::get_z4() const { return z4_spin_->value(); }
 099double AutoFocusWidget::get_z5() const { return z5_spin_->value(); }
 0100double AutoFocusWidget::get_z6() const { return z6_spin_->value(); }
 0101double AutoFocusWidget::get_z7() const { return z7_spin_->value(); }
 0102double AutoFocusWidget::get_z8() const { return z8_spin_->value(); }
 0103double AutoFocusWidget::get_z9() const { return z9_spin_->value(); }
 0104double AutoFocusWidget::get_z10() const { return z10_spin_->value(); }
 105
 0106void AutoFocusWidget::set_z2(double value) { z2_spin_->setValue(value); }
 0107void AutoFocusWidget::set_z3(double value) { z3_spin_->setValue(value); }
 0108void AutoFocusWidget::set_z4(double value) { z4_spin_->setValue(value); }
 0109void AutoFocusWidget::set_z5(double value) { z5_spin_->setValue(value); }
 0110void AutoFocusWidget::set_z6(double value) { z6_spin_->setValue(value); }
 0111void AutoFocusWidget::set_z7(double value) { z7_spin_->setValue(value); }
 0112void AutoFocusWidget::set_z8(double value) { z8_spin_->setValue(value); }
 0113void AutoFocusWidget::set_z9(double value) { z9_spin_->setValue(value); }
 0114void AutoFocusWidget::set_z10(double value) { z10_spin_->setValue(value); }
 115
 0116void AutoFocusWidget::set_zernike_value(int noll_index, double value) {
 0117  switch (noll_index) {
 118  case 2:
 0119    set_z2(value);
 0120    break;
 121  case 3:
 0122    set_z3(value);
 0123    break;
 124  case 4:
 0125    set_z4(value);
 0126    break;
 127  case 5:
 0128    set_z5(value);
 0129    break;
 130  case 6:
 0131    set_z6(value);
 0132    break;
 133  case 7:
 0134    set_z7(value);
 0135    break;
 136  case 8:
 0137    set_z8(value);
 0138    break;
 139  case 9:
 0140    set_z9(value);
 0141    break;
 142  case 10:
 0143    set_z10(value);
 144    break;
 145  default:
 146    break;
 147  }
 0148}
 149
 150void AutoFocusWidget::set_zernike_values(const std::vector<int>   &noll_indexes,
 0151                                         const std::vector<float> &values) {
 0152  reset_zernike_values();
 153
 0154  const auto count = std::min(noll_indexes.size(), values.size());
 0155  for (size_t i = 0; i < count; ++i) {
 0156    set_zernike_value(noll_indexes[i], values[i]);
 0157  }
 0158}
 159
 0160void AutoFocusWidget::reset_zernike_values() {
 0161  set_z2(0.0);
 0162  set_z3(0.0);
 0163  set_z4(0.0);
 0164  set_z5(0.0);
 0165  set_z6(0.0);
 0166  set_z7(0.0);
 0167  set_z8(0.0);
 0168  set_z9(0.0);
 0169  set_z10(0.0);
 0170}
 171
 172// Per-coefficient enable flags
 0173bool AutoFocusWidget::is_z2_enabled() const { return z2_checkbox_->isChecked(); }
 0174bool AutoFocusWidget::is_z3_enabled() const { return z3_checkbox_->isChecked(); }
 0175bool AutoFocusWidget::is_z4_enabled() const { return z4_checkbox_->isChecked(); }
 0176bool AutoFocusWidget::is_z5_enabled() const { return z5_checkbox_->isChecked(); }
 0177bool AutoFocusWidget::is_z6_enabled() const { return z6_checkbox_->isChecked(); }
 0178bool AutoFocusWidget::is_z7_enabled() const { return z7_checkbox_->isChecked(); }
 0179bool AutoFocusWidget::is_z8_enabled() const { return z8_checkbox_->isChecked(); }
 0180bool AutoFocusWidget::is_z9_enabled() const { return z9_checkbox_->isChecked(); }
 0181bool AutoFocusWidget::is_z10_enabled() const { return z10_checkbox_->isChecked(); }
 182
 0183void AutoFocusWidget::set_z2_enabled(bool enabled) { z2_checkbox_->setChecked(enabled); }
 0184void AutoFocusWidget::set_z3_enabled(bool enabled) { z3_checkbox_->setChecked(enabled); }
 0185void AutoFocusWidget::set_z4_enabled(bool enabled) { z4_checkbox_->setChecked(enabled); }
 0186void AutoFocusWidget::set_z5_enabled(bool enabled) { z5_checkbox_->setChecked(enabled); }
 0187void AutoFocusWidget::set_z6_enabled(bool enabled) { z6_checkbox_->setChecked(enabled); }
 0188void AutoFocusWidget::set_z7_enabled(bool enabled) { z7_checkbox_->setChecked(enabled); }
 0189void AutoFocusWidget::set_z8_enabled(bool enabled) { z8_checkbox_->setChecked(enabled); }
 0190void AutoFocusWidget::set_z9_enabled(bool enabled) { z9_checkbox_->setChecked(enabled); }
 0191void AutoFocusWidget::set_z10_enabled(bool enabled) { z10_checkbox_->setChecked(enabled); }
 192
 0193void AutoFocusWidget::set_enabled(bool enabled) {
 0194  enable_check_->setChecked(enabled);
 0195  set_controls_enabled(enabled);
 0196}
 197
 0198void AutoFocusWidget::set_skip_subapertures_outside_pupil(bool skip) {
 0199  skip_subapertures_outside_pupil_checkbox_->setChecked(skip);
 0200}
 201
 0202void AutoFocusWidget::set_use_graph_laplacian(bool enabled) {
 0203  slope_recovery_combo_->setCurrentIndex(enabled ? kGraphLaplacianIndex : kSingleReferenceIndex);
 0204  update_mode_dependent_controls();
 0205}
 206
 0207void AutoFocusWidget::clear_validation_styles() {
 0208  clear_validation_error(nb_subaps_spin_);
 0209  clear_validation_error(nb_iter_spin_);
 0210}
 211
 0212void AutoFocusWidget::mark_nb_subaps_invalid() { mark_validation_error(nb_subaps_spin_); }
 0213void AutoFocusWidget::mark_nb_iter_invalid() { mark_validation_error(nb_iter_spin_); }
 214
 215// Widget accessors
 0216QCheckBox *AutoFocusWidget::z2_checkbox() { return z2_checkbox_; }
 0217QCheckBox *AutoFocusWidget::z3_checkbox() { return z3_checkbox_; }
 0218QCheckBox *AutoFocusWidget::z4_checkbox() { return z4_checkbox_; }
 0219QCheckBox *AutoFocusWidget::z5_checkbox() { return z5_checkbox_; }
 0220QCheckBox *AutoFocusWidget::z6_checkbox() { return z6_checkbox_; }
 0221QCheckBox *AutoFocusWidget::z7_checkbox() { return z7_checkbox_; }
 0222QCheckBox *AutoFocusWidget::z8_checkbox() { return z8_checkbox_; }
 0223QCheckBox *AutoFocusWidget::z9_checkbox() { return z9_checkbox_; }
 0224QCheckBox *AutoFocusWidget::z10_checkbox() { return z10_checkbox_; }
 225
 0226QSpinBox *AutoFocusWidget::nb_subaps_spin() { return nb_subaps_spin_; }
 0227QSpinBox *AutoFocusWidget::nb_iter_spin() { return nb_iter_spin_; }
 228
 0229void AutoFocusWidget::setup_ui() {
 0230  auto *outer_layout = new QVBoxLayout(this);
 0231  outer_layout->setContentsMargins(0, 0, 0, 0);
 0232  outer_layout->setSpacing(6);
 233
 0234  enable_check_ = create_checkbox(this, false, "Enable auto focus");
 0235  outer_layout->addWidget(enable_check_);
 236
 0237  content_container_ = new QWidget(this);
 0238  outer_layout->addWidget(content_container_);
 239
 0240  auto *inner_layout = new QGridLayout(content_container_);
 0241  inner_layout->setContentsMargins(0, 0, 0, 0);
 0242  inner_layout->setHorizontalSpacing(6);
 0243  inner_layout->setVerticalSpacing(4);
 0244  int row = 0;
 245
 246  const auto add_label_widget_row = [&](const QString &label, QWidget *widget) {
 247    inner_layout->addWidget(new QLabel(label, content_container_), row, 0);
 248    inner_layout->addWidget(widget, row, 1, 1, 2);
 249    ++row;
 0250  };
 251
 252  const auto add_zernike_row = [&](const QString &label, QCheckBox *&checkbox,
 253                                   QDoubleSpinBox *&spin_box) {
 254    inner_layout->addWidget(new QLabel(label, content_container_), row, 0);
 255
 256    checkbox = create_checkbox(content_container_, false);
 257    inner_layout->addWidget(checkbox, row, 1);
 258
 259    spin_box = create_display_double_spin_box(content_container_, 0.0);
 260    inner_layout->addWidget(spin_box, row, 2);
 261
 262    ++row;
 0263  };
 264
 0265  nb_subaps_spin_ =
 266      create_odd_spin_box(content_container_, kDefaultNbSubaps, kMinNbSubaps, kMaxNbSubaps,
 267                          "Choose an odd number of subapertures per dimension.");
 0268  add_label_widget_row("Nb Subaps:", nb_subaps_spin_);
 269
 0270  nb_iter_spin_ = create_spin_box(content_container_, kDefaultNbIter, kMinNbIter, kMaxNbIter,
 271                                  "Number of Shack-Hartmann correction passes.");
 0272  add_label_widget_row("Nb Iter:", nb_iter_spin_);
 273
 0274  slope_recovery_combo_ = new QComboBox(content_container_);
 0275  slope_recovery_combo_->addItems({"Single reference", "Graph Laplacian (full pairwise)"});
 0276  slope_recovery_combo_->setToolTip(
 277      "Choose center-reference registration or full-pairwise graph-Laplacian recovery.");
 0278  add_label_widget_row("Slope recovery:", slope_recovery_combo_);
 279
 0280  skip_subapertures_outside_pupil_checkbox_ =
 281      create_checkbox(content_container_, true, "Skip subapertures outside pupil");
 0282  skip_subapertures_outside_pupil_checkbox_->setToolTip(
 283      "Exclude subapertures whose centers lie outside the circular pupil during Zernike fitting.");
 0284  inner_layout->addWidget(skip_subapertures_outside_pupil_checkbox_, row++, 0, 1, 3);
 285
 0286  add_zernike_row("Z2 - Tilt X:", z2_checkbox_, z2_spin_);
 0287  add_zernike_row("Z3 - Tilt Y:", z3_checkbox_, z3_spin_);
 0288  add_zernike_row("Z4 - Defocus:", z4_checkbox_, z4_spin_);
 0289  add_zernike_row("Z5 - Astigmatism 45°:", z5_checkbox_, z5_spin_);
 0290  add_zernike_row("Z6 - Astigmatism 0°:", z6_checkbox_, z6_spin_);
 291
 0292  add_zernike_row("Z7 - Vertical trefoil:", z7_checkbox_, z7_spin_);
 0293  add_zernike_row("Z8 - Vertical coma:", z8_checkbox_, z8_spin_);
 0294  add_zernike_row("Z9 - Horizontal coma:", z9_checkbox_, z9_spin_);
 0295  add_zernike_row("Z10 - Oblique trefoil:", z10_checkbox_, z10_spin_);
 296
 0297  inner_layout->setColumnStretch(0, 1);
 0298  inner_layout->setColumnStretch(1, 0);
 0299  inner_layout->setColumnStretch(2, 1);
 300
 0301  inner_layout->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding), row,
 302                        0, 1, 3);
 0303}
 304
 0305void AutoFocusWidget::connect_signals() {
 0306  connect(enable_check_, &QCheckBox::toggled, this, [this](bool enabled) {
 307    set_controls_enabled(enabled);
 308    emit settings_changed();
 309  });
 310
 0311  connect(nb_subaps_spin_, qOverload<int>(&QSpinBox::valueChanged), this, [this](int value) {
 312    if ((value % 2) == 0) {
 313      const int      corrected = value < nb_subaps_spin_->maximum() ? value + 1 : value - 1;
 314      QSignalBlocker blocker(nb_subaps_spin_);
 315      nb_subaps_spin_->setValue(corrected);
 316    }
 317
 318    emit settings_changed();
 319  });
 320
 0321  connect(nb_iter_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 322          &AutoFocusWidget::settings_changed);
 0323  connect(slope_recovery_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 324          [this](int) {
 325            update_mode_dependent_controls();
 326            emit settings_changed();
 327          });
 0328  connect(skip_subapertures_outside_pupil_checkbox_, &QCheckBox::toggled, this,
 329          &AutoFocusWidget::settings_changed);
 330
 0331  connect(z2_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0332  connect(z3_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0333  connect(z4_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0334  connect(z5_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0335  connect(z6_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 336
 0337  connect(z7_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0338  connect(z8_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0339  connect(z9_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0340  connect(z10_checkbox_, &QCheckBox::toggled, this, &AutoFocusWidget::settings_changed);
 0341}
 342
 0343void AutoFocusWidget::set_controls_enabled(bool enabled) {
 0344  content_container_->setEnabled(enabled);
 0345  update_mode_dependent_controls();
 0346}
 347
 0348void AutoFocusWidget::update_mode_dependent_controls() {}
 349
 350} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\export_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/export_widget.hh"
 16#include "ui/widgets/validation_style.hh"
 17
 18#include <QGridLayout>
 19#include <QHBoxLayout>
 20#include <QLabel>
 21#include <QSpacerItem>
 22#include <QVBoxLayout>
 23
 24namespace holovibes::ui {
 25
 26namespace {
 27
 028QSpinBox *create_spin_box(QWidget *parent, int minimum, int maximum, int value) {
 029  auto *spin_box = new QSpinBox(parent);
 030  spin_box->setRange(minimum, maximum);
 031  spin_box->setValue(value);
 032  return spin_box;
 033}
 34
 035QComboBox *create_combo_box(QWidget *parent, const QStringList &items) {
 036  auto *combo_box = new QComboBox(parent);
 037  combo_box->addItems(items);
 038  return combo_box;
 039}
 40
 41} // namespace
 42
 043ExportWidget::ExportWidget(QWidget *parent) : QGroupBox("EXPORT", parent) {
 044  setup_ui();
 045  connect_signals();
 046  setChecked(false);
 047}
 48
 049QString ExportWidget::get_image_type() const { return image_type_combo_->currentText(); }
 050QString ExportWidget::get_file_path() const { return file_line_edit_->text(); }
 051QString ExportWidget::get_tag() const { return tag_combo_->currentText(); }
 052bool    ExportWidget::is_frame_count_enabled() const { return frames_check_->isChecked(); }
 053int     ExportWidget::get_frame_count() const { return frames_spin_->value(); }
 054bool    ExportWidget::isChecked() const { return enable_check_->isChecked(); }
 55
 056void ExportWidget::set_file_path(const QString &path) { file_line_edit_->setText(path); }
 057void ExportWidget::set_frame_count(int count) { frames_spin_->setValue(count); }
 058void ExportWidget::set_image_type(const QString &type) { image_type_combo_->setCurrentText(type); }
 059void ExportWidget::setChecked(bool checked) {
 060  enable_check_->setChecked(checked);
 061  set_export_controls_enabled(checked);
 062}
 63
 064void ExportWidget::set_record_enabled(bool enabled) { record_button_->setEnabled(enabled); }
 065void ExportWidget::set_stop_enabled(bool enabled) { stop_button_->setEnabled(enabled); }
 66
 067void ExportWidget::mark_file_invalid() { mark_validation_error(file_line_edit_); }
 068void ExportWidget::mark_frames_invalid() { mark_validation_error(frames_spin_); }
 69
 070QComboBox   *ExportWidget::image_type_combo() { return image_type_combo_; }
 071QLineEdit   *ExportWidget::file_line_edit() { return file_line_edit_; }
 072QPushButton *ExportWidget::browse_button() { return browse_button_; }
 073QComboBox   *ExportWidget::tag_combo() { return tag_combo_; }
 074QCheckBox   *ExportWidget::frames_check() { return frames_check_; }
 075QSpinBox    *ExportWidget::frames_spin() { return frames_spin_; }
 076QPushButton *ExportWidget::record_button() { return record_button_; }
 077QPushButton *ExportWidget::stop_button() { return stop_button_; }
 078QPushButton *ExportWidget::stop_fan_button() { return stop_fan_button_; }
 79
 080void ExportWidget::setup_ui() {
 081  auto *outer_layout = new QVBoxLayout(this);
 082  outer_layout->setContentsMargins(0, 0, 0, 0);
 083  outer_layout->setSpacing(6);
 84
 085  enable_check_ = new QCheckBox("Enable export", this);
 086  outer_layout->addWidget(enable_check_);
 87
 088  content_container_ = new QWidget(this);
 089  outer_layout->addWidget(content_container_);
 90
 091  auto *layout = new QGridLayout(content_container_);
 092  layout->setContentsMargins(0, 0, 0, 0);
 093  layout->setHorizontalSpacing(6);
 094  layout->setVerticalSpacing(4);
 095  int row = 0;
 96
 097  image_type_combo_ =
 98      create_combo_box(content_container_, QStringList{"Raw Image", "Processed Image"});
 099  layout->addWidget(image_type_combo_, row, 0, 1, 2);
 0100  ++row;
 101
 0102  file_line_edit_ = new QLineEdit(content_container_);
 0103  file_line_edit_->setText("holovibes\\capture");
 0104  file_line_edit_->setReadOnly(true);
 0105  layout->addWidget(file_line_edit_, row, 0);
 106
 0107  browse_button_ = new QPushButton("...", content_container_);
 0108  browse_button_->setFixedWidth(30);
 0109  layout->addWidget(browse_button_, row, 1);
 0110  ++row;
 111
 0112  layout->addWidget(new QLabel("Tag", content_container_), row, 0);
 0113  tag_combo_ = create_combo_box(content_container_, QStringList{"None", "Left Eye", "Right Eye"});
 0114  layout->addWidget(tag_combo_, row, 1);
 0115  ++row;
 116
 0117  frames_check_ = new QCheckBox("Nb. of frames", content_container_);
 0118  frames_check_->setChecked(true);
 0119  layout->addWidget(frames_check_, row, 0);
 0120  frames_spin_ = create_spin_box(content_container_, 1, 999999, 2048);
 0121  layout->addWidget(frames_spin_, row, 1);
 0122  ++row;
 123
 0124  auto *button_layout = new QHBoxLayout();
 0125  record_button_      = new QPushButton("Record", content_container_);
 0126  stop_button_        = new QPushButton("Stop", content_container_);
 0127  stop_button_->setEnabled(false);
 0128  stop_fan_button_ = new QPushButton("Stop fan", content_container_);
 0129  record_button_->setEnabled(false);
 0130  button_layout->addWidget(record_button_);
 0131  button_layout->addWidget(stop_button_);
 0132  button_layout->addWidget(stop_fan_button_);
 0133  layout->addLayout(button_layout, row, 0, 1, 2);
 0134  ++row;
 135
 0136  layout->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding), row, 0, 1,
 137                  2);
 0138}
 139
 0140void ExportWidget::connect_signals() {
 0141  connect(enable_check_, &QCheckBox::toggled, this, [this](bool enabled) {
 142    set_export_controls_enabled(enabled);
 143    emit settings_changed();
 144  });
 0145  connect(record_button_, &QPushButton::clicked, this, &ExportWidget::record_clicked);
 0146  connect(stop_button_, &QPushButton::clicked, this, &ExportWidget::stop_clicked);
 0147  connect(stop_fan_button_, &QPushButton::clicked, this, &ExportWidget::stop_fan_clicked);
 0148  connect(browse_button_, &QPushButton::clicked, this, &ExportWidget::browse_clicked);
 149
 150  // Emit settings_changed for all control changes
 0151  connect(image_type_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 152          &ExportWidget::settings_changed);
 0153  connect(file_line_edit_, &QLineEdit::textChanged, this, &ExportWidget::settings_changed);
 0154  connect(tag_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 155          &ExportWidget::settings_changed);
 0156  connect(frames_check_, &QCheckBox::toggled, this, &ExportWidget::settings_changed);
 0157  connect(frames_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 158          &ExportWidget::settings_changed);
 0159}
 160
 0161void ExportWidget::clear_validation_styles() {
 0162  clear_validation_error(file_line_edit_);
 0163  clear_validation_error(frames_spin_);
 0164}
 165
 0166void ExportWidget::set_export_controls_enabled(bool enabled) {
 0167  content_container_->setEnabled(enabled);
 0168}
 169
 170} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\image_rendering_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/image_rendering_widget.hh"
 16#include "ui/widgets/validation_style.hh"
 17
 18#include <QCoreApplication>
 19#include <QDir>
 20#include <QFileInfo>
 21#include <QGridLayout>
 22#include <QHBoxLayout>
 23#include <QLabel>
 24#include <QSpacerItem>
 25#include <QStandardItemModel>
 26#include <QStandardPaths>
 27
 28namespace holovibes::ui {
 29
 30namespace {
 31
 32// constexpr int kLargeSpinMax = 1024 * 1024;
 33constexpr int kLargeSpinMax = std::numeric_limits<int>::max();
 34
 035QSpinBox *create_spin_box(QWidget *parent, int minimum, int maximum, int value) {
 036  auto *spin_box = new QSpinBox(parent);
 037  spin_box->setRange(minimum, maximum);
 038  spin_box->setValue(value);
 039  return spin_box;
 040}
 41
 042QComboBox *create_combo_box(QWidget *parent, const QStringList &items) {
 043  auto *combo_box = new QComboBox(parent);
 044  combo_box->addItems(items);
 045  return combo_box;
 046}
 47
 48} // namespace
 49
 050ImageRenderingWidget::ImageRenderingWidget(QWidget *parent) : QGroupBox("IMAGE RENDERING", parent) {
 051  setup_ui();
 052  connect_signals();
 053}
 54
 055QString ImageRenderingWidget::get_image_mode() const { return image_combo_->currentText(); }
 056int     ImageRenderingWidget::get_batch_size() const { return batch_size_spin_->value(); }
 057int     ImageRenderingWidget::get_time_stride() const { return time_stride_spin_->value(); }
 058bool    ImageRenderingWidget::is_filter_2d_enabled() const { return filter_2d_check_->isChecked(); }
 059int     ImageRenderingWidget::get_filter_inner() const { return filter_2d_inner_spin_->value(); }
 060int     ImageRenderingWidget::get_filter_outer() const { return filter_2d_outer_spin_->value(); }
 061QString ImageRenderingWidget::get_space_transform() const {
 062  return space_transform_combo_->currentText();
 063}
 064QString ImageRenderingWidget::get_time_transform() const {
 065  return time_transform_combo_->currentText();
 066}
 067int  ImageRenderingWidget::get_time_window() const { return time_window_spin_->value(); }
 068int  ImageRenderingWidget::get_lambda() const { return lambda_spin_->value(); }
 069int  ImageRenderingWidget::get_focus() const { return focus_spin_->value(); }
 070bool ImageRenderingWidget::is_asp_padding_enabled() const {
 071  return asp_padding_check_->isChecked();
 072}
 073int ImageRenderingWidget::get_asp_padded_width() const { return asp_padded_width_spin_->value(); }
 074int ImageRenderingWidget::get_asp_padded_height() const { return asp_padded_height_spin_->value(); }
 075QString ImageRenderingWidget::get_convolution() const { return convolution_combo_->currentText(); }
 076bool    ImageRenderingWidget::is_convolution_divide() const {
 077  return convolution_divide_check_->isChecked();
 078}
 79
 080void ImageRenderingWidget::set_batch_size(int value) { batch_size_spin_->setValue(value); }
 081void ImageRenderingWidget::set_time_stride(int value) { time_stride_spin_->setValue(value); }
 082void ImageRenderingWidget::set_filter_2d_enabled(bool enabled) {
 083  filter_2d_check_->setChecked(enabled);
 084}
 085void ImageRenderingWidget::set_filter_inner(int value) { filter_2d_inner_spin_->setValue(value); }
 086void ImageRenderingWidget::set_filter_outer(int value) { filter_2d_outer_spin_->setValue(value); }
 087void ImageRenderingWidget::set_space_transform(const QString &method) {
 088  space_transform_combo_->setCurrentText(method);
 089}
 090void ImageRenderingWidget::set_time_transform(const QString &method) {
 091  time_transform_combo_->setCurrentText(method);
 092}
 093void ImageRenderingWidget::set_time_window(int value) { time_window_spin_->setValue(value); }
 094void ImageRenderingWidget::set_lambda(int value) { lambda_spin_->setValue(value); }
 095void ImageRenderingWidget::set_focus(int value) { focus_spin_->setValue(value); }
 096void ImageRenderingWidget::set_asp_padding_enabled(bool enabled) {
 097  asp_padding_check_->setChecked(enabled);
 098}
 099void ImageRenderingWidget::set_asp_padded_width(int value) {
 0100  asp_padded_width_spin_->setValue(value);
 0101}
 0102void ImageRenderingWidget::set_asp_padded_height(int value) {
 0103  asp_padded_height_spin_->setValue(value);
 0104}
 0105void ImageRenderingWidget::set_convolution(const QString &kernel) {
 0106  convolution_combo_->setCurrentText(kernel);
 0107}
 0108void ImageRenderingWidget::set_convolution_divide(bool enabled) {
 0109  convolution_divide_check_->setChecked(enabled);
 0110}
 111
 0112void ImageRenderingWidget::mark_batch_size_invalid() { mark_validation_error(batch_size_spin_); }
 0113void ImageRenderingWidget::mark_time_stride_invalid() { mark_validation_error(time_stride_spin_); }
 0114void ImageRenderingWidget::mark_filter_2d_invalid() { mark_validation_error(filter_2d_check_); }
 0115void ImageRenderingWidget::mark_filter_inner_invalid() {
 0116  mark_validation_error(filter_2d_inner_spin_);
 0117}
 0118void ImageRenderingWidget::mark_filter_outer_invalid() {
 0119  mark_validation_error(filter_2d_outer_spin_);
 0120}
 0121void ImageRenderingWidget::mark_time_window_invalid() { mark_validation_error(time_window_spin_); }
 0122void ImageRenderingWidget::mark_space_transform_invalid() {
 0123  mark_validation_error(space_transform_combo_);
 0124}
 0125void ImageRenderingWidget::mark_asp_padding_invalid() {
 0126  mark_validation_error(asp_padding_check_);
 0127  mark_validation_error(asp_padded_width_spin_);
 0128  mark_validation_error(asp_padded_height_spin_);
 0129}
 0130void ImageRenderingWidget::mark_time_transform_invalid() {
 0131  mark_validation_error(time_transform_combo_);
 0132}
 0133void ImageRenderingWidget::mark_convolution_invalid() { mark_validation_error(convolution_combo_); }
 134
 0135QComboBox *ImageRenderingWidget::image_combo() { return image_combo_; }
 0136QSpinBox  *ImageRenderingWidget::batch_size_spin() { return batch_size_spin_; }
 0137QSpinBox  *ImageRenderingWidget::time_stride_spin() { return time_stride_spin_; }
 0138QCheckBox *ImageRenderingWidget::filter_2d_check() { return filter_2d_check_; }
 0139QSpinBox  *ImageRenderingWidget::filter_2d_inner_spin() { return filter_2d_inner_spin_; }
 0140QSpinBox  *ImageRenderingWidget::filter_2d_outer_spin() { return filter_2d_outer_spin_; }
 0141QComboBox *ImageRenderingWidget::space_transform_combo() { return space_transform_combo_; }
 0142QComboBox *ImageRenderingWidget::time_transform_combo() { return time_transform_combo_; }
 0143QSpinBox  *ImageRenderingWidget::time_window_spin() { return time_window_spin_; }
 0144QSpinBox  *ImageRenderingWidget::lambda_spin() { return lambda_spin_; }
 0145QSpinBox  *ImageRenderingWidget::focus_spin() { return focus_spin_; }
 0146QSlider   *ImageRenderingWidget::focus_slider() { return focus_slider_; }
 0147QCheckBox *ImageRenderingWidget::asp_padding_check() { return asp_padding_check_; }
 0148QSpinBox  *ImageRenderingWidget::asp_padded_width_spin() { return asp_padded_width_spin_; }
 0149QSpinBox  *ImageRenderingWidget::asp_padded_height_spin() { return asp_padded_height_spin_; }
 0150QComboBox *ImageRenderingWidget::convolution_combo() { return convolution_combo_; }
 0151QCheckBox *ImageRenderingWidget::convolution_divide_check() { return convolution_divide_check_; }
 0152AutoFocusWidget *ImageRenderingWidget::autofocus_widget() { return autofocus_widget_; }
 153
 0154void ImageRenderingWidget::setup_ui() {
 0155  auto *layout = new QGridLayout(this);
 0156  layout->setContentsMargins(0, 0, 0, 0);
 0157  layout->setHorizontalSpacing(6);
 0158  layout->setVerticalSpacing(4);
 0159  layout->setColumnStretch(1, 1);
 0160  int row = 0;
 161
 162  auto add_combo_row = [&](const QString &label, QComboBox *&combo, const QStringList &items) {
 163    layout->addWidget(new QLabel(label, this), row, 0);
 164    combo = create_combo_box(this, items);
 165    layout->addWidget(combo, row, 1);
 166    ++row;
 0167  };
 168
 169  auto add_spin_row = [&](const QString &label, QSpinBox *&spin, int minimum, int maximum,
 170                          int value) {
 171    layout->addWidget(new QLabel(label, this), row, 0);
 172    spin = create_spin_box(this, minimum, maximum, value);
 173    layout->addWidget(spin, row, 1);
 174    ++row;
 0175  };
 176
 0177  add_combo_row("Image:", image_combo_, QStringList{"Raw", "Processed"});
 0178  add_spin_row("Batch Size:", batch_size_spin_, 1, kLargeSpinMax, 32);
 0179  add_spin_row("Time Stride:", time_stride_spin_, 1, kLargeSpinMax, 32);
 0180  image_combo_->setCurrentText("Processed");
 181
 0182  filter_2d_check_ = new QCheckBox("Filter 2D", this);
 0183  layout->addWidget(filter_2d_check_, row, 0);
 184
 0185  auto *filter_controls = new QWidget(this);
 0186  auto *filter_layout   = new QHBoxLayout(filter_controls);
 0187  filter_layout->setContentsMargins(0, 0, 0, 0);
 0188  filter_layout->setSpacing(6);
 0189  filter_2d_inner_spin_ = create_spin_box(filter_controls, 0, kLargeSpinMax, 0);
 0190  filter_layout->addWidget(filter_2d_inner_spin_);
 0191  filter_2d_outer_spin_ = create_spin_box(filter_controls, 0, kLargeSpinMax, 1024);
 0192  filter_layout->addWidget(filter_2d_outer_spin_);
 0193  layout->addWidget(filter_controls, row, 1);
 0194  ++row;
 195
 0196  auto space_transforms = QStringList{"None", "Fresnel Diffraction", "Angular Spectrum"};
 0197  add_combo_row("Space Transform:", space_transform_combo_, space_transforms);
 198
 0199  asp_padding_check_ = new QCheckBox("ASP Padding", this);
 0200  layout->addWidget(asp_padding_check_, row, 0);
 0201  asp_padding_controls_    = new QWidget(this);
 0202  auto *asp_padding_layout = new QHBoxLayout(asp_padding_controls_);
 0203  asp_padding_layout->setContentsMargins(0, 0, 0, 0);
 0204  asp_padding_layout->setSpacing(6);
 0205  asp_padded_width_spin_ = create_spin_box(asp_padding_controls_, 1, kLargeSpinMax, 1024);
 0206  asp_padded_width_spin_->setPrefix("W ");
 0207  asp_padding_layout->addWidget(asp_padded_width_spin_);
 0208  asp_padded_height_spin_ = create_spin_box(asp_padding_controls_, 1, kLargeSpinMax, 1024);
 0209  asp_padded_height_spin_->setPrefix("H ");
 0210  asp_padding_layout->addWidget(asp_padded_height_spin_);
 0211  layout->addWidget(asp_padding_controls_, row, 1);
 0212  ++row;
 213
 0214  auto time_transforms = QStringList{"None", "RFFT", "FFT", "Principal Component Analysis"};
 0215  add_combo_row("Time Transform:", time_transform_combo_, time_transforms);
 0216  add_spin_row("Time Window:", time_window_spin_, 1, kLargeSpinMax, 32);
 0217  add_spin_row("Lambda (nm):", lambda_spin_, 1, kLargeSpinMax, 852);
 0218  add_spin_row("Focus (mm):", focus_spin_, 1, kLargeSpinMax, 380);
 219
 0220  focus_slider_ = new QSlider(Qt::Horizontal, this);
 0221  focus_slider_->setRange(0, 1000);
 0222  focus_slider_->setValue(focus_spin_->value());
 0223  layout->addWidget(focus_slider_, row, 0, 1, 2);
 0224  ++row;
 225
 0226  autofocus_widget_ = new AutoFocusWidget(this);
 227
 0228  layout->addWidget(new QLabel("Convolution:", this), row, 0, 1, 2);
 0229  ++row;
 230
 0231  convolution_combo_ = create_combo_box(this, load_available_kernels());
 0232  layout->addWidget(convolution_combo_, row, 0);
 0233  convolution_divide_check_ = new QCheckBox("Divide", this);
 0234  layout->addWidget(convolution_divide_check_, row, 1, 1, 1, Qt::AlignRight);
 0235  ++row;
 236
 0237  layout->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding), row, 0, 1,
 238                  2);
 0239  update_asp_padding_visibility();
 0240}
 241
 0242void ImageRenderingWidget::connect_signals() {
 243  // Synchronize focus spin and slider
 0244  connect(focus_spin_, &QSpinBox::valueChanged, this, [this](int value) {
 245    if (focus_slider_->value() != value) {
 246      focus_slider_->blockSignals(true);
 247      focus_slider_->setValue(value);
 248      focus_slider_->blockSignals(false);
 249    }
 250    emit settings_changed();
 251  });
 252
 0253  connect(focus_slider_, &QSlider::valueChanged, this, [this](int value) {
 254    if (focus_spin_->value() != value) {
 255      focus_spin_->blockSignals(true);
 256      focus_spin_->setValue(value);
 257      focus_spin_->blockSignals(false);
 258    }
 259    emit settings_changed();
 260  });
 261
 262  // Emit settings_changed for all control changes
 0263  connect(image_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 264          &ImageRenderingWidget::settings_changed);
 0265  connect(batch_size_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 266          &ImageRenderingWidget::settings_changed);
 0267  connect(time_stride_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 268          &ImageRenderingWidget::settings_changed);
 0269  connect(filter_2d_check_, &QCheckBox::toggled, this, &ImageRenderingWidget::settings_changed);
 0270  connect(filter_2d_inner_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 271          &ImageRenderingWidget::settings_changed);
 0272  connect(filter_2d_outer_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 273          &ImageRenderingWidget::settings_changed);
 0274  connect(space_transform_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
 275    update_asp_padding_visibility();
 276    emit settings_changed();
 277  });
 0278  connect(asp_padding_check_, &QCheckBox::toggled, this, [this](bool enabled) {
 279    asp_padding_controls_->setEnabled(enabled);
 280    emit settings_changed();
 281  });
 0282  connect(asp_padded_width_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 283          &ImageRenderingWidget::settings_changed);
 0284  connect(asp_padded_height_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 285          &ImageRenderingWidget::settings_changed);
 0286  connect(time_transform_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 287          &ImageRenderingWidget::settings_changed);
 0288  connect(time_window_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 289          &ImageRenderingWidget::settings_changed);
 0290  connect(lambda_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 291          &ImageRenderingWidget::settings_changed);
 0292  connect(convolution_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 293          &ImageRenderingWidget::settings_changed);
 0294  connect(convolution_divide_check_, &QCheckBox::toggled, this,
 295          &ImageRenderingWidget::settings_changed);
 0296  connect(autofocus_widget_, &AutoFocusWidget::settings_changed, this, [this]() {
 297    set_angular_spectrum_enabled(!autofocus_widget_->is_enabled());
 298    emit settings_changed();
 299  });
 0300}
 301
 0302void ImageRenderingWidget::update_asp_padding_visibility() {
 0303  const bool visible = space_transform_combo_->currentText() == "Angular Spectrum";
 0304  asp_padding_check_->setVisible(visible);
 0305  asp_padding_controls_->setVisible(visible);
 0306  asp_padding_controls_->setEnabled(asp_padding_check_->isChecked());
 0307}
 308
 0309void ImageRenderingWidget::set_angular_spectrum_enabled(bool enabled) {
 0310  constexpr auto kAngularSpectrum = "Angular Spectrum";
 0311  constexpr auto kFresnel         = "Fresnel Diffraction";
 312
 0313  const int index = space_transform_combo_->findText(kAngularSpectrum);
 0314  auto     *model = qobject_cast<QStandardItemModel *>(space_transform_combo_->model());
 0315  if (index >= 0 && model != nullptr) {
 0316    if (auto *item = model->item(index); item != nullptr) {
 0317      item->setEnabled(enabled);
 318    }
 319  }
 320
 0321  if (!enabled && space_transform_combo_->currentText() == kAngularSpectrum) {
 0322    space_transform_combo_->setCurrentText(kFresnel);
 323  }
 0324}
 325
 0326QStringList ImageRenderingWidget::load_available_kernels() {
 0327  QString appDataBase = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 0328  QString appDataPath = appDataBase + "/" + QCoreApplication::applicationVersion();
 0329  QString convolutionsKernelsPath = appDataPath + "/" + "convolution_kernels";
 0330  QDir    dir(convolutionsKernelsPath);
 331
 0332  QStringList filters;
 0333  filters << QStringLiteral("*.json");
 0334  QFileInfoList files = dir.entryInfoList(filters, QDir::Files | QDir::Readable, QDir::Name);
 335
 0336  QStringList names;
 0337  names << QStringLiteral("None");
 0338  for (const QFileInfo &fi : files) {
 0339    names << fi.completeBaseName();
 0340  }
 341
 0342  return names;
 0343}
 344
 0345void ImageRenderingWidget::clear_validation_styles() {
 0346  clear_validation_error(batch_size_spin_);
 0347  clear_validation_error(time_stride_spin_);
 0348  clear_validation_error(filter_2d_check_);
 0349  clear_validation_error(filter_2d_inner_spin_);
 0350  clear_validation_error(filter_2d_outer_spin_);
 0351  clear_validation_error(time_window_spin_);
 0352  clear_validation_error(space_transform_combo_);
 0353  clear_validation_error(asp_padding_check_);
 0354  clear_validation_error(asp_padded_width_spin_);
 0355  clear_validation_error(asp_padded_height_spin_);
 0356  clear_validation_error(time_transform_combo_);
 0357  clear_validation_error(convolution_combo_);
 0358}
 359
 360} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\import_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/import_widget.hh"
 16#include "ui/widgets/validation_style.hh"
 17
 18#include <QCoreApplication>
 19#include <QDir>
 20#include <QDoubleSpinBox>
 21#include <QFile>
 22#include <QFileInfo>
 23#include <QGridLayout>
 24#include <QHBoxLayout>
 25#include <QJsonDocument>
 26#include <QJsonObject>
 27#include <QLabel>
 28#include <QSpinBox>
 29#include <QStackedLayout>
 30#include <QStandardPaths>
 31#include <QVBoxLayout>
 32
 33namespace holovibes::ui {
 34
 35namespace {
 36
 037QSpinBox *create_spin_box(QWidget *parent, int minimum, int maximum, int value) {
 038  auto *spin_box = new QSpinBox(parent);
 039  spin_box->setRange(minimum, maximum);
 040  spin_box->setValue(value);
 041  return spin_box;
 042}
 43
 044QComboBox *create_combo_box(QWidget *parent, const QStringList &items) {
 045  auto *combo_box = new QComboBox(parent);
 046  combo_box->addItems(items);
 047  return combo_box;
 048}
 49
 50constexpr int kLargeSpinMax = 1024 * 1024;
 51
 52} // namespace
 53
 054ImportWidget::ImportWidget(QWidget *parent) : QGroupBox("IMPORT", parent) {
 055  setup_ui();
 056  connect_signals();
 057}
 58
 059bool               ImportWidget::is_camera_mode() const { return cam_check_->isChecked(); }
 060QString            ImportWidget::get_file_path() const { return file_line_edit_->text(); }
 061std::optional<int> ImportWidget::get_fps_limit() const {
 062  if (fps_spin_->value() <= 0) {
 063    return std::nullopt;
 64  }
 65
 066  return fps_spin_->value();
 067}
 068double ImportWidget::get_sampling_frequency_hz() const {
 069  return sampling_frequency_spin_->value();
 070}
 071int     ImportWidget::get_start_index() const { return start_index_spin_->value(); }
 072int     ImportWidget::get_end_index() const { return end_index_spin_->value(); }
 073QString ImportWidget::get_load_method() const { return load_method_combo_->currentText(); }
 074QString ImportWidget::get_camera_type() const { return camera_combo_->currentText(); }
 075QString ImportWidget::get_camera_config() const { return camera_config_combo_->currentText(); }
 76
 077void ImportWidget::set_fps_limit(std::optional<int> value) {
 078  fps_spin_->setValue(value.value_or(0));
 079}
 080void ImportWidget::set_sampling_frequency_hz(double value) {
 081  sampling_frequency_spin_->setValue(value);
 082}
 083void ImportWidget::set_start_index(int value) { start_index_spin_->setValue(value); }
 084void ImportWidget::set_end_index(int value) { end_index_spin_->setValue(value); }
 085void ImportWidget::set_end_index_range(int min, int max) { end_index_spin_->setRange(min, max); }
 086void ImportWidget::set_load_method(const QString &method) {
 087  load_method_combo_->setCurrentText(method);
 088}
 089void ImportWidget::set_camera_type(const QString &type) { camera_combo_->setCurrentText(type); }
 090void ImportWidget::set_camera_mode(bool enabled) { cam_check_->setChecked(enabled); }
 91
 092void ImportWidget::set_start_enabled(bool enabled) { start_button_->setEnabled(enabled); }
 093void ImportWidget::set_stop_enabled(bool enabled) { stop_button_->setEnabled(enabled); }
 094bool ImportWidget::is_stop_enabled() const { return stop_button_->isEnabled(); }
 95
 096void ImportWidget::mark_file_invalid() { mark_validation_error(file_line_edit_); }
 097void ImportWidget::mark_fps_invalid() { mark_validation_error(fps_spin_); }
 098void ImportWidget::mark_sampling_frequency_invalid() {
 099  mark_validation_error(sampling_frequency_spin_);
 0100}
 0101void ImportWidget::mark_start_index_invalid() { mark_validation_error(start_index_spin_); }
 0102void ImportWidget::mark_end_index_invalid() { mark_validation_error(end_index_spin_); }
 0103void ImportWidget::mark_camera_config_invalid() { mark_validation_error(camera_config_combo_); }
 104
 0105QLineEdit   *ImportWidget::file_line_edit() { return file_line_edit_; }
 0106QPushButton *ImportWidget::browse_button() { return browse_button_; }
 0107QPushButton *ImportWidget::start_button() { return start_button_; }
 0108QPushButton *ImportWidget::stop_button() { return stop_button_; }
 0109QSpinBox       *ImportWidget::fps_spin() { return fps_spin_; }
 0110QDoubleSpinBox *ImportWidget::sampling_frequency_spin() { return sampling_frequency_spin_; }
 0111QSpinBox       *ImportWidget::start_index_spin() { return start_index_spin_; }
 0112QSpinBox       *ImportWidget::end_index_spin() { return end_index_spin_; }
 0113QComboBox      *ImportWidget::load_method_combo() { return load_method_combo_; }
 0114QCheckBox      *ImportWidget::cam_check() { return cam_check_; }
 0115QComboBox      *ImportWidget::camera_combo() { return camera_combo_; }
 0116QComboBox      *ImportWidget::camera_config_combo() { return camera_config_combo_; }
 117
 0118void ImportWidget::setup_ui() {
 0119  auto *main_layout = new QVBoxLayout(this);
 0120  main_layout->setContentsMargins(0, 0, 0, 0);
 0121  main_layout->setSpacing(6);
 122
 0123  cam_check_ = new QCheckBox("Use Camera", this);
 0124  main_layout->addWidget(cam_check_);
 125
 0126  stack_ = new QStackedLayout();
 0127  main_layout->addLayout(stack_);
 128
 0129  stack_->addWidget(create_file_page());
 0130  stack_->addWidget(create_camera_page());
 131
 0132  auto *frequency_layout = new QHBoxLayout();
 0133  frequency_layout->addWidget(new QLabel("Input sampling frequency", this));
 0134  sampling_frequency_spin_ = new QDoubleSpinBox(this);
 0135  sampling_frequency_spin_->setRange(0.001, 1.0e9);
 0136  sampling_frequency_spin_->setDecimals(6);
 0137  sampling_frequency_spin_->setValue(1.0e6 / 27.0);
 0138  sampling_frequency_spin_->setSuffix(" Hz");
 0139  frequency_layout->addWidget(sampling_frequency_spin_);
 0140  main_layout->addLayout(frequency_layout);
 141
 0142  auto *button_layout = new QHBoxLayout();
 0143  start_button_       = new QPushButton("Start", this);
 0144  stop_button_        = new QPushButton("Stop", this);
 0145  stop_button_->setEnabled(false);
 0146  button_layout->addWidget(start_button_);
 0147  button_layout->addWidget(stop_button_);
 0148  main_layout->addLayout(button_layout);
 149
 0150  main_layout->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
 151
 0152  stack_->setCurrentIndex(0);
 0153}
 154
 0155void ImportWidget::connect_signals() {
 0156  connect(cam_check_, &QCheckBox::toggled, this, [this](bool checked) {
 157    stack_->setCurrentIndex(checked ? 1 : 0);
 158    if (checked) {
 159      update_sampling_frequency_from_camera_config();
 160    }
 161    emit settings_changed();
 162  });
 163
 0164  connect(start_button_, &QPushButton::clicked, this, &ImportWidget::start_clicked);
 0165  connect(stop_button_, &QPushButton::clicked, this, &ImportWidget::stop_clicked);
 0166  connect(browse_button_, &QPushButton::clicked, this, &ImportWidget::browse_clicked);
 167
 168  // Emit settings_changed for all control changes
 0169  connect(file_line_edit_, &QLineEdit::editingFinished, this, &ImportWidget::settings_changed);
 0170  connect(fps_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 171          &ImportWidget::settings_changed);
 0172  connect(sampling_frequency_spin_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 173          &ImportWidget::settings_changed);
 0174  connect(start_index_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 175          &ImportWidget::settings_changed);
 0176  connect(end_index_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 177          &ImportWidget::settings_changed);
 0178  connect(load_method_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 179          &ImportWidget::settings_changed);
 0180  connect(camera_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 181          &ImportWidget::settings_changed);
 0182  connect(camera_config_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
 183    if (is_camera_mode()) {
 184      update_sampling_frequency_from_camera_config();
 185    }
 186    emit settings_changed();
 187  });
 0188}
 189
 0190QWidget *ImportWidget::create_file_page() {
 0191  auto *page = new QWidget(this);
 0192  auto *grid = new QGridLayout(page);
 0193  grid->setContentsMargins(0, 0, 0, 0);
 0194  grid->setHorizontalSpacing(6);
 0195  grid->setVerticalSpacing(4);
 0196  int row = 0;
 197
 0198  file_line_edit_ = new QLineEdit(page);
 0199  file_line_edit_->setPlaceholderText("Select File");
 0200  file_line_edit_->setReadOnly(true);
 0201  grid->addWidget(file_line_edit_, row, 0);
 202
 0203  browse_button_ = new QPushButton("...", page);
 0204  browse_button_->setFixedWidth(30);
 0205  grid->addWidget(browse_button_, row, 1);
 0206  ++row;
 207
 208  auto add_spin_row = [&](const QString &label, QSpinBox *&target, int minimum, int maximum,
 209                          int value) {
 210    grid->addWidget(new QLabel(label, page), row, 0);
 211    target = create_spin_box(page, minimum, maximum, value);
 212    grid->addWidget(target, row, 1);
 213    ++row;
 0214  };
 215
 0216  add_spin_row("Input FPS Limit", fps_spin_, 0, 999999, 0);
 0217  fps_spin_->setSpecialValueText("Unlimited");
 0218  fps_spin_->setToolTip("Set to Unlimited to read the holofile as fast as possible.");
 0219  add_spin_row("Start Index", start_index_spin_, 0, 999999, 1);
 0220  add_spin_row("End Index", end_index_spin_, 1, 999999, 60);
 221
 0222  auto load_methods  = QStringList{"Read Live", "Load in CPU RAM", "Load in GPU RAM"};
 0223  load_method_combo_ = create_combo_box(page, load_methods);
 0224  grid->addWidget(load_method_combo_, row, 0, 1, 2);
 0225  ++row;
 226
 0227  grid->setRowStretch(row, 1);
 228
 0229  return page;
 0230}
 231
 0232QWidget *ImportWidget::create_camera_page() {
 0233  auto *page = new QWidget(this);
 0234  auto *grid = new QGridLayout(page);
 0235  grid->setContentsMargins(0, 0, 0, 0);
 0236  grid->setHorizontalSpacing(6);
 0237  grid->setVerticalSpacing(4);
 0238  int row = 0;
 239
 0240  grid->addWidget(new QLabel("Camera", page), row, 0);
 0241  camera_combo_ = create_combo_box(
 242      page, QStringList{"Ametek S710 Euresys Coaxlink Octo", "Ametek S711 Euresys Coaxlink QSFP+"});
 0243  grid->addWidget(camera_combo_, row, 1);
 0244  ++row;
 245
 0246  grid->addWidget(new QLabel("Config File", page), row, 0);
 0247  camera_config_combo_ = create_combo_box(page, load_available_camera_configs());
 0248  grid->addWidget(camera_config_combo_, row, 1);
 0249  ++row;
 250
 0251  grid->setRowStretch(row, 1);
 252
 0253  return page;
 0254}
 255
 0256QStringList ImportWidget::load_available_camera_configs() {
 0257  QString appDataBase       = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 0258  QString appDataPath       = appDataBase + "/" + QCoreApplication::applicationVersion();
 0259  QString cameraConfigsPath = appDataPath + "/" + "camera_configs";
 0260  QDir    dir(cameraConfigsPath);
 261
 0262  QStringList filters;
 0263  filters << QStringLiteral("*.json");
 0264  QFileInfoList files = dir.entryInfoList(filters, QDir::Files | QDir::Readable, QDir::Name);
 265
 0266  QStringList names;
 0267  for (const QFileInfo &fi : files) {
 0268    names << fi.completeBaseName();
 0269  }
 270
 0271  return names;
 0272}
 273
 0274void ImportWidget::update_sampling_frequency_from_camera_config() {
 0275  const QString app_data_base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
 0276  const QString config_path =
 277      app_data_base + "/" + QCoreApplication::applicationVersion() + "/camera_configs/" +
 278      camera_config_combo_->currentText() + ".json";
 0279  QFile config_file(config_path);
 0280  if (!config_file.open(QIODevice::ReadOnly)) {
 0281    return;
 282  }
 283
 0284  const auto document = QJsonDocument::fromJson(config_file.readAll());
 0285  if (!document.isObject()) {
 0286    return;
 287  }
 288
 0289  const auto root = document.object();
 0290  QJsonObject camera;
 0291  if (root.value("s711").isObject()) {
 0292    camera = root.value("s711").toObject();
 0293  } else if (root.value("s710").isObject()) {
 0294    camera = root.value("s710").toObject();
 0295  } else {
 0296    return;
 297  }
 298
 0299  const double cycle_minimum_period_us = camera.value("CycleMinimumPeriod").toDouble();
 0300  if (cycle_minimum_period_us > 0.0) {
 0301    set_sampling_frequency_hz(1.0e6 / cycle_minimum_period_us);
 302  }
 0303}
 304
 0305void ImportWidget::set_file_path(const QString &path) { file_line_edit_->setText(path); }
 306
 0307void ImportWidget::clear_validation_styles() {
 0308  clear_validation_error(file_line_edit_);
 0309  clear_validation_error(fps_spin_);
 0310  clear_validation_error(sampling_frequency_spin_);
 0311  clear_validation_error(start_index_spin_);
 0312  clear_validation_error(end_index_spin_);
 0313  clear_validation_error(camera_config_combo_);
 0314}
 315
 316} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\quiver_widget.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "quiver_widget.hh"
 16
 17#include <QPainter>
 18#include <QtMath>
 19
 020QuiverWidget::QuiverWidget(QWidget *parent) : QWidget(parent) { setMinimumSize(240, 240); }
 21
 022void QuiverWidget::set_arrows(std::vector<Arrow> arrows) {
 023  arrows_ = std::move(arrows);
 024  update();
 025}
 026void QuiverWidget::set_bounds(QRectF b) {
 027  bounds_ = b;
 028  update();
 029}
 030void QuiverWidget::set_grid_step(QPointF s) {
 031  grid_step_ = s;
 032  update();
 033}
 034void QuiverWidget::set_arrow_scale(double s) {
 035  arrow_scale_ = s;
 036  update();
 037}
 038void QuiverWidget::set_arrow_head(double len_px, double ang_deg) {
 039  head_len_px_  = len_px;
 040  head_ang_deg_ = ang_deg;
 041  update();
 042}
 43
 044QPointF QuiverWidget::map_to_px(QPointF p) const {
 045  const QRect plot = rect().adjusted(margin_l_, margin_t_, -margin_r_, -margin_b_);
 46
 047  const double x0 = bounds_.left();
 048  const double x1 = bounds_.right();
 049  const double y0 = bounds_.top();
 050  const double y1 = bounds_.bottom();
 51
 052  const double nx = (p.x() - x0) / (x1 - x0);
 053  const double ny = (p.y() - y0) / (y1 - y0);
 54
 55  // Qt Y axis is downward; invert to get math-like coords.
 056  const double px = plot.left() + nx * plot.width();
 057  const double py = plot.bottom() - ny * plot.height();
 058  return {px, py};
 059}
 60
 061void QuiverWidget::paintEvent(QPaintEvent *) {
 062  QPainter qp(this);
 063  qp.setRenderHint(QPainter::Antialiasing, true);
 64
 65  // Background
 066  qp.fillRect(rect(), Qt::white);
 67
 068  const QRect plot = rect().adjusted(margin_l_, margin_t_, -margin_r_, -margin_b_);
 069  qp.setPen(QPen(Qt::black, 1.0));
 070  qp.drawRect(plot);
 71
 72  // Grid
 073  qp.setPen(QPen(QColor(200, 200, 200), 1.0, Qt::DashLine));
 74
 75  auto draw_vline = [&](double x) {
 76    const auto a = map_to_px({x, bounds_.top()});
 77    const auto b = map_to_px({x, bounds_.bottom()});
 78    qp.drawLine(a, b);
 079  };
 80  auto draw_hline = [&](double y) {
 81    const auto a = map_to_px({bounds_.left(), y});
 82    const auto b = map_to_px({bounds_.right(), y});
 83    qp.drawLine(a, b);
 084  };
 85
 086  for (double x = std::ceil(bounds_.left() / grid_step_.x()) * grid_step_.x();
 087       x <= bounds_.right() + 1e-9; x += grid_step_.x()) {
 088    draw_vline(x);
 089  }
 090  for (double y = std::ceil(bounds_.top() / grid_step_.y()) * grid_step_.y();
 091       y <= bounds_.bottom() + 1e-9; y += grid_step_.y()) {
 092    draw_hline(y);
 093  }
 94
 95  // Ticks + labels (simple)
 096  qp.setPen(QPen(Qt::black, 1.0));
 097  qp.setFont(QFont("Sans", 9));
 98
 099  for (double x = std::ceil(bounds_.left() / grid_step_.x()) * grid_step_.x();
 0100       x <= bounds_.right() + 1e-9; x += grid_step_.x()) {
 0101    const auto p = map_to_px({x, bounds_.top()});
 0102    qp.drawLine(QPointF(p.x(), plot.bottom()), QPointF(p.x(), plot.bottom() + 5));
 0103    qp.drawText(QPointF(p.x() - 10, plot.bottom() + 18), QString::number(x));
 0104  }
 0105  for (double y = std::ceil(bounds_.top() / grid_step_.y()) * grid_step_.y();
 0106       y <= bounds_.bottom() + 1e-9; y += grid_step_.y()) {
 0107    const auto p = map_to_px({bounds_.left(), y});
 0108    qp.drawLine(QPointF(plot.left() - 5, p.y()), QPointF(plot.left(), p.y()));
 0109    qp.drawText(QPointF(5, p.y() + 4), QString::number(y));
 0110  }
 111
 112  // Arrows (quiver)
 0113  qp.setPen(QPen(Qt::black, 2.0));
 0114  qp.setBrush(Qt::NoBrush);
 115
 0116  const double head_ang = qDegreesToRadians(head_ang_deg_);
 117
 0118  for (const auto &a : arrows_) {
 0119    const QPointF p0 = map_to_px(a.pos);
 0120    const QPointF p1 = map_to_px(a.pos + arrow_scale_ * a.vec);
 121
 0122    qp.drawLine(p0, p1);
 123
 124    // Arrowhead in pixel space
 0125    const QPointF d = p0 - p1; // direction from tip backwards
 0126    const double  L = std::hypot(d.x(), d.y());
 0127    if (L < 1e-6)
 0128      continue;
 129
 0130    const QPointF u   = d / L; // unit vector backwards from tip
 131    const auto    rot = [&](double ang) {
 132      const double c = std::cos(ang), s = std::sin(ang);
 133      return QPointF(c * u.x() - s * u.y(), s * u.x() + c * u.y());
 0134    };
 135
 0136    const QPointF h1 = p1 + head_len_px_ * rot(+head_ang);
 0137    const QPointF h2 = p1 + head_len_px_ * rot(-head_ang);
 138
 0139    qp.drawLine(p1, h1);
 0140    qp.drawLine(p1, h2);
 0141  }
 142
 143  // Title (optional)
 0144  qp.setFont(QFont("Sans", 11, QFont::Bold));
 0145  qp.drawText(QRect(0, 0, width(), margin_t_), Qt::AlignCenter,
 146              "Wavefront Slopes from Sub-aperture Shifts");
 0147}

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\quiver_widget.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QPointF>
 18#include <QWidget>
 19#include <vector>
 20
 21class QuiverWidget final : public QWidget {
 22  Q_OBJECT
 23public:
 24  struct Arrow {
 25    QPointF pos; // logical coordinates (x,y)
 26    QPointF vec; // logical vector (u,v)
 27  };
 28
 29  explicit QuiverWidget(QWidget *parent = nullptr);
 30
 31  void set_arrows(std::vector<Arrow> arrows);
 32  void set_bounds(QRectF logical_bounds);          // e.g. (-0.5,-0.5)-(2.5,2.5)
 33  void set_grid_step(QPointF step);                // e.g. (1,1)
 34  void set_arrow_scale(double logical_to_logical); // multiply vec before drawing
 35  void set_arrow_head(double head_len_px, double head_angle_deg = 25.0);
 36
 37protected:
 38  void paintEvent(QPaintEvent *) override;
 39
 40private:
 41  QPointF map_to_px(QPointF p) const;
 42
 43  std::vector<Arrow> arrows_;
 044  QRectF             bounds_{-0.5, -0.5, 3.0, 3.0};
 045  QPointF            grid_step_{1.0, 1.0};
 46
 047  double arrow_scale_  = 1.0;  // scales vectors in logical units
 048  double head_len_px_  = 10.0; // arrowhead length in pixels
 049  double head_ang_deg_ = 25.0; // arrowhead angle
 50
 051  int margin_l_ = 50, margin_r_ = 20, margin_t_ = 20, margin_b_ = 40;
 52};

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\selected_widget_settings_panel.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "selected_widget_settings_panel.hh"
 16
 17#include <QCheckBox>
 18#include <QComboBox>
 19#include <QDoubleSpinBox>
 20#include <QLabel>
 21#include <QPushButton>
 22#include <QScrollArea>
 23#include <QSignalBlocker>
 24#include <QStackedWidget>
 25#include <QVBoxLayout>
 26
 27#include <cmath>
 28
 29#include "ui/widgets/validation_style.hh"
 30#include "ui/widgets/zernike_history_widget.hh"
 31
 32namespace holovibes::ui {
 33
 34class ZernikeHistorySettingsWidget : public QWidget {
 35public:
 036  explicit ZernikeHistorySettingsWidget(QWidget *parent = nullptr) : QWidget(parent) {
 037    auto *layout = new QVBoxLayout(this);
 038    layout->setContentsMargins(2, 2, 2, 2);
 039    layout->setSpacing(7);
 40
 041    auto *widget_title = new QLabel("Zernike metrics", this);
 042    widget_title->setObjectName("settingsWidgetTitle");
 043    layout->addWidget(widget_title);
 44
 45    auto add_section_label = [this, layout](const QString &text) {
 46      auto *label = new QLabel(text, this);
 47      label->setObjectName("settingsSectionLabel");
 48      layout->addSpacing(5);
 49      layout->addWidget(label);
 050    };
 51    auto add_field = [layout](QLabel *label, QWidget *field) {
 52      layout->addWidget(label);
 53      field->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
 54      layout->addWidget(field);
 055    };
 56
 057    add_section_label("TIME AXIS");
 058    auto *time_window_label = new QLabel("Time window", this);
 059    time_window_spin_       = new QDoubleSpinBox(this);
 060    time_window_spin_->setRange(0.01, 3600.0);
 061    time_window_spin_->setDecimals(3);
 062    time_window_spin_->setSingleStep(0.5);
 063    time_window_spin_->setSuffix(" s");
 064    add_field(time_window_label, time_window_spin_);
 65
 066    add_section_label("Y AXIS");
 067    auto *scaling_mode_label = new QLabel("Scaling mode", this);
 068    scaling_mode_combo_      = new QComboBox(this);
 069    scaling_mode_combo_->addItem("Visible window",
 70                                 static_cast<int>(YAxisScalingMode::VisibleWindow));
 071    scaling_mode_combo_->addItem("Recorded extrema",
 72                                 static_cast<int>(YAxisScalingMode::RecordedExtrema));
 073    scaling_mode_combo_->addItem("Manual", static_cast<int>(YAxisScalingMode::Manual));
 074    add_field(scaling_mode_label, scaling_mode_combo_);
 75
 076    manual_minimum_label_ = new QLabel("Manual minimum", this);
 077    manual_minimum_spin_  = make_y_spin_box();
 078    add_field(manual_minimum_label_, manual_minimum_spin_);
 79
 080    manual_maximum_label_ = new QLabel("Manual maximum", this);
 081    manual_maximum_spin_  = make_y_spin_box();
 082    add_field(manual_maximum_label_, manual_maximum_spin_);
 83
 084    add_section_label("STATISTICS");
 085    show_statistics_check_ = new QCheckBox("Show mean and standard deviation", this);
 086    layout->addWidget(show_statistics_check_);
 87
 088    auto *reset_range_button = new QPushButton("Reset range state", this);
 089    reset_display_button_    = new QPushButton("Reset display settings", this);
 090    layout->addSpacing(6);
 091    layout->addWidget(reset_range_button);
 092    layout->addWidget(reset_display_button_);
 093    layout->addStretch(1);
 94
 095    connect(time_window_spin_, &QDoubleSpinBox::editingFinished, this, [this]() {
 96      if (!target_.isNull()) {
 97        target_->set_time_window_seconds(time_window_spin_->value());
 98      }
 99    });
 0100    connect(scaling_mode_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 101            [this](int index) {
 102              if (target_.isNull() || index < 0) {
 103                return;
 104              }
 105              target_->set_y_axis_scaling_mode(
 106                  static_cast<YAxisScalingMode>(scaling_mode_combo_->itemData(index).toInt()));
 107            });
 0108    connect(manual_minimum_spin_, &QDoubleSpinBox::editingFinished, this,
 109            [this]() { apply_manual_range(); });
 0110    connect(manual_maximum_spin_, &QDoubleSpinBox::editingFinished, this,
 111            [this]() { apply_manual_range(); });
 0112    connect(show_statistics_check_, &QCheckBox::toggled, this, [this](bool checked) {
 113      if (target_.isNull()) {
 114        return;
 115      }
 116      auto settings            = target_->display_settings();
 117      settings.show_statistics = checked;
 118      target_->set_display_settings(settings);
 119    });
 0120    connect(reset_range_button, &QPushButton::clicked, this, [this]() {
 121      if (!target_.isNull()) {
 122        target_->reset_recorded_range_state();
 123      }
 124    });
 0125    connect(reset_display_button_, &QPushButton::clicked, this, [this]() {
 126      if (!target_.isNull()) {
 127        target_->reset_display_settings();
 128      }
 129    });
 0130  }
 131
 0132  void set_target(ZernikeHistoryWidget *target) {
 0133    if (settings_connection_) {
 0134      disconnect(settings_connection_);
 135    }
 0136    target_ = target;
 0137    if (target_.isNull()) {
 0138      return;
 139    }
 140
 0141    settings_connection_ =
 142        connect(target_.data(), &ZernikeHistoryWidget::display_settings_changed, this,
 143                [this](const ZernikeHistoryDisplaySettings &) { synchronize(); });
 0144    synchronize();
 0145  }
 146
 147private:
 0148  QDoubleSpinBox *make_y_spin_box() {
 0149    auto *spin = new QDoubleSpinBox(this);
 0150    spin->setRange(-1e9, 1e9);
 0151    spin->setDecimals(6);
 0152    spin->setSingleStep(0.1);
 0153    return spin;
 0154  }
 155
 0156  void synchronize() {
 0157    if (target_.isNull()) {
 0158      return;
 159    }
 160
 0161    const auto           settings = target_->display_settings();
 0162    const QSignalBlocker time_blocker(time_window_spin_);
 0163    const QSignalBlocker mode_blocker(scaling_mode_combo_);
 0164    const QSignalBlocker minimum_blocker(manual_minimum_spin_);
 0165    const QSignalBlocker maximum_blocker(manual_maximum_spin_);
 0166    const QSignalBlocker statistics_blocker(show_statistics_check_);
 167
 0168    time_window_spin_->setValue(settings.time_window_seconds);
 0169    const int mode_index = scaling_mode_combo_->findData(static_cast<int>(settings.y_scaling_mode));
 0170    scaling_mode_combo_->setCurrentIndex(mode_index);
 0171    manual_minimum_spin_->setValue(settings.manual_y_minimum);
 0172    manual_maximum_spin_->setValue(settings.manual_y_maximum);
 0173    show_statistics_check_->setChecked(settings.show_statistics);
 0174    set_manual_fields_enabled(settings.y_scaling_mode == YAxisScalingMode::Manual);
 0175    clear_validation_error(manual_minimum_spin_);
 0176    clear_validation_error(manual_maximum_spin_);
 0177  }
 178
 0179  void set_manual_fields_enabled(bool enabled) {
 0180    manual_minimum_label_->setEnabled(enabled);
 0181    manual_minimum_spin_->setEnabled(enabled);
 0182    manual_maximum_label_->setEnabled(enabled);
 0183    manual_maximum_spin_->setEnabled(enabled);
 0184  }
 185
 0186  void apply_manual_range() {
 0187    if (target_.isNull()) {
 0188      return;
 189    }
 190
 0191    const double minimum = manual_minimum_spin_->value();
 0192    const double maximum = manual_maximum_spin_->value();
 0193    if (!std::isfinite(minimum) || !std::isfinite(maximum) || minimum >= maximum) {
 0194      mark_validation_error(manual_minimum_spin_);
 0195      mark_validation_error(manual_maximum_spin_);
 0196      return;
 197    }
 198
 0199    clear_validation_error(manual_minimum_spin_);
 0200    clear_validation_error(manual_maximum_spin_);
 0201    target_->set_manual_y_range(minimum, maximum);
 0202  }
 203
 204  QPointer<ZernikeHistoryWidget> target_;
 205  QMetaObject::Connection        settings_connection_;
 0206  QDoubleSpinBox                *time_window_spin_      = nullptr;
 0207  QComboBox                     *scaling_mode_combo_    = nullptr;
 0208  QLabel                        *manual_minimum_label_  = nullptr;
 0209  QDoubleSpinBox                *manual_minimum_spin_   = nullptr;
 0210  QLabel                        *manual_maximum_label_  = nullptr;
 0211  QDoubleSpinBox                *manual_maximum_spin_   = nullptr;
 0212  QCheckBox                     *show_statistics_check_ = nullptr;
 0213  QPushButton                   *reset_display_button_  = nullptr;
 214};
 215
 216SelectedWidgetSettingsPanel::SelectedWidgetSettingsPanel(QWidget *parent)
 0217    : QGroupBox("WIDGET SETTINGS", parent) {
 0218  setObjectName("selectedWidgetSettingsPanel");
 0219  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 220
 0221  auto *layout = new QVBoxLayout(this);
 0222  layout->setContentsMargins(0, 0, 0, 0);
 0223  layout->setSpacing(0);
 224
 0225  pages_ = new QStackedWidget(this);
 226
 0227  empty_page_        = new QWidget(pages_);
 0228  auto *empty_layout = new QVBoxLayout(empty_page_);
 0229  empty_layout->setContentsMargins(8, 12, 8, 8);
 0230  empty_placeholder_ = new QLabel("Click a visualization to configure it", empty_page_);
 0231  empty_placeholder_->setObjectName("settingsPlaceholder");
 0232  empty_placeholder_->setWordWrap(true);
 0233  empty_placeholder_->setAlignment(Qt::AlignHCenter | Qt::AlignTop);
 0234  empty_layout->addWidget(empty_placeholder_);
 0235  empty_layout->addStretch(1);
 0236  pages_->addWidget(empty_page_);
 237
 0238  auto *scroll_area = new QScrollArea(pages_);
 0239  scroll_area->setObjectName("widgetSettingsScrollArea");
 0240  scroll_area->setFrameShape(QFrame::NoFrame);
 0241  scroll_area->setWidgetResizable(true);
 0242  scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
 0243  scroll_area->setMinimumHeight(0);
 0244  scroll_area->viewport()->setObjectName("widgetSettingsScrollViewport");
 0245  zernike_history_page_ = new ZernikeHistorySettingsWidget(scroll_area);
 0246  zernike_history_page_->setObjectName("zernikeHistorySettingsPage");
 0247  scroll_area->setWidget(zernike_history_page_);
 0248  pages_->addWidget(scroll_area);
 249
 0250  layout->addWidget(pages_);
 0251  pages_->setCurrentWidget(empty_page_);
 0252}
 253
 0254void SelectedWidgetSettingsPanel::set_selected_widget(ZernikeHistoryWidget *widget) {
 0255  if (selected_widget_ == widget) {
 0256    zernike_history_page_->set_target(widget);
 0257    return;
 258  }
 259
 0260  if (destroyed_connection_) {
 0261    disconnect(destroyed_connection_);
 262  }
 0263  selected_widget_ = widget;
 0264  if (selected_widget_.isNull()) {
 0265    clear_selection();
 0266    return;
 267  }
 268
 0269  destroyed_connection_ =
 270      connect(selected_widget_.data(), &QObject::destroyed, this, [this]() { clear_selection(); });
 0271  zernike_history_page_->set_target(selected_widget_.data());
 0272  pages_->setCurrentIndex(1);
 0273}
 274
 0275void SelectedWidgetSettingsPanel::clear_selection() {
 0276  if (destroyed_connection_) {
 0277    disconnect(destroyed_connection_);
 0278    destroyed_connection_ = {};
 279  }
 0280  selected_widget_.clear();
 0281  zernike_history_page_->set_target(nullptr);
 0282  empty_placeholder_->setText("Click a visualization to configure it");
 0283  pages_->setCurrentWidget(empty_page_);
 0284}
 285
 0286void SelectedWidgetSettingsPanel::show_no_configurable_settings() {
 0287  if (destroyed_connection_) {
 0288    disconnect(destroyed_connection_);
 0289    destroyed_connection_ = {};
 290  }
 0291  selected_widget_.clear();
 0292  zernike_history_page_->set_target(nullptr);
 0293  empty_placeholder_->setText("No configurable settings");
 0294  pages_->setCurrentWidget(empty_page_);
 0295}
 296
 297} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\selected_widget_settings_panel.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QGroupBox>
 18#include <QPointer>
 19
 20class QStackedWidget;
 21class QLabel;
 22
 23namespace holovibes::ui {
 24
 25class ZernikeHistorySettingsWidget;
 26class ZernikeHistoryWidget;
 27
 28class SelectedWidgetSettingsPanel : public QGroupBox {
 29  Q_OBJECT
 30
 31public:
 32  explicit SelectedWidgetSettingsPanel(QWidget *parent = nullptr);
 33
 34  void set_selected_widget(ZernikeHistoryWidget *widget);
 35  void show_no_configurable_settings();
 36  void clear_selection();
 37
 38private:
 039  QStackedWidget                *pages_                = nullptr;
 040  QWidget                       *empty_page_           = nullptr;
 041  QLabel                        *empty_placeholder_    = nullptr;
 042  ZernikeHistorySettingsWidget  *zernike_history_page_ = nullptr;
 43  QPointer<ZernikeHistoryWidget> selected_widget_;
 44  QMetaObject::Connection        destroyed_connection_;
 45};
 46
 47} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\signal_history.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "signal_history.hh"
 16
 17#include <algorithm>
 18#include <cmath>
 19#include <stdexcept>
 20
 21namespace holovibes::ui {
 22
 23SignalHistory::SignalHistory(double time_window_seconds)
 124    : time_window_seconds_(time_window_seconds) {
 125  if (!std::isfinite(time_window_seconds_) || time_window_seconds_ <= 0.0) {
 126    throw std::invalid_argument("signal history time window must be positive and finite");
 27  }
 128}
 29
 130void SignalHistory::set_time_window_seconds(double time_window_seconds) {
 131  if (!std::isfinite(time_window_seconds) || time_window_seconds <= 0.0) {
 032    throw std::invalid_argument("signal history time window must be positive and finite");
 33  }
 34
 135  time_window_seconds_ = time_window_seconds;
 136  trim();
 137}
 38
 039void SignalHistory::clear() { samples_.clear(); }
 40
 141bool SignalHistory::append(SignalSample sample) {
 142  if (!std::isfinite(sample.time_seconds) || !std::isfinite(sample.value)) {
 143    return false;
 44  }
 145  if (!samples_.empty() && sample.time_seconds < samples_.back().time_seconds) {
 146    return false;
 47  }
 48
 149  samples_.push_back(sample);
 150  trim();
 151  return true;
 152}
 53
 054double SignalHistory::time_window_seconds() const { return time_window_seconds_; }
 55
 156const std::deque<SignalSample> &SignalHistory::samples() const { return samples_; }
 57
 58std::optional<SignalStatistics>
 159SignalHistory::statistics(double minimum_time_seconds) const {
 160  std::size_t sample_count          = 0;
 161  double      mean                  = 0.0;
 162  double      squared_deviation_sum = 0.0;
 63
 164  for (const auto &sample : samples_) {
 165    if (sample.time_seconds < minimum_time_seconds) {
 166      continue;
 67    }
 68
 169    ++sample_count;
 170    const double delta = sample.value - mean;
 171    mean += delta / static_cast<double>(sample_count);
 172    squared_deviation_sum += delta * (sample.value - mean);
 173  }
 74
 175  if (sample_count == 0) {
 176    return std::nullopt;
 77  }
 78
 179  const double variance =
 80      std::max(0.0, squared_deviation_sum / static_cast<double>(sample_count));
 181  return SignalStatistics{
 82      .sample_count       = sample_count,
 83      .mean               = mean,
 84      .standard_deviation = std::sqrt(variance),
 85  };
 186}
 87
 188void SignalHistory::trim() {
 189  if (samples_.empty()) {
 090    return;
 91  }
 92
 193  const double oldest_time = samples_.back().time_seconds - time_window_seconds_;
 194  while (!samples_.empty() && samples_.front().time_seconds < oldest_time) {
 195    samples_.pop_front();
 196  }
 197}
 98
 99} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\system_monitor_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/system_monitor_widget.hh"
 16
 17#include <QGridLayout>
 18#include <QLabel>
 19#include <QVBoxLayout>
 20
 21namespace holovibes::ui {
 22
 023SystemMonitorWidget::SystemMonitorWidget(QWidget *parent) : QGroupBox("SYSTEM MONITOR", parent) {
 024  setup_ui();
 025}
 26
 027void SystemMonitorWidget::set_gpu_load(const QString &text) { gpu_load_value_->setText(text); }
 028void SystemMonitorWidget::set_cpu_load(const QString &text) { cpu_load_value_->setText(text); }
 029void SystemMonitorWidget::set_input_throughput_fps(const QString &text) {
 030  input_throughput_fps_value_->setText(text);
 031}
 032void SystemMonitorWidget::set_input_throughput_bytes(const QString &text) {
 033  input_throughput_bytes_value_->setText(text);
 034}
 035void SystemMonitorWidget::set_cpu_throughput(const QString &text) {
 036  cpu_throughput_value_->setText(text);
 037}
 038void SystemMonitorWidget::set_gpu_throughput(const QString &text) {
 039  gpu_throughput_value_->setText(text);
 040}
 041void SystemMonitorWidget::set_ram_usage(const QString &text) { ram_usage_value_->setText(text); }
 042void SystemMonitorWidget::set_vram_usage(const QString &text) { vram_usage_value_->setText(text); }
 043void SystemMonitorWidget::set_dropped_frames(const QString &text) {
 044  dropped_frames_value_->setText(text);
 045}
 046void SystemMonitorWidget::set_pipeline_latency(const QString &text) {
 047  pipeline_latency_value_->setText(text);
 048}
 49
 050void SystemMonitorWidget::setup_ui() {
 051  auto *layout = new QVBoxLayout(this);
 052  layout->setContentsMargins(0, 0, 0, 0);
 053  layout->setSpacing(8);
 54
 55  // Line metrics
 056  auto *metrics_layout = new QGridLayout();
 057  metrics_layout->setContentsMargins(0, 0, 0, 0);
 058  metrics_layout->setHorizontalSpacing(8);
 059  metrics_layout->setVerticalSpacing(4);
 060  metrics_layout->setColumnStretch(1, 1);
 61
 62  auto add_metric_row = [&](int row, const QString &label, QLabel **value_label,
 63                            const QString &value) {
 64    auto *name_label = new QLabel(label, this);
 65    name_label->setMinimumWidth(0);
 66    name_label->setWordWrap(true);
 67    metrics_layout->addWidget(name_label, row, 0);
 68    *value_label = new QLabel(value, this);
 69    (*value_label)->setMinimumWidth(0);
 70    (*value_label)->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
 71    metrics_layout->addWidget(*value_label, row, 1);
 072  };
 73
 074  add_metric_row(0, "GPU Load:", &gpu_load_value_, "N/A");
 075  add_metric_row(1, "CPU Load:", &cpu_load_value_, "N/A");
 076  add_metric_row(2, "Input Throughput (FPS):", &input_throughput_fps_value_, "N/A");
 077  add_metric_row(3, "Input Throughput (Bytes):", &input_throughput_bytes_value_, "N/A");
 078  add_metric_row(4, "CPU Throughput:", &cpu_throughput_value_, "N/A");
 079  add_metric_row(5, "GPU Throughput:", &gpu_throughput_value_, "N/A");
 080  add_metric_row(6, "RAM Usage:", &ram_usage_value_, "N/A");
 081  add_metric_row(7, "VRAM Usage:", &vram_usage_value_, "N/A");
 082  add_metric_row(8, "Dropped Frames:", &dropped_frames_value_, "N/A");
 083  add_metric_row(9, "Pipeline Latency:", &pipeline_latency_value_, "N/A");
 84
 085  layout->addLayout(metrics_layout);
 86
 87  // Queue metrics
 088  auto *queue_group  = new QGroupBox("QUEUES", this);
 089  auto *queue_layout = new QVBoxLayout(queue_group);
 090  queue_layout->setContentsMargins(0, 0, 0, 0);
 091  queue_layout->setSpacing(6);
 92  auto configure_bar = [&](QProgressBar **bar, const QString &title, int value, int maximum) {
 93    queue_layout->addWidget(new QLabel(title, queue_group));
 94    *bar = new QProgressBar(queue_group);
 95    (*bar)->setRange(0, maximum);
 96    (*bar)->setValue(value);
 97    (*bar)->setFormat("%v / %m");
 98    (*bar)->setTextVisible(true);
 99    queue_layout->addWidget(*bar);
 0100  };
 101
 0102  configure_bar(&input_queue_bar_, "Input Queue", 48, 64);
 0103  configure_bar(&output_queue_bar_, "Output Queue", 22, 64);
 0104  configure_bar(&record_queue_bar_, "Record Queue", 12, 32);
 105
 0106  layout->addWidget(queue_group);
 0107}
 108
 0109void SystemMonitorWidget::update_metrics(double input_fps) {
 0110  if (input_fps < 0.0) {
 0111    input_fps = 0.0;
 112  }
 113
 0114  const QString text = QStringLiteral("%1 fps").arg(QString::number(input_fps, 'f', 1));
 0115  input_throughput_fps_value_->setText(text);
 116
 0117  gpu_load_value_->setText("N/A");
 0118  cpu_load_value_->setText("N/A");
 0119  input_throughput_bytes_value_->setText("N/A");
 0120  cpu_throughput_value_->setText("N/A");
 0121  gpu_throughput_value_->setText("N/A");
 0122  ram_usage_value_->setText("N/A");
 0123  vram_usage_value_->setText("N/A");
 0124  dropped_frames_value_->setText("N/A");
 0125  pipeline_latency_value_->setText("N/A");
 0126}
 127
 128} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\tensor_display_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/tensor_display_widget.hh"
 16
 17#include <QDebug>
 18#include <QLabel>
 19#include <QMetaType>
 20#include <QOpenGLExtraFunctions>
 21#include <QOpenGLShaderProgram>
 22#include <QPaintEvent>
 23#include <QPainter>
 24#include <QStyleOption>
 25#include <QVBoxLayout>
 26#include <algorithm>
 27#include <cmath>
 28#include <cstring>
 29#include <vector>
 30
 31#include "bug.hh"
 32#include "logger.hh"
 33
 34namespace holovibes::ui {
 35
 36static const char *kVS = R"(#version 330 core
 37layout(location=0) in vec2 inPos;
 38layout(location=1) in vec2 inUV;
 39out vec2 uv;
 40void main(){ uv=inUV; gl_Position=vec4(inPos,0.0,1.0); })";
 41
 42static const char *kFS = R"(#version 330 core
 43in vec2 uv;
 44out vec4 frag;
 45uniform sampler2D tex;
 46uniform sampler2D colormap_tex;
 47uniform int use_colormap;
 48uniform float vmin;
 49uniform float vmax;
 50
 51void main(){
 52  float raw_val = texture(tex, uv).r;
 53
 54  // Normalize the value to 0.0 - 1.0 based on bounds
 55  float g = clamp((raw_val - vmin) / (vmax - vmin), 0.0, 1.0);
 56
 57  if (use_colormap == 1) {
 58    // Sample the colormap lookup texture
 59    frag = texture(colormap_tex, vec2(g, 0.5));
 60  } else {
 61    // Standard Grayscale
 62    frag = vec4(g, g, g, 1.0);
 63  }
 64})";
 65
 66static const char *kReticleVS = R"(#version 330 core
 67layout(location=0) in vec2 inPos;
 68void main(){ gl_Position=vec4(inPos,0.0,1.0); })";
 69
 70static const char *kReticleFS = R"(#version 330 core
 71out vec4 frag;
 72uniform vec4 color;
 73void main(){ frag = color; })";
 74
 075TensorDisplayWidget::TensorDisplayWidget(QWidget *p) : QOpenGLWidget(p) {
 076  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 077  setMinimumSize(0, 0);
 78
 079  auto *layout = new QVBoxLayout(this);
 080  layout->setContentsMargins(24, 24, 24, 24);
 081  waiting_label_ = new QLabel(tr("Waiting for data..."), this);
 082  waiting_label_->setObjectName("visualizationWorkspacePlaceholder");
 083  waiting_label_->setAlignment(Qt::AlignCenter);
 084  waiting_label_->setWordWrap(true);
 085  waiting_label_->setAttribute(Qt::WA_TransparentForMouseEvents);
 086  layout->addWidget(waiting_label_, 1);
 087}
 88
 089void TensorDisplayWidget::set_fixed_aspect(std::optional<QSize> size) {
 090  HOLOVIBES_CHECK(!size || size->width() > 0);
 091  HOLOVIBES_CHECK(!size || size->height() > 0);
 092  fixed_aspect_size_ = size;
 093  updateGeometry();
 094}
 95
 096bool TensorDisplayWidget::hasHeightForWidth() const { return fixed_aspect_size_.has_value(); }
 97
 098int TensorDisplayWidget::heightForWidth(int w) const {
 099  if (!fixed_aspect_size_)
 0100    return QOpenGLWidget::heightForWidth(w);
 101
 0102  auto ar = float(fixed_aspect_size_->width()) / float(fixed_aspect_size_->height());
 0103  HOLOVIBES_CHECK(ar > 0.f);
 0104  return int(std::round(float(w) / ar));
 0105}
 106
 0107void TensorDisplayWidget::closeEvent(QCloseEvent *event) {
 0108  event->ignore();
 0109  this->hide();
 0110}
 111
 0112void TensorDisplayWidget::set_colormap(Colormap cmap) {
 0113  if (cmap_ != cmap) {
 0114    cmap_ = cmap;
 0115    update();
 116  }
 0117}
 118
 0119void TensorDisplayWidget::set_value_range(float vmin, float vmax) {
 0120  vmin_ = vmin;
 0121  vmax_ = vmax;
 0122  update();
 0123}
 124
 0125void TensorDisplayWidget::initializeGL() {
 0126  initializeOpenGLFunctions();
 0127  const QColor background = palette().color(QPalette::Window);
 0128  glClearColor(background.redF(), background.greenF(), background.blueF(), 1.0f);
 129
 130  // QOpenGLWidget receives a new context when ADS reparents it between docked and floating
 131  // top-level windows. Image dimensions survive that transition, but texture storage does not.
 0132  texture_storage_allocated_ = false;
 133
 134  // Quad: pos(x,y), uv
 135  const float quad[] = {
 0136      -1.f, -1.f, 0.f, 0.f, 1.f, -1.f, 1.f, 0.f, -1.f, 1.f, 0.f, 1.f, 1.f, 1.f, 1.f, 1.f,
 137  };
 138
 0139  glGenVertexArrays(1, &vao_);
 0140  glBindVertexArray(vao_);
 0141  glGenBuffers(1, &vbo_);
 0142  glBindBuffer(GL_ARRAY_BUFFER, vbo_);
 0143  glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);
 0144  glEnableVertexAttribArray(0);
 0145  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0);
 0146  glEnableVertexAttribArray(1);
 0147  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)(2 * sizeof(float)));
 148
 0149  auto *sp = new QOpenGLShaderProgram(this);
 0150  sp->addShaderFromSourceCode(QOpenGLShader::Vertex, kVS);
 0151  sp->addShaderFromSourceCode(QOpenGLShader::Fragment, kFS);
 0152  sp->link();
 0153  prog_ = sp->programId();
 154
 0155  tex_loc_      = glGetUniformLocation(prog_, "tex");
 0156  cmap_tex_loc_ = glGetUniformLocation(prog_, "colormap_tex");
 0157  use_cmap_loc_ = glGetUniformLocation(prog_, "use_colormap");
 0158  vmin_loc_     = glGetUniformLocation(prog_, "vmin");
 0159  vmax_loc_     = glGetUniformLocation(prog_, "vmax");
 160
 0161  glGenTextures(1, &tex_);
 0162  glBindTexture(GL_TEXTURE_2D, tex_);
 163  // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 164  // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 0165  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
 0166  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
 0167  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 0168  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 169
 170  // Swizzle RED to RGB for single-channel base mapping
 0171  GLint swz[4] = {GL_RED, GL_RED, GL_RED, GL_ONE};
 0172  glTexParameteriv(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_RGBA, swz);
 173
 0174  initializeColormaps();
 0175  initializeReticle();
 0176}
 177
 0178void TensorDisplayWidget::initializeColormaps() {
 179  // Generate a 1D mapping texture for the Twilight Colormap (size 256x1)
 180  struct ColorStop {
 181    float pos, r, g, b;
 182  };
 0183  const ColorStop stops[]   = {{0.00f, 0.886f, 0.851f, 0.886f}, {0.15f, 0.584f, 0.682f, 0.843f},
 0184                               {0.30f, 0.341f, 0.443f, 0.690f}, {0.50f, 0.118f, 0.125f, 0.216f},
 0185                               {0.70f, 0.549f, 0.251f, 0.349f}, {0.85f, 0.784f, 0.522f, 0.584f},
 0186                               {1.00f, 0.886f, 0.851f, 0.886f}};
 0187  const int       num_stops = sizeof(stops) / sizeof(ColorStop);
 188
 189  unsigned char cmap_data[256 * 3];
 0190  for (int i = 0; i < 256; ++i) {
 0191    float t   = i / 255.0f;
 0192    int   idx = 0;
 0193    while (idx < num_stops - 2 && t > stops[idx + 1].pos)
 0194      idx++;
 195
 0196    float t0 = stops[idx].pos;
 0197    float t1 = stops[idx + 1].pos;
 0198    float f  = (t - t0) / (t1 - t0);
 199
 0200    cmap_data[i * 3 + 0] =
 201        static_cast<unsigned char>(255.0f * (stops[idx].r + f * (stops[idx + 1].r - stops[idx].r)));
 0202    cmap_data[i * 3 + 1] =
 203        static_cast<unsigned char>(255.0f * (stops[idx].g + f * (stops[idx + 1].g - stops[idx].g)));
 0204    cmap_data[i * 3 + 2] =
 205        static_cast<unsigned char>(255.0f * (stops[idx].b + f * (stops[idx + 1].b - stops[idx].b)));
 0206  }
 207
 0208  glGenTextures(1, &colormap_tex_);
 0209  glBindTexture(GL_TEXTURE_2D, colormap_tex_);
 0210  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, cmap_data);
 0211  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 0212  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 0213  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 0214  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 0215}
 216
 0217void TensorDisplayWidget::initializeReticle() {
 0218  reticle_prog_ = glCreateProgram();
 219
 0220  GLuint      vs        = glCreateShader(GL_VERTEX_SHADER);
 0221  const char *vs_source = kReticleVS;
 0222  glShaderSource(vs, 1, &vs_source, nullptr);
 0223  glCompileShader(vs);
 0224  glAttachShader(reticle_prog_, vs);
 225
 0226  GLuint      fs        = glCreateShader(GL_FRAGMENT_SHADER);
 0227  const char *fs_source = kReticleFS;
 0228  glShaderSource(fs, 1, &fs_source, nullptr);
 0229  glCompileShader(fs);
 0230  glAttachShader(reticle_prog_, fs);
 231
 0232  glLinkProgram(reticle_prog_);
 0233  glDeleteShader(vs);
 0234  glDeleteShader(fs);
 235
 0236  reticle_color_loc_ = glGetUniformLocation(reticle_prog_, "color");
 237
 0238  glGenVertexArrays(1, &reticle_vao_);
 0239  glGenBuffers(1, &reticle_vbo_);
 0240}
 241
 0242void TensorDisplayWidget::ensureTexture(int w, int h, holoflow::core::DType dtype) {
 0243  if (texture_storage_allocated_ && w == img_w_ && h == img_h_ && dtype == current_dtype_)
 0244    return;
 245
 0246  img_w_         = w;
 0247  img_h_         = h;
 0248  current_dtype_ = dtype;
 249
 0250  GLint  internal_format = GL_R8;
 0251  GLenum type            = GL_UNSIGNED_BYTE;
 252
 0253  if (dtype == holoflow::core::DType::U16) {
 0254    internal_format = GL_R16;
 0255    type            = GL_UNSIGNED_SHORT;
 0256  } else if (dtype == holoflow::core::DType::F32) { // Assuming F32 exists
 0257    internal_format = GL_R32F;
 0258    type            = GL_FLOAT;
 259  }
 260
 0261  glBindTexture(GL_TEXTURE_2D, tex_);
 0262  glTexImage2D(GL_TEXTURE_2D, 0, internal_format, w, h, 0, GL_RED, type, nullptr);
 0263  texture_storage_allocated_ = true;
 264
 0265  updateLetterboxViewport();
 0266}
 267
 268void TensorDisplayWidget::updateTexture(const void *pixels, int w, int h,
 0269                                        holoflow::core::DType dtype) {
 0270  ensureTexture(w, h, dtype);
 0271  glBindTexture(GL_TEXTURE_2D, tex_);
 0272  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
 273
 0274  if (dtype == holoflow::core::DType::U8) {
 0275    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RED, GL_UNSIGNED_BYTE, pixels);
 0276  } else if (dtype == holoflow::core::DType::U16) {
 0277    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RED, GL_UNSIGNED_SHORT, pixels);
 0278  } else if (dtype == holoflow::core::DType::F32) {
 0279    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, w, h, GL_RED, GL_FLOAT, pixels);
 280  }
 0281}
 282
 283void TensorDisplayWidget::presentTensor(const QByteArray &bytes, int w, int h,
 0284                                        holoflow::core::DType dtype) {
 0285  if (!isVisible() || !isValid())
 0286    return;
 287
 0288  qsizetype need = qsizetype(w) * qsizetype(h);
 289
 0290  if (dtype == holoflow::core::DType::U16) {
 0291    need *= 2;
 0292  } else if (dtype == holoflow::core::DType::F32) {
 0293    need *= 4;
 294  }
 295
 0296  if (w <= 0 || h <= 0 || bytes.size() < need)
 0297    return;
 298
 0299  makeCurrent();
 0300  updateTexture(reinterpret_cast<const void *>(bytes.constData()), w, h, dtype);
 0301  doneCurrent();
 302
 0303  waiting_label_->hide();
 0304  update();
 0305  emit tensorDisplayed();
 0306}
 307
 0308void TensorDisplayWidget::resizeGL(int, int) { updateLetterboxViewport(); }
 309
 0310void TensorDisplayWidget::updateLetterboxViewport() {
 0311  const int widget_w = std::max(0, width());
 0312  const int widget_h = std::max(0, height());
 0313  if (img_w_ <= 0 || img_h_ <= 0 || widget_w <= 0 || widget_h <= 0) {
 0314    glViewport(0, 0, widget_w, widget_h);
 0315    return;
 316  }
 317
 0318  const int aspect_w = fixed_aspect_size_ ? fixed_aspect_size_->width() : img_w_;
 0319  const int aspect_h = fixed_aspect_size_ ? fixed_aspect_size_->height() : img_h_;
 320
 0321  const float wnd = float(widget_w) / float(widget_h);
 0322  const float img = float(aspect_w) / float(aspect_h);
 0323  if (wnd > img) {
 0324    int h = widget_h;
 0325    int w = int(img * h);
 0326    int x = (widget_w - w) / 2;
 0327    glViewport(x, 0, w, h);
 0328  } else {
 0329    int w = widget_w;
 0330    int h = int(w / img);
 0331    int y = (widget_h - h) / 2;
 0332    glViewport(0, y, w, h);
 333  }
 0334}
 335
 0336void TensorDisplayWidget::set_reticle_enabled(bool enabled) {
 0337  reticle_enabled_ = enabled;
 0338  update();
 0339}
 340
 0341void TensorDisplayWidget::set_reticle_radius(double radius) {
 0342  reticle_radius_ = qBound(0.05, radius, 1.0);
 0343  update();
 0344}
 345
 0346void TensorDisplayWidget::show_waiting_placeholder(const QString &message) {
 0347  waiting_label_->setText(message.isEmpty() ? tr("Waiting for data...") : message);
 0348  waiting_label_->show();
 0349  img_w_ = 0;
 0350  img_h_ = 0;
 0351  update();
 0352}
 353
 0354void TensorDisplayWidget::drawReticle() {
 0355  if (!reticle_enabled_ || img_w_ <= 0 || img_h_ <= 0)
 0356    return;
 357
 0358  const int          segments = 64;
 0359  std::vector<float> vertices;
 360
 0361  for (int i = 0; i <= segments; ++i) {
 0362    float angle = 2.0f * 3.14159265359f * float(i) / float(segments);
 0363    vertices.push_back(std::cos(angle) * reticle_radius_);
 0364    vertices.push_back(std::sin(angle) * reticle_radius_);
 0365  }
 366
 0367  glBindVertexArray(reticle_vao_);
 0368  glBindBuffer(GL_ARRAY_BUFFER, reticle_vbo_);
 0369  glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_DYNAMIC_DRAW);
 0370  glEnableVertexAttribArray(0);
 0371  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void *)0);
 372
 0373  glUseProgram(reticle_prog_);
 0374  glUniform4f(reticle_color_loc_, 1.0f, 0.0f, 0.0f, 1.0f); // Red color
 375
 0376  glLineWidth(2.0f);
 0377  glDrawArrays(GL_LINE_STRIP, 0, segments + 1);
 0378  glLineWidth(1.0f);
 0379}
 380
 0381void TensorDisplayWidget::paintGL() {
 0382  if (img_w_ <= 0 || img_h_ <= 0) {
 0383    const QColor background = palette().color(QPalette::Window);
 0384    glClearColor(background.redF(), background.greenF(), background.blueF(), 1.0f);
 0385    glClear(GL_COLOR_BUFFER_BIT);
 0386    return;
 387  }
 388
 0389  glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
 0390  glClear(GL_COLOR_BUFFER_BIT);
 391
 0392  updateLetterboxViewport();
 393
 0394  glUseProgram(prog_);
 395
 396  // Pass bounds
 0397  glUniform1f(vmin_loc_, vmin_);
 0398  glUniform1f(vmax_loc_, vmax_);
 399
 400  // Bind Main Image Texture
 0401  glActiveTexture(GL_TEXTURE0);
 0402  glBindTexture(GL_TEXTURE_2D, tex_);
 0403  glUniform1i(tex_loc_, 0);
 404
 405  // Bind Colormap Texture
 0406  glActiveTexture(GL_TEXTURE1);
 0407  glBindTexture(GL_TEXTURE_2D, colormap_tex_);
 0408  glUniform1i(cmap_tex_loc_, 1);
 409
 410  // Tell the shader which map we are using
 0411  glUniform1i(use_cmap_loc_, (cmap_ == Colormap::Twilight) ? 1 : 0);
 412
 0413  glBindVertexArray(vao_);
 0414  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 415
 0416  drawReticle();
 0417}
 418
 419} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\tensor_display_widget.hh

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QByteArray>
 18#include <QCloseEvent>
 19#include <QImage>
 20#include <QOpenGLExtraFunctions>
 21#include <QOpenGLFunctions>
 22#include <QOpenGLWidget>
 23#include <QPixmap>
 24#include <QString>
 25#include <QWidget>
 26#include <optional>
 27
 28#include "holoflow/core/tensor.hh"
 29
 30class QLabel;
 31
 32namespace holovibes::ui {
 33
 34/// Available colormaps for tensor visualization
 35enum class Colormap { Grayscale, Twilight };
 36
 37/// Widget that renders a 2D tensor as an image (with optional colormaps and float support).
 38class TensorDisplayWidget : public QOpenGLWidget, protected QOpenGLExtraFunctions {
 039  Q_OBJECT
 40
 41public:
 42  explicit TensorDisplayWidget(QWidget *parent = nullptr);
 43
 44  void set_fixed_aspect(std::optional<QSize> size);
 45  void set_reticle_enabled(bool enabled);
 46  void set_reticle_radius(double radius);
 47  void show_waiting_placeholder(const QString &message = {});
 48
 49  /// Set the active colormap
 50  void set_colormap(Colormap cmap);
 51
 52  /// Set the display range for mapping float data (or adjusting contrast on integers)
 53  void set_value_range(float vmin, float vmax);
 54
 55public slots:
 56  void presentTensor(const QByteArray &bytes, int width, int height, holoflow::core::DType dtype);
 57
 58signals:
 59  void tensorDisplayed();
 60
 61protected:
 62  void closeEvent(QCloseEvent *event) override;
 63  void initializeGL() override;
 64  void resizeGL(int w, int h) override;
 65  void paintGL() override;
 66
 67  bool hasHeightForWidth() const override;
 68  int  heightForWidth(int w) const override;
 69
 70private:
 71  void  ensureTexture(int w, int h, holoflow::core::DType dtype);
 72  void  updateTexture(const void *data, int w, int h, holoflow::core::DType dtype);
 73  void  updateLetterboxViewport();
 74  QRect getLetterboxRect() const;
 75  void  initializeReticle();
 76  void  initializeColormaps();
 77  void  drawReticle();
 78
 079  GLuint tex_  = 0;
 080  GLuint vao_  = 0;
 081  GLuint vbo_  = 0;
 082  GLuint prog_ = 0;
 83
 084  GLuint reticle_vao_       = 0;
 085  GLuint reticle_vbo_       = 0;
 086  GLuint reticle_prog_      = 0;
 087  GLint  reticle_color_loc_ = -1;
 88
 089  GLuint colormap_tex_ = 0;
 090  GLint  tex_loc_      = -1;
 091  GLint  cmap_tex_loc_ = -1;
 092  GLint  use_cmap_loc_ = -1;
 093  GLint  vmin_loc_     = -1;
 094  GLint  vmax_loc_     = -1;
 95
 096  int  img_w_ = 0, img_h_ = 0;
 097  bool texture_storage_allocated_ = false;
 98
 099  bool   reticle_enabled_ = false;
 0100  double reticle_radius_  = 1.0;
 101
 0102  Colormap cmap_ = Colormap::Grayscale;
 0103  float    vmin_ = 0.0f;
 0104  float    vmax_ = 1.0f;
 105
 106  // Track current dtype to reallocate texture format if it changes
 0107  holoflow::core::DType current_dtype_ = holoflow::core::DType::U8;
 108
 0109  std::optional<QSize> fixed_aspect_size_{std::nullopt};
 0110  QLabel              *waiting_label_ = nullptr;
 111};
 112
 113} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\validation_style.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QStyle>
 18#include <QVariant>
 19#include <QWidget>
 20
 21namespace holovibes::ui {
 22
 023inline void refresh_validation_style(QWidget *widget) {
 024  if (widget == nullptr) {
 025    return;
 26  }
 27
 028  widget->style()->unpolish(widget);
 029  widget->style()->polish(widget);
 030  widget->update();
 031}
 32
 033inline void mark_validation_error(QWidget *widget) {
 034  if (widget == nullptr) {
 035    return;
 36  }
 37
 038  widget->setProperty("validationState", "error");
 039  refresh_validation_style(widget);
 040}
 41
 042inline void clear_validation_error(QWidget *widget) {
 043  if (widget == nullptr) {
 044    return;
 45  }
 46
 047  widget->setProperty("validationState", QVariant());
 048  refresh_validation_style(widget);
 049}
 50
 51} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\view_widget.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "ui/widgets/view_widget.hh"
 16#include "ui/widgets/validation_style.hh"
 17
 18#include <QGridLayout>
 19#include <QLabel>
 20#include <QSpacerItem>
 21#include <QVBoxLayout>
 22#include <algorithm>
 23
 24namespace holovibes::ui {
 25
 26namespace {
 27
 28constexpr int kLargeSpinMax = 1024 * 1024;
 29
 030QSpinBox *create_spin_box(QWidget *parent, int minimum, int maximum, int value) {
 031  auto *spin_box = new QSpinBox(parent);
 032  spin_box->setRange(minimum, maximum);
 033  spin_box->setValue(value);
 034  return spin_box;
 035}
 36
 37QDoubleSpinBox *create_double_spin_box(QWidget *parent, double minimum, double maximum, double step,
 038                                       double value, int decimals = 2) {
 039  auto *spin_box = new QDoubleSpinBox(parent);
 040  spin_box->setRange(minimum, maximum);
 041  spin_box->setSingleStep(step);
 042  spin_box->setDecimals(decimals);
 043  spin_box->setValue(value);
 044  return spin_box;
 045}
 46
 047QComboBox *create_combo_box(QWidget *parent, const QStringList &items) {
 048  auto *combo_box = new QComboBox(parent);
 049  combo_box->addItems(items);
 050  return combo_box;
 051}
 52
 53} // namespace
 54
 055ViewWidget::ViewWidget(QWidget *parent) : QGroupBox("VIEW", parent) {
 056  setup_ui();
 057  connect_signals();
 058}
 59
 060QString ViewWidget::get_image_type() const { return image_type_combo_->currentText(); }
 061bool    ViewWidget::is_flatfield_enabled() const { return flatfield_check_->isChecked(); }
 062int     ViewWidget::get_x_origin() const { return x_spin_->value(); }
 063int     ViewWidget::get_x_width() const { return x_width_spin_->value(); }
 064int     ViewWidget::get_y_origin() const { return y_spin_->value(); }
 065int     ViewWidget::get_y_width() const { return y_width_spin_->value(); }
 066int     ViewWidget::get_z_origin() const { return z_spin_->value(); }
 067int     ViewWidget::get_z_width() const { return z_width_spin_->value(); }
 068int     ViewWidget::get_accumulation() const { return accumulation_spin_->value(); }
 069double  ViewWidget::get_flatfield_cutoff_period_um() const {
 070  return flatfield_cutoff_period_um_->value();
 071}
 072int    ViewWidget::get_range_start() const { return range_start_spin_->value(); }
 073int    ViewWidget::get_range_end() const { return range_end_spin_->value(); }
 074bool   ViewWidget::is_registration_enabled() const { return registration_check_->isChecked(); }
 075double ViewWidget::get_registration_radius() const { return registration_radius_->value(); }
 076bool   ViewWidget::is_reticle_enabled() const { return reticle_check_->isChecked(); }
 077double ViewWidget::get_reticle_radius() const { return reticle_radius_->value(); }
 078bool   ViewWidget::is_pct_enabled() const { return pct_check_->isChecked(); }
 079double ViewWidget::get_pct_radius() const { return pct_radius_->value(); }
 80
 81// Setters
 082void ViewWidget::set_xy_extent(int width, int height) {
 083  width  = std::max(width, 1);
 084  height = std::max(height, 1);
 85
 086  x_spin_->setRange(0, 0);
 087  x_width_spin_->setRange(width, width);
 088  y_spin_->setRange(0, 0);
 089  y_width_spin_->setRange(height, height);
 090}
 91
 092void ViewWidget::set_z_origin(int value) { z_spin_->setValue(value); }
 093void ViewWidget::set_z_width(int value) { z_width_spin_->setValue(value); }
 094void ViewWidget::set_flatfield_enabled(bool enabled) {
 095  flatfield_check_->setChecked(enabled);
 096  flatfield_cutoff_period_um_->setEnabled(enabled);
 097}
 098void ViewWidget::set_accumulation(int value) { accumulation_spin_->setValue(value); }
 099void ViewWidget::set_flatfield_cutoff_period_um(double value) {
 0100  flatfield_cutoff_period_um_->setValue(value);
 0101}
 0102void ViewWidget::set_reticle_radius(int value) { reticle_radius_->setValue(value); }
 0103void ViewWidget::set_registration_enabled(bool enabled) {
 0104  registration_check_->setChecked(enabled);
 0105}
 0106void ViewWidget::set_registration_radius(int value) { registration_radius_->setValue(value); }
 0107void ViewWidget::set_pct_enabled(bool enabled) { pct_check_->setChecked(enabled); }
 0108void ViewWidget::set_pct_radius(double value) { pct_radius_->setValue(value); }
 109
 110// Validation
 0111void ViewWidget::mark_z_invalid() { mark_validation_error(z_spin_); }
 0112void ViewWidget::mark_z_width_invalid() { mark_validation_error(z_width_spin_); }
 0113void ViewWidget::mark_accumulation_invalid() { mark_validation_error(accumulation_spin_); }
 0114void ViewWidget::mark_flatfield_cutoff_period_invalid() {
 0115  mark_validation_error(flatfield_cutoff_period_um_);
 0116}
 0117void ViewWidget::mark_registration_invalid() { mark_validation_error(registration_check_); }
 118
 119// Access to widgets for connection setup
 0120QComboBox      *ViewWidget::image_type_combo() { return image_type_combo_; }
 0121QCheckBox      *ViewWidget::flatfield_check() { return flatfield_check_; }
 0122QGroupBox      *ViewWidget::post_processing_group() { return post_processing_group_; }
 0123QSpinBox       *ViewWidget::z_spin() { return z_spin_; }
 0124QSpinBox       *ViewWidget::z_width_spin() { return z_width_spin_; }
 0125QSpinBox       *ViewWidget::accumulation_spin() { return accumulation_spin_; }
 0126QDoubleSpinBox *ViewWidget::flatfield_cutoff_period_um() { return flatfield_cutoff_period_um_; }
 0127QSpinBox       *ViewWidget::range_start_spin() { return range_start_spin_; }
 0128QSpinBox       *ViewWidget::range_end_spin() { return range_end_spin_; }
 0129QCheckBox      *ViewWidget::registration_check() { return registration_check_; }
 0130QDoubleSpinBox *ViewWidget::registration_radius() { return registration_radius_; }
 0131QCheckBox      *ViewWidget::reticle_check() { return reticle_check_; }
 0132QDoubleSpinBox *ViewWidget::reticle_radius() { return reticle_radius_; }
 0133QCheckBox      *ViewWidget::pct_check() { return pct_check_; }
 0134QDoubleSpinBox *ViewWidget::pct_radius() { return pct_radius_; }
 135
 0136void ViewWidget::setup_ui() {
 0137  auto *layout = new QGridLayout(this);
 0138  layout->setContentsMargins(0, 0, 0, 0);
 0139  layout->setHorizontalSpacing(6);
 0140  layout->setVerticalSpacing(4);
 0141  int row = 0;
 142
 143  auto add_combo_row = [&](const QString &label, QComboBox *&combo, const QStringList &items) {
 144    layout->addWidget(new QLabel(label, this), row, 0);
 145    combo = create_combo_box(this, items);
 146    layout->addWidget(combo, row, 1);
 147    ++row;
 0148  };
 149
 0150  add_combo_row("Image Type:", image_type_combo_, QStringList{"M0", "M1", "M2"});
 151
 0152  auto *axes_layout = new QGridLayout();
 0153  axes_layout->setContentsMargins(0, 0, 0, 0);
 0154  axes_layout->setHorizontalSpacing(6);
 0155  axes_layout->setVerticalSpacing(4);
 0156  int  axis_row     = 0;
 157  auto add_axis_row = [&](const QString &axis, QSpinBox *&origin_spin, QSpinBox *&width_spin) {
 158    axes_layout->addWidget(new QLabel(axis + ":", this), axis_row, 0);
 159    origin_spin = create_spin_box(this, 0, kLargeSpinMax, 0);
 160    axes_layout->addWidget(origin_spin, axis_row, 1);
 161    axes_layout->addWidget(new QLabel("Width:", this), axis_row, 2);
 162    width_spin = create_spin_box(this, 1, kLargeSpinMax, 1);
 163    axes_layout->addWidget(width_spin, axis_row, 3);
 164    ++axis_row;
 0165  };
 166
 0167  add_axis_row("X", x_spin_, x_width_spin_);
 0168  add_axis_row("Y", y_spin_, y_width_spin_);
 0169  add_axis_row("Z", z_spin_, z_width_spin_);
 0170  set_xy_extent(1, 1);
 0171  x_spin_->setEnabled(false);
 0172  x_width_spin_->setEnabled(false);
 0173  y_spin_->setEnabled(false);
 0174  y_width_spin_->setEnabled(false);
 0175  const auto xy_tooltip =
 176      tr("X/Y ranges are fixed to the full source image until X/Y slicing is implemented.");
 0177  x_spin_->setToolTip(xy_tooltip);
 0178  x_width_spin_->setToolTip(xy_tooltip);
 0179  y_spin_->setToolTip(xy_tooltip);
 0180  y_width_spin_->setToolTip(xy_tooltip);
 0181  layout->addLayout(axes_layout, row, 0, 1, 2);
 0182  ++row;
 183
 0184  layout->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding), row, 0, 1,
 185                  2);
 186
 0187  post_processing_group_ = new QGroupBox("POST PROCESSING", this);
 0188  auto *post_layout      = new QGridLayout(post_processing_group_);
 0189  post_layout->setContentsMargins(0, 0, 0, 0);
 0190  post_layout->setHorizontalSpacing(6);
 0191  post_layout->setVerticalSpacing(4);
 192
 0193  post_layout->addWidget(new QLabel("Accumulation:", post_processing_group_), 0, 0);
 0194  accumulation_spin_ = create_spin_box(post_processing_group_, 1, kLargeSpinMax, 1);
 0195  post_layout->addWidget(accumulation_spin_, 0, 1);
 196
 0197  flatfield_check_ = new QCheckBox("Flatfield", post_processing_group_);
 0198  flatfield_check_->setChecked(true);
 0199  post_layout->addWidget(flatfield_check_, 1, 0);
 0200  post_layout->addWidget(new QLabel("Cutoff (um):", post_processing_group_), 1, 1);
 0201  flatfield_cutoff_period_um_ =
 202      create_double_spin_box(post_processing_group_, 1.0, 1000000.0, 100.0, 2000.0, 1);
 0203  post_layout->addWidget(flatfield_cutoff_period_um_, 1, 2);
 204
 0205  post_layout->addWidget(new QLabel("Range:", post_processing_group_), 2, 0);
 0206  range_start_spin_ = create_spin_box(post_processing_group_, 1, kLargeSpinMax, 0);
 0207  post_layout->addWidget(range_start_spin_, 2, 1);
 0208  range_end_spin_ = create_spin_box(post_processing_group_, 1, kLargeSpinMax, 255);
 0209  post_layout->addWidget(range_end_spin_, 2, 2);
 210
 0211  reticle_check_ = new QCheckBox("Display reticle", post_processing_group_);
 0212  post_layout->addWidget(reticle_check_, 3, 0);
 0213  reticle_radius_ = create_double_spin_box(post_processing_group_, 0.05, 1.0, 0.05, 1.0);
 0214  post_layout->addWidget(reticle_radius_, 3, 1, 1, 2);
 215
 0216  pct_check_ = new QCheckBox("PCT", post_processing_group_);
 0217  post_layout->addWidget(pct_check_, 4, 0);
 0218  pct_radius_ = create_double_spin_box(post_processing_group_, 0.05, 1.0, 0.05, 1.0);
 0219  post_layout->addWidget(pct_radius_, 4, 1, 1, 2);
 220
 0221  registration_check_ = new QCheckBox("Registration", post_processing_group_);
 0222  post_layout->addWidget(registration_check_, 5, 0);
 0223  registration_radius_ = create_double_spin_box(post_processing_group_, 0.05, 1.0, 0.05, 1.0);
 0224  post_layout->addWidget(registration_radius_, 5, 1, 1, 2);
 0225}
 226
 0227void ViewWidget::connect_signals() {
 228  // Special signals for UI changes
 0229  connect(reticle_check_, &QCheckBox::toggled, this, &ViewWidget::reticle_toggled);
 0230  connect(reticle_radius_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 231          &ViewWidget::reticle_radius_changed);
 232
 233  // Emit settings_changed for all control changes
 0234  connect(image_type_combo_, qOverload<int>(&QComboBox::currentIndexChanged), this,
 235          &ViewWidget::settings_changed);
 0236  connect(z_spin_, qOverload<int>(&QSpinBox::valueChanged), this, &ViewWidget::settings_changed);
 0237  connect(z_width_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 238          &ViewWidget::settings_changed);
 0239  connect(accumulation_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 240          &ViewWidget::settings_changed);
 0241  connect(flatfield_check_, &QCheckBox::toggled, flatfield_cutoff_period_um_,
 242          &QDoubleSpinBox::setEnabled);
 0243  connect(flatfield_check_, &QCheckBox::toggled, this, &ViewWidget::settings_changed);
 0244  connect(flatfield_cutoff_period_um_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 245          &ViewWidget::settings_changed);
 0246  connect(range_start_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 247          &ViewWidget::settings_changed);
 0248  connect(range_end_spin_, qOverload<int>(&QSpinBox::valueChanged), this,
 249          &ViewWidget::settings_changed);
 0250  connect(registration_check_, &QCheckBox::toggled, this, &ViewWidget::settings_changed);
 0251  connect(registration_radius_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 252          &ViewWidget::settings_changed);
 0253  connect(pct_check_, &QCheckBox::toggled, this, &ViewWidget::settings_changed);
 0254  connect(pct_radius_, qOverload<double>(&QDoubleSpinBox::valueChanged), this,
 255          &ViewWidget::settings_changed);
 0256}
 257
 0258void ViewWidget::clear_validation_styles() {
 0259  clear_validation_error(z_spin_);
 0260  clear_validation_error(z_width_spin_);
 0261  clear_validation_error(flatfield_cutoff_period_um_);
 0262  clear_validation_error(registration_check_);
 0263}
 264
 265} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\view_widget.hh

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QCheckBox>
 18#include <QComboBox>
 19#include <QDoubleSpinBox>
 20#include <QGroupBox>
 21#include <QSpinBox>
 22
 23namespace holovibes::ui {
 24
 25class ViewWidget : public QGroupBox {
 026  Q_OBJECT
 27
 28public:
 29  explicit ViewWidget(QWidget *parent = nullptr);
 30
 31  // Getters
 32  QString get_image_type() const;
 33  bool    is_flatfield_enabled() const;
 34  int     get_x_origin() const;
 35  int     get_x_width() const;
 36  int     get_y_origin() const;
 37  int     get_y_width() const;
 38  int     get_z_origin() const;
 39  int     get_z_width() const;
 40  int     get_accumulation() const;
 41  double  get_flatfield_cutoff_period_um() const;
 42  int     get_range_start() const;
 43  int     get_range_end() const;
 44  bool    is_registration_enabled() const;
 45  double  get_registration_radius() const;
 46  bool    is_reticle_enabled() const;
 47  double  get_reticle_radius() const;
 48  bool    is_pct_enabled() const;
 49  double  get_pct_radius() const;
 50
 51  // Setters
 52  void set_xy_extent(int width, int height);
 53  void set_z_origin(int value);
 54  void set_z_width(int value);
 55  void set_flatfield_enabled(bool enabled);
 56  void set_accumulation(int value);
 57  void set_flatfield_cutoff_period_um(double value);
 58  void set_reticle_radius(int value);
 59  void set_registration_enabled(bool enabled);
 60  void set_registration_radius(int value);
 61  void set_pct_enabled(bool enabled);
 62  void set_pct_radius(double value);
 63
 64  // Validation
 65  void clear_validation_styles();
 66  void mark_z_invalid();
 67  void mark_z_width_invalid();
 68  void mark_accumulation_invalid();
 69  void mark_flatfield_cutoff_period_invalid();
 70  void mark_registration_invalid();
 71
 72  // Access to widgets for connection setup
 73  QComboBox      *image_type_combo();
 74  QCheckBox      *flatfield_check();
 75  QGroupBox      *post_processing_group();
 76  QSpinBox       *z_spin();
 77  QSpinBox       *z_width_spin();
 78  QSpinBox       *accumulation_spin();
 79  QDoubleSpinBox *flatfield_cutoff_period_um();
 80  QSpinBox       *range_start_spin();
 81  QSpinBox       *range_end_spin();
 82  QCheckBox      *registration_check();
 83  QDoubleSpinBox *registration_radius();
 84  QCheckBox      *reticle_check();
 85  QDoubleSpinBox *reticle_radius();
 86  QCheckBox      *pct_check();
 87  QDoubleSpinBox *pct_radius();
 88
 89signals:
 90  void settings_changed();
 91  void reticle_toggled(bool enabled);
 92  void reticle_radius_changed(double radius);
 93
 94private:
 95  void setup_ui();
 96  void connect_signals();
 97
 98  QComboBox      *image_type_combo_;
 99  QSpinBox       *x_spin_;
 100  QSpinBox       *x_width_spin_;
 101  QSpinBox       *y_spin_;
 102  QSpinBox       *y_width_spin_;
 103  QSpinBox       *z_spin_;
 104  QSpinBox       *z_width_spin_;
 105  QGroupBox      *post_processing_group_;
 106  QSpinBox       *accumulation_spin_;
 107  QCheckBox      *flatfield_check_;
 108  QDoubleSpinBox *flatfield_cutoff_period_um_;
 109  QSpinBox       *range_start_spin_;
 110  QSpinBox       *range_end_spin_;
 111  QCheckBox      *registration_check_;
 112  QDoubleSpinBox *registration_radius_;
 113  QCheckBox      *reticle_check_;
 114  QDoubleSpinBox *reticle_radius_;
 115  QCheckBox      *pct_check_;
 116  QDoubleSpinBox *pct_radius_;
 117};
 118
 119} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\zernike_history_widget.cc

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "zernike_history_widget.hh"
 16
 17#include <QFont>
 18#include <QImage>
 19#include <QLabel>
 20#include <QMetaObject>
 21#include <QMouseEvent>
 22#include <QPainter>
 23#include <QPalette>
 24#include <QPointer>
 25#include <QPolygon>
 26#include <QTimer>
 27#include <QVBoxLayout>
 28
 29#include <algorithm>
 30#include <array>
 31#include <cmath>
 32#include <condition_variable>
 33#include <cstdint>
 34#include <deque>
 35#include <limits>
 36#include <mutex>
 37#include <optional>
 38#include <thread>
 39#include <utility>
 40#include <vector>
 41
 42namespace holovibes::ui {
 43
 44namespace {
 45
 46constexpr int    kRefreshIntervalMs         = 33;
 47constexpr int    kTickCount                 = 2;
 48constexpr double kUnsafeRasterCoordinateAbs = 1.0e6;
 49constexpr double kPlotGap                   = 8.0;
 50
 51const std::array<QColor, 9> kCurveColors{
 52    QColor("#3DAEE9"), QColor("#E67E22"), QColor("#2ECC71"), QColor("#E74C3C"), QColor("#9B59B6"),
 53    QColor("#F1C40F"), QColor("#1ABC9C"), QColor("#E84393"), QColor("#95A5A6"),
 54};
 55
 56struct PlotFonts {
 57  QFont widget_title;
 58  QFont subplot_title;
 59  QFont statistics;
 60  QFont axis;
 61};
 62
 63struct GridLayout {
 64  QRectF content;
 65  int    columns     = 0;
 66  int    rows        = 0;
 67  double cell_width  = 0.0;
 68  double cell_height = 0.0;
 69
 070  [[nodiscard]] bool valid() const { return cell_width > 1.0 && cell_height > 1.0; }
 71
 072  [[nodiscard]] QRectF cell(size_t index) const {
 073    const int column = static_cast<int>(index) % columns;
 074    const int row    = static_cast<int>(index) / columns;
 075    return {content.left() + column * (cell_width + kPlotGap),
 76            content.top() + row * (cell_height + kPlotGap), cell_width, cell_height};
 077  }
 78};
 79
 80struct CurveGeometry {
 81  std::vector<QPolygon> segments;
 82  QRectF                clip_rect;
 83  QColor                color;
 084  bool                  needs_clipping = false;
 85};
 86
 087std::optional<std::pair<double, double>> finite_extrema(const std::deque<SignalSample> &samples) {
 088  double minimum = std::numeric_limits<double>::infinity();
 089  double maximum = -std::numeric_limits<double>::infinity();
 90
 091  for (const auto &sample : samples) {
 092    if (!std::isfinite(sample.value)) {
 093      continue;
 94    }
 095    minimum = std::min(minimum, sample.value);
 096    maximum = std::max(maximum, sample.value);
 097  }
 98
 099  if (!std::isfinite(minimum) || !std::isfinite(maximum)) {
 0100    return std::nullopt;
 101  }
 0102  return std::pair{minimum, maximum};
 0103}
 104
 0105AxisRange make_display_range(double minimum, double maximum) {
 0106  if (!std::isfinite(minimum) || !std::isfinite(maximum) || minimum > maximum) {
 0107    return {-1.0, 1.0};
 108  }
 109
 0110  const double span  = maximum - minimum;
 0111  const double scale = std::max(std::abs(minimum), std::abs(maximum));
 0112  if (span <= std::max(1e-12, scale * 1e-9)) {
 0113    const double half_range = std::max(std::abs(minimum) * 0.1, 1e-3);
 0114    return {minimum - half_range, maximum + half_range};
 115  }
 116
 0117  const double margin =
 118      std::max({span * 0.08, std::abs(minimum) * 0.02, std::abs(maximum) * 0.02, 1e-6});
 0119  return {minimum - margin, maximum + margin};
 0120}
 121
 0122bool is_valid_y_axis_scaling_mode(YAxisScalingMode mode) {
 0123  switch (mode) {
 124  case YAxisScalingMode::VisibleWindow:
 125  case YAxisScalingMode::RecordedExtrema:
 126  case YAxisScalingMode::Manual:
 0127    return true;
 128  }
 0129  return false;
 0130}
 131
 0132QString format_axis_value(double value) {
 0133  const double magnitude = std::abs(value);
 0134  if ((magnitude > 0.0 && magnitude < 1e-3) || magnitude >= 1e4) {
 0135    return QString::number(value, 'e', 2);
 136  }
 0137  return QString::number(value, 'f', magnitude < 1.0 ? 3 : 2);
 0138}
 139
 0140PlotFonts make_plot_fonts(const QPainter &painter) {
 0141  const QString family = painter.font().family();
 142
 0143  PlotFonts fonts;
 0144  fonts.widget_title = QFont(family);
 0145  fonts.widget_title.setBold(true);
 146
 0147  fonts.subplot_title = QFont(family);
 0148  fonts.subplot_title.setBold(true);
 0149  fonts.subplot_title.setPixelSize(11);
 150
 0151  fonts.statistics = QFont(family);
 0152  fonts.statistics.setPixelSize(9);
 153
 0154  fonts.axis = QFont(family);
 0155  fonts.axis.setPixelSize(7);
 0156  return fonts;
 0157}
 158
 0159GridLayout make_grid_layout(const QRect &widget_rect, int series_count) {
 0160  GridLayout layout;
 0161  layout.columns     = series_count <= 3 ? 1 : series_count <= 6 ? 2 : 3;
 0162  layout.rows        = (series_count + layout.columns - 1) / layout.columns;
 0163  layout.content     = QRectF(widget_rect).adjusted(8.0, 28.0, -8.0, -8.0);
 0164  layout.cell_width  = (layout.content.width() - (layout.columns - 1) * kPlotGap) / layout.columns;
 0165  layout.cell_height = (layout.content.height() - (layout.rows - 1) * kPlotGap) / layout.rows;
 0166  return layout;
 0167}
 168
 0169QRectF make_plot_rect(const QRectF &cell, bool show_statistics) {
 0170  const double header_height = show_statistics ? 32.0 : 20.0;
 0171  return cell.adjusted(50.0, header_height, -8.0, -30.0);
 0172}
 173
 174void draw_plot_frame(QPainter &painter, const QRectF &cell, const QRectF &plot, int noll_index,
 175                     const QString &statistics_text, double time_window, AxisRange y_range,
 176                     const QColor &text_color, const QColor &grid_color, const PlotFonts &fonts,
 0177                     const QString &time_axis_label) {
 0178  painter.setFont(fonts.subplot_title);
 0179  painter.setPen(text_color);
 0180  painter.drawText(QRectF(cell.left(), cell.top(), cell.width(), 16.0), Qt::AlignCenter,
 181                   QString("a%1 (rad)").arg(noll_index));
 182
 0183  if (!statistics_text.isEmpty()) {
 0184    painter.setFont(fonts.statistics);
 0185    painter.drawText(QRectF(cell.left(), cell.top() + 16.0, cell.width(), 14.0), Qt::AlignCenter,
 186                     statistics_text);
 187  }
 188
 0189  painter.setFont(fonts.axis);
 0190  painter.setPen(QPen(grid_color, 1.0));
 191
 0192  for (int tick = 0; tick <= kTickCount; ++tick) {
 0193    const double fraction = static_cast<double>(tick) / kTickCount;
 0194    const double x        = plot.left() + fraction * plot.width();
 0195    const double y        = plot.bottom() - fraction * plot.height();
 196
 0197    painter.drawLine(QPointF(x, plot.top()), QPointF(x, plot.bottom()));
 0198    painter.drawLine(QPointF(plot.left(), y), QPointF(plot.right(), y));
 199
 0200    const double  relative_time = -time_window + fraction * time_window;
 0201    const double  y_value       = y_range.minimum + fraction * (y_range.maximum - y_range.minimum);
 0202    const QString time_label    = QString::number(relative_time, 'f', time_window < 2.0 ? 2 : 1);
 0203    const QString value_label   = format_axis_value(y_value);
 204
 0205    painter.setPen(text_color);
 0206    painter.drawText(QRectF(x - 28.0, plot.bottom() + 2.0, 56.0, 13.0), Qt::AlignHCenter,
 207                     time_label);
 0208    painter.drawText(QRectF(cell.left(), y - 7.0, 45.0, 14.0), Qt::AlignRight | Qt::AlignVCenter,
 209                     value_label);
 0210    painter.setPen(QPen(grid_color, 1.0));
 0211  }
 212
 0213  painter.setPen(QPen(text_color, 1.0));
 0214  painter.drawRect(plot);
 0215  painter.drawText(QRectF(plot.left(), plot.bottom() + 15.0, plot.width(), 13.0), Qt::AlignCenter,
 216                   time_axis_label);
 0217}
 218
 219CurveGeometry build_curve(const std::deque<SignalSample> &samples, const QRectF &plot,
 220                          AxisRange y_range, double oldest_time, double newest_time,
 0221                          double time_window, const QColor &color) {
 0222  CurveGeometry curve{
 0223      .clip_rect = plot,
 0224      .color     = color,
 225  };
 226
 0227  QPolygon segment;
 0228  segment.reserve(static_cast<qsizetype>(samples.size()));
 229
 230  const auto flush_segment = [&]() {
 231    if (segment.empty()) {
 232      return;
 233    }
 234    curve.segments.push_back(std::move(segment));
 235    segment = QPolygon{};
 0236  };
 237
 238  const auto map_to_plot = [&](const SignalSample &sample) {
 239    const double relative_time = sample.time_seconds - newest_time;
 240    const double x_fraction    = (relative_time + time_window) / time_window;
 241    const double y_fraction =
 242        (sample.value - y_range.minimum) / (y_range.maximum - y_range.minimum);
 243    return QPointF(plot.left() + x_fraction * plot.width(),
 244                   plot.bottom() - y_fraction * plot.height());
 0245  };
 246
 0247  for (const auto &sample : samples) {
 248    if (!std::isfinite(sample.time_seconds) || !std::isfinite(sample.value) ||
 0249        sample.time_seconds < oldest_time || sample.time_seconds > newest_time) {
 0250      flush_segment();
 0251      continue;
 252    }
 253
 0254    const QPointF point = map_to_plot(sample);
 255    if (!std::isfinite(point.x()) || !std::isfinite(point.y()) ||
 0256        std::abs(point.x()) > kUnsafeRasterCoordinateAbs ||
 257        std::abs(point.y()) > kUnsafeRasterCoordinateAbs) {
 0258      flush_segment();
 0259      continue;
 260    }
 261
 0262    curve.needs_clipping |= !plot.contains(point);
 263
 264    // Curves are rendered without antialiasing, so sub-pixel coordinates provide no benefit.
 0265    const QPoint raster_point = point.toPoint();
 0266    if (segment.empty() || segment.back() != raster_point) {
 0267      segment.append(raster_point);
 268    }
 0269  }
 270
 0271  flush_segment();
 0272  return curve;
 0273}
 274
 275} // namespace
 276
 277class ZernikeHistoryWidget::CurveRenderWorker {
 278public:
 279  struct Job {
 280    uint64_t                   revision = 0;
 281    QSize                      logical_size;
 282    qreal                      device_pixel_ratio = 1.0;
 283    std::vector<CurveGeometry> curves;
 284  };
 285
 0286  explicit CurveRenderWorker(ZernikeHistoryWidget *widget)
 0287      : widget_(widget), thread_([this]() { run(); }) {}
 288
 0289  ~CurveRenderWorker() {
 290    {
 0291      std::lock_guard lock(mutex_);
 0292      stopping_ = true;
 0293      pending_job_.reset();
 0294    }
 0295    condition_.notify_one();
 0296    thread_.join();
 0297  }
 298
 0299  void submit(Job job) {
 300    {
 0301      std::lock_guard lock(mutex_);
 0302      if (is_duplicate(job)) {
 0303        return;
 304      }
 305
 0306      last_submitted_revision_           = job.revision;
 0307      last_submitted_size_               = job.logical_size;
 0308      last_submitted_device_pixel_ratio_ = job.device_pixel_ratio;
 0309      pending_job_                       = std::move(job);
 0310    }
 0311    condition_.notify_one();
 0312  }
 313
 0314  [[nodiscard]] std::optional<QImage> latest_image(const QSize &logical_size) const {
 0315    std::lock_guard lock(mutex_);
 0316    if (!ready_image_.has_value() ||
 317        ready_image_->deviceIndependentSize().toSize() != logical_size) {
 0318      return std::nullopt;
 319    }
 0320    return ready_image_;
 0321  }
 322
 323private:
 0324  [[nodiscard]] bool is_duplicate(const Job &job) const {
 0325    return last_submitted_revision_ == job.revision && last_submitted_size_ == job.logical_size &&
 326           qFuzzyCompare(last_submitted_device_pixel_ratio_, job.device_pixel_ratio);
 0327  }
 328
 0329  [[nodiscard]] std::optional<Job> wait_for_job() {
 0330    std::unique_lock lock(mutex_);
 0331    condition_.wait(lock, [this]() { return stopping_ || pending_job_.has_value(); });
 0332    if (stopping_) {
 0333      return std::nullopt;
 334    }
 335
 0336    Job job = std::move(*pending_job_);
 0337    pending_job_.reset();
 0338    return job;
 0339  }
 340
 0341  static QImage render(const Job &job) {
 0342    const QSize image_size(std::max(1, qRound(job.logical_size.width() * job.device_pixel_ratio)),
 343                           std::max(1, qRound(job.logical_size.height() * job.device_pixel_ratio)));
 344
 0345    QImage image(image_size, QImage::Format_ARGB32_Premultiplied);
 0346    image.setDevicePixelRatio(job.device_pixel_ratio);
 0347    image.fill(Qt::transparent);
 348
 0349    QPainter painter(&image);
 0350    painter.setRenderHint(QPainter::Antialiasing, false);
 0351    painter.setRenderHint(QPainter::SmoothPixmapTransform, false);
 352
 0353    for (const auto &curve : job.curves) {
 0354      painter.save();
 355
 356      // A zero-width cosmetic pen maps directly to a one-device-pixel raster line and avoids
 357      // Qt's more expensive general wide-pen stroker.
 0358      QPen pen(curve.color);
 0359      pen.setWidth(0);
 0360      pen.setCosmetic(true);
 0361      pen.setStyle(Qt::SolidLine);
 0362      pen.setCapStyle(Qt::FlatCap);
 0363      pen.setJoinStyle(Qt::BevelJoin);
 0364      painter.setPen(pen);
 0365      painter.setBrush(Qt::NoBrush);
 366
 0367      if (curve.needs_clipping) {
 0368        painter.setClipRect(curve.clip_rect, Qt::ReplaceClip);
 0369      } else {
 0370        painter.setClipping(false);
 371      }
 372
 0373      for (const auto &segment : curve.segments) {
 0374        if (segment.size() >= 2) {
 0375          painter.drawPolyline(segment);
 0376        } else if (segment.size() == 1) {
 0377          painter.drawPoint(segment.front());
 378        }
 0379      }
 380
 0381      painter.restore();
 0382    }
 383
 0384    painter.end();
 0385    return image;
 0386  }
 387
 0388  bool publish(QImage image) {
 0389    std::lock_guard lock(mutex_);
 0390    if (stopping_) {
 0391      return false;
 392    }
 0393    ready_image_ = std::move(image);
 0394    return true;
 0395  }
 396
 0397  void notify_widget() const {
 0398    const QPointer<ZernikeHistoryWidget> widget = widget_;
 0399    if (widget.isNull()) {
 0400      return;
 401    }
 402
 0403    QMetaObject::invokeMethod(
 404        widget.data(),
 405        [widget]() {
 406          if (!widget.isNull()) {
 407            widget->update();
 408          }
 409        },
 410        Qt::QueuedConnection);
 0411  }
 412
 0413  void run() {
 0414    while (const auto job = wait_for_job()) {
 0415      if (publish(render(*job))) {
 0416        notify_widget();
 417      }
 0418    }
 0419  }
 420
 421  QPointer<ZernikeHistoryWidget> widget_;
 422
 423  mutable std::mutex      mutex_;
 424  std::condition_variable condition_;
 425  std::optional<Job>      pending_job_;
 426  std::optional<QImage>   ready_image_;
 0427  bool                    stopping_ = false;
 428
 0429  uint64_t last_submitted_revision_ = std::numeric_limits<uint64_t>::max();
 430  QSize    last_submitted_size_;
 0431  qreal    last_submitted_device_pixel_ratio_ = 0.0;
 432
 433  std::thread thread_;
 434};
 435
 0436ZernikeHistoryWidget::ZernikeHistoryWidget(QWidget *parent) : QWidget(parent) {
 0437  setMinimumSize(320, 220);
 0438  setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
 439
 0440  auto *layout = new QVBoxLayout(this);
 0441  layout->setContentsMargins(24, 24, 24, 24);
 442
 0443  waiting_label_ = new QLabel(tr("Waiting for data..."), this);
 0444  waiting_label_->setObjectName("visualizationWorkspacePlaceholder");
 0445  waiting_label_->setAlignment(Qt::AlignCenter);
 0446  waiting_label_->setWordWrap(true);
 0447  waiting_label_->setAttribute(Qt::WA_TransparentForMouseEvents);
 0448  layout->addWidget(waiting_label_, 1);
 449
 0450  curve_render_worker_ = std::make_unique<CurveRenderWorker>(this);
 451
 0452  refresh_timer_ = new QTimer(this);
 0453  refresh_timer_->setInterval(kRefreshIntervalMs);
 0454  refresh_timer_->setTimerType(Qt::CoarseTimer);
 0455  connect(refresh_timer_, &QTimer::timeout, this, [this]() {
 456    if (!refresh_dirty_) {
 457      return;
 458    }
 459    refresh_dirty_ = false;
 460    update();
 461  });
 0462}
 463
 0464ZernikeHistoryWidget::~ZernikeHistoryWidget() = default;
 465
 0466void ZernikeHistoryWidget::start_run(double time_window_seconds, const std::vector<int> &indexes) {
 0467  const bool time_window_changed = display_settings_.time_window_seconds != time_window_seconds;
 0468  display_settings_.time_window_seconds = time_window_seconds;
 469
 0470  set_series(indexes);
 0471  for (auto &series : series_) {
 0472    series.history.set_time_window_seconds(display_settings_.time_window_seconds);
 0473    series.history.clear();
 0474    series.recorded_range.reset();
 0475  }
 476
 0477  waiting_label_->show();
 0478  active_ = true;
 0479  refresh_timer_->start();
 0480  request_refresh();
 481
 0482  if (time_window_changed) {
 0483    emit display_settings_changed(display_settings_);
 484  }
 0485}
 486
 0487void ZernikeHistoryWidget::resume_run() {
 0488  active_ = true;
 0489  refresh_timer_->start();
 0490  request_refresh();
 0491}
 492
 0493void ZernikeHistoryWidget::stop_run() {
 0494  active_ = false;
 0495  refresh_timer_->stop();
 0496}
 497
 0498void ZernikeHistoryWidget::set_series(const std::vector<int> &indexes) {
 0499  const bool unchanged =
 500      series_.size() == indexes.size() &&
 501      std::equal(series_.begin(), series_.end(), indexes.begin(),
 502                 [](const Series &series, int index) { return series.noll_index == index; });
 0503  if (unchanged) {
 0504    return;
 505  }
 506
 0507  series_.clear();
 0508  series_.reserve(indexes.size());
 0509  for (const int index : indexes) {
 0510    series_.push_back({index, SignalHistory(display_settings_.time_window_seconds)});
 0511  }
 512
 0513  waiting_label_->show();
 0514  request_refresh();
 0515}
 516
 0517void ZernikeHistoryWidget::append_samples(std::vector<ZernikeHistorySample> samples) {
 0518  if (!active_) {
 0519    return;
 520  }
 521
 0522  bool appended = false;
 0523  for (const auto &sample : samples) {
 0524    if (auto *series = find_series(sample.noll_index);
 0525        series != nullptr && series->history.append(sample.sample)) {
 0526      update_recorded_range(*series, sample.sample.value);
 0527      appended = true;
 528    }
 0529  }
 530
 0531  if (!appended) {
 0532    return;
 533  }
 534
 0535  waiting_label_->hide();
 0536  request_refresh();
 0537  emit samplesDisplayed();
 0538}
 539
 0540void ZernikeHistoryWidget::show_waiting_placeholder(const QString &message) {
 0541  waiting_label_->setText(message.isEmpty() ? tr("Waiting for data...") : message);
 0542  waiting_label_->show();
 0543  request_refresh();
 0544}
 545
 0546ZernikeHistoryDisplaySettings ZernikeHistoryWidget::display_settings() const {
 0547  return display_settings_;
 0548}
 549
 0550AxisRange ZernikeHistoryWidget::displayed_y_range() const {
 0551  switch (display_settings_.y_scaling_mode) {
 552  case YAxisScalingMode::VisibleWindow: {
 0553    double minimum = std::numeric_limits<double>::infinity();
 0554    double maximum = -std::numeric_limits<double>::infinity();
 0555    for (const auto &series : series_) {
 0556      if (const auto extrema = finite_extrema(series.history.samples())) {
 0557        minimum = std::min(minimum, extrema->first);
 0558        maximum = std::max(maximum, extrema->second);
 559      }
 0560    }
 0561    return std::isfinite(minimum) && std::isfinite(maximum) ? make_display_range(minimum, maximum)
 562                                                            : AxisRange{-1.0, 1.0};
 563  }
 564
 565  case YAxisScalingMode::RecordedExtrema: {
 0566    double minimum = std::numeric_limits<double>::infinity();
 0567    double maximum = -std::numeric_limits<double>::infinity();
 0568    for (const auto &series : series_) {
 0569      if (series.recorded_range) {
 0570        minimum = std::min(minimum, series.recorded_range->minimum);
 0571        maximum = std::max(maximum, series.recorded_range->maximum);
 572      }
 0573    }
 0574    return std::isfinite(minimum) && std::isfinite(maximum) ? make_display_range(minimum, maximum)
 575                                                            : AxisRange{-1.0, 1.0};
 576  }
 577
 578  case YAxisScalingMode::Manual:
 0579    return {display_settings_.manual_y_minimum, display_settings_.manual_y_maximum};
 580  }
 581
 0582  return {-1.0, 1.0};
 0583}
 584
 0585bool ZernikeHistoryWidget::set_display_settings(const ZernikeHistoryDisplaySettings &settings) {
 0586  const bool valid =
 587      std::isfinite(settings.time_window_seconds) && settings.time_window_seconds > 0.0 &&
 588      is_valid_y_axis_scaling_mode(settings.y_scaling_mode) &&
 589      std::isfinite(settings.manual_y_minimum) && std::isfinite(settings.manual_y_maximum) &&
 590      settings.manual_y_minimum < settings.manual_y_maximum;
 0591  if (!valid) {
 0592    return false;
 593  }
 594
 0595  if (settings == display_settings_) {
 0596    return true;
 597  }
 598
 0599  display_settings_ = settings;
 0600  for (auto &series : series_) {
 0601    series.history.set_time_window_seconds(display_settings_.time_window_seconds);
 0602  }
 603
 0604  request_refresh();
 0605  emit display_settings_changed(display_settings_);
 0606  return true;
 0607}
 608
 0609void ZernikeHistoryWidget::set_time_window_seconds(double time_window_seconds) {
 0610  auto settings                = display_settings_;
 0611  settings.time_window_seconds = time_window_seconds;
 0612  set_display_settings(settings);
 0613}
 614
 0615bool ZernikeHistoryWidget::set_manual_y_range(double minimum, double maximum) {
 0616  auto settings             = display_settings_;
 0617  settings.manual_y_minimum = minimum;
 0618  settings.manual_y_maximum = maximum;
 0619  return set_display_settings(settings);
 0620}
 621
 0622void ZernikeHistoryWidget::set_y_axis_scaling_mode(YAxisScalingMode mode) {
 0623  if (mode == display_settings_.y_scaling_mode) {
 0624    return;
 625  }
 626
 0627  auto settings = display_settings_;
 0628  if (mode == YAxisScalingMode::Manual) {
 0629    const AxisRange current_range = displayed_y_range();
 0630    settings.manual_y_minimum     = current_range.minimum;
 0631    settings.manual_y_maximum     = current_range.maximum;
 632  }
 0633  settings.y_scaling_mode = mode;
 0634  set_display_settings(settings);
 0635}
 636
 0637void ZernikeHistoryWidget::reset_recorded_range_state() {
 0638  for (auto &series : series_) {
 0639    series.recorded_range.reset();
 0640  }
 0641  initialize_recorded_ranges();
 0642  request_refresh();
 0643}
 644
 0645void ZernikeHistoryWidget::reset_display_settings() { set_display_settings({}); }
 646
 0647QSize ZernikeHistoryWidget::sizeHint() const { return {520, 300}; }
 648
 0649void ZernikeHistoryWidget::paintEvent(QPaintEvent *) {
 0650  QPainter painter(this);
 0651  painter.setRenderHint(QPainter::Antialiasing, false);
 652
 0653  const bool has_samples = std::ranges::any_of(
 654      series_, [](const Series &series) { return !series.history.samples().empty(); });
 0655  painter.fillRect(rect(), palette().color(has_samples ? QPalette::Base : QPalette::Window));
 0656  if (!has_samples) {
 0657    return;
 658  }
 659
 0660  const QColor text_color = palette().color(QPalette::Text);
 0661  QColor       grid_color = text_color;
 0662  grid_color.setAlpha(45);
 663
 0664  const PlotFonts fonts = make_plot_fonts(painter);
 0665  painter.setPen(text_color);
 0666  painter.setFont(fonts.widget_title);
 0667  painter.drawText(QRectF(0.0, 2.0, width(), 22.0), Qt::AlignCenter, tr("Zernike metrics"));
 668
 0669  const GridLayout layout = make_grid_layout(rect(), static_cast<int>(series_.size()));
 0670  if (!layout.valid()) {
 0671    return;
 672  }
 673
 0674  double newest_time = 0.0;
 0675  for (const auto &series : series_) {
 0676    const auto &samples = series.history.samples();
 0677    if (!samples.empty()) {
 0678      newest_time = std::max(newest_time, samples.back().time_seconds);
 679    }
 0680  }
 681
 0682  const double time_window = display_settings_.time_window_seconds;
 0683  const double oldest_time = newest_time - time_window;
 684
 0685  std::vector<CurveGeometry> curves;
 0686  curves.reserve(series_.size());
 687
 0688  for (size_t i = 0; i < series_.size(); ++i) {
 0689    const QRectF cell = layout.cell(i);
 0690    const QRectF plot = make_plot_rect(cell, display_settings_.show_statistics);
 0691    if (plot.width() <= 1.0 || plot.height() <= 1.0) {
 0692      continue;
 693    }
 694
 0695    const Series   &series  = series_[i];
 0696    const auto     &samples = series.history.samples();
 0697    const AxisRange y_range = displayed_y_range(series);
 698
 0699    QString statistics_text;
 0700    if (display_settings_.show_statistics) {
 0701      if (const auto statistics = series.history.statistics(oldest_time)) {
 0702        statistics_text = tr("Mean %1   SD %2")
 703                              .arg(format_axis_value(statistics->mean),
 704                                   format_axis_value(statistics->standard_deviation));
 705      }
 706    }
 707
 0708    draw_plot_frame(painter, cell, plot, series.noll_index, statistics_text, time_window, y_range,
 709                    text_color, grid_color, fonts, tr("Time (s)"));
 710
 0711    if (samples.empty()) {
 0712      painter.drawText(plot, Qt::AlignCenter, tr("Waiting for data"));
 0713      continue;
 714    }
 715
 0716    curves.push_back(build_curve(samples, plot, y_range, oldest_time, newest_time, time_window,
 717                                 kCurveColors[i % kCurveColors.size()]));
 0718  }
 719
 0720  curve_render_worker_->submit({
 721      .revision           = render_revision_,
 722      .logical_size       = size(),
 723      .device_pixel_ratio = devicePixelRatioF(),
 724      .curves             = std::move(curves),
 725  });
 726
 0727  if (const auto image = curve_render_worker_->latest_image(size())) {
 0728    painter.drawImage(QPointF(0.0, 0.0), *image);
 0729  }
 0730}
 731
 0732void ZernikeHistoryWidget::mousePressEvent(QMouseEvent *event) {
 0733  if (event->button() == Qt::LeftButton) {
 0734    emit configuration_requested(this);
 735  }
 0736  QWidget::mousePressEvent(event);
 0737}
 738
 0739ZernikeHistoryWidget::Series *ZernikeHistoryWidget::find_series(int noll_index) {
 0740  const auto it = std::ranges::find(series_, noll_index, &Series::noll_index);
 0741  return it == series_.end() ? nullptr : &*it;
 0742}
 743
 0744AxisRange ZernikeHistoryWidget::displayed_y_range(const Series &series) const {
 0745  switch (display_settings_.y_scaling_mode) {
 746  case YAxisScalingMode::VisibleWindow:
 0747    if (const auto extrema = finite_extrema(series.history.samples())) {
 0748      return make_display_range(extrema->first, extrema->second);
 749    }
 0750    return {-1.0, 1.0};
 751
 752  case YAxisScalingMode::RecordedExtrema:
 0753    if (series.recorded_range) {
 0754      return make_display_range(series.recorded_range->minimum, series.recorded_range->maximum);
 755    }
 0756    return {-1.0, 1.0};
 757
 758  case YAxisScalingMode::Manual:
 0759    return {display_settings_.manual_y_minimum, display_settings_.manual_y_maximum};
 760  }
 761
 0762  return {-1.0, 1.0};
 0763}
 764
 0765void ZernikeHistoryWidget::update_recorded_range(Series &series, double value) {
 0766  if (!std::isfinite(value)) {
 0767    return;
 768  }
 769
 0770  if (!series.recorded_range) {
 0771    series.recorded_range = AxisRange{value, value};
 0772    return;
 773  }
 774
 0775  series.recorded_range->minimum = std::min(series.recorded_range->minimum, value);
 0776  series.recorded_range->maximum = std::max(series.recorded_range->maximum, value);
 0777}
 778
 0779void ZernikeHistoryWidget::initialize_recorded_ranges() {
 0780  for (auto &series : series_) {
 0781    for (const auto &sample : series.history.samples()) {
 0782      update_recorded_range(series, sample.value);
 0783    }
 0784  }
 0785}
 786
 0787void ZernikeHistoryWidget::request_refresh() {
 0788  ++render_revision_;
 0789  refresh_dirty_ = true;
 790
 0791  if (!refresh_timer_->isActive()) {
 0792    refresh_dirty_ = false;
 0793    update();
 794  }
 0795}
 796
 797} // namespace holovibes::ui

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holovibes\src\ui\widgets\zernike_history_widget.hh

#LineLine coverage
 1// Copyright 2026 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#pragma once
 16
 17#include <QString>
 18#include <QWidget>
 19
 20#include <cstdint>
 21#include <memory>
 22#include <optional>
 23#include <vector>
 24
 25#include "signal_history.hh"
 26
 27class QLabel;
 28class QTimer;
 29
 30namespace holovibes::ui {
 31
 32enum class YAxisScalingMode {
 33  VisibleWindow,
 34  RecordedExtrema,
 35  Manual,
 36};
 37
 38struct AxisRange {
 39  double minimum;
 40  double maximum;
 41};
 42
 43struct ZernikeHistoryDisplaySettings {
 44  double           time_window_seconds = 8.0;
 45  YAxisScalingMode y_scaling_mode      = YAxisScalingMode::VisibleWindow;
 46  double           manual_y_minimum    = -1.0;
 47  double           manual_y_maximum    = 1.0;
 48  bool             show_statistics     = true;
 49
 50  bool operator==(const ZernikeHistoryDisplaySettings &) const = default;
 51};
 52
 53struct ZernikeHistorySample {
 54  int          noll_index;
 55  SignalSample sample;
 56};
 57
 58class ZernikeHistoryWidget : public QWidget {
 059  Q_OBJECT
 60
 61public:
 62  explicit ZernikeHistoryWidget(QWidget *parent = nullptr);
 63  ~ZernikeHistoryWidget() override;
 64
 65  void start_run(double time_window_seconds, const std::vector<int> &indexes);
 66  void resume_run();
 67  void stop_run();
 68
 69  void set_series(const std::vector<int> &indexes);
 70  void append_samples(std::vector<ZernikeHistorySample> samples);
 71  void show_waiting_placeholder(const QString &message = {});
 72
 73  [[nodiscard]] ZernikeHistoryDisplaySettings display_settings() const;
 74  [[nodiscard]] AxisRange                     displayed_y_range() const;
 75
 76  bool set_display_settings(const ZernikeHistoryDisplaySettings &settings);
 77  void set_time_window_seconds(double time_window_seconds);
 78  bool set_manual_y_range(double minimum, double maximum);
 79  void set_y_axis_scaling_mode(YAxisScalingMode mode);
 80  void reset_recorded_range_state();
 81  void reset_display_settings();
 82
 83  [[nodiscard]] QSize sizeHint() const override;
 84
 85signals:
 86  void configuration_requested(holovibes::ui::ZernikeHistoryWidget *widget);
 87  void display_settings_changed(const holovibes::ui::ZernikeHistoryDisplaySettings &settings);
 88  void samplesDisplayed();
 89
 90protected:
 91  void paintEvent(QPaintEvent *event) override;
 92  void mousePressEvent(QMouseEvent *event) override;
 93
 94private:
 95  class CurveRenderWorker;
 96
 97  struct Series {
 98    int                      noll_index;
 99    SignalHistory            history;
 100    std::optional<AxisRange> recorded_range;
 101  };
 102
 103  [[nodiscard]] Series   *find_series(int noll_index);
 104  [[nodiscard]] AxisRange displayed_y_range(const Series &series) const;
 105  void                    update_recorded_range(Series &series, double value);
 106  void                    initialize_recorded_ranges();
 107  void                    request_refresh();
 108
 109  std::vector<Series>           series_;
 110  ZernikeHistoryDisplaySettings display_settings_;
 111
 0112  QTimer *refresh_timer_ = nullptr;
 0113  QLabel *waiting_label_ = nullptr;
 114
 0115  bool     active_          = false;
 0116  bool     refresh_dirty_   = true;
 0117  uint64_t render_revision_ = 0;
 118
 119  std::unique_ptr<CurveRenderWorker> curve_render_worker_;
 120};
 121
 122} // namespace holovibes::ui

Methods/Properties