mupp: expose all collections inside a <python> block (coll[]/collErr[]).

A <python> 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 <idx>' /
'select <idx>') 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:55:38 +02:00
parent 9ff5d179f1
commit 47ff6d5de7
5 changed files with 174 additions and 16 deletions
+50 -5
View File
@@ -107,7 +107,7 @@ run.
INPUT - injected into the Python namespace before your code runs:
* For every collection parameter <p>:
* For every parameter <p> of the LINKED collection (the one given by 'col'):
<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,
@@ -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 <python> block can compute - and
combine - variables for several collections without leaving the block:
coll[<idx>]['<param>'] # value list of parameter <param>
collErr[<idx>]['<param>'] # error list of parameter <param>
The collection can be addressed either by its integer INDEX (the same index
used by the 'col <idx>' command and by 'select <idx>') 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 <name>:
<name> : the value list (length = number of runs)
@@ -177,14 +191,44 @@ and add the <python> ... </python> 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
<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)
</python>
col 0 : SigmaSC_10
col 1 : SigmaSC_150
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.
* Several <python> blocks are allowed: each "var <name> = 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[<that idx>] provides).
* 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.
@@ -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 <python> 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).
================================================================================
+17 -2
View File
@@ -1432,9 +1432,17 @@ void PmuppGui::addVar()
*/
void PmuppGui::check(QString varStr, QVector<int> idx)
{
// expose every loaded collection inside a <python> block (coll[]/collErr[]),
// addressable by index and by name, so a check exercises the same namespace
// the actual evaluation will see.
std::vector<PmuppCollection*> allColl;
for (int i=0; i<fParamDataHandler->GetNoOfCollections(); i++)
allColl.push_back(fParamDataHandler->GetCollection(i));
int count = 0;
for (int i=0; i<idx.size(); i++) {
PVarHandler var_check(fParamDataHandler->GetCollection(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<int> idx)
// the PVarHandler class handles only ONE variable of ONE collection.
QStringList varNames = getVarNames(varStr);
// expose every loaded collection inside a <python> block (coll[]/collErr[]),
// addressable by index and by name.
std::vector<PmuppCollection*> allColl;
for (int i=0; i<fParamDataHandler->GetNoOfCollections(); i++)
allColl.push_back(fParamDataHandler->GetCollection(i));
// go through all collections
for (int i=0; i<idx.size(); i++) {
// go through all the defined variables
for (int j=0; j<varNames.count(); j++) {
PVarHandler var(fParamDataHandler->GetCollection(idx[i]),
varStr.toLatin1().data(),
varNames[j].toLatin1().data());
varNames[j].toLatin1().data(),
allColl);
if (!var.isValid()) {
parseErrMsgDlg();
return;
+6 -1
View File
@@ -1051,8 +1051,13 @@ int PmuppScript::var_cmd(const QString str, int script_idx)
<< "' declared as python, but no <python> ... </python> block was found." << std::endl;
return 1;
}
// expose every loaded collection inside the <python> block (coll[]/collErr[]),
// addressable by index and by name, so one block can serve several collections.
std::vector<PmuppCollection*> allColl;
for (int i=0; i<fParamDataHandler->GetNoOfCollections(); 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")));
@@ -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 &lt;python&gt; 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<PmuppCollection*> &allColl = std::vector<PmuppCollection*>());
/**
* @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<PmuppCollection*> 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 <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.
* 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 <idx>') and by collection name;
* index and name keys reference the same per-parameter dict. This lets a
* single <python> block compute (and combine) variables for several
* collections. 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.
*/
+82 -2
View File
@@ -109,6 +109,59 @@ static std::string mupp_pyList(const std::vector<double> &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<s.size(); i++) {
if (s[i] == '\\' || s[i] == '\'')
os << '\\';
os << s[i];
}
os << "'";
return os.str();
}
//--------------------------------------------------------------------------
/**
* @brief Collects a parameter's value across all runs of an arbitrary collection.
*
* Collection-pointer counterpart of PVarHandler::getData(), used to inject every
* loaded collection into the Python namespace (not just the bound one).
*/
static std::vector<double> mupp_collValues(PmuppCollection *coll, int pidx)
{
std::vector<double> v;
for (int i=0; i<coll->GetNoOfRuns(); 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<double> mupp_collErrors(PmuppCollection *coll, int pidx)
{
std::vector<double> v;
for (int i=0; i<coll->GetNoOfRuns(); 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<PmuppCollection*> &allColl) :
fColl(coll), fAllColl(allColl), fParseStr(parse_str), fVarName(var_name), fIsValid(false)
{
// route <python> ... </python> definitions to the embedded Python interpreter
if (fParseStr.find("<python>") != std::string::npos) {
@@ -361,6 +416,31 @@ void PVarHandler::evaluatePython()
}
}
// expose every loaded collection explicitly so that a single <python> block
// can compute (and combine) variables for several collections. coll[<idx>]
// mirrors the script 'col <idx>' and coll['<name>'] 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; c<fAllColl.size(); c++) {
PmuppCollection *pc = fAllColl[c];
if (pc == nullptr || pc->GetNoOfRuns() == 0)
continue;
py << "_c = {}\n";
py << "_ce = {}\n";
int np = pc->GetRun(0).GetNoOfParam();
for (int j=0; j<np; j++) {
std::string pname = pc->GetRun(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";