Skip to content
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

Merged
merged 3 commits into from Apr 9, 2024

Conversation

jmarrec
Copy link
Collaborator

@jmarrec jmarrec commented Mar 25, 2024

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

  • Model API Changes / Additions
  • Any new or modified fields have been implemented in the EnergyPlus ForwardTranslator (and ReverseTranslator as appropriate)
  • Model API methods are tested (in src/model/test)
  • EnergyPlus ForwardTranslator Tests (in src/energyplus/Test)
  • If a new object or method, added a test in NREL/OpenStudio-resources: Add Link
  • If needed, added VersionTranslation rules for the objects (src/osversion/VersionTranslator.cpp)
  • Verified that C# bindings built fine on Windows, partial classes used as needed, etc.
  • All new and existing tests passes
  • If methods have been deprecated, update rest of code to use the new methods

Labels:

  • If change to an IDD file, add the label IDDChange
  • If breaking existing API, add the label APIChange
  • If deemed ready, add label Pull Request - Ready for CI so that CI builds your PR

Review Checklist

This will not be exhaustively relevant to every PR.

  • Perform a Code Review on GitHub
  • Code Style, strip trailing whitespace, etc.
  • All related changes have been implemented: model changes, model tests, FT changes, FT tests, VersionTranslation, OS App
  • Labeling is ok
  • If defect, verify by running develop branch and reproducing defect, then running PR and reproducing fix
  • If feature, test running new feature, try creative ways to break it
  • CI status: all green or justified

@jmarrec jmarrec added component - Model Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. labels Mar 25, 2024
Copy link
Collaborator Author

@jmarrec jmarrec left a 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();
Copy link
Collaborator Author

@jmarrec jmarrec Mar 26, 2024

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:

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 calls WorkspaceObject_Impl::getObjectsByName. If you have more than one candidate, it looks at reference lists too (but OS: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

Copy link
Collaborator Author

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}")

Copy link
Contributor

@kbenne kbenne Apr 9, 2024

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 .

Comment on lines +46 to +48
/** 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;
Copy link
Collaborator Author

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

Comment on lines +25 to +34
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;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It relies on getField

Comment on lines +104 to +107
/** 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;
Copy link
Collaborator Author

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)"

Comment on lines +212 to +214
boost::optional<std::string> IdfObject_Impl::getField(unsigned index, bool returnDefault) const {
return IdfObject_Impl::getString(index, returnDefault, false);
}
Copy link
Collaborator Author

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 {
Copy link
Collaborator Author

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

Comment on lines -69 to -73
/** 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;
Copy link
Collaborator Author

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;
Copy link
Collaborator Author

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) {
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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.

@jmarrec jmarrec self-assigned this Mar 26, 2024
@jmarrec jmarrec marked this pull request as ready for review March 26, 2024 08:17
@jmarrec
Copy link
Collaborator Author

jmarrec commented Mar 26, 2024

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
@jmarrec jmarrec force-pushed the 5121_ExtensibleGroups_Issues_when_name_is_same branch from 7ec2f53 to 0996673 Compare March 27, 2024 17:49
@ci-commercialbuildings
Copy link
Collaborator

ci-commercialbuildings commented Mar 27, 2024

@joseph-robertson joseph-robertson added this to the OpenStudio SDK 3.8.0 milestone Apr 2, 2024
@kbenne kbenne merged commit b809318 into develop Apr 9, 2024
3 of 6 checks passed
@kbenne kbenne deleted the 5121_ExtensibleGroups_Issues_when_name_is_same branch April 9, 2024 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component - Model Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants