New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
#5121 - Extensible Groups problems in ModelObject/WorkspaceObject #5122
#5121 - Extensible Groups problems in ModelObject/WorkspaceObject #5122
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a doozy to track down, but I think the fix is sound. I added a thorough review to explain what's going on and why I did these changes, since I can't expect anyone to waste the required amount of time needed to figure it out themselves.
@@ -825,14 +829,14 @@ namespace detail { | |||
StringVector temp = result; | |||
IdfExtensibleGroup eg = getExtensibleGroup(i); | |||
OS_ASSERT(!eg.empty()); | |||
result = eg.fields(); | |||
result = eg.fieldsWithHandles(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so here is develop version of eraseExtensibleGroup:
OpenStudio/src/utilities/idf/IdfObject.cpp
Lines 806 to 852 in f916fc1
std::vector<std::string> IdfObject_Impl::eraseExtensibleGroup(unsigned groupIndex, bool checkValidity) { | |
StringVector result; | |
if (groupIndex >= numExtensibleGroups()) { | |
return result; | |
} | |
UnsignedVector indices = getExtensibleGroup(groupIndex).mf_indices(); | |
// start at bottom | |
result = popExtensibleGroup(false); | |
if (result.empty()) { | |
return result; | |
} | |
// record diffs at start | |
unsigned diffSize = m_diffs.size(); | |
bool ok = true; | |
// pop was successful. roll up until overwrite groupIndex | |
for (int i = numExtensibleGroups() - 1; i >= static_cast<int>(groupIndex); --i) { | |
StringVector temp = result; | |
IdfExtensibleGroup eg = getExtensibleGroup(i); | |
OS_ASSERT(!eg.empty()); | |
result = eg.fields(); | |
ok = eg.setFields(temp, checkValidity); | |
if (!ok) { | |
// roll back changes and return | |
for (unsigned j = i + 1, n = numExtensibleGroups(); j < n; ++j) { | |
eg = getExtensibleGroup(j); | |
OS_ASSERT(!eg.empty()); | |
result = eg.fields(); | |
OS_ASSERT(result.size() == temp.size()); | |
eg.setFields(temp, false); | |
temp = result; | |
} | |
eg = pushExtensibleGroup(temp, false); | |
OS_ASSERT(!eg.empty()); | |
// remove the diffs | |
m_diffs.resize(diffSize); | |
return {}; | |
} | |
} | |
OS_ASSERT(ok); | |
return result; | |
} |
Basically it goes:
- popExtensibleGroup (remove last extensible group). This will actually resolve to WorkspaceObject_Impl::popExtensibleGroup which nullify the pointers too
- Then loop from last to groupIndex and copy groups / rollup until overwrite groupIndex
- That rollup for each does basically this
for i in [numExtensibleGroups() -1, groupIndex):
prev_fields = extensibleGroups[i+1].fields()
extensibleGroups[i].setFields(prev_fields)
-
The issue is that IdfExtensibleGroup::fields() calls m_impl->getString which as reminder for a WorkspaceObject, actually resolves any objectList field to the Target workspaceobject's name, rather than return the handle.
-
setFields in turns calls setString, which for a workspace object, if passed an object name for an objectList field, will convert to a target handle via
WorkspaceObject_Impl::convertToTargetHandle
which callsWorkspaceObject_Impl::getObjectsByName
. If you have more than one candidate, it looks at reference lists too (butOS:ZoneHVAC:EquipmentList
's Equipment Name has\reference AllObjects
(maybe that's a mistake too, but outside the point here), but bottom line is that if there are more than one candidate, it returns the first.
This means that in the case of a ZoneHVACEquipmentList that points to an Equipment named XXXX
and you do have another modelObject named XXXX
and it is found first, the call sequence goes:
prev_fields = ["XXXX", "", "", ""]
eg.setFields(prev_fields)
eg.setString(0, "XXXX")
#converToTargetHandle grabs first one
handleOfFirstModelObjectNamedXXXX = "{handle of the wrong modelobject}"
eg.setPointer(0, handleOfFirstModelObjectNamedXXXX) => succeed because AllObjects allows it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My fix is to always grab source fields as handles.
prev_fields = ["{handle of equipment}", "", "", ""]
eg.setFields(prev_fields)
eg.setPointer(0, "{handle of equipment}")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this also a peformance improvement? Surely yes.
Thanks for the narrative @jmarrec .
/** Returns this extensible group's fields. Return value will be empty() if this group is empty(). | ||
* Unlike fields(), in the case it's a WorkspaceObject_Impl, it uses handles as string and not the name of the target object */ | ||
std::vector<std::string> fieldsWithHandles(bool returnDefault = false) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New method to return always the handles as strings
StringVector IdfExtensibleGroup::fieldsWithHandles(bool returnDefault) const { | ||
StringVector result; | ||
const unsigned n = numFields(); | ||
result.reserve(n); | ||
for (unsigned i = 0; i < n; ++i) { | ||
OptionalString str = getField(i, returnDefault); | ||
result.push_back(str.get_value_or("")); | ||
} | ||
return result; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It relies on getField
/** Like getString except for reference fields getString will return the name of the referenced object. | ||
* This method, getField, will always return the string value of the field. | ||
* For IdfObject, this is the same as getString */ | ||
virtual boost::optional<std::string> getField(unsigned index, bool returnDefault = false) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getField
was only a WorkspaceObject method before. Now it's on IdfObject for the public side, IdfObject_Impl on the impl side and overriden in WorkspaceObject_Impl.
This allows IdfExtensibleGroup, since it uses a std::shared_ptr<IdfObject_Impl>, to use polymorphism to "do the right thing(TM)"
boost::optional<std::string> IdfObject_Impl::getField(unsigned index, bool returnDefault) const { | ||
return IdfObject_Impl::getString(index, returnDefault, false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For IdfObject, getField does getString.
@@ -188,7 +188,7 @@ namespace detail { | |||
return IdfObject_Impl::getString(index, returnDefault, returnUninitializedEmpty); | |||
} | |||
|
|||
boost::optional<std::string> WorkspaceObject_Impl::getField(unsigned index) const { | |||
boost::optional<std::string> WorkspaceObject_Impl::getField(unsigned index, bool returnDefault) const { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for WorkspaceObject, it does IdfObject_Impl::getString for any non-source field, and grabs the handle otherwise
/** Like getString except for reference fields getString will return the | ||
* name of the referenced object. This method, getField, will always return the string value | ||
* of the field. | ||
*/ | ||
boost::optional<std::string> getField(unsigned index) const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The public forward to impl is done on IdfObject.
@@ -129,7 +129,7 @@ namespace detail { | |||
* (non-extensible) fields and fields with empty data, if a default exists. */ | |||
virtual boost::optional<std::string> getString(unsigned index, bool returnDefault = false, bool returnUninitializedEmpty = false) const override; | |||
|
|||
boost::optional<std::string> getField(unsigned index) const; | |||
virtual boost::optional<std::string> getField(unsigned index, bool returnDefault = false) const override; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a returnDefault param to match the others, with the original default of false
@@ -585,6 +591,59 @@ TEST_F(IdfFixture, WorkspaceObject_setName_allObjects) { | |||
} | |||
} | |||
|
|||
TEST_F(IdfFixture, WorkspaceObject_eraseExtensibleGroups) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new test
values = wsConstruction.eraseExtensibleGroup(1); | ||
ASSERT_EQ(static_cast<unsigned>(1), values.size()); | ||
EXPECT_EQ("Insulation", values[0]); // Brick, Insulation, Gypsum | ||
EXPECT_EQ(static_cast<unsigned>(1), temp[2].numSources()); | ||
EXPECT_EQ(openstudio::toString(wsInsulation.handle()), values[0]); // Brick, Insulation, Gypsum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjust existing test since return of eraseExtensibleGroup(index) for WorkspaceObject changes from object name to handle string for source fields. I don't think we care about this change.
Note: the windows build is bricked because it built another PR (the schedule change from Joe). The perf tests is bricked because it's missing a precondition (@wenyikuang), and is failing on all PRs. |
Seems like the problem is that ModelObject, via WorkspaceObject resolves any handle field to the name the target, and during eraseExtensibleGroup, that goes nuclear, and if the Schedule is named like the object, and the schedule is found first in the workspace, then you end up with the Schedule being on the ZoneHVACEquipmentList.
…WorkspaceObject changes from object name to handle string for source fields
7ec2f53
to
0996673
Compare
CI Results for 0996673:
|
Pull request overview
Seems like the problem is that ModelObject, via WorkspaceObject resolves any handle field to the name the target, and during eraseExtensibleGroup, that goes nuclear, and if the Schedule is named like the object, and the schedule is found first in the workspace, then you end up with the Schedule being on the ZoneHVACEquipmentList...
Pull Request Author
src/model/test
)src/energyplus/Test
)src/osversion/VersionTranslator.cpp
)Labels:
IDDChange
APIChange
Pull Request - Ready for CI
so that CI builds your PRReview Checklist
This will not be exhaustively relevant to every PR.