Files
musrfit/src/musrview.cpp
T
suter_aandClaude Opus 5 3c043af4db musrview: rewrite on top of the ROOT7 web graphics
musrview is now a client/server application following the ROOT7 architecture
(S. Linev et al., J. Phys. Conf. Ser. 1525 (2020) 012092): the C++ side holds
the data, an ROOT::RWebWindow ships it as JSON to a JSROOT page which renders
it. This addresses three long standing problems.

The fit parameter, theory and run info blocks were TPaveText/TLegend objects
which scaled their content down until it became unreadable as soon as a fit had
many parameters. They are HTML panels now: fixed font size, own scrollbar, and
each of them can be shown and hidden individually. The run info keeps listing
every run with the very symbol it is plotted with.

The command keys ('d', 'f', 'a', ...) were executed even while a text or latex
label was being typed into the canvas. The page checks whether a text input has
the focus, so typing a 'd' inserts a 'd'.

The plot views were encoded in a set of entangled variables plus a static one
which leaked state between canvases. PMusrViewMode describes them as four
orthogonal properties - domain, Fourier component, difference including whether
it is formed before or after the transform, and average - so that combinations
such as 'Fourier-difference-average' are well defined. PMusrViewModel::GetCurves()
is the single place which maps a view mode onto histograms; plotting and ascii
export both go through it and can no longer disagree, which removed some 700
lines of duplicated view logic from the export.

New classes, namespace musrview:
  PMusrViewMode   the decoupled view state, free of ROOT and GUI, unit tested
  PMusrViewModel  the GUI-free data layer, physics taken over unchanged
  PMusrWebCanvas  the web front end and the graphics export

Further notes:
- The interactive path builds no TCanvas at all, the spectra travel as a TList
  of drawables. A TCanvas creates and maps its window already in the
  constructor, which would put an empty ROOT canvas next to the musrview window.
- The graphics export keeps the classic pad layout and its 800x600 size and
  runs with ROOT in batch mode, so batch operation needs no browser.
- The window opens at 1200x900, since toolbar and panels take their share.
- Non-muSR axis labels are taken from the data file again, and ROOT/TLatex
  markup is rendered in the HTML panels as well.
- musrview needs a ROOT built with -Droot7=ON -Dwebgui=ON, plus -Dqt6web=ON for
  the embedded window (--embedded, the default). --browser uses the standard
  browser of the system, --browser=server only prints the URL.
- PMusrCanvas is no longer part of the build, the files are kept for reference.

Tests: 87/87 pass, among them a new unit test of the view modes and a new
musrview test covering the non-muSR fit type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H6YiUNBTMDX9uA3Ts1w2nW
2026-09-11 14:42:55 +02:00

531 lines
20 KiB
C++

/***************************************************************************
musrview.cpp
Author: Andreas Suter
e-mail: andreas.suter@psi.ch
***************************************************************************/
/***************************************************************************
* Copyright (C) 2007-2026 by Andreas Suter *
* andreas.suter@psi.ch *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
***************************************************************************/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <chrono>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <TApplication.h>
#include <TROOT.h>
#include <TSAXParser.h>
#include <TSystem.h>
#include <ROOT/RWebWindowsManager.hxx>
#ifdef HAVE_GIT_REV_H
#include "git-revision.h"
#endif
#include "PMusr.h"
#include "PMsgBox.h"
#include "PMsrHandler.h"
#include "PMusrWebCanvas.h"
#include "PRunDataHandler.h"
#include "PRunListCollection.h"
#include "PStartupHandler.h"
//--------------------------------------------------------------------------
/**
* <p>Sends the usage description to the standard output.
*/
void musrview_syntax()
{
std::cout << std::endl << "usage: musrview <msr-file> [Options]";
std::cout << std::endl << " <msr-file>: msr/mlog input file";
std::cout << std::endl << " Options:";
std::cout << std::endl << " --help : display this help and exit.";
std::cout << std::endl << " --version : output version information and exit.";
std::cout << std::endl << " --show-dynamic-path : dumps the dynamic search paths and exit.";
std::cout << std::endl << " -f, --fourier: will directly present the Fourier transform of the <msr-file>.";
std::cout << std::endl << " -a, --avg: will directly present the averaged data/Fourier of the <msr-file>.";
std::cout << std::endl << " -1, --one_to_one: calculate theory points only at data points.";
std::cout << std::endl << " display options (musrview is web based, see the musrfit documentation):";
std::cout << std::endl << " --embedded : show the plots in an embedded browser window, i.e. musrview";
std::cout << std::endl << " behaves like a stand-alone application. This is the default";
std::cout << std::endl << " as long as ROOT provides the Qt6 web display.";
std::cout << std::endl << " --browser : show the plots in the standard web browser of the system.";
std::cout << std::endl << " --browser=<kind> : use a specific display, e.g. chrome, firefox, qt6, server.";
std::cout << std::endl << " 'server' only prints the URL and starts no browser at all,";
std::cout << std::endl << " which is what to use when musrfit runs on a remote machine.";
std::cout << std::endl << " --<graphic-format-extension>: ";
std::cout << std::endl << " will produce a graphics-output-file without opening a window.";
std::cout << std::endl << " the name is based on the <msr-file>, e.g. 3310.msr -> 3310_0.png";
std::cout << std::endl << " supported graphic-format-extension:";
std::cout << std::endl << " eps, pdf, gif, jpg, png, svg, xpm, root";
std::cout << std::endl << " example: musrview 3310.msr --png, will produce a files 3310_X.png";
std::cout << std::endl << " where 'X' stands for the plot number (starting form 0)";
std::cout << std::endl << " --ascii: ";
std::cout << std::endl << " will produce an ascii dump of the data and fit as plotted.";
std::cout << std::endl << " --timeout <timeout>: <timeout> given in seconds after which musrview terminates.";
std::cout << std::endl << " If <timeout> <= 0, no timeout will take place. Default <timeout> is 0.";
std::cout << std::endl << " -s, --show-errMsgBox: if this tag is defined, error message boxes are shown,";
std::cout << std::endl << " rather than only stderr output.";
std::cout << std::endl << std::endl;
}
//--------------------------------------------------------------------------
/**
* <p>Shows an error message in a message box.
*
* \param errMsg the message to be shown
*/
void musrview_error_msg(std::string errMsg)
{
int argc=0;
char **argv=nullptr;
TApplication app("musrviewErrorMsg", &argc, argv);
new PMsgBox(errMsg, gClient->GetRoot(), 600, 200);
app.Run();
}
//--------------------------------------------------------------------------
/**
* <p>Decides which web display musrview should use.
*
* <p>Unless the user asked for something specific, the embedded Qt6 WebEngine
* window is preferred, since it makes musrview appear as the stand-alone
* application it has always been. If ROOT was built without the Qt6 web
* display, the standard browser of the system is used instead.
*
* \param requested what the user asked for, empty if nothing was requested
*
* \return the display string to be handed to ROOT::RWebWindow::Show()
*/
std::string musrview_select_display(const std::string &requested)
{
if (!requested.empty())
return requested;
if (const char *env = gSystem->Getenv("MUSRVIEW_BROWSER"))
if (strlen(env) > 0)
return std::string(env);
// prefer the embedded window if ROOT provides it
gSystem->RedirectOutput("/dev/null", "a");
const Int_t loaded = gSystem->Load("libROOTQt6WebDisplay");
gSystem->RedirectOutput(nullptr);
if (loaded >= 0)
return "qt6";
std::cout << std::endl << ">> musrview **INFO** ROOT has no Qt6 web display, using the standard browser.";
std::cout << std::endl << ">> Rebuild ROOT with -Dqt6web=ON for an embedded window." << std::endl;
return std::string();
}
//--------------------------------------------------------------------------
/**
* <p>The musrview program is used to show muSR fit results in graphical form.
* From it also Fourier transforms, difference between data an theory can be formed.
*
* <p>Since version 1.13 musrview is based on the ROOT7 web graphics: the plots
* are rendered by JSROOT either in an embedded browser window or in the
* standard browser of the system, while the C++ side only holds the data.
*
* <b>return:</b>
* - PMUSR_SUCCESS if everthing went smooth
* - PMUSR_WRONG_STARTUP_SYNTAX if syntax error is encountered
* - line number if an error in the msr-file was encountered which cannot be handled.
*
* \param argc number of input arguments
* \param argv list of input arguments
*/
int main(int argc, char *argv[])
{
int result{PMUSR_SUCCESS};
bool show_syntax{false};
int status;
bool success{true};
std::string fileName{};
bool fourier{false};
bool avg{false};
bool theoAtData{false}; // theory points only at data points
bool graphicsOutput{false};
bool asciiOutput{false};
std::string graphicsExtension{};
std::string display{};
int timeout{0};
bool show_errMsgBox{false};
std::stringstream errMsg;
// add default shared library path /usr/local/lib if not already persent
const char *dsp = gSystem->GetDynamicPath();
if (strstr(dsp, "/usr/local/lib") == nullptr)
gSystem->AddDynamicPath("/usr/local/lib");
// check input arguments
if (argc == 1) {
musrview_syntax();
return PMUSR_SYNTAX_REQUEST;
}
for (int i=1; i<argc; i++) {
if (strstr(argv[i], ".msr") || strstr(argv[i], ".mlog")) {
if (fileName.empty()) {
fileName = argv[i];
} else {
std::cerr << std::endl << "**ERROR** only one file name allowed." << std::endl;
show_syntax = true;
break;
}
} else if (!strcmp(argv[i], "--version")) {
#ifdef HAVE_CONFIG_H
#ifdef HAVE_GIT_REV_H
std::cout << std::endl << "musrview version: " << PACKAGE_VERSION << ", git-branch: " << GIT_BRANCH << ", git-rev: " << GIT_CURRENT_SHA1 << " (" << BUILD_TYPE << "), ROOT version: " << ROOT_VERSION_USED << std::endl << std::endl;
#else
std::cout << std::endl << "musrview version: " << PACKAGE_VERSION << " (" << BUILD_TYPE << "), ROOT version: " << ROOT_VERSION_USED << std::endl << std::endl;
#endif
#else
#ifdef HAVE_GIT_REV_H
std::cout << std::endl << "musrview git-branch: " << GIT_BRANCH << ", git-rev: " << GIT_CURRENT_SHA1 << std::endl << std::endl;
#else
std::cout << std::endl << "musrview version: unknown." << std::endl << std::endl;
#endif
#endif
return PMUSR_SUCCESS;
} else if (!strcmp(argv[i], "--show-dynamic-path")) {
std::cout << std::endl << "musrview: internal dynamic search paths for shared libraries/root dictionaries:";
std::cout << std::endl << " '" << gSystem->GetDynamicPath() << "'" << std::endl << std::endl;
return PMUSR_SUCCESS;
} else if (!strcmp(argv[i], "--help")) {
show_syntax = true;
break;
} else if (!strcmp(argv[i], "-f") || !strcmp(argv[i], "--fourier")) {
fourier = true;
} else if (!strcmp(argv[i], "-a") || !strcmp(argv[i], "--avg")) {
avg = true;
} else if (!strcmp(argv[i], "-1") || !strcmp(argv[i], "--one_to_one")) {
theoAtData = true;
} else if (!strcmp(argv[i], "--embedded")) {
display = "qt6";
} else if (!strcmp(argv[i], "--browser")) {
display = "default";
} else if (!strncmp(argv[i], "--browser=", 10)) {
display = argv[i]+10;
if (display.empty()) {
show_syntax = true;
break;
}
} else if (!strcmp(argv[i], "--eps") || !strcmp(argv[i], "--pdf") || !strcmp(argv[i], "--gif") ||
!strcmp(argv[i], "--jpg") || !strcmp(argv[i], "--png") || !strcmp(argv[i], "--svg") ||
!strcmp(argv[i], "--xpm") || !strcmp(argv[i], "--root")) {
graphicsOutput = true;
graphicsExtension = argv[i]+2;
} else if (!strcmp(argv[i], "--ascii")) {
asciiOutput = true;
} else if (!strcmp(argv[i], "--timeout")) {
if (i+1 < argc) {
TString str(argv[i+1]);
if (str.IsFloat()) {
timeout = str.Atoi();
} else {
show_syntax = true;
break;
}
i++;
} else {
show_syntax = true;
break;
}
} else if (!strcmp(argv[i], "-s") || !strcmp(argv[i], "--show-errMsgBox")) {
show_errMsgBox = true;
} else {
show_syntax = true;
break;
}
}
if (show_syntax) {
musrview_syntax();
return PMUSR_WRONG_STARTUP_SYNTAX;
}
const bool batchMode = graphicsOutput || asciiOutput;
// read startup file
std::string startup_path_name{};
std::unique_ptr<TSAXParser> saxParser = std::make_unique<TSAXParser>();
std::unique_ptr<PStartupHandler> startupHandler = std::make_unique<PStartupHandler>();
if (!startupHandler->StartupFileFound()) {
std::cerr << std::endl << ">> musrview **WARNING** couldn't find " << startupHandler->GetStartupFilePath().Data();
std::cerr << std::endl;
} else {
startup_path_name = startupHandler->GetStartupFilePath().Data();
saxParser->ConnectToHandler("PStartupHandler", startupHandler.get());
// parsing the file via ParseFile() seems to lead to problems in certain
// environments; use parseXmlFile (see PStartupHandler.cpp) instead
status = parseXmlFile(saxParser.get(), startup_path_name.c_str());
if (status) { // error
std::cerr << std::endl << ">> musrview **WARNING** Reading/parsing musrfit_startup.xml failed.";
std::cerr << std::endl << ">> Any graph will appear with random symbols and colors!";
std::cerr << std::endl;
} else {
startupHandler->CheckLists();
}
}
// read msr-file
std::unique_ptr<PMsrHandler> msrHandler = std::make_unique<PMsrHandler>(fileName.c_str());
status = msrHandler->ReadMsrFile();
if (status != PMUSR_SUCCESS) {
errMsg << msrHandler->GetLastErrorMsg();
switch (status) {
case PMUSR_MSR_FILE_NOT_FOUND:
errMsg << "\n";
errMsg << ">> musrview **ERROR** couldn't find '" << fileName << "'\n\n";
break;
case PMUSR_MSR_SYNTAX_ERROR:
errMsg << "\n";
errMsg << ">> musrview **SYNTAX ERROR** in file " << fileName << ", full stop here.\n\n";
break;
default:
errMsg << "\n";
errMsg << ">> musrview **UNKNOWN ERROR** when trying to read the msr-file.\n\n";
break;
}
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
return status;
}
// make a plot list vector
PMsrPlotList *msrPlotList = msrHandler->GetMsrPlotList();
PIntVector plotList;
bool runPresent;
for (unsigned int i=0; i<msrPlotList->size(); i++) {
for (unsigned int j=0; j<msrPlotList->at(i).fRuns.size(); j++) {
runPresent = false;
for (unsigned int k=0; k<plotList.size(); k++) {
if (msrPlotList->at(i).fRuns[j] == static_cast<int>(plotList[k])) {
runPresent = true;
break;
}
}
if (!runPresent)
plotList.push_back(static_cast<int>(msrPlotList->at(i).fRuns[j]));
}
}
// read all the necessary runs (raw data)
std::unique_ptr<PRunDataHandler> dataHandler;
if (startupHandler)
dataHandler = std::make_unique<PRunDataHandler>(msrHandler.get(), startupHandler->GetDataPathList());
else
dataHandler = std::make_unique<PRunDataHandler>(msrHandler.get());
dataHandler->ReadData();
success = dataHandler->IsAllDataAvailable();
if (!success) {
errMsg << ">> musrview **ERROR** Couldn't read all data files, will quit ...\n";
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
result = PMUSR_DATA_FILE_READ_ERROR;
}
// generate the necessary histogramms for the view
std::unique_ptr<PRunListCollection> runListCollection;
if (result == PMUSR_SUCCESS) {
runListCollection = std::make_unique<PRunListCollection>(msrHandler.get(), dataHandler.get(), theoAtData);
for (unsigned int i=0; i<msrHandler->GetMsrRunList()->size(); i++) {
runPresent = false;
for (unsigned int j=0; j<plotList.size(); j++) {
if (static_cast<unsigned int>(plotList[j]) == i+1) {
runPresent = true;
break;
}
}
if (runPresent) {
success = runListCollection->Add(i, kView);
if (!success) {
errMsg << ">> musrview **ERROR** Couldn't handle run no " << i << " ";
errMsg << (*msrHandler->GetMsrRunList())[i].GetRunName()->Data() << "\n";
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
result = PMUSR_MSR_RUN_ERROR;
break;
}
}
}
}
if (result != PMUSR_SUCCESS) {
plotList.clear();
return result;
}
// A ROOT application is needed so that the web display and the graphics
// export find a working environment.
//
// In the interactive case no TCanvas is created at all - the spectra travel
// to JSROOT as a list of drawables - hence ROOT never opens a window of its
// own. Only the graphics export builds a TCanvas, and that runs in batch.
std::vector<char*> args;
args.push_back(argv[0]);
if (batchMode)
args.push_back(const_cast<char*>("-b"));
int cc = static_cast<int>(args.size());
TApplication app("musrview", &cc, args.data());
std::vector<std::unique_ptr<musrview::PMusrWebCanvas>> canvasList;
bool ok = true;
for (unsigned int i=0; i<msrHandler->GetMsrPlotList()->size(); i++) {
auto canvas = std::make_unique<musrview::PMusrWebCanvas>(
static_cast<Int_t>(i), msrHandler->GetMsrTitle()->Data(),
1200, 900,
startupHandler ? startupHandler->GetFourierDefaults() : PMsrFourierStructure{},
startupHandler ? startupHandler->GetMarkerList() : PIntVector{},
startupHandler ? startupHandler->GetColorList() : PIntVector{},
batchMode, fourier, avg, theoAtData);
// ugly but rootcling cannot handle the spirit-parser framework
canvas->SetMsrHandler(msrHandler.get());
canvas->SetRunListCollection(runListCollection.get());
if (!canvas->Build()) {
errMsg << ">> musrview **ERROR** " << canvas->GetLastError() << "\n";
std::cerr << errMsg.str();
if (show_errMsgBox)
musrview_error_msg(errMsg.str());
ok = false;
result = PMUSR_MSR_RUN_ERROR;
break;
}
canvas->SetTimeout(timeout);
canvasList.push_back(std::move(canvas));
}
if (!ok) {
canvasList.clear();
plotList.clear();
return result;
}
if (graphicsOutput) {
for (auto &canvas : canvasList) {
if (!canvas->SaveGraphics(fileName, graphicsExtension)) {
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
result = PMUSR_MSR_RUN_ERROR;
}
}
}
if (asciiOutput) {
// generate the export data file name
TString str(fileName.c_str());
str.Remove(str.Last('.'));
for (size_t i=0; i<canvasList.size(); i++) {
TString name(str);
if (canvasList.size() > 1) {
name += "_";
name += static_cast<Int_t>(i);
}
name += ".dat";
if (!canvasList[i]->ExportData(name.Data())) {
std::cerr << std::endl << ">> musrview **ERROR** " << canvasList[i]->GetLastError() << std::endl;
result = PMUSR_MSR_RUN_ERROR;
}
}
}
if (!batchMode) {
const std::string where = musrview_select_display(display);
// one window per PLOT block, exactly as before
ROOT::RWebWindowsManager::SetSingleConnMode(false);
for (auto &canvas : canvasList) {
if (!canvas->Show(where)) {
std::cerr << std::endl << ">> musrview **ERROR** " << canvas->GetLastError() << std::endl;
result = PMUSR_MSR_RUN_ERROR;
ok = false;
break;
}
}
if (ok) {
// run until every window has been closed, or until the timeout expired
const auto start = std::chrono::steady_clock::now();
while (true) {
gSystem->ProcessEvents();
gSystem->Sleep(20);
bool allDone = true;
for (auto &canvas : canvasList) {
if (!canvas->IsDone()) {
allDone = false;
break;
}
}
if (allDone)
break;
if (timeout > 0) {
const auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now-start).count() >= timeout) {
std::cout << std::endl << ">> musrview: timeout of " << timeout << " s reached, terminating." << std::endl;
break;
}
}
}
}
// End the process here. Tearing down an RWebWindow which drives the
// embedded display blocks, because the Qt event loop ROOT started on our
// behalf is never stopped again. Everything of interest has been written by
// now, so leave immediately rather than hang after the last window closed.
std::cout.flush();
std::cerr.flush();
gSystem->Exit(result, kFALSE);
}
// only reached in batch mode, where no window was ever opened
canvasList.clear();
plotList.clear();
return result;
}