From 47ff6d5de710d2936811ba543f160a3cd4988fb7 Mon Sep 17 00:00:00 2001 From: Andreas Suter Date: Wed, 27 May 2026 13:55:38 +0200 Subject: [PATCH] mupp: expose all collections inside a block (coll[]/collErr[]). A variable block previously saw only the parameters of the single collection the variable is linked to via 'col', injected as bare names plus par[]/parErr[]. To compute variables for several collections one therefore needed a separate block per collection. Now every loaded collection is additionally injected as coll[]/collErr[] dictionaries, addressable both by integer index (matching 'col ' / 'select ') and by collection name; index and name keys reference the same per-parameter dict. A single block placed after all 'var = python' declarations can thus compute - and combine - variables for any collection, e.g. coll[0]['Sigma'] or coll['NAME.db']['Sigma']. The bound-collection bare names / par[] / parErr[] are unchanged, so existing scripts and GUI variables keep working. The collection list is threaded through PmuppScript::var_cmd and the GUI add()/check() paths into PVarHandler. Verified: a one-block script using both an index key and a name key reproduces the numeric output of the equivalent two-block script bit-for-bit. Co-Authored-By: Claude Opus 4.7 --- src/musredit_qt6/mupp/MUPP_PY.README | 55 ++++++++++-- src/musredit_qt6/mupp/PmuppGui.cpp | 19 ++++- src/musredit_qt6/mupp/PmuppScript.cpp | 7 +- .../mupp/var/include/PVarHandler.h | 25 ++++-- src/musredit_qt6/mupp/var/src/PVarHandler.cpp | 84 ++++++++++++++++++- 5 files changed, 174 insertions(+), 16 deletions(-) diff --git a/src/musredit_qt6/mupp/MUPP_PY.README b/src/musredit_qt6/mupp/MUPP_PY.README index 56514d0d5..db91509f1 100644 --- a/src/musredit_qt6/mupp/MUPP_PY.README +++ b/src/musredit_qt6/mupp/MUPP_PY.README @@ -107,7 +107,7 @@ run. INPUT - injected into the Python namespace before your code runs: - * For every collection parameter

: + * For every parameter

of the LINKED collection (the one given by 'col'):

: a list with one value per run

Err : a list with the corresponding errors (one per run) The error is the geometric mean of the asymmetric fit errors, @@ -115,10 +115,24 @@ INPUT - injected into the Python namespace before your code runs: * 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: + the dictionaries that always contain every parameter of the linked collection: par['lambda'] # value list parErr['lambda'] # error list + * EVERY loaded collection (not just the linked one) is additionally available + through two dictionaries, so a single block can compute - and + combine - variables for several collections without leaving the block: + coll[][''] # value list of parameter + collErr[][''] # error list of parameter + The collection can be addressed either by its integer INDEX (the same index + used by the 'col ' command and by 'select ') or by its NAME (the + collection name as loaded, e.g. 'YBCO-...-Tscan.db'); both keys reference the + same per-parameter dictionary: + coll[0]['Sigma'] # by index + coll['YBCO-...-B150mT-Tscan.db']['Sigma'] # by name (equivalent) + The bare names / par[] / parErr[] above are just a convenience alias for the + linked collection; coll[]/collErr[] expose all of them explicitly. + OUTPUT - your code must assign, for a variable named : : the value list (length = number of runs) @@ -177,14 +191,44 @@ and add the ... block anywhere in the script: y sigSC plot sigSC-vs-temp.pdf +Several collections in ONE block: use coll[]/collErr[] to address each collection +explicitly (by index or by name), so a single block can serve them all: + + loadPath ./ + load YBCO-40nm-FC-E3p8keV-B10mT-Tscan.db # collection 0 + load YBCO-40nm-FC-E3p8keV-B150mT-Tscan.db # collection 1 + + var SigmaSC_10 = python + var SigmaSC_10Err = python + var SigmaSC_150 = python + var SigmaSC_150Err = python + + + import numpy as np + def sigSC(idx, B0): + s = np.array(coll[idx]['Sigma']) + se = np.array(collErr[idx]['Sigma']) + sc = np.sqrt(np.abs(s**2 - B0**2)) + return sc, np.sqrt((s*se)**2 + (B0*0.0025)**2)/sc + SigmaSC_10, SigmaSC_10Err = sigSC(0, 0.11) + SigmaSC_150, SigmaSC_150Err = sigSC(1, 0.075) + + + col 0 : SigmaSC_10 + col 1 : SigmaSC_150 + Notes for scripts: * The ... 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 must be on its own line. - * Only ONE block per script is supported; it may define several - variables. + * Several blocks are allowed: each "var = python" declaration is + paired with the next block in script order. Alternatively, a single block + placed AFTER all the declarations may define every variable; address the + individual collections via coll[]/collErr[] as shown above. + * Each variable's output length must match the number of runs of the collection + it is linked to with 'col' (this is what coll[] provides). * As with X3 variables, only the value variable is linked with 'col'; its error variable (Err) is associated implicitly. * Tip: loadPath/savePath strip a leading '/'; use $HOME/... or a relative path. @@ -196,5 +240,6 @@ Notes for scripts: * Requires ROOT built with -Dtpython=ON (see section 1). * No automatic error propagation in Python mode - errors are user-supplied. - * One block per variable definition / per script. + * coll[]/collErr[] expose collections by index and by name; the name is the + collection name as loaded (it includes the .db/.dat extension). ================================================================================ diff --git a/src/musredit_qt6/mupp/PmuppGui.cpp b/src/musredit_qt6/mupp/PmuppGui.cpp index c911d6898..3a34c1f57 100644 --- a/src/musredit_qt6/mupp/PmuppGui.cpp +++ b/src/musredit_qt6/mupp/PmuppGui.cpp @@ -1432,9 +1432,17 @@ void PmuppGui::addVar() */ void PmuppGui::check(QString varStr, QVector idx) { + // expose every loaded collection inside a block (coll[]/collErr[]), + // addressable by index and by name, so a check exercises the same namespace + // the actual evaluation will see. + std::vector allColl; + for (int i=0; iGetNoOfCollections(); i++) + allColl.push_back(fParamDataHandler->GetCollection(i)); + int count = 0; for (int i=0; iGetCollection(i), varStr.toLatin1().data()); + PVarHandler var_check(fParamDataHandler->GetCollection(i), varStr.toLatin1().data(), + std::string(), allColl); if (var_check.isValid()) { count++; } else { @@ -1461,13 +1469,20 @@ void PmuppGui::add(QString varStr, QVector idx) // the PVarHandler class handles only ONE variable of ONE collection. QStringList varNames = getVarNames(varStr); + // expose every loaded collection inside a block (coll[]/collErr[]), + // addressable by index and by name. + std::vector allColl; + for (int i=0; iGetNoOfCollections(); i++) + allColl.push_back(fParamDataHandler->GetCollection(i)); + // go through all collections for (int i=0; iGetCollection(idx[i]), varStr.toLatin1().data(), - varNames[j].toLatin1().data()); + varNames[j].toLatin1().data(), + allColl); if (!var.isValid()) { parseErrMsgDlg(); return; diff --git a/src/musredit_qt6/mupp/PmuppScript.cpp b/src/musredit_qt6/mupp/PmuppScript.cpp index e74883760..493c8a150 100644 --- a/src/musredit_qt6/mupp/PmuppScript.cpp +++ b/src/musredit_qt6/mupp/PmuppScript.cpp @@ -1051,8 +1051,13 @@ int PmuppScript::var_cmd(const QString str, int script_idx) << "' declared as python, but no ... block was found." << std::endl; return 1; } + // expose every loaded collection inside the block (coll[]/collErr[]), + // addressable by index and by name, so one block can serve several collections. + std::vector allColl; + for (int i=0; iGetNoOfCollections(); i++) + allColl.push_back(fParamDataHandler->GetCollection(i)); PVarHandler varHandler(fParamDataHandler->GetCollection(idx), - pyBlock.toLatin1().data(), tok[1].toLatin1().data()); + pyBlock.toLatin1().data(), tok[1].toLatin1().data(), allColl); if (!varHandler.isValid()) { // dump error messages if present QString mupp_err = QString("%1/.musrfit/mupp/mupp_err.log").arg(QString(qgetenv("HOME"))); diff --git a/src/musredit_qt6/mupp/var/include/PVarHandler.h b/src/musredit_qt6/mupp/var/include/PVarHandler.h index c215cd609..acb7f1da8 100644 --- a/src/musredit_qt6/mupp/var/include/PVarHandler.h +++ b/src/musredit_qt6/mupp/var/include/PVarHandler.h @@ -85,8 +85,14 @@ class PVarHandler { * @param coll pointer to the PmuppCollection containing run data and parameters * @param parse_str the variable definition string to parse (e.g., "var x = $T1 + 1.0") * @param var_name optional variable name to extract from the evaluation results; if empty, only parsing/checking is performed + * @param allColl optional list of all loaded collections (in handler-index + * order). Only used by the Python path, where it is exposed inside + * the <python> block as coll[]/collErr[] (see evaluatePython()). + * If empty, only the bound collection 'coll' is injected (bare names + * and par[]/parErr[]). */ - PVarHandler(PmuppCollection *coll, std::string parse_str, std::string var_name=""); + PVarHandler(PmuppCollection *coll, std::string parse_str, std::string var_name="", + const std::vector &allColl = std::vector()); /** * @brief Checks if the parsing and evaluation were successful. @@ -120,6 +126,7 @@ class PVarHandler { private: PmuppCollection *fColl; ///< pointer to collection containing run data needed for parsing and evaluation + std::vector fAllColl; ///< all loaded collections (handler-index order); Python path only, exposed as coll[]/collErr[] std::string fParseStr; ///< the variable input string to be parsed std::string fVarName; ///< name of the variable to extract from evaluation results mupp::prog::PVarHandler fVar; ///< variable handler storing the computed values and errors @@ -141,11 +148,17 @@ class PVarHandler { * @brief Evaluates the variable using an embedded Python3 interpreter (TPython). * * Used when the input string contains a ... 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 (Err). - * Both value and error arrays are read back and stored in fVar. + * The bound collection's 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'). In addition, every loaded collection passed via fAllColl is + * exposed through the coll[]/collErr[] dictionaries, addressable both by + * integer index (matching the script 'col ') and by collection name; + * index and name keys reference the same per-parameter dict. This lets a + * single block compute (and combine) variables for several + * collections. The user script must assign the requested variable and its + * error counterpart (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. */ diff --git a/src/musredit_qt6/mupp/var/src/PVarHandler.cpp b/src/musredit_qt6/mupp/var/src/PVarHandler.cpp index 3d5a3097b..fc3a97555 100644 --- a/src/musredit_qt6/mupp/var/src/PVarHandler.cpp +++ b/src/musredit_qt6/mupp/var/src/PVarHandler.cpp @@ -109,6 +109,59 @@ static std::string mupp_pyList(const std::vector &v) return os.str(); } +//-------------------------------------------------------------------------- +/** + * @brief Builds a single-quoted Python string literal, escaping \ and '. + * + * Used for dictionary keys (parameter and collection names) so that names with + * special characters cannot break the generated script. + */ +static std::string mupp_pyStr(const std::string &s) +{ + std::ostringstream os; + os << "'"; + for (size_t i=0; i mupp_collValues(PmuppCollection *coll, int pidx) +{ + std::vector v; + for (int i=0; iGetNoOfRuns(); i++) + v.push_back(coll->GetRun(i).GetParam(pidx).GetValue()); + return v; +} + +//-------------------------------------------------------------------------- +/** + * @brief Collects a parameter's error across all runs of an arbitrary collection. + * + * Uses the same convention as PVarHandler::getDataErr(): the geometric mean of + * the positive and negative fit errors, sqrt(abs(posErr * negErr)). + */ +static std::vector mupp_collErrors(PmuppCollection *coll, int pidx) +{ + std::vector v; + for (int i=0; iGetNoOfRuns(); i++) { + double p = coll->GetRun(i).GetParam(pidx).GetPosErr(); + double n = coll->GetRun(i).GetParam(pidx).GetNegErr(); + v.push_back(std::sqrt(std::fabs(p*n))); + } + return v; +} + //-------------------------------------------------------------------------- /** * @brief Checks whether name is a valid Python identifier and not a keyword. @@ -254,9 +307,11 @@ PVarHandler::PVarHandler() : * @param coll pointer to the collection containing run data * @param parse_str the variable definition string to parse * @param var_name optional variable name to extract; if empty, only validation is performed + * @param allColl optional list of all loaded collections (Python path only) */ -PVarHandler::PVarHandler(PmuppCollection *coll, std::string parse_str, std::string var_name) : - fColl(coll), fParseStr(parse_str), fVarName(var_name), fIsValid(false) +PVarHandler::PVarHandler(PmuppCollection *coll, std::string parse_str, std::string var_name, + const std::vector &allColl) : + fColl(coll), fAllColl(allColl), fParseStr(parse_str), fVarName(var_name), fIsValid(false) { // route ... definitions to the embedded Python interpreter if (fParseStr.find("") != std::string::npos) { @@ -361,6 +416,31 @@ void PVarHandler::evaluatePython() } } + // expose every loaded collection explicitly so that a single block + // can compute (and combine) variables for several collections. coll[] + // mirrors the script 'col ' and coll[''] uses the collection name; + // both keys reference the same per-parameter dict (likewise for collErr). + py << "coll = {}\n"; + py << "collErr = {}\n"; + for (size_t c=0; cGetNoOfRuns() == 0) + continue; + py << "_c = {}\n"; + py << "_ce = {}\n"; + int np = pc->GetRun(0).GetNoOfParam(); + for (int j=0; jGetRun(0).GetParam(j).GetName().toLatin1().data(); + py << "_c[" << mupp_pyStr(pname) << "] = " << mupp_pyList(mupp_collValues(pc, j)) << "\n"; + py << "_ce[" << mupp_pyStr(pname) << "] = " << mupp_pyList(mupp_collErrors(pc, j)) << "\n"; + } + std::string cname = pc->GetName().toLatin1().data(); + py << "coll[" << c << "] = _c\n"; + py << "collErr[" << c << "] = _ce\n"; + py << "coll[" << mupp_pyStr(cname) << "] = _c\n"; + py << "collErr[" << mupp_pyStr(cname) << "] = _ce\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";