Skip to content

Commit bbcde68

Browse files
committed
fix name conflict loading
1 parent c090191 commit bbcde68

7 files changed

Lines changed: 260 additions & 4 deletions

File tree

Framework/API/src/ExperimentInfo.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,10 @@ void ExperimentInfo::populateInstrumentParameters() {
367367
for (const auto &item : paramInfoFromIDF) {
368368
const auto &nameComp = item.first;
369369
const auto &paramInfo = item.second;
370-
const std::string &paramN = nameComp.first;
370+
// Use the parameter's short name (e.g. "Alpha0"), not the cache key. The cache key may be
371+
// function-qualified (e.g. "IkedaCarpenterPV:Alpha0") to keep two functions on the same
372+
// component from clobbering each other, but downstream consumers always look up by short name.
373+
const std::string &paramN = paramInfo->m_paramName;
371374

372375
try {
373376
// Special case where user has specified r-position,t-position, and/or
@@ -1310,7 +1313,11 @@ void ExperimentInfo::populateWithParameter(Geometry::ParameterMap &paramMap,
13101313
<< paramInfo.m_constraint[0] << " , " << paramInfo.m_constraint[1] << " , " << paramInfo.m_penaltyFactor
13111314
<< " , " << paramInfo.m_tie << " , " << paramInfo.m_formula << " , " << paramInfo.m_formulaUnit << " , "
13121315
<< paramInfo.m_resultUnit << " , " << (*(paramInfo.m_interpolation));
1313-
paramMap.add("fitting", paramInfo.m_component, name, str.str(), pDescription, pVisible);
1316+
// Dedupe by (name, fitting function) — otherwise two functions on the same component sharing
1317+
// a parameter short name (e.g. Bk2BkExpConvPV:Gamma and IkedaCarpenterPV:Gamma) would clobber
1318+
// each other in the map.
1319+
paramMap.addFittingParameter(paramInfo.m_component, name, paramInfo.m_fittingFunction, str.str(), pDescription,
1320+
pVisible);
13141321
} else if (category == "string") {
13151322
paramMap.addString(paramInfo.m_component, name, paramInfo.m_value, pDescription, pVisible);
13161323
} else if (category == "bool") {

Framework/API/src/IFunction.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1231,7 +1231,15 @@ void IFunction::setMatrixWorkspace(std::shared_ptr<const API::MatrixWorkspace> w
12311231

12321232
for (size_t i = 0; i < nParams(); i++) {
12331233
if (!isExplicitlySet(i)) {
1234-
Geometry::Parameter_sptr param = paramMap.getRecursive(detectorPtr, parameterName(i), "fitting");
1234+
// Use the fitting-function-aware lookup: two functions on the same component can declare a
1235+
// parameter with the same short name (e.g. Bk2BkExpConvPV:Gamma and IkedaCarpenterPV:Gamma).
1236+
// The plain getRecursive() short-circuits on the first match and would pick the wrong one.
1237+
Geometry::Parameter_sptr param =
1238+
paramMap.getRecursiveFittingParameter(detectorPtr, parameterName(i), this->name());
1239+
if (!param) {
1240+
// Fall back for IDFs that omit the function prefix (no embedded function name to match).
1241+
param = paramMap.getRecursive(detectorPtr, parameterName(i), "fitting");
1242+
}
12351243
if (param != Geometry::Parameter_sptr()) {
12361244
// get FitParameter
12371245
const auto &fitParam = param->value<Geometry::FitParameter>();

Framework/API/test/ExperimentInfoTest.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
#include "MantidGeometry/Instrument/Detector.h"
1515
#include "MantidGeometry/Instrument/DetectorGroup.h"
1616
#include "MantidGeometry/Instrument/DetectorInfo.h"
17+
#include "MantidGeometry/Instrument/FitParameter.h"
18+
#include "MantidGeometry/Instrument/XMLInstrumentParameter.h"
1719
#include "MantidKernel/ConfigService.h"
1820
#include "MantidKernel/DateAndTime.h"
21+
#include "MantidKernel/Interpolation.h"
1922
#include "MantidKernel/Matrix.h"
2023
#include "MantidKernel/SingletonHolder.h"
2124

@@ -703,6 +706,58 @@ class ExperimentInfoTest : public CxxTest::TestSuite {
703706
TS_ASSERT_EQUALS(value, "11;22;33;44");
704707
}
705708

709+
void test_populateInstrumentParameters_handles_two_fitting_functions_sharing_parameter_name() {
710+
// Regression test for the IDF parse bug: two functions on the same component declaring the
711+
// same parameter short name (e.g. Bk2BkExpConvPV:Gamma and IkedaCarpenterPV:Gamma) used to
712+
// collide in the IDF cache; the fix function-qualifies the cache key, while the runtime
713+
// ParameterMap still stores entries under the short name "Gamma".
714+
Instrument_sptr inst = ComponentCreationHelper::createTestInstrumentCylindrical(1);
715+
ExperimentInfo expt;
716+
expt.setInstrument(inst);
717+
718+
const auto comp = inst->getChild(0).get();
719+
auto makeXMLParam = [&](const std::string &fittingFunction, const std::string &value) {
720+
auto interp = std::make_shared<Kernel::Interpolation>();
721+
std::string penaltyFactor;
722+
// populateWithParameter indexes m_constraint at [0] and [1]; mirror the IDF parser, which
723+
// always passes a 2-element vector.
724+
std::vector<std::string> constraint(2, "");
725+
return std::make_shared<XMLInstrumentParameter>(
726+
/*logfileID*/ "", /*value*/ value, interp, /*formula*/ "", /*formulaUnit*/ "", /*resultUnit*/ "",
727+
/*paramName*/ "Gamma", /*type*/ "fitting", /*tie*/ "", constraint, penaltyFactor,
728+
/*fitFunc*/ fittingFunction, /*extractSingleValueAs*/ "", /*eq*/ "", comp, /*angleConvertConst*/ 1.0,
729+
/*description*/ "", /*visible*/ "true");
730+
};
731+
732+
// Insert into the cache under function-qualified keys, mirroring what InstrumentDefinitionParser
733+
// now does.
734+
auto &cache = inst->getLogfileCache();
735+
cache.insert({{"Bk2BkExpConvPV:Gamma", comp}, makeXMLParam("Bk2BkExpConvPV", "1.5")});
736+
cache.insert({{"IkedaCarpenterPV:Gamma", comp}, makeXMLParam("IkedaCarpenterPV", "2.5")});
737+
738+
expt.populateInstrumentParameters();
739+
740+
const auto &paramMap = expt.instrumentParameters();
741+
742+
// Both entries must be present in the runtime ParameterMap.
743+
auto bk2bk = paramMap.getRecursiveFittingParameter(comp, "Gamma", "Bk2BkExpConvPV");
744+
TS_ASSERT(bk2bk);
745+
TS_ASSERT_EQUALS(bk2bk->value<FitParameter>().getFunction(), "Bk2BkExpConvPV");
746+
TS_ASSERT_DELTA(bk2bk->value<FitParameter>().getValue(), 1.5, 1e-12);
747+
748+
auto ikedaPV = paramMap.getRecursiveFittingParameter(comp, "Gamma", "IkedaCarpenterPV");
749+
TS_ASSERT(ikedaPV);
750+
TS_ASSERT_EQUALS(ikedaPV->value<FitParameter>().getFunction(), "IkedaCarpenterPV");
751+
TS_ASSERT_DELTA(ikedaPV->value<FitParameter>().getValue(), 2.5, 1e-12);
752+
753+
// Backwards-compat: short-name lookup must still find a fitting parameter (callers like
754+
// NormaliseByDetector that don't supply a function name should keep working).
755+
auto byShortName = paramMap.getRecursive(comp, "Gamma", "fitting");
756+
TS_ASSERT(byShortName);
757+
const auto &fp = byShortName->value<FitParameter>();
758+
TS_ASSERT(fp.getFunction() == "Bk2BkExpConvPV" || fp.getFunction() == "IkedaCarpenterPV");
759+
}
760+
706761
private:
707762
void addInstrumentWithParameter(ExperimentInfo &expt, const std::string &name, const std::string &value) {
708763
Instrument_sptr inst = ComponentCreationHelper::createTestInstrumentCylindrical(1);

Framework/Geometry/inc/MantidGeometry/Instrument/ParameterMap.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ class MANTID_GEOMETRY_DLL ParameterMap {
131131
void add(const IComponent *comp, const std::shared_ptr<Parameter> &par,
132132
const std::string *const pDescription = nullptr);
133133

134+
/// Add a fitting parameter. Dedupes by (name, fittingFunction) rather than just name so that two
135+
/// functions attached to the same component sharing a parameter name (e.g. IkedaCarpenterPV:Alpha0
136+
/// and IkedaCarpenterMD:Alpha0) can coexist in the map.
137+
void addFittingParameter(const IComponent *comp, const std::string &name, const std::string &fittingFunction,
138+
const std::string &value, const std::string *const pDescription = nullptr,
139+
const std::string &pVisible = "true");
140+
134141
/** @name Helper methods for adding and updating parameter types */
135142
/// Create or adjust "pos" parameter for a component
136143
void addPositionCoordinate(const IComponent *comp, const std::string &name, const double value,
@@ -195,6 +202,12 @@ class MANTID_GEOMETRY_DLL ParameterMap {
195202
/// a parameter with a specified type.
196203
std::shared_ptr<Parameter> getRecursiveByType(const IComponent *comp, const std::string &type) const;
197204

205+
/// Look for a fitting parameter recursively, picking the one whose embedded FitParameter function name
206+
/// matches fittingFunction. Multiple fitting parameters can share a short name on the same component
207+
/// (one per function) — getRecursive() would short-circuit on the first match, this walks them all.
208+
std::shared_ptr<Parameter> getRecursiveFittingParameter(const IComponent *comp, const std::string &name,
209+
const std::string &fittingFunction) const;
210+
198211
/** Get the values of a given parameter of all the components that have the
199212
* name: compName
200213
* @tparam The parameter type

Framework/Geometry/src/Instrument/InstrumentDefinitionParser.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2365,7 +2365,12 @@ void InstrumentDefinitionParser::setLogfile(const Geometry::IComponent *comp, co
23652365
description = pDescription->getAttribute("is");
23662366
}
23672367

2368-
auto cacheKey = std::make_pair(paramName, comp);
2368+
// Qualify the cache key with the fitting-function name (when present) so that two functions
2369+
// attached to the same component sharing a parameter name (e.g. IkedaCarpenterPV:Alpha0 and
2370+
// IkedaCarpenterMD:Alpha0) do not overwrite each other. The stored XMLInstrumentParameter
2371+
// still keeps the unqualified paramName so downstream lookups by short name continue to work.
2372+
const std::string cacheParamKey = fittingFunction.empty() ? paramName : fittingFunction + ":" + paramName;
2373+
auto cacheKey = std::make_pair(cacheParamKey, comp);
23692374
auto cacheValue = std::make_shared<XMLInstrumentParameter>(
23702375
logfileID, value, interpolation, formula, formulaUnit, resultUnit, paramName, type, tie, constraint,
23712376
penaltyFactor, fittingFunction, extractSingleValueAs, eq, comp, m_angleConvertConst, description, visible);

Framework/Geometry/src/Instrument/ParameterMap.cpp

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "MantidGeometry/Instrument.h"
1010
#include "MantidGeometry/Instrument/ComponentInfo.h"
1111
#include "MantidGeometry/Instrument/DetectorInfo.h"
12+
#include "MantidGeometry/Instrument/FitParameter.h"
1213
#include "MantidGeometry/Instrument/ParComponentFactory.h"
1314
#include "MantidGeometry/Instrument/ParameterFactory.h"
1415
#include "MantidKernel/Cache.h"
@@ -389,6 +390,55 @@ void ParameterMap::add(const IComponent *comp, const std::shared_ptr<Parameter>
389390
}
390391
}
391392

393+
/** Add a fitting parameter, deduplicating by (name, fittingFunction) instead of name alone.
394+
*
395+
* Two different fitting functions attached to the same component can declare a parameter with
396+
* the same short name (e.g. Bk2BkExpConvPV:Gamma and IkedaCarpenterPV:Gamma). The regular add()
397+
* path uses positionOf() which only matches by name and would let one entry silently overwrite
398+
* the other.
399+
*
400+
* @param comp :: Component the parameter is attached to
401+
* @param name :: Parameter short name (e.g. "Gamma")
402+
* @param fittingFunction :: Name of the fitting function this parameter applies to (may be empty)
403+
* @param value :: Serialised FitParameter value string (as built by ExperimentInfo)
404+
* @param pDescription :: Optional description
405+
* @param pVisible :: Visibility flag
406+
*/
407+
void ParameterMap::addFittingParameter(const IComponent *comp, const std::string &name,
408+
const std::string &fittingFunction, const std::string &value,
409+
const std::string *const pDescription, const std::string &pVisible) {
410+
checkIsNotMaskingParameter(name);
411+
auto param = ParameterFactory::create("fitting", name, pVisible);
412+
param->fromString(value);
413+
if (pDescription)
414+
param->setDescription(*pDescription);
415+
416+
// Look for an existing fitting parameter on this component with the same name AND the same
417+
// embedded function; only that one should be replaced.
418+
if (!m_map.empty()) {
419+
const ComponentID id = comp->getComponentID();
420+
auto it_found = m_map.find(id);
421+
if (it_found != m_map.end()) {
422+
auto itrs = m_map.equal_range(id);
423+
for (auto itr = itrs.first; itr != itrs.second; ++itr) {
424+
const auto &existing = itr->second;
425+
if (existing->type() == "fitting" && strcasecmp(existing->nameAsCString(), name.c_str()) == 0) {
426+
try {
427+
if (existing->value<FitParameter>().getFunction() == fittingFunction) {
428+
std::atomic_store(&(itr->second), param);
429+
return;
430+
}
431+
} catch (...) {
432+
// Not a FitParameter value despite the type tag, fall through to insert.
433+
}
434+
}
435+
}
436+
}
437+
}
438+
439+
m_map.insert({comp->getComponentID(), param});
440+
}
441+
392442
/** Create or adjust "pos" parameter for a component
393443
* Assumed that name either equals "x", "y" or "z" otherwise this
394444
* method will not add or modify "pos" parameter
@@ -881,6 +931,49 @@ Parameter_sptr ParameterMap::getRecursive(const IComponent *comp, const char *na
881931
return result;
882932
}
883933

934+
/** Find a fitting parameter recursively, picking the one whose embedded FitParameter function name
935+
* matches the requested fittingFunction.
936+
*
937+
* When two functions attached to the same component share a parameter short name
938+
* (e.g. Bk2BkExpConvPV:Gamma and IkedaCarpenterPV:Gamma), getRecursive() short-circuits on the first
939+
* match and may return the wrong function's parameter. This method walks every entry on each level
940+
* of the component tree so it can disambiguate by function name.
941+
*
942+
* @param comp :: The component to start the search with
943+
* @param name :: Parameter short name (e.g. "Gamma")
944+
* @param fittingFunction :: The fitting function name this parameter must belong to
945+
* @returns the matching parameter, or a null shared pointer if none is found
946+
*/
947+
Parameter_sptr ParameterMap::getRecursiveFittingParameter(const IComponent *comp, const std::string &name,
948+
const std::string &fittingFunction) const {
949+
checkIsNotMaskingParameter(name);
950+
if (!comp || m_map.empty())
951+
return Parameter_sptr();
952+
953+
std::shared_ptr<const IComponent> compInFocus(comp, NoDeleting());
954+
while (compInFocus != nullptr) {
955+
const ComponentID id = compInFocus->getComponentID();
956+
auto it_found = m_map.find(id);
957+
if (it_found != m_map.end()) {
958+
auto itrs = m_map.equal_range(id);
959+
for (auto itr = itrs.first; itr != itrs.second; ++itr) {
960+
const auto &param = itr->second;
961+
if (param->type() == "fitting" && strcasecmp(param->nameAsCString(), name.c_str()) == 0) {
962+
try {
963+
if (param->value<FitParameter>().getFunction() == fittingFunction) {
964+
return std::atomic_load(&itr->second);
965+
}
966+
} catch (...) {
967+
// Not a FitParameter value despite the type tag, keep looking.
968+
}
969+
}
970+
}
971+
}
972+
compInFocus = compInFocus->getParent();
973+
}
974+
return Parameter_sptr();
975+
}
976+
884977
/**
885978
* Return the value of a parameter as a string
886979
* @param comp :: Component to which parameter is related

Framework/Geometry/test/ParameterMapTest.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include "MantidBeamline/DetectorInfo.h"
1111
#include "MantidFrameworkTestHelpers/ComponentCreationHelper.h"
1212
#include "MantidGeometry/Instrument/Detector.h"
13+
#include "MantidGeometry/Instrument/FitParameter.h"
1314
#include "MantidGeometry/Instrument/Parameter.h"
1415
#include "MantidGeometry/Instrument/ParameterFactory.h"
1516
#include "MantidGeometry/Instrument/ParameterMap.h"
@@ -609,6 +610,80 @@ class ParameterMapTest : public CxxTest::TestSuite {
609610
TS_ASSERT_EQUALS(pmap.get(comp, "v")->asString(), "[0.123456789012345,0.123456789012345,0.123456789012345]");
610611
}
611612

613+
void test_addFittingParameter_keeps_entries_for_different_functions() {
614+
// Two functions on the same component declare a parameter with the same short name
615+
// (e.g. Bk2BkExpConvPV:Gamma and IkedaCarpenterPV:Gamma). addFittingParameter must
616+
// dedupe by (name, function), so both entries should coexist.
617+
ParameterMap pmap;
618+
IComponent_sptr comp = m_testInstrument->getChild(0);
619+
pmap.addFittingParameter(comp.get(), "Gamma", "Bk2BkExpConvPV", "1.5 , Bk2BkExpConvPV , Gamma");
620+
pmap.addFittingParameter(comp.get(), "Gamma", "IkedaCarpenterPV", "2.5 , IkedaCarpenterPV , Gamma");
621+
622+
TS_ASSERT_EQUALS(pmap.size(), 2);
623+
624+
auto bk2bk = pmap.getRecursiveFittingParameter(comp.get(), "Gamma", "Bk2BkExpConvPV");
625+
TS_ASSERT(bk2bk);
626+
TS_ASSERT_EQUALS(bk2bk->value<Mantid::Geometry::FitParameter>().getFunction(), "Bk2BkExpConvPV");
627+
TS_ASSERT_DELTA(bk2bk->value<Mantid::Geometry::FitParameter>().getValue(), 1.5, 1e-12);
628+
629+
auto ikedaPV = pmap.getRecursiveFittingParameter(comp.get(), "Gamma", "IkedaCarpenterPV");
630+
TS_ASSERT(ikedaPV);
631+
TS_ASSERT_EQUALS(ikedaPV->value<Mantid::Geometry::FitParameter>().getFunction(), "IkedaCarpenterPV");
632+
TS_ASSERT_DELTA(ikedaPV->value<Mantid::Geometry::FitParameter>().getValue(), 2.5, 1e-12);
633+
634+
// Lookup for a function that was never registered yields nothing.
635+
TS_ASSERT(!pmap.getRecursiveFittingParameter(comp.get(), "Gamma", "IkedaCarpenterMD"));
636+
}
637+
638+
void test_addFittingParameter_replaces_only_same_function_entry() {
639+
// A second add for the SAME (name, function) must replace, not duplicate.
640+
ParameterMap pmap;
641+
IComponent_sptr comp = m_testInstrument->getChild(0);
642+
pmap.addFittingParameter(comp.get(), "Gamma", "Bk2BkExpConvPV", "1.5 , Bk2BkExpConvPV , Gamma");
643+
pmap.addFittingParameter(comp.get(), "Gamma", "IkedaCarpenterPV", "2.5 , IkedaCarpenterPV , Gamma");
644+
645+
pmap.addFittingParameter(comp.get(), "Gamma", "Bk2BkExpConvPV", "9.9 , Bk2BkExpConvPV , Gamma");
646+
647+
// Still two entries — the IkedaCarpenterPV one is untouched, Bk2BkExpConvPV one is overwritten.
648+
TS_ASSERT_EQUALS(pmap.size(), 2);
649+
650+
auto bk2bk = pmap.getRecursiveFittingParameter(comp.get(), "Gamma", "Bk2BkExpConvPV");
651+
TS_ASSERT(bk2bk);
652+
TS_ASSERT_DELTA(bk2bk->value<Mantid::Geometry::FitParameter>().getValue(), 9.9, 1e-12);
653+
654+
auto ikedaPV = pmap.getRecursiveFittingParameter(comp.get(), "Gamma", "IkedaCarpenterPV");
655+
TS_ASSERT(ikedaPV);
656+
TS_ASSERT_DELTA(ikedaPV->value<Mantid::Geometry::FitParameter>().getValue(), 2.5, 1e-12);
657+
}
658+
659+
void test_getRecursiveFittingParameter_walks_up_component_tree() {
660+
// Parameter declared on the parent should be visible from a child component.
661+
ParameterMap pmap;
662+
IComponent_sptr parent = m_testInstrument; // the instrument itself
663+
IComponent_sptr child = m_testInstrument->getChild(0);
664+
pmap.addFittingParameter(parent.get(), "Gamma", "IkedaCarpenterPV", "3.3 , IkedaCarpenterPV , Gamma");
665+
666+
auto found = pmap.getRecursiveFittingParameter(child.get(), "Gamma", "IkedaCarpenterPV");
667+
TS_ASSERT(found);
668+
TS_ASSERT_DELTA(found->value<Mantid::Geometry::FitParameter>().getValue(), 3.3, 1e-12);
669+
}
670+
671+
void test_getRecursive_still_finds_one_entry_after_addFittingParameter() {
672+
// Backwards-compat: callers that don't care which function the param belongs to should
673+
// still get a hit from the plain getRecursive("Gamma", "fitting") path.
674+
ParameterMap pmap;
675+
IComponent_sptr comp = m_testInstrument->getChild(0);
676+
pmap.addFittingParameter(comp.get(), "Gamma", "Bk2BkExpConvPV", "1.5 , Bk2BkExpConvPV , Gamma");
677+
pmap.addFittingParameter(comp.get(), "Gamma", "IkedaCarpenterPV", "2.5 , IkedaCarpenterPV , Gamma");
678+
679+
auto found = pmap.getRecursive(comp.get(), "Gamma", "fitting");
680+
TS_ASSERT(found);
681+
// Both entries have type "fitting" with name "Gamma", so getRecursive returns whichever the
682+
// multimap iterates first — we don't assert which, but we assert the FitParameter is valid.
683+
const auto &fp = found->value<Mantid::Geometry::FitParameter>();
684+
TS_ASSERT(fp.getFunction() == "Bk2BkExpConvPV" || fp.getFunction() == "IkedaCarpenterPV");
685+
}
686+
612687
private:
613688
template <typename ValueType>
614689
void doCopyAndUpdateTestUsingGenericAdd(const std::string &type, const ValueType &origValue,

0 commit comments

Comments
 (0)