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

Fix #4869 - Handle AirToAirComponents (ERVs) when cloning AirLoopHVACOutdoorAirSystem #4872

Merged
merged 7 commits into from
Jun 29, 2023

Conversation

jmarrec
Copy link
Collaborator

@jmarrec jmarrec commented May 2, 2023

Pull request overview

Pull Request Author

  • Model API Changes / Additions
  • Any new or modified fields have been implemented in the EnergyPlus ForwardTranslator (and ReverseTranslator as appropriate): N/A
  • Model API methods are tested (in src/model/test)
  • EnergyPlus ForwardTranslator Tests (in src/energyplus/Test): N/A
  • If a new object or method, added a test in NREL/OpenStudio-resources: N/A
  • If needed, added VersionTranslation rules for the objects (src/osversion/VersionTranslator.cpp): N/A
  • 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: N/A

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 severity - Normal Bug component - HVAC component - Model Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. labels May 2, 2023
@jmarrec jmarrec self-assigned this May 2, 2023
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.

Comment on lines +169 to +173
TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_Clone_Simple) {
Model m;

ControllerOutdoorAir controller(m);
AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No ERV in that one. Passes before fix.

Comment on lines +54 to +60
TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_GettersSetters) {
Model m;

ControllerOutdoorAir controller(m);
AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
oaSystem.setName("My AirLoopHVACOutdoorAirSystem");
EXPECT_EQ(controller, oaSystem.getControllerOutdoorAir());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Testing was inexistant for this class. So adding some

Comment on lines +449 to +476
TEST_F(ModelFixture, AirLoopHVACOutdoorAirSystem_HeatCoolFuelTypes) {
Model m;

ControllerOutdoorAir controller(m);
AirLoopHVACOutdoorAirSystem oaSystem(m, controller);
auto outboardReliefNode = oaSystem.outboardReliefNode().get();
auto outboardOANode = oaSystem.outboardOANode().get();

EXPECT_EQ(ComponentType(ComponentType::None), oaSystem.componentType());
testFuelTypeEquality({}, oaSystem.coolingFuelTypes());
testFuelTypeEquality({}, oaSystem.heatingFuelTypes());
testAppGFuelTypeEquality({}, oaSystem.appGHeatingFuelTypes());

CoilHeatingElectric oaCoil(m);
EXPECT_TRUE(oaCoil.addToNode(outboardOANode));

EXPECT_EQ(ComponentType(ComponentType::Heating), oaSystem.componentType());
testFuelTypeEquality({}, oaSystem.coolingFuelTypes());
testFuelTypeEquality({FuelType::Electricity}, oaSystem.heatingFuelTypes());
testAppGFuelTypeEquality({AppGFuelType::Electric}, oaSystem.appGHeatingFuelTypes());

CoilCoolingDXVariableSpeed reliefCoil(m);
EXPECT_TRUE(reliefCoil.addToNode(outboardReliefNode));

EXPECT_EQ(ComponentType(ComponentType::Both), oaSystem.componentType());
testFuelTypeEquality({FuelType::Electricity}, oaSystem.coolingFuelTypes());
testFuelTypeEquality({FuelType::Electricity}, oaSystem.heatingFuelTypes());
testAppGFuelTypeEquality({AppGFuelType::Electric}, oaSystem.appGHeatingFuelTypes());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Add test for this while I'm at it too

Comment on lines +249 to +255
EXPECT_EQ(10, oaSystem.components().size()); // ERV counted twice
EXPECT_EQ(5, oaSystem.reliefComponents().size()); // o-----ERV-----o-----RECoil----(RE)
EXPECT_EQ(5, oaSystem.oaComponents().size()); // (OA)-----ERV-----o-----OACoil-----o
// <=>
// (RE)---- RECoil-----o-----ERV----------------------o
// X
// (OA)----------------------ERV-----o-----OACoil-----o
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Simpler test with a single ERV. It fails before fix, ERV is cloned twice.

Comment on lines +342 to +348
EXPECT_EQ(18, oaSystem.components().size()); // ERV counted twice
EXPECT_EQ(9, oaSystem.reliefComponents().size()); // o-----RECoilA-----o-----ERVA-----o-----ERVB-----o-----RECoilB-----(RE)
EXPECT_EQ(9, oaSystem.oaComponents().size()); // (OA)-----ERVB-----o-----OACoilB-----o-----ERVA-----o-----OACoilA-----o
// <=>
// (RE)---- RECoilB-----o----ERVB-----o-----------------------ERVA-----o-----RECoilA-----o
// X X
// (OA)----------------------ERVB-----o-----OACoilB-----o-----ERVA-----o-----OACoilA-----o
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

More complex test with two ERVs to ensure we do end up with the correct layout after.

Comment on lines +228 to +263
if (reliefAirToAirComps.empty()) {
// Business as usual, but this is faster this way as we keep the same node to connect to throughout
std::vector<Node> reliefNodes;

for (const auto& comp : reliefComps) {
if (comp.iddObjectType() == Node::iddObjectType()) {
reliefNodes.push_back(comp.cast<Node>());
} else {
auto compClone = comp.clone(model).cast<HVACComponent>();
compClone.addToNode(reliefNodeClone);
}
}
} else {
auto targetNode = oaclone.reliefAirModelObject()->cast<Node>();

for (const auto& comp : reliefComps) {
if (comp.iddObjectType() == Node::iddObjectType()) {
reliefNodes.push_back(comp.cast<Node>());
} else if (comp.optionalCast<AirToAirComponent>()) {
auto reliefCloneMo_ = targetNode.outletModelObject();
OS_ASSERT(reliefCloneMo_);
auto reliefCloneAirToAir_ = reliefCloneMo_->optionalCast<AirToAirComponent>();
OS_ASSERT(reliefCloneAirToAir_);
auto mo_ = reliefCloneAirToAir_->secondaryAirOutletModelObject();
OS_ASSERT(mo_);
targetNode = mo_->cast<Node>();
} else {
auto compClone = comp.clone(model).cast<HVACComponent>();
compClone.addToNode(targetNode);
auto [ok, mo_] = getOutletNodeForComponent(compClone);
if (!ok) {
LOG_AND_THROW("For " << briefDescription() << ", unexpected reliefComponent found: " << compClone.briefDescription());
}
OS_ASSERT(mo_);
targetNode = mo_->cast<Node>();
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Actual fix is here.... When we have an ERV, we need to be smarter on the relief side, because cloning the oa intake side already added the ERVs. So we track the outlet nodes instead of trying to add to the relief air node constantly.

Copy link
Contributor

Choose a reason for hiding this comment

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

I looked through all of this PR and thought ok this is great, lots of good cleanup and small fixes, but where is the actual fix to the bug? Then I realized github had collapsed the diff in this file because it was a large diff. So here we go. I appreciate having the fast path if there are no ERVs.

Comment on lines +504 to +513
if (node == airLoop_->supplyOutletNodes().front() && node.inletModelObject().get() == airLoop_->supplyInletNode()) {
const unsigned oldOutletPort = node.connectedObjectPort(node.inletPort()).get();
const unsigned oldInletPort = node.inletPort();
ModelObject oldSourceModelObject = node.connectedObject(node.inletPort()).get();
ModelObject oldTargetModelObject = node;

if (!airLoop.supplyComponents(this->iddObjectType()).empty()) {
return false;
}
_model.connect(std::move(oldSourceModelObject), oldOutletPort, thisModelObject, returnAirPort());
_model.connect(std::move(thisModelObject), mixedAirPort(), std::move(oldTargetModelObject), oldInletPort);
return true;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Modernize the entire AirLoopHVACOutdoorAirSystem file generally speaking, trying to move as much as possible in particular.

std::vector<ModelObject> oaComponents() const;
/** Returns a vector of model objects that are on the path of the incoming outdoor air stream.
* This is orderded like the airflow: from the outdoorOANode (OA Intake) towards the OASystem itself **/
std::vector<ModelObject> oaComponents(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) 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.

pass IddObjectType for oaComponents, reliefComponents, and components

Copy link
Contributor

Choose a reason for hiding this comment

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

I think I see. I guess the public API never before offered to accept a type argument.


/** Returns the optional ModelObject with the Handle given. The optional
* will be false if the given handle does not correspond to the a ModelObject
* that is not part of the outdoor air system.
**/
boost::optional<ModelObject> component(openstudio::Handle handle);
boost::optional<ModelObject> component(openstudio::Handle handle) 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.

const correctness

Comment on lines +113 to +116
boost::optional<Node> outboardOANode() const; // TODO: shouldn't be optional

/** Returns the most outboard relief air Node. **/
boost::optional<Node> outboardReliefNode() const;
boost::optional<Node> outboardReliefNode() const; // TODO: shouldn't be optional
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not breaking API, but to consider for a breaking release

@@ -148,7 +151,7 @@ namespace model {
bool setControllerOutdoorAir(const ControllerOutdoorAir& controllerOutdoorAir);

/** Reimplemented from HVACComponent. **/
boost::optional<AirLoopHVAC> airLoop() const;
boost::optional<AirLoopHVAC> airLoop() const; // TODO: this shouldn't exist!!!
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not breaking API, but to consider for a breaking release. I made it forward to the Impl_::airLoopHVAC() like it should have been

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines +94 to +95
// Take a shortcut to avoid also checking each AirLoopHVAC's OutdoorAirSystem's component like HVACComponent logically does
virtual boost::optional<AirLoopHVAC> airLoopHVAC() 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.

Instead of defining an airLoop() method, just override the HVACComponent_Impl::airLoopHVAC() so we can shortcut

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines -124 to -125
std::vector<ModelObject> oaComponentsAsModelObjects() const;
std::vector<ModelObject> reliefComponentsAsModelObjects() 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 actual method oaComponents() already returns a vector of ModelObject so this is pointless

@jmarrec jmarrec force-pushed the 4869_AirLoopHVACOASystem_clone branch from a1ca453 to 4491c50 Compare May 10, 2023 10:16
@jmarrec jmarrec added this to the OpenStudio SDK 3.7.0 milestone May 31, 2023
Copy link
Contributor

@kbenne kbenne left a comment

Choose a reason for hiding this comment

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

Left some comments for discussion, but this looks great for merging to me.

@@ -900,9 +898,9 @@ namespace model {
return {demandComps.begin(), end};
}

std::vector<ModelObject> AirLoopHVAC_Impl::oaComponents(openstudio::IddObjectType /*type*/) const {
std::vector<ModelObject> AirLoopHVAC_Impl::oaComponents(openstudio::IddObjectType type) const {
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like ignoring type argument might have been a bug in its own right.

@@ -63,6 +63,9 @@ namespace model {
*/
explicit AirLoopHVACOutdoorAirSystem(Model& model, const ControllerOutdoorAir& controller);

/** A default ControllerOutdoorAir will be created for you */
explicit AirLoopHVACOutdoorAirSystem(Model& model);
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you. I appreciate constructors that only require a model and I think other people do too.

boost::optional<ModelObject> reliefAirModelObject();

/** Returns the optional ModelObject attached to the mixer air port. **/
boost::optional<ModelObject> mixedAirModelObject();

/** Returns the most outboard outdoor air Node. **/
boost::optional<Node> outboardOANode() const;
boost::optional<Node> outboardOANode() const; // TODO: shouldn't be optional
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought everyone liked a good optional /s

std::vector<ModelObject> oaComponents() const;
/** Returns a vector of model objects that are on the path of the incoming outdoor air stream.
* This is orderded like the airflow: from the outdoorOANode (OA Intake) towards the OASystem itself **/
std::vector<ModelObject> oaComponents(openstudio::IddObjectType type = openstudio::IddObjectType("Catchall")) const;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think I see. I guess the public API never before offered to accept a type argument.

@@ -148,7 +151,7 @@ namespace model {
bool setControllerOutdoorAir(const ControllerOutdoorAir& controllerOutdoorAir);

/** Reimplemented from HVACComponent. **/
boost::optional<AirLoopHVAC> airLoop() const;
boost::optional<AirLoopHVAC> airLoop() const; // TODO: this shouldn't exist!!!
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

Comment on lines +94 to +95
// Take a shortcut to avoid also checking each AirLoopHVAC's OutdoorAirSystem's component like HVACComponent logically does
virtual boost::optional<AirLoopHVAC> airLoopHVAC() const override;
Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@@ -69,6 +71,20 @@ namespace model {

namespace detail {

// Helpers
Copy link
Contributor

Choose a reason for hiding this comment

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

My first thought was that it was odd to see this seemingly general purpose helper function down here in the oa system implementation, but now that I think about what is going on I guess the context matters in terms of what "outletNode" is, so I guess it makes sense to have this function here.

Comment on lines +228 to +263
if (reliefAirToAirComps.empty()) {
// Business as usual, but this is faster this way as we keep the same node to connect to throughout
std::vector<Node> reliefNodes;

for (const auto& comp : reliefComps) {
if (comp.iddObjectType() == Node::iddObjectType()) {
reliefNodes.push_back(comp.cast<Node>());
} else {
auto compClone = comp.clone(model).cast<HVACComponent>();
compClone.addToNode(reliefNodeClone);
}
}
} else {
auto targetNode = oaclone.reliefAirModelObject()->cast<Node>();

for (const auto& comp : reliefComps) {
if (comp.iddObjectType() == Node::iddObjectType()) {
reliefNodes.push_back(comp.cast<Node>());
} else if (comp.optionalCast<AirToAirComponent>()) {
auto reliefCloneMo_ = targetNode.outletModelObject();
OS_ASSERT(reliefCloneMo_);
auto reliefCloneAirToAir_ = reliefCloneMo_->optionalCast<AirToAirComponent>();
OS_ASSERT(reliefCloneAirToAir_);
auto mo_ = reliefCloneAirToAir_->secondaryAirOutletModelObject();
OS_ASSERT(mo_);
targetNode = mo_->cast<Node>();
} else {
auto compClone = comp.clone(model).cast<HVACComponent>();
compClone.addToNode(targetNode);
auto [ok, mo_] = getOutletNodeForComponent(compClone);
if (!ok) {
LOG_AND_THROW("For " << briefDescription() << ", unexpected reliefComponent found: " << compClone.briefDescription());
}
OS_ASSERT(mo_);
targetNode = mo_->cast<Node>();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I looked through all of this PR and thought ok this is great, lots of good cleanup and small fixes, but where is the actual fix to the bug? Then I realized github had collapsed the diff in this file because it was a large diff. So here we go. I appreciate having the fast path if there are no ERVs.

OptionalModelObject modelObject;

modelObject = this->outdoorAirModelObject();
OptionalModelObject modelObject = this->outdoorAirModelObject();

while (modelObject) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This kind of walking the flow path and switching based on HVACComponent type has been done a lot, but is always kind of stinky to me. At some point along the way we introduced a graph traversing function in findModelObjects. I wonder if that idea could be applied here for the oa system.

@jmarrec jmarrec merged commit 5a34b4c into develop Jun 29, 2023
@jmarrec jmarrec deleted the 4869_AirLoopHVACOASystem_clone branch June 29, 2023 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component - HVAC component - Model Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. severity - Normal Bug
Projects
None yet
3 participants