mupp: optional Python3 interface for variables via ROOT TPython.

Variables can now be computed by an embedded Python3 interpreter in addition
to the Boost.Spirit X3 expression engine. A <python> ... </python> block
receives all collection parameters as per-run lists (bare names, plus
par[]/parErr[] dictionaries as a fallback for names that are not valid Python
identifiers such as 'lambda') and must assign <name> and <name>Err; errors are
user supplied. Works both in the GUI variable dialog and in scripts.

The feature is optional and only enabled when ROOT is built with TPython
(CMake target ROOT::ROOTTPython); otherwise it compiles out. See MUPP_PY.README
for ROOT configure requirements and usage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 19:06:07 +02:00
parent 64f4e1d2dd
commit 9d859950d2
9 changed files with 671 additions and 10 deletions
+11
View File
@@ -87,6 +87,17 @@ target_include_directories(mupp
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/var/include>
)
#--- optional Python support for variable evaluation via ROOT's TPython -------
#--- only available if ROOT was built with -Dtpython=ON -----------------------
if (TARGET ROOT::ROOTTPython)
target_compile_definitions(mupp PRIVATE MUPP_PYTHON)
target_link_libraries(mupp PRIVATE ROOT::ROOTTPython)
target_include_directories(mupp PRIVATE $<BUILD_INTERFACE:${ROOT_INCLUDE_DIRS}>)
message(STATUS "mupp: optional Python variable support enabled (ROOT TPython found)")
else (TARGET ROOT::ROOTTPython)
message(STATUS "mupp: optional Python variable support disabled (ROOT built without tpython)")
endif (TARGET ROOT::ROOTTPython)
#--- use the Widgets and XML modules from Qt5 ---------------------------------
target_link_libraries(mupp PRIVATE Qt6::Widgets Qt6::Xml)
+200
View File
@@ -0,0 +1,200 @@
================================================================================
mupp - optional Python3 interface for variables
================================================================================
mupp lets you define "variables": derived quantities computed from the fit
parameters of a collection (one value per run). Besides the built-in expression
engine (Boost.Spirit X3, addressed with '$param'), a variable can optionally be
computed by an embedded Python3 interpreter. This is useful when the calculation
is more naturally done with numpy/scipy or any other Python code.
The Python evaluation runs in-process through ROOT's TPython, so it is only
available if ROOT was built with TPython support (see "ROOT requirements").
This document applies to the Qt6 build of mupp (src/musredit_qt6/mupp).
--------------------------------------------------------------------------------
1. ROOT requirements (configure / build of ROOT)
--------------------------------------------------------------------------------
The Python interface uses ROOT's TPython class (C++ -> Python). Your ROOT
installation must therefore be built with both PyROOT and TPython enabled.
a) Install the Python3 development headers (CPython dev package). They are
required for ROOT to detect Python at configure time. On Fedora:
sudo dnf install python3-devel
If they are missing, ROOT's configure step fails with:
Could NOT find Python3 (missing: Python3_INCLUDE_DIRS Development.Module)
b) Configure ROOT with PyROOT *and* TPython enabled, pointing at the desired
interpreter:
cmake -S <root-source> -B <root-build> \
-Dpyroot=ON \
-Dtpython=ON \
-DPython3_EXECUTABLE=$(which python3) \
<your other options>
NOTE: a minimal ROOT build (-Dgminimal=ON) turns optional components OFF by
default, including tpython. In that case you MUST add -Dtpython=ON
explicitly, otherwise only PyROOT (Python -> ROOT) is built and the C++ ->
Python direction that mupp needs is missing.
If you reconfigure an existing ROOT build directory and the flags do not
seem to take effect, delete <root-build>/CMakeCache.txt and configure again.
c) Build / install ROOT, then verify that TPython is present:
ls $(root-config --incdir)/TPython.h # header present
ls $(root-config --libdir)/libROOTTPython.* # library present
(Note: "root-config --features" does not necessarily list "python"; the two
checks above are the reliable test.)
d) The Python interpreter that ROOT was built against must contain whatever
modules your variable code uses, e.g. numpy:
python3 -c "import numpy; print(numpy.__version__)"
--------------------------------------------------------------------------------
2. Building mupp
--------------------------------------------------------------------------------
mupp's CMake detects TPython automatically via the imported target
ROOT::ROOTTPython:
* If available, the macro MUPP_PYTHON is defined, mupp links ROOT::ROOTTPython,
and you will see during configure:
-- mupp: optional Python variable support enabled (ROOT TPython found)
* If ROOT was built without tpython, the feature is compiled out and you see:
-- mupp: optional Python variable support disabled (ROOT built without tpython)
In that case <python> ... </python> definitions are rejected at runtime with
a clear error message; the X3 expression engine is unaffected.
After rebuilding ROOT with TPython, reconfigure the mupp/musrfit build so the
new ROOT::ROOTTPython target is picked up (delete CMakeCache.txt if necessary).
--------------------------------------------------------------------------------
3. Runtime requirements
--------------------------------------------------------------------------------
* No environment setup is required for the Python modules: mupp prepends ROOT's
library directory (TROOT::GetLibDir()) to PYTHONPATH before the interpreter is
initialized, so PyROOT/cppyy import correctly even if thisroot.sh has not been
sourced.
* Errors from the Python evaluation (syntax errors, undefined names, wrong
result length, ...) are written to:
~/.musrfit/mupp/mupp_err.log
--------------------------------------------------------------------------------
4. The data contract (what Python receives and must return)
--------------------------------------------------------------------------------
A variable maps the parameters of a collection to one value (and one error) per
run.
INPUT - injected into the Python namespace before your code runs:
* For every collection parameter <p>:
<p> : a list with one value per run
<p>Err : a list with the corresponding errors (one per run)
The error is the geometric mean of the asymmetric fit errors,
sqrt(|posErr * negErr|), i.e. the same convention as the X3 engine.
* Parameter names that are NOT valid Python identifiers - most importantly the
Python keyword 'lambda' - are NOT available as bare names. Reach them through
the dictionaries that always contain every parameter:
par['lambda'] # value list
parErr['lambda'] # error list
OUTPUT - your code must assign, for a variable named <name>:
<name> : the value list (length = number of runs)
<name>Err : the error list (length = number of runs)
* Errors are USER-SUPPLIED: there is no automatic error propagation in Python
mode (unlike the X3 engine). You compute the error yourself.
* The value list length must equal the number of runs in the collection.
* Lists or numpy arrays are both accepted.
--------------------------------------------------------------------------------
5. Usage in the GUI
--------------------------------------------------------------------------------
Open "Add Variable", declare each variable (and its error) with "= python", and
provide the calculation inside a <python> ... </python> block:
var sigSC = python
var sigSCErr = python
<python>
import numpy as np
s = np.array(Sigma)
se = np.array(SigmaErr)
sigSC = np.sqrt(np.abs(s**2 - 0.11**2))
sigSCErr = np.where(sigSC > 0.0, s*se/np.where(sigSC > 0.0, sigSC, 1.0), 0.0)
</python>
Then select the variable as x- or y-axis and plot as usual.
--------------------------------------------------------------------------------
6. Usage in a script (mupp -s <script>)
--------------------------------------------------------------------------------
Declare the variable(s) with "= python", link them to a collection with "col",
and add the <python> ... </python> block anywhere in the script:
loadPath $HOME/Apps/musrfit/doc/examples/mupp
load YBCO-40nm-FC-E3p8keV-B10mT-Tscan.db
var sigSC = python
var sigSCErr = python
col 0 : sigSC
<python>
import numpy as np
s = np.array(Sigma)
se = np.array(SigmaErr)
sigSC = np.sqrt(np.abs(s**2 - 0.11**2))
sigSCErr = np.where(sigSC > 0.0, s*se/np.where(sigSC > 0.0, sigSC, 1.0), 0.0)
</python>
select 0
x dataT
y sigSC
plot sigSC-vs-temp.pdf
Notes for scripts:
* The <python> ... </python> block is read VERBATIM: indentation and the
characters '#', '%', '//' inside the block are preserved (the normal mupp
comment handling is bypassed for the block).
* The closing tag </python> must be on its own line.
* Only ONE <python> block per script is supported; it may define several
variables.
* As with X3 variables, only the value variable is linked with 'col'; its error
variable (<name>Err) is associated implicitly.
* Tip: loadPath/savePath strip a leading '/'; use $HOME/... or a relative path.
--------------------------------------------------------------------------------
7. Limitations
--------------------------------------------------------------------------------
* Requires ROOT built with -Dtpython=ON (see section 1).
* No automatic error propagation in Python mode - errors are user-supplied.
* One <python> block per variable definition / per script.
================================================================================
+31 -2
View File
@@ -262,7 +262,24 @@ void PVarDialog::help()
"An identifier is an addressed variable which is defined\n"\
"by a preceeding '$' before the variable name.\n"\
"Example: variable sigma -> identifier $sigma.\n"\
"Example:\nvar sigSC = pow(abs(pow($sigma,2.0)-pow(0.11,2.0)),0.5)");
"Example:\nvar sigSC = pow(abs(pow($sigma,2.0)-pow(0.11,2.0)),0.5)\n"\
"\n"\
"Python mode (optional):\n"\
"Declare the variables with '= python', then provide the\n"\
"calculation inside a <python> ... </python> block.\n"\
"Collection parameters are available as bare-name lists (one\n"\
"value per run); the error of a parameter 'p' is 'pErr'. Names\n"\
"that clash with Python (e.g. 'lambda') are reachable via\n"\
"par['lambda'] / parErr['lambda']. The script must assign the\n"\
"variable and its error (<var_name>Err). Example:\n"\
"var sigSC = python\n"\
"var sigSCErr = python\n"\
"<python>\n"\
"import numpy as np\n"\
"T = np.array(sigma)\n"\
"sigSC = np.sqrt(np.abs(T**2 - 0.11**2))\n"\
"sigSCErr = T/np.where(sigSC==0,1,sigSC) * np.array(sigmaErr)\n"\
"</python>");
}
//--------------------------------------------------------------------------
@@ -325,8 +342,15 @@ bool PVarDialog::basic_check()
return false;
}
// for a <python> block only the part before the block carries mupp 'var'
// declarations; the python code itself must not be tokenized as mupp syntax.
QString declStr = varStr;
int pyIdx = varStr.indexOf("<python>");
if (pyIdx != -1)
declStr = varStr.left(pyIdx);
// tokenize variable input
QStringList strList = varStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
QStringList strList = declStr.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
// check if there are ANY var definitions
ok = false;
@@ -374,6 +398,11 @@ bool PVarDialog::var_consistency_check()
{
QString varStr = fVarEdit->toPlainText();
// a python block uses bare parameter names (no '$' identifiers); validity of
// those references can only be checked when the script is executed.
if (varStr.contains("<python>"))
return true;
// collect all identifiers
int idx = 0, idxEnd = 0;
QStringList varNames;
+6
View File
@@ -1536,6 +1536,12 @@ QStringList PmuppGui::getVarNames(QString parseStr)
QString str;
int idxS=0, idxE=0;
// a <python> block carries the mupp 'var' declarations only in the part
// before the block; the python code must not be scanned for 'var'.
int pyIdx = parseStr.indexOf("<python>");
if (pyIdx != -1)
parseStr = parseStr.left(pyIdx);
while (idxS != -1) {
idxS = parseStr.indexOf("var", idxS);
if (idxS != -1) {
+53 -8
View File
@@ -170,6 +170,8 @@ int PmuppScript::executeScript()
status = var_cmd(cmd);
} else if (cmd.startsWith("col")) {
// nothing to be done here, since var handles it internally
} else if (cmd.startsWith("<python>")) {
// the python block is consumed by var_cmd of the linked variable
} else {
std::cerr << "**ERROR** found unkown script command '" << cmd.toLatin1().constData() << "'." << std::endl << std::endl;
status = -2;
@@ -1020,6 +1022,35 @@ int PmuppScript::var_cmd(const QString str)
if (idx == -1) // var not linked to collection, ignore it
return 0;
// python-mode variable: 'var <name> = python' means the value/error are
// computed in a <python> ... </python> block elsewhere in the script.
if (tok.last() == "python") {
QString pyBlock = getPythonBlock();
if (pyBlock.isEmpty()) {
std::cerr << "**ERROR** variable '" << tok[1].toLatin1().data()
<< "' declared as python, but no <python> ... </python> block was found." << std::endl;
return 1;
}
PVarHandler varHandler(fParamDataHandler->GetCollection(idx),
pyBlock.toLatin1().data(), tok[1].toLatin1().data());
if (!varHandler.isValid()) {
// dump error messages if present
QString mupp_err = QString("%1/.musrfit/mupp/mupp_err.log").arg(QString(qgetenv("HOME")));
QFile fout(mupp_err);
if (fout.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString msg;
while (!fout.atEnd()) {
msg = fout.readLine();
std::cerr << msg.toLatin1().data();
}
QFile::remove(mupp_err);
}
return 1;
}
fVarHandler.push_back(varHandler);
return 0;
}
// check if the related error variable is present
QString varErr = QString("%1%2").arg(tok[1]).arg("Err");
QString varErrCmd("");
@@ -1230,18 +1261,32 @@ QString PmuppScript::getNicerLabel(const QString label)
//--------------------------------------------------------------------------
/**
* @brief Gets the collection index associated with a variable.
* @brief Returns the verbatim <python> ... </python> block of the script.
*
* Searches the script for a "col" command linking the variable to a
* collection. The col command format is:
* "col <collection_idx> -> <variable_name>"
* The script reader stores a python block as a single list entry. Only a
* single block is supported; it may define several variables.
*
* This association is necessary for variable evaluation, as the
* variable needs to know which collection's parameters to use.
* @return the python block (including the tags), or an empty string if none
*/
QString PmuppScript::getPythonBlock()
{
for (int i=0; i<fScript.size(); i++) {
if (fScript.at(i).startsWith("<python>"))
return fScript.at(i);
}
return QString();
}
//--------------------------------------------------------------------------
/**
* @brief Gets the collection index a variable is linked to.
*
* @param var_name variable name to search for
* Scans the script for the 'col' command linking the given variable name to a
* collection and returns that collection index.
*
* @return collection index (0-based), or -1 if not found
* @param var_name the variable name to look up
*
* @return the linked collection index, or -1 if not linked
*/
int PmuppScript::getCollectionIndex(const QString var_name)
{
+6
View File
@@ -281,6 +281,12 @@ class PmuppScript : public QObject
* @return collection index, or -1 if not found
*/
int getCollectionIndex(const QString var_name);
/**
* @brief Returns the verbatim <python> ... </python> block of the script.
* @return the python block (including tags), or an empty string if none present
*/
QString getPythonBlock();
};
#endif // _PMUPPSCRIPT_H_
+37
View File
@@ -107,6 +107,12 @@ void mupp_script_syntax()
std::cout << " <expr> is a mathematical expression where" << std::endl;
std::cout << " collection variables are addressed via the '$'," << std::endl;
std::cout << " e.g. dataT is addressed by $dataT, etc." << std::endl;
std::cout << " var <var_name> = python : the variable is computed in a python block" << std::endl;
std::cout << " <python> ... </python> given later in the script." << std::endl;
std::cout << " Inside the block, collection parameters are bare" << std::endl;
std::cout << " names (e.g. Sigma, SigmaErr); the block must assign" << std::endl;
std::cout << " <var_name> and <var_name>Err. Requires ROOT built" << std::endl;
std::cout << " with -Dtpython=ON." << std::endl;
std::cout << " col <nn> : <var_name> : links <var_name> to the collection <nn>, where" << std::endl;
std::cout << " <nn> is the number of the collection as defined" << std::endl;
std::cout << " by the order of load, starting with 0." << std::endl;
@@ -175,8 +181,29 @@ int mupp_script_read(const char *fln, QStringList &list)
QTextStream fin(&file);
QString line;
int pos;
bool inPython = false; // true while reading a <python> ... </python> block
QString pyBlock;
while (!fin.atEnd()) {
line = fin.readLine();
// a <python> ... </python> block is read verbatim (indentation and the
// characters #, %, // are significant in python) and stored as a single
// list entry, so that the normal command processing below is bypassed.
if (!inPython && line.trimmed().startsWith("<python>")) {
inPython = true;
pyBlock = line + "\n";
continue;
}
if (inPython) {
pyBlock += line + "\n";
if (line.trimmed() == "</python>") {
list << pyBlock;
pyBlock.clear();
inPython = false;
}
continue;
}
line = line.simplified();
if (line.isEmpty())
continue;
@@ -196,6 +223,13 @@ int mupp_script_read(const char *fln, QStringList &list)
// feed script list
list << line;
}
if (inPython) {
std::cerr << std::endl;
std::cerr << "*********" << std::endl;
std::cerr << "**ERROR** <python> block not terminated with </python> in '" << fln << "'." << std::endl;
std::cerr << "*********" << std::endl;
return -1;
}
return 0;
}
@@ -472,6 +506,9 @@ int mupp_script_syntax_check(QStringList &list)
return -5;
}
var_linked[idx] = true;
} else if (str.startsWith("<python>")) {
// a python block is evaluated through the linked 'var ... = python'
// declarations; nothing to validate here.
} else {
std::cerr << "*********" << std::endl;
std::cerr << "**ERROR** found unrecognized script command: '" << str.toLatin1().constData() << "'." << std::endl;
@@ -137,6 +137,20 @@ class PVarHandler {
*/
void injectPredefVariables();
/**
* @brief Evaluates the variable using an embedded Python3 interpreter (TPython).
*
* Used when the input string contains a <python> ... </python> block.
* All collection parameters are injected into the interpreter as bare-name lists
* (one value per run) plus par[]/parErr[] dictionaries as a fallback for names
* that are not valid Python identifiers (e.g. the keyword 'lambda'). The user
* script must assign the requested variable and its error counterpart (<name>Err).
* Both value and error arrays are read back and stored in fVar.
*
* If mupp was built without TPython support, the handler is marked invalid.
*/
void evaluatePython();
/**
* @brief Gets the variable name at a specific parameter index.
* @param idx the parameter index in the collection
@@ -28,6 +28,9 @@
***************************************************************************/
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <string>
#include "PVarHandler.h"
@@ -39,6 +42,187 @@
#include <boost/spirit/home/x3.hpp>
#ifdef MUPP_PYTHON
#include <cmath>
#include <cstring>
#include <cctype>
#include <set>
#include <sstream>
#include <iomanip>
#include <TROOT.h>
#include <TSystem.h>
#include <TPython.h>
#endif // MUPP_PYTHON
//--------------------------------------------------------------------------
/**
* @brief Appends an error message to ~/.musrfit/mupp/mupp_err.log and stderr.
*
* The mupp callers (GUI and script) read this log on failure, so the Python
* path writes to the same place as the Spirit X3 path.
*
* @param msg the message to log
*/
static void mupp_writeErrLog(const std::string &msg)
{
const char *home = std::getenv("HOME");
std::string path = std::string(home ? home : ".") + "/.musrfit/mupp/mupp_err.log";
std::ofstream fout(path.c_str(), std::ios::app);
if (fout.is_open()) {
fout << msg << std::endl;
fout.close();
}
std::cerr << msg << std::endl;
}
#ifdef MUPP_PYTHON
//--------------------------------------------------------------------------
/**
* @brief Formats a double as a Python-literal-safe token (handles nan/inf).
*/
static std::string mupp_pyNum(double d)
{
if (std::isnan(d))
return "float('nan')";
if (std::isinf(d))
return (d < 0) ? "float('-inf')" : "float('inf')";
std::ostringstream os;
os << std::setprecision(17) << d;
return os.str();
}
//--------------------------------------------------------------------------
/**
* @brief Builds a Python list literal from a vector of doubles.
*/
static std::string mupp_pyList(const std::vector<double> &v)
{
std::ostringstream os;
os << "[";
for (size_t i=0; i<v.size(); i++) {
if (i)
os << ", ";
os << mupp_pyNum(v[i]);
}
os << "]";
return os.str();
}
//--------------------------------------------------------------------------
/**
* @brief Checks whether name is a valid Python identifier and not a keyword.
*
* Such names can be injected as bare variables; everything else is only made
* available through the par[]/parErr[] dictionaries.
*/
static bool mupp_isSafePyName(const std::string &s)
{
if (s.empty())
return false;
if (!(std::isalpha((unsigned char)s[0]) || s[0] == '_'))
return false;
for (size_t i=0; i<s.size(); i++) {
if (!(std::isalnum((unsigned char)s[i]) || s[i] == '_'))
return false;
}
static const std::set<std::string> kw = {
"False","None","True","and","as","assert","async","await","break","class",
"continue","def","del","elif","else","except","finally","for","from","global",
"if","import","in","is","lambda","nonlocal","not","or","pass","raise","return",
"try","while","with","yield"
};
return kw.find(s) == kw.end();
}
//--------------------------------------------------------------------------
/**
* @brief Indents every line of code with the given prefix (for try-block wrapping).
*
* Guarantees at least a 'pass' statement so an empty/comment-only block is valid.
*/
static std::string mupp_indentLines(const std::string &code, const std::string &prefix)
{
std::ostringstream os;
std::istringstream is(code);
std::string line;
while (std::getline(is, line))
os << prefix << line << "\n";
return os.str();
}
//--------------------------------------------------------------------------
/**
* @brief Retrieves a Python string expression into a std::string.
*
* ROOT 6.40 dropped TPython::Eval, so values are passed back via the
* Exec(cmd, std::any*) mechanism using ROOT.std.make_any. The python str is
* converted to std::string by cppyy. Requires 'import ROOT' to have run.
*
* @param pyExpr a python expression evaluating to something string-convertible
* @param out receives the resulting string on success
* @return true on success
*/
static bool mupp_getPyString(const std::string &pyExpr, std::string &out)
{
std::any res;
std::string cmd = "_anyresult = ROOT.std.make_any['std::string'](" + pyExpr + ")";
if (!TPython::Exec(cmd.c_str(), &res))
return false;
if (!res.has_value())
return false;
try {
out = std::any_cast<std::string>(res);
} catch (const std::bad_any_cast &) {
return false;
}
return true;
}
//--------------------------------------------------------------------------
/**
* @brief Reads a Python global as a vector of doubles.
*
* The python side serializes the iterable to a "1 v0 v1 ..." string (status 1)
* or "0" if the name is undefined or not an iterable of numbers; this is then
* transferred back and parsed in C++.
*
* @param name the Python global name to read
* @param ok set to true if the global exists and is an iterable of numbers
* @return the values; empty if not ok
*/
static std::vector<double> mupp_readPyList(const std::string &name, bool &ok)
{
ok = false;
std::vector<double> out;
// serialize on the python side; the try/except guards undefined/non-iterable
std::string prep =
"try:\n"
" _mupp_s = '1 ' + ' '.join(repr(float(_mupp_x)) for _mupp_x in " + name + ")\n"
"except Exception:\n"
" _mupp_s = '0'\n";
if (!TPython::Exec(prep.c_str()))
return out;
std::string s;
if (!mupp_getPyString("_mupp_s", s))
return out;
std::istringstream is(s);
int status = 0;
if (!(is >> status) || status != 1)
return out;
double d;
while (is >> d)
out.push_back(d);
ok = true;
return out;
}
#endif // MUPP_PYTHON
//--------------------------------------------------------------------------
/**
* @brief Default constructor creating an invalid handler.
@@ -74,6 +258,12 @@ PVarHandler::PVarHandler() :
PVarHandler::PVarHandler(PmuppCollection *coll, std::string parse_str, std::string var_name) :
fColl(coll), fParseStr(parse_str), fVarName(var_name), fIsValid(false)
{
// route <python> ... </python> definitions to the embedded Python interpreter
if (fParseStr.find("<python>") != std::string::npos) {
evaluatePython();
return;
}
injectPredefVariables();
typedef std::string::const_iterator iterator_type;
@@ -113,6 +303,129 @@ PVarHandler::PVarHandler(PmuppCollection *coll, std::string parse_str, std::stri
}
}
//--------------------------------------------------------------------------
/**
* @brief Evaluates a <python> ... </python> variable block via ROOT's TPython.
*
* See the declaration in PVarHandler.h for the data contract. On any failure a
* message is written to ~/.musrfit/mupp/mupp_err.log and fIsValid stays false.
*/
void PVarHandler::evaluatePython()
{
#ifdef MUPP_PYTHON
// The embedded Python interpreter reads PYTHONPATH at initialization, and
// TPython's first call imports cppyy/ROOT. Make sure ROOT's library directory
// (which holds those modules) is on PYTHONPATH before the first TPython call,
// so the read-back works even when thisroot.sh was not sourced.
static bool s_pythonPathSet = false;
if (!s_pythonPathSet) {
TString pp = TROOT::GetLibDir();
const char *cur = gSystem->Getenv("PYTHONPATH");
if (cur != nullptr && cur[0] != '\0') {
pp += ":";
pp += cur;
}
gSystem->Setenv("PYTHONPATH", pp.Data());
s_pythonPathSet = true;
}
// extract the code between the <python> and </python> tags
const std::string openTag("<python>");
const std::string closeTag("</python>");
std::size_t p0 = fParseStr.find(openTag);
std::size_t p1 = fParseStr.find(closeTag, p0);
if (p0 == std::string::npos || p1 == std::string::npos) {
mupp_writeErrLog("**ERROR** python block: missing <python> ... </python> tags.");
fIsValid = false;
return;
}
std::size_t codeStart = p0 + openTag.size();
std::string code = fParseStr.substr(codeStart, p1 - codeStart);
// assemble the script: inject all collection parameters, then run the user code
std::ostringstream py;
py << "import ROOT\n"; // needed for the make_any based read-back below
py << "par = {}\n";
py << "parErr = {}\n";
int nParam = fColl->GetRun(0).GetNoOfParam();
for (int i=0; i<nParam; i++) {
std::string name = fColl->GetRun(0).GetParam(i).GetName().toLatin1().data();
std::vector<double> val = getData(i);
std::vector<double> err = getDataErr(i);
py << "par['" << name << "'] = " << mupp_pyList(val) << "\n";
py << "parErr['" << name << "'] = " << mupp_pyList(err) << "\n";
if (mupp_isSafePyName(name)) {
py << name << " = par['" << name << "']\n";
py << name << "Err = parErr['" << name << "']\n";
}
}
// run the user code inside a try/except so we can capture the traceback
py << "import traceback as _mupp_tb\n";
py << "_mupp_err = ''\n";
py << "try:\n";
py << mupp_indentLines(code, " ");
py << " pass\n";
py << "except Exception:\n";
py << " _mupp_err = _mupp_tb.format_exc()\n";
if (!TPython::Exec(py.str().c_str())) {
mupp_writeErrLog("**ERROR** python block failed to execute "
"(syntax error or interpreter problem). See stderr for details.");
fIsValid = false;
return;
}
// did the user code raise at run time?
std::string errStr;
if (mupp_getPyString("_mupp_err", errStr) && !errStr.empty()) {
mupp_writeErrLog(std::string("**ERROR** python evaluation failed:\n") + errStr);
fIsValid = false;
return;
}
// check-only mode (no variable requested): success if the code ran
if (fVarName.empty()) {
fIsValid = true;
return;
}
// read back value and (user-supplied) error
bool okVal = false, okErr = false;
std::vector<double> val = mupp_readPyList(fVarName, okVal);
std::vector<double> err = mupp_readPyList(fVarName + "Err", okErr);
if (!okVal) {
mupp_writeErrLog(std::string("**ERROR** python block did not define an iterable variable '") + fVarName + "'.");
fIsValid = false;
return;
}
int nRuns = fColl->GetNoOfRuns();
if ((int)val.size() != nRuns) {
std::ostringstream msg;
msg << "**ERROR** python variable '" << fVarName << "' has " << val.size()
<< " values, but the collection has " << nRuns << " runs.";
mupp_writeErrLog(msg.str());
fIsValid = false;
return;
}
// the error is user supplied; default to zeros if not provided / size mismatch
if (!okErr || err.size() != val.size())
err.assign(val.size(), 0.0);
fVar.SetValue(val);
fVar.SetError(err);
fIsValid = true;
#else
mupp_writeErrLog("**ERROR** this mupp build has no Python support; "
"rebuild ROOT with -Dtpython=ON to use <python> variable blocks.");
fIsValid = false;
#endif // MUPP_PYTHON
}
//--------------------------------------------------------------------------
/**
* @brief Gets the computed values for the variable.