-
Notifications
You must be signed in to change notification settings - Fork 389
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
SetPointManager Refactor #10537
SetPointManager Refactor #10537
Conversation
@@ -7872,7 +7872,7 @@ | |||
PACKAGED VAV WITH REHEAT Supply Equipment Outlet Node; !- Setpoint Node or NodeList Name | |||
|
|||
SetpointManager:MixedAir, | |||
PACKAGED VAV WITH REHEAT_HeatC SAT Manager, !- Name | |||
PACKAGED VAV WITH REHEAT_HeatC SAT Manager MA, !- Name |
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.
Duplicate SetPointManager names are no longer allowed.
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 changed the labels on this PR to include IDDChange
, because this is changing input requirements even though the idd itself isn't changing. Will need transition for this.
@@ -699,9 +699,6 @@ namespace AirflowNetwork { | |||
// Using/Aliasing | |||
auto &NumPrimaryAirSys = state.dataHVACGlobal->NumPrimaryAirSys; | |||
|
|||
// SUBROUTINE PARAMETER DEFINITIONS: | |||
int constexpr CycFanCycComp(1); // fan cycles with compressor operation | |||
|
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 not used.
@@ -338,8 +338,7 @@ namespace BoilerSteam { | |||
} else { | |||
// need call to EMS to check node | |||
bool FatalError = false; // but not really fatal yet, but should be. | |||
EMSManager::CheckIfNodeSetPointManagedByEMS( | |||
state, this->BoilerOutletNodeNum, EMSManager::SPControlType::TemperatureSetPoint, FatalError); | |||
EMSManager::CheckIfNodeSetPointManagedByEMS(state, this->BoilerOutletNodeNum, HVAC::CtrlVarType::Temp, FatalError); |
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.
Unified a few redundant types to HVAC::CtrlVarType
.
@@ -62,6 +62,16 @@ struct EnergyPlusData; | |||
|
|||
namespace DataEnvironment { | |||
|
|||
enum class GroundTempType |
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.
Made the different GroundTemp field groups into std::array
s indexed by this enum
.
json const alphas = legacy_idd["alphas"]; | ||
if (alphas.find("fields") != alphas.end()) { | ||
NumAlpha += alphas["fields"].size(); | ||
if (auto found = legacy_idd.find("alphas"); found != legacy_idd.end()) { |
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.
Eliminate redundant lookups.
// The System Node Reset Humidity Setpoint Managers | ||
for (SetPtMgrNum = 1; SetPtMgrNum <= state.dataSetPointManager->NumSystemNodeResetHumSetPtMgrs; ++SetPtMgrNum) { | ||
state.dataSetPointManager->SystemNodeResetSetPtMgr(SetPtMgrNum + state.dataSetPointManager->NumSystemNodeResetTempSetPtMgrs).calculate(state); | ||
for (auto *spm : state.dataSetPointManager->spms) { |
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.
Ahhhhhhhhhh.
// SUBROUTINE LOCAL VARIABLE DECLARATIONS: | ||
auto &CondWaterSetPoint = state.dataSetPointManager->CondWaterSetPoint; |
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.
Bad use of shortcut refs.
@@ -66,61 +68,40 @@ struct EnergyPlusData; | |||
|
|||
namespace SetPointManager { | |||
|
|||
enum class CtrlNodeType |
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.
Moved to DataHVACGlobals
.
bool ManagerOn = false; | ||
bool GetInputFlag = true; // First time, input is "gotten" | ||
|
||
bool InitSetPointManagersOneTimeFlag = true; | ||
bool InitSetPointManagersOneTimeFlag2 = true; | ||
Real64 DCESPMDsn_EntCondTemp = 0.0; |
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.
Most of these were actually local variables.
@@ -133,16 +133,19 @@ TEST_F(EnergyPlusFixture, HVACControllers_ResetHumidityRatioCtrlVarType) | |||
|
|||
GetSetPointManagerInputs(*state); | |||
// check specified control variable type is "HumidityRatio" | |||
ASSERT_TRUE(compare_enums(SetPointManager::CtrlVarType::HumRat, state->dataSetPointManager->AllSetPtMgr(1).CtrlTypeMode)); | |||
ASSERT_EQ((int)HVAC::CtrlVarType::HumRat, (int)state->dataSetPointManager->spms(1)->ctrlVar); |
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 is better than compare_enums
. Just use this from now on.
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 disagree. IMHO The custom function is clearer, and doesn't require casting, so you don't have to remember how to do it properly. But that's not even the real point here.
The point is that compare_enums will not let you shoot yourself in the foot. You cannot compare different enums with it, it will not compile.
If your beef is with the way the error message is structured, we can work on that to improve it.
And I'd avoid C-like cast, so at least it should be a static_cast, which gets verbose.
An alternative would be define the C++23 std::to_underlying: https://en.cppreference.com/w/cpp/utility/to_underlying
That's dead simple
// std::to_underlying only in C++23
template <class T>
inline constexpr typename std::underlying_type_t<T> to_underlying(T val) noexcept {
return static_cast<typename std::underlying_type<T>::type>(val);
}
But it'd still won't ensure you use the right types
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.
How about we figure a way to have EXPECT_ENUM_EQ
and a EXPECT_ENUM_NE
macros.
You could use them like
auto v1 = EnumClass::val1;
EXPECT_ENUM_EQ(v1, EnumClass::val2);
EXPECT_ENUM_NE(v1, EnumClass::val1);
And they'd format to
/path/tofile.cpp:107: Failure
In comparing enums of type 'EnumClass', Expected equality of these values:
v1
Which is: 0
EnumClass::val2
Which is: 1
/path/tofile.cpp:110: Failure
In comparing enums of type 'EnumClass'
Expected: (v1) != (EnumClass::val1)
Actual: both are 0
or maybe just
/path/tofile.cpp:107: Failure
Expected equality of these values:
v1
Which is: EnumClass(0)
EnumClass::val2
Which is: EnumClass(1)
/path/tofile.cpp:110: Failure
Expected: (v1) != (EnumClass::val1) actual: both are EnumClass(0)
I'd be happy to work on that!
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 beef is not with the error message, it's with the compare_enums(X1, X2, false)
"gotcha", so I like the EXPECT_ENUM_EQ
/EXPECT_ENUM_NE
solution. We don't have to do it in this PR. I will revert these changes and we can do this separately. Thanks.
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 reverted.
{ | ||
auto found = state.dataSetPointManager->spmMap.find(Name); | ||
return (found != state.dataSetPointManager->spmMap.end()) ? found->second : 0; | ||
} // GetSetPointManagerIndex() |
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.
So this is the reason all SPMs must have a unique name? Not that having unique names is a bad thing, or that users should want to have unique names across all SPMs, just that a name/type would always be unique if a unique name is used within each class. I am only thinking about this because of UnitarySystem now managing more parent object classes and whether there is anything I can do about not forcing the requirement of unique names (and the new getIndex function added to UnitarySystem in #10452). I think I am landing on unique names being a good thing going forward. Why would you name a PTAC the same as a CoilSystem? or a scheduled SPM the same as a mixed air SPM? In fact, it may be a good thing to force unique object names for every object in E+.
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.
Well, there are at least three things going on here. First, previously name uniqueness was not forced on SPMs at all. Not within an SPM sub-class, not across sub-classes. And I am guessing this is because some created a situation in which SPM input data was read twice and couldn't think of a way to resolve this issue other than disabling the check.
Second, I don't think we actually need to enforce name uniqueness within the instances of a certain IDF object because epJSON does that already. It uses the object names as keys and you can't have duplicate keys.
Third, the "convention" within EnergyPlus right now is to enforce name uniqueness within a "parent" class for lack of a better term. So, there is name uniqueness across all curves, and all schedules, and all chillers, etc. This is what "Global Names" is used for and this is what I did here. We currently do not enforce name uniqueness across an entire IDF file. We can start to do that, I am not sure how much that will help. Name uniqueness within the same class hierarchy is helpful because ultimately you are going to want to handle members of that hierarchy in a uniform way (so you can delete 65% of the code!) and that means storing them in the same data structures. If you have multiple objects of the same name in the same data structure, things can get a bit messy/verbose because then you need a secondary key everywhere to tell one from the other. But you're not going to run into that problem for objects that are not in the same class hierarchy. Having a Schedule and a Fan with the same name is not a problem internally because those two objects will never be handled by the same piece of code.
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.
Couple of nitpicks (I know, no one asked for my opinion, sorry).
src/EnergyPlus/SetPointManager.cc
Outdated
"MINIMUMMASSFLOWRATE"}; | ||
|
||
constexpr std::array<std::string_view, static_cast<int>(ControlStrategy::Num)> strategyNamesUC = { | ||
constexpr std::array<std::string_view, (int)HVAC::CtrlVarType::Num> ctrlVarTypeNames = { |
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.
Im curious why change the static_cast to a C-style cast?
C++ style cast are checked by the compiler, more restrictive, and can be searched for easily.
(Never use a C-style cast in C++ is my rule)
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.
Your opinion is more than welcome. I don't mark you as a reviewer because I figured you have enough other things to do (as do I, but that's a different story), but if you have the bandwidth and interest.
As for this, this is the only idiom (enum as an array index) where I use C-style casts, and it's mostly for readability. My presumption is that all enums are implemented as int
s by the compiler. In my 30+ years of programming, I have never seen them implemented any other way, so the assumption is that this upcast is safe even without a check. Furthermore, if you are using an enum
as a std::array
index, you are already explicitly saying that you are using the enum
as an it.
Note, I agree with you about the EXPECT_EQ
thing. That's a different idiom, and using int casts there was a bridge too far. But I don't see a problem with them here in this idiom.
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 understand the rationale.
For completeness, The default is that C++ enum class
uses integer, C enums are unsigned. In other words:
enum e1 {}; <=> enum e1: unsigned {}
enum class e2 {}; <=> enum e2: int (};
If you use the enum value for indexing into a std::array (and you can only do that because we do not have the related conversion compiler warnings enabled), you're actually saying myarray[static_cast<std::size_t>(myEnumValue)]
.
Which is probably fine in E+ since we typically go from Invalid = -1 to something small anyways, except if you were use the Invalid = -1 we have, and do myarray[MyEnum::Invalid]
. static_cast<size_t>(-1) gives 18,446,744,073,709,551,616, which is definitely going to raise an out-of-bounds array indexing for our enums, but in a debug build only. In release, this is UB.
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.
There are actually two kinds of enum
s in EnergyPlus. Those that come from the IDF file, and those that are used only internally. The ones used internally should never be set to Invalid
and probably don't even need an Invalid
value. The ones that come from EnergyPlus probably also don't need an Invalid
value either because @rraustad loves to remind me, those get trapped in the epJson parser and never get to EnergyPlus proper.
You can go ahead and enable the compiler warnings. In fact, I see that somehow a -w
snuck into flags.cmake
in energypluslib.dir
which you probably want to get rid of while you are at it.
src/EnergyPlus/SetPointManager.hh
Outdated
DefineSZMinHumSetPointManager() : NumZones(0), NumCtrlNodes(0), SetPt(0.0) | ||
{ | ||
} | ||
int schedNum = 0; | ||
|
||
void calculate(EnergyPlusData &state); |
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.
void calculate(EnergyPlusData &state); | |
void calculate(EnergyPlusData &state) override; |
Same comment applies to all of them deriving from SPMBase.
Your code works, but marking it override has two advantages:
- It's clearer that's what your doing, and clearer is good.
- The compiler will be nice enough to yell at you if you mess up the function signature and there's no such parent function to override.
- (I do realize in this specific case since your parent virtual function is a pure virtual one, so if you messed it up the compiler would still yell at you, but still)
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.
TIL why it's good to use override
. It always seemed nonsensical to me. Thanks!
src/EnergyPlus/SetPointManager.hh
Outdated
enum class SupplyFlowTempStrategy | ||
{ | ||
Invalid = -1, | ||
MaxTemp, | ||
MinTemp, | ||
Num | ||
}; | ||
|
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.
Trailing whitespaces in that file.
Real64 TotEnergy = 0; // Total energy consumptions at this time step | ||
Real64 TotEnergyPre = 0; // Total energy consumptions at the previous time step | ||
|
||
Array1D<SetPointManager::SPMBase *> spms; |
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'm pretty sure you picked a raw pointer on purpose after consideration, but in the off chance not, I'd prefer using std::unique_ptr<SPMBase>
. (And maybe a std::array or EPVector to contain 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.
"But performance" you may say. well,
https://quick-bench.com/q/9xIjZoQqSyiz-uhlVFTFpN7fsn0
Locally I get more similar results, but still, my point is that this does not hinder performance.
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'm pretty sure you picked a raw pointer on purpose after consideration, but in the off chance not, I'd prefer using
std::unique_ptr<SPMBase>
. (And maybe a std::array or EPVector to contain it?)
I did use a raw pointer on consideration. What is the consideration for std::unique_pointer
?EnergyPlus is not a traditional scientific program, but it does have the same simple memory management pattern as scientific programs do. You allocate everything at the beginning, then you compute, then you deallocate. There's no need for reference counting and garbage collection in that model. Just add delete
loops to clear_state
. To me, std::unique_pointer
is just one more box in C++-library bingo.
Also, I'm out on EPVector
. I no longer see the point and in fact I am convinced that a container that can be accessed as both a 0-based and 1-based array is only a source of bugs. I could change to std::vector
and I have done that in some modules that are more self-contained than SPM
(e.g., OutputProcessor
). It will probably take another day or so to dig up the corner cases that arise. Is that worth it for this PR?
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.
"But performance" you may say. well,
https://quick-bench.com/q/9xIjZoQqSyiz-uhlVFTFpN7fsn0
Locally I get more similar results, but still, my point is that this does not hinder performance.
I will not say "but performance". I will say "C++ bingo".
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.
Regarding EPVector
, the original intent was as a shim to move toward std::vector
and std::array
and not as something we should keep around forever. With EPVector
, we wanted something that would work well enough with Array1D
(which allows 0-based and 1-based indexing and is the reason that EPVector
does it) that switching wouldn't break everything.
New code should definitely use std::vector/array
unless there's a compelling reason to go another direction. At the time that EPVector
was introduced, there were a lot of problematic cases (slicing etc.) that are less present now. Anything that's an EPVector
now should (hopefully) just need the indexing addressed in order to be switched.
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 agree. I think this is why I changed my mind about it. I was pro EPVector
earlier because there was just a lot of Array1D
madness that we needed to move away from. But now that we've moved away from a lot of it, the 0-based/1-based duality has become somewhat of a bug rather than a feature for external references (I was bitten by it a few times during the Fans
transition). FWIW I actually think that Array1D also allows mixed indexing to some degree and we should disable it.
I also agree that we need to move to 0-based indices incrementally and I was thinking of doing that here, but it took me a little while to actually work the kinks out of this module and by the time I was done I was too tired to deal with that type of change. I will probably go back and do it in a separate pass.
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.
There used to be way too much slicing going on, that's for sure. Let's not add any new Array1D
objects if we can help it. With Fortran indexing being what it is (negative indices and all that), disabling either operator()
or operator[]
on Array1D
might be harder than just moving to EPVector
and then on to std::vector
.
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 was thinking of disabling operator []
on it. I don't think it's used in more than a handful of places, maybe I'm wrong.
Lots of fantastic stuff here. And my god, NET -6000 lines of code is amazing. I appreciate your commentary @jmarrec, some of which led to commits and other things which are tabled for later. I am happy to apply Clang Format and get that cleaned, but as @mjwitte noted, this will need a transition rule. It will need to, at a minimum, alert the user about the unique name requirement, and at a maximum, actually rename things. I'm guessing the former is sufficient. @mjwitte do you know of an example of that already in place in one of the transition versions? Or should I just try it out from scratch? |
I have one more quick commit based on @jmarrec comments. There is already a duplicate name error printed. |
You are referring to the EnergyPlus code itself failing, right? Should we also alert the user during the transition process? This is really a change of behavior from 24.1 to 24.2, so I think the idea is to let them know about the change while they are updating their input files. Of course if someone makes a brand new file in 24.2, we'd also warn them that it is invalid when the execute that file. I'm not strongly opinionated here, but happy to chip in on a transition warning if it is warranted. |
Yes, I was referring to EnergyPlus itself. I don't know about the other thing. If anything, we should probably have been alerting the user to the fact that duplicate names were not being check-for previously since that seems to be the "rule" for pretty much every other "family" of objects in EnergyPlus. |
I can't think of a similar transition. The good thing is that SPM names are never used elsewhere in the input except possibly for the two output variables for SetpointManager:WarmestTemperatureFlow. |
@Myoldmopar I made a working implementation of detecting non unique SPM names accross all types and throwing a fatal if not in #10542 |
Because you’re a sicko, that’s why. |
SPM - Fortran Transition - throw if SPM names are not unique
Alright, thanks to @jmarrec this now has the transition rules in place to enforce (warn) uniqueness during transition workflows. I just applied Clang Format, so CI should be clean. There has been some good conversation here about the overall architecture and philosophy, and that can continue for a bit while we wait on things to get confirmed. |
We're not discussing architecture per se, we're discussing use of various C++ features and idioms. Specifically:
|
I merged develop in and resolved the conflicts due to the merge of |
OK, I think this one is ready to go. I'll let CI wrap up since it is almost done, but it looks clean, just BND diffs. Speak up quick if there is anything else here! |
OK fine, merging! Thanks @amirroth. And reviewers. |
Object-oriented refactor of the
SetPointManager
module. The original module had a class hierarchy but instances of each class were contained in separate arrays resulting in a significant amount of redundant code. The new implementation uses a single array of pointers to objects of allSetPointManager
sub-classes, enabling significant code reuse. Overall, almost 6,000 lines of code (65%) were removed fromSetPointManager.[cc|hh]
. This was an especially redundant module and so savings of this magnitude may not be possible elsewhere, but savings of about half that should be. We'll have to see.Some highlights:
GetInput
routine.GetInput
routine, there is code that is guarded by aPRESERVE_IDF_ORDER
macro. This establishes a template for preserving IDF order in epJSON-based input code. The approach is the same as the one used ingetObjectItem
(thanks @mjwitte).getRealFieldValue
andgetAlphaFieldValue
.CI is green except for a few diffs in some
.bnd
files. These are caused by the fact that SetPointManager input data is now read and processed only once. In some simulations it used to be processed twice, once inInternalGains
and then again later inManageHVAC
. Double processing caused differences in the number of timesLoopNode
objects were referenced. I suspect that this is also why there wasn't a duplicate name check. When input is processed twice, every name is a duplicate.