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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
================================================================================
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user