Skip to content

Comments

Fix #4673 - Allow setting ZoneMixing objects at Space level#4700

Merged
jmarrec merged 4 commits intodevelopfrom
4673_ZoneMixing_SpaceLevel
Oct 3, 2022
Merged

Fix #4673 - Allow setting ZoneMixing objects at Space level#4700
jmarrec merged 4 commits intodevelopfrom
4673_ZoneMixing_SpaceLevel

Conversation

@jmarrec
Copy link
Collaborator

@jmarrec jmarrec commented Sep 28, 2022

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)
  • 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 - HVAC component - Model component - IDF Translation Pull Request - Ready for CI This pull request if finalized and is ready for continuous integration verification prior to merge. APIChange A motivated non-backward compatible change that breaks the existing API and needs to be communicated labels Sep 28, 2022
@jmarrec jmarrec self-assigned this Sep 28, 2022
@jmarrec jmarrec force-pushed the 4673_ZoneMixing_SpaceLevel branch from 2808696 to a4fd9e6 Compare September 30, 2022 14:37
Comment on lines +1468 to +1476
for (ZoneMixing& zm : space.supplyZoneMixing()) {
zm.hardSize();
zm.setParent(newSpace);
}

for (ZoneMixing& zm : space.exhaustZoneMixing()) {
// We only need to hardSize when it's the RECEIVING zone
zm.setSourceSpace(newSpace);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ThermalZone::combineSpaces

Comment on lines +345 to +367
bool ZoneMixing_Impl::hardSize() {
boost::optional<Space> space = this->space();
if (!space) {
return false;
}

// source zone cannot be the same as this zone
if (zone.handle() != this->zone().handle()) {
result = setPointer(OS_ZoneMixingFields::SourceZoneName, zone.handle());
if (this->designFlowRate()) {
return true;
}
return result;
}

void ZoneMixing_Impl::resetSourceZone() {
bool result = setString(OS_ZoneMixingFields::SourceZoneName, "");
OS_ASSERT(result);
if (boost::optional<double> flowRateperFloorArea = this->flowRateperFloorArea()) {
return this->setDesignFlowRate(*flowRateperFloorArea * space->floorArea());
}

if (boost::optional<double> flowRateperPerson = this->flowRateperPerson()) {
return this->setDesignFlowRate(*flowRateperPerson * space->numberOfPeople());
}

if (boost::optional<double> airChangesperHour = this->airChangesperHour()) {
return this->setDesignFlowRate(*airChangesperHour * space->volume() / 3600.0);
}

return 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.

hardSize method

Comment on lines +170 to +199
bool ZoneMixing_Impl::setSourceZone(const ThermalZone& zone) {

auto receiving = this->zoneOrSpace();
// source zone cannot be the same as this zone
if (zone.handle() == receiving.handle()) {
LOG(Warn, "For " << briefDescription() << ", Source Zone cannot be the same as the Receiving Zone '" << zone.nameString() << "'.");
return false;
}
if (receiving.iddObjectType() != IddObjectType::OS_ThermalZone) {
LOG(Warn, "For " << briefDescription() << ", Receiving is a Space, so you cannot set Source as a Zone.");
return false;
}

return setPointer(OS_ZoneMixingFields::SourceZoneorSpaceName, zone.handle());
}

bool ZoneMixing_Impl::setSourceSpace(const Space& space) {
auto receiving = this->zoneOrSpace();
// source zone cannot be the same as this zone
if (space.handle() == receiving.handle()) {
LOG(Warn, "For " << briefDescription() << ", Source Space cannot be the same as the Receiving Space '" << space.nameString() << "'.");
return false;
}
if (receiving.iddObjectType() != IddObjectType::OS_Space) {
LOG(Warn, "For " << briefDescription() << ", Receiving is a Zone, so you cannot set Source as a Space.");
return false;
}

return setPointer(OS_ZoneMixingFields::SourceZoneorSpaceName, space.handle());
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Protect users from themselves

Comment on lines +74 to +76
boost::optional<ThermalZone> zone() const;
boost::optional<Space> space() const;
ModelObject zoneOrSpace() 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.

this is the API break

Comment on lines +85 to +86
OS_DEPRECATED boost::optional<double> flowRateperZoneFloorArea() const;
boost::optional<double> flowRateperFloorArea() 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.

Lots of deprecations

Comment on lines +218 to +286
TEST_F(ModelFixture, ZoneMixing_CantMixSpacesAndZones) {

Model m;

constexpr double width = 10.0;
constexpr double height = 3.6; // It's convenient for ACH, since 3600 s/hr
// constexpr double spaceFloorArea = width * width;
// constexpr double spaceVolume = spaceFloorArea * height;
// constexpr double oneWallArea = width * height;

// y (=North)
// ▲
// │ building height = 3m
// 10├────────┼────────┼────────┤
// │ │ │ │
// │ Zone 1 │ Zone 1 │ Zone 2 │
// │ Space 1│ Space 2│ Space 3│
// │ │ │ │
// └────────┴────────┴────────┴─────► x
// 0 10 20 30

// Counterclockwise points
std::vector<Point3d> floorPointsSpace1{{0.0, 0.0, 0.0}, {0.0, width, 0.0}, {width, width, 0.0}, {width, 0.0, 0.0}};

auto space1 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
auto space2 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space2.setXOrigin(width);
auto space3 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space3.setXOrigin(width * 2);

ThermalZone z1(m);
space1.setThermalZone(z1);
space2.setThermalZone(z1);

ThermalZone z2(m);
space3.setThermalZone(z2);

{
ZoneMixing zm(space1);
// Can't mix Spaces and Zones
EXPECT_FALSE(zm.setSourceZone(z1));
EXPECT_FALSE(zm.sourceZoneOrSpace());
EXPECT_FALSE(zm.sourceZone());
EXPECT_FALSE(zm.sourceSpace());

EXPECT_TRUE(zm.setSourceSpace(space3));
ASSERT_TRUE(zm.sourceZoneOrSpace());
EXPECT_EQ(space3, zm.sourceZoneOrSpace().get());
ASSERT_TRUE(zm.sourceSpace());
EXPECT_EQ(space3, zm.sourceSpace().get());
EXPECT_FALSE(zm.sourceZone());
}

{
ZoneMixing zm(z1);
// Can't mix Spaces and Zones
EXPECT_FALSE(zm.setSourceSpace(space1));
EXPECT_FALSE(zm.sourceZoneOrSpace());
EXPECT_FALSE(zm.sourceZone());
EXPECT_FALSE(zm.sourceSpace());

EXPECT_TRUE(zm.setSourceZone(z2));
ASSERT_TRUE(zm.sourceZoneOrSpace());
EXPECT_EQ(z2, zm.sourceZoneOrSpace().get());
ASSERT_TRUE(zm.sourceZone());
EXPECT_EQ(z2, zm.sourceZone().get());
EXPECT_FALSE(zm.sourceSpace());
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Test protecting users

Comment on lines +288 to +481
TEST_F(ModelFixture, ZoneMixing_CombineSpacesDifferentZones) {

Model m;

constexpr double width = 10.0;
constexpr double height = 3.6; // It's convenient for ACH, since 3600 s/hr
constexpr double spaceFloorArea = width * width;
constexpr double spaceVolume = spaceFloorArea * height;
constexpr double numPeoplePerSpace = 10.0;

// y (=North)
// ▲
// │ building height = 3m
// 10├────────┼────────┼────────┼────────┤
// │ │ │ │ │
// │ Zone 1 │ Zone 1 │ Zone 2 │ Zone 2 │
// │ Space 1│ Space 2│ Space 3│ Space 4│
// │ │ │ │ │
// └────────┴────────┴────────┴────────┴──► x
// 0 10 20 30 40

// Counterclockwise points
std::vector<Point3d> floorPointsSpace1{{0.0, 0.0, 0.0}, {0.0, width, 0.0}, {width, width, 0.0}, {width, 0.0, 0.0}};

auto space1 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
auto space2 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space2.setXOrigin(width);
auto space3 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space3.setXOrigin(width * 2);
auto space4 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space4.setXOrigin(width * 3);

ThermalZone z1(m);
space1.setThermalZone(z1);
space2.setThermalZone(z1);

ThermalZone z2(m);
space3.setThermalZone(z2);
space4.setThermalZone(z2);

// Add 10 People in each space
SpaceType buildingSpaceType(m);
buildingSpaceType.setName("buildingSpaceType");
PeopleDefinition pd(m);
pd.setNumberofPeople(numPeoplePerSpace);
People p(pd);
EXPECT_TRUE(p.setSpaceType(buildingSpaceType));
auto building = m.getUniqueModelObject<Building>();
EXPECT_TRUE(building.setSpaceType(buildingSpaceType));

ZoneMixing zmAbs(space3);
EXPECT_TRUE(zmAbs.setSourceSpace(space2));
constexpr double m3sAbs = 0.6;
EXPECT_TRUE(zmAbs.setDesignFlowRate(m3sAbs));

ZoneMixing zmFloor(space2);
EXPECT_TRUE(zmFloor.setSourceSpace(space4));
constexpr double m3sPerFlow = 0.001;
constexpr double equivm3s_Floor = m3sPerFlow * spaceFloorArea;
EXPECT_TRUE(zmFloor.setFlowRateperFloorArea(m3sPerFlow));

ZoneMixing zmPerson(space4);
EXPECT_TRUE(zmPerson.setSourceSpace(space1));
constexpr double m3sPerPerson = 0.03;
constexpr double equivm3s_Person = m3sPerPerson * numPeoplePerSpace;
EXPECT_TRUE(zmPerson.setFlowRateperPerson(m3sPerPerson));

ZoneMixing zmACH(space1);
EXPECT_TRUE(zmACH.setSourceSpace(space3));
constexpr double ach = 0.5;
constexpr double equivm3s_ACH = ach * spaceVolume / 3600.0;
EXPECT_TRUE(zmACH.setAirChangesperHour(ach));

// | Zone Mixing | Receiving | Source | Flow/Space | Per Floor Area | Per Person | ACH ) || Calculated Total |
// | Name | Space | Space | (m3/s) | (m3/s-m2) | (m3/s.p) | (1/hr) || For 1 Space (m3/s) |
// |------------------|-----------|---------|------------|----------------|------------|----------||--------------------|
// | zmAbs | space3 | space2 | 0.6 | | | || 0.6 |
// | zmFloor | space2 | space4 | | 0.001 | | || 0.1 |
// | zmPerson | space4 | space1 | | | 0.03 | || 0.3 |
// | zmACH | space1 | space3 | | | | 0.5 || 0.05 |

EXPECT_EQ(0.6, m3sAbs);
EXPECT_EQ(0.1, equivm3s_Floor);
EXPECT_EQ(0.3, equivm3s_Person);
EXPECT_EQ(0.05, equivm3s_ACH);

// Combine Zone 1's space
// Only receiving spaces trigger a hardsize, so zmFloor and zmACH should be hardsized
auto space12_ = z1.combineSpaces();
ASSERT_TRUE(space12_);

EXPECT_EQ(3, m.getConcreteModelObjects<Space>().size());

ASSERT_TRUE(zmAbs.designFlowRate());
EXPECT_EQ(m3sAbs, zmAbs.designFlowRate().get());
EXPECT_FALSE(zmAbs.flowRateperFloorArea());
EXPECT_FALSE(zmAbs.flowRateperPerson());
EXPECT_FALSE(zmAbs.airChangesperHour());
ASSERT_TRUE(zmAbs.space());
EXPECT_EQ(space3, zmAbs.space().get());
EXPECT_FALSE(zmAbs.zone());
ASSERT_TRUE(zmAbs.sourceSpace());
EXPECT_EQ(space12_.get(), zmAbs.sourceSpace().get());
EXPECT_FALSE(zmAbs.sourceZone());

ASSERT_TRUE(zmFloor.designFlowRate());
EXPECT_EQ(equivm3s_Floor, zmFloor.designFlowRate().get());
EXPECT_FALSE(zmFloor.flowRateperFloorArea());
EXPECT_FALSE(zmFloor.flowRateperPerson());
EXPECT_FALSE(zmFloor.airChangesperHour());
ASSERT_TRUE(zmFloor.space());
EXPECT_EQ(space12_.get(), zmFloor.space().get());
EXPECT_FALSE(zmFloor.zone());
ASSERT_TRUE(zmFloor.sourceSpace());
EXPECT_EQ(space4, zmFloor.sourceSpace().get());
EXPECT_FALSE(zmFloor.sourceZone());

EXPECT_FALSE(zmPerson.designFlowRate());
EXPECT_FALSE(zmPerson.flowRateperFloorArea());
ASSERT_TRUE(zmPerson.flowRateperPerson());
EXPECT_EQ(m3sPerPerson, zmPerson.flowRateperPerson().get());
EXPECT_FALSE(zmPerson.airChangesperHour());
ASSERT_TRUE(zmPerson.space());
EXPECT_EQ(space4, zmPerson.space().get());
EXPECT_FALSE(zmPerson.zone());
ASSERT_TRUE(zmPerson.sourceSpace());
EXPECT_EQ(space12_.get(), zmPerson.sourceSpace().get());
EXPECT_FALSE(zmPerson.sourceZone());

ASSERT_TRUE(zmACH.designFlowRate());
EXPECT_EQ(equivm3s_ACH, zmACH.designFlowRate().get());
EXPECT_FALSE(zmACH.flowRateperFloorArea());
EXPECT_FALSE(zmACH.flowRateperPerson());
EXPECT_FALSE(zmACH.airChangesperHour());
ASSERT_TRUE(zmACH.space());
EXPECT_EQ(space12_.get(), zmACH.space().get());
EXPECT_FALSE(zmACH.zone());
ASSERT_TRUE(zmACH.sourceSpace());
EXPECT_EQ(space3, zmACH.sourceSpace().get());
EXPECT_FALSE(zmACH.sourceZone());

auto space34_ = z2.combineSpaces();
ASSERT_TRUE(space34_);

EXPECT_EQ(2, m.getConcreteModelObjects<Space>().size());

ASSERT_TRUE(zmAbs.designFlowRate());
EXPECT_EQ(m3sAbs, zmAbs.designFlowRate().get());
EXPECT_FALSE(zmAbs.flowRateperFloorArea());
EXPECT_FALSE(zmAbs.flowRateperPerson());
EXPECT_FALSE(zmAbs.airChangesperHour());
ASSERT_TRUE(zmAbs.space());
EXPECT_EQ(space34_.get(), zmAbs.space().get());
EXPECT_FALSE(zmAbs.zone());
ASSERT_TRUE(zmAbs.sourceSpace());
EXPECT_EQ(space12_.get(), zmAbs.sourceSpace().get());
EXPECT_FALSE(zmAbs.sourceZone());

ASSERT_TRUE(zmFloor.designFlowRate());
EXPECT_EQ(equivm3s_Floor, zmFloor.designFlowRate().get());
EXPECT_FALSE(zmFloor.flowRateperFloorArea());
EXPECT_FALSE(zmFloor.flowRateperPerson());
EXPECT_FALSE(zmFloor.airChangesperHour());
ASSERT_TRUE(zmFloor.space());
EXPECT_EQ(space12_.get(), zmFloor.space().get());
EXPECT_FALSE(zmFloor.zone());
ASSERT_TRUE(zmFloor.sourceSpace());
EXPECT_EQ(space34_.get(), zmFloor.sourceSpace().get());
EXPECT_FALSE(zmFloor.sourceZone());

ASSERT_TRUE(zmPerson.designFlowRate());
EXPECT_EQ(equivm3s_Person, zmPerson.designFlowRate().get());
EXPECT_FALSE(zmPerson.flowRateperFloorArea());
EXPECT_FALSE(zmPerson.flowRateperPerson());
EXPECT_FALSE(zmPerson.airChangesperHour());
ASSERT_TRUE(zmPerson.space());
EXPECT_EQ(space34_.get(), zmPerson.space().get());
EXPECT_FALSE(zmPerson.zone());
ASSERT_TRUE(zmPerson.sourceSpace());
EXPECT_EQ(space12_.get(), zmPerson.sourceSpace().get());
EXPECT_FALSE(zmPerson.sourceZone());

ASSERT_TRUE(zmACH.designFlowRate());
EXPECT_EQ(equivm3s_ACH, zmACH.designFlowRate().get());
EXPECT_FALSE(zmACH.flowRateperFloorArea());
EXPECT_FALSE(zmACH.flowRateperPerson());
EXPECT_FALSE(zmACH.airChangesperHour());
ASSERT_TRUE(zmACH.space());
EXPECT_EQ(space12_.get(), zmACH.space().get());
EXPECT_FALSE(zmACH.zone());
ASSERT_TRUE(zmACH.sourceSpace());
EXPECT_EQ(space34_.get(), zmACH.sourceSpace().get());
EXPECT_FALSE(zmACH.sourceZone());
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Detail ThermalZone::combineSpaces test, and the order in which things are done. Some of that is tested in FT, but not all. So I think I like having both tests

Comment on lines +56 to +97
ModelObject zoneOrSpace = modelObject.zoneOrSpace();
boost::optional<ModelObject> sourceZoneOrSpace = modelObject.sourceZoneOrSpace();

auto getParentObjectName = [this](const ModelObject& mo) {
if (!m_excludeSpaceTranslation) {
return mo.nameString();
}

if (auto space_ = mo.optionalCast<Space>()) {
if (auto thermalZone_ = space_->thermalZone()) {
return thermalZone_->nameString();
} else {
OS_ASSERT(false); // This shouldn't happen, since we removed all orphaned spaces earlier in the FT
}
}

return mo.nameString();
};

if (!sourceZoneOrSpace) {
if (m_excludeSpaceTranslation && modelObject.space()) {
LOG(Warn, modelObject.briefDescription()
<< " doesn't have a Source Zone or Space, it will not be translated. As you were using Space-Level ZoneMixing, and you are not "
"translating to Spaces, it's possible it was pointing to two spaces inside the same zone");
} else {
LOG(Warn, modelObject.briefDescription() << " doesn't have a Source Zone or Space, it will not be translated.");
}
return boost::none;
}

if (zoneOrSpace == sourceZoneOrSpace.get()) {
// This isn't going to happen, because zm.setSourceSpace(newSpace) in ThermalZone::combineSpaces will be rejected
// Let's play it safe though
LOG(Warn, modelObject.briefDescription() << " has the same Receiving and Source Zone or Space, it will not be translated.");
if (!m_excludeSpaceTranslation) {
// We don't allow this at model time, the only reason we expect this to happen is when m_excludeSpaceTranslation is true, we call
// combineSpaces, and if the user has a ZoneMixing pointing to two spaces from the same ThermalZone, you end up with matching Receiving and
// Source Spaces
OS_ASSERT(false);
}
return boost::none;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ft adjust + warnings in case the user had a ZoneMixing pointing to two spaces inside the same zone AND opting out of space translation. Yep, that's the state of all the corner cases we have to test for nowadays...

Comment on lines +68 to +388
TEST_F(EnergyPlusFixture, ForwardTranslatorZoneMixing_DifferentZones) {

Model m;

constexpr double width = 10.0;
constexpr double height = 3.6; // It's convenient for ACH, since 3600 s/hr
constexpr double spaceFloorArea = width * width;
constexpr double spaceVolume = spaceFloorArea * height;
constexpr double numPeoplePerSpace = 10.0;

// y (=North)
// ▲
// │ building height = 3m
// 10├────────┼────────┼────────┼────────┤
// │ │ │ │ │
// │ Zone 1 │ Zone 1 │ Zone 2 │ Zone 2 │
// │ Space 1│ Space 2│ Space 3│ Space 4│
// │ │ │ │ │
// └────────┴────────┴────────┴────────┴──► x
// 0 10 20 30 40

// Counterclockwise points
std::vector<Point3d> floorPointsSpace1{{0.0, 0.0, 0.0}, {0.0, width, 0.0}, {width, width, 0.0}, {width, 0.0, 0.0}};

auto space1 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
auto space2 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space2.setXOrigin(width);
auto space3 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space3.setXOrigin(width * 2);
auto space4 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space4.setXOrigin(width * 3);

ThermalZone z1(m);
space1.setThermalZone(z1);
space2.setThermalZone(z1);

ThermalZone z2(m);
space3.setThermalZone(z2);
space4.setThermalZone(z2);

// Add 10 People in each space
SpaceType buildingSpaceType(m);
buildingSpaceType.setName("buildingSpaceType");
PeopleDefinition pd(m);
pd.setNumberofPeople(numPeoplePerSpace);
People p(pd);
EXPECT_TRUE(p.setSpaceType(buildingSpaceType));
auto building = m.getUniqueModelObject<Building>();
EXPECT_TRUE(building.setSpaceType(buildingSpaceType));

ZoneMixing zmAbs(space3);
zmAbs.setName("zmAbs");
EXPECT_TRUE(zmAbs.setSourceSpace(space2));
constexpr double m3sAbs = 0.6;
EXPECT_TRUE(zmAbs.setDesignFlowRate(m3sAbs));

ZoneMixing zmFloor(space2);
zmFloor.setName("zmFloor");
EXPECT_TRUE(zmFloor.setSourceSpace(space4));
constexpr double m3sPerFlow = 0.001;
constexpr double equivm3s_Floor = m3sPerFlow * spaceFloorArea;
EXPECT_TRUE(zmFloor.setFlowRateperFloorArea(m3sPerFlow));

ZoneMixing zmPerson(space4);
zmPerson.setName("zmPerson");
EXPECT_TRUE(zmPerson.setSourceSpace(space1));
constexpr double m3sPerPerson = 0.03;
constexpr double equivm3s_Person = m3sPerPerson * numPeoplePerSpace;
EXPECT_TRUE(zmPerson.setFlowRateperPerson(m3sPerPerson));

ZoneMixing zmACH(space1);
zmACH.setName("zmACH");
EXPECT_TRUE(zmACH.setSourceSpace(space3));
constexpr double ach = 0.5;
constexpr double equivm3s_ACH = ach * spaceVolume / 3600.0;
EXPECT_TRUE(zmACH.setAirChangesperHour(ach));

ZoneMixing zmZone(z1);
zmZone.setName("zmZone");
EXPECT_TRUE(zmZone.setSourceZone(z2));
EXPECT_TRUE(zmZone.setFlowRateperFloorArea(0.008));

// | Zone Mixing | Receiving | Source | Flow/Space | Per Floor Area | Per Person | ACH ) || Calculated Total |
// | Name | Entity | Entity | (m3/s) | (m3/s-m2) | (m3/s.p) | (1/hr) || For 1 Space (m3/s) |
// |------------------|-----------|---------|------------|----------------|------------|----------||--------------------|
// | zmAbs | space3 | space2 | 0.6 | | | || 0.6 |
// | zmFloor | space2 | space4 | | 0.001 | | || 0.1 |
// | zmPerson | space4 | space1 | | | 0.03 | || 0.3 |
// | zmACH | space1 | space3 | | | | 0.5 || 0.05 |
// | zmZone | **z1** | **z2** | | 0.008 | | 0.5 || 0.05 |

EXPECT_EQ(0.6, m3sAbs);
EXPECT_EQ(0.1, equivm3s_Floor);
EXPECT_EQ(0.3, equivm3s_Person);
EXPECT_EQ(0.05, equivm3s_ACH);

EXPECT_TRUE(zmAbs.setDeltaTemperature(0.5));

ScheduleConstant mixingSch(m);
mixingSch.setName("mixingSch");
EXPECT_TRUE(zmZone.setSchedule(mixingSch));

ScheduleConstant deltaTempSch(m);
deltaTempSch.setName("deltaTempSch");
EXPECT_TRUE(zmZone.setDeltaTemperatureSchedule(deltaTempSch));

ScheduleConstant minimumReceivingTempSch(m);
minimumReceivingTempSch.setName("minimumReceivingTempSch");
EXPECT_TRUE(zmZone.setMinimumReceivingTemperatureSchedule(minimumReceivingTempSch));

ScheduleConstant minimumSourceTempSch(m);
minimumSourceTempSch.setName("minimumSourceTempSch");
EXPECT_TRUE(zmZone.setMinimumSourceTemperatureSchedule(minimumSourceTempSch));

ScheduleConstant minimumOutdoorTempSch(m);
minimumOutdoorTempSch.setName("minimumOutdoorTempSch");
EXPECT_TRUE(zmZone.setMinimumOutdoorTemperatureSchedule(minimumOutdoorTempSch));

ScheduleConstant maximumReceivingTempSch(m);
maximumReceivingTempSch.setName("maximumReceivingTempSch");
EXPECT_TRUE(zmZone.setMaximumReceivingTemperatureSchedule(maximumReceivingTempSch));

ScheduleConstant maximumSourceTempSch(m);
maximumSourceTempSch.setName("maximumSourceTempSch");
EXPECT_TRUE(zmZone.setMaximumSourceTemperatureSchedule(maximumSourceTempSch));

ScheduleConstant maximumOutdoorTempSch(m);
maximumOutdoorTempSch.setName("maximumOutdoorTempSch");
EXPECT_TRUE(zmZone.setMaximumOutdoorTemperatureSchedule(maximumOutdoorTempSch));

auto alwaysOnContinuousSchedule = m.alwaysOnContinuousSchedule();

ForwardTranslator ft;
{
ft.setExcludeSpaceTranslation(false);

Workspace w = ft.translateModel(m);

auto zones = w.getObjectsByType(IddObjectType::Zone);
ASSERT_EQ(2, zones.size());
auto zone = zones[0];
EXPECT_EQ(4, w.getObjectsByType(IddObjectType::Space).size());

auto zms = w.getObjectsByType(IddObjectType::ZoneMixing);
ASSERT_EQ(5, zms.size());

for (const auto& zm : zms) {
auto name = zm.nameString();
auto receiving_ = zm.getTarget(ZoneMixingFields::ZoneorSpaceName);
ASSERT_TRUE(receiving_);
auto source_ = zm.getTarget(ZoneMixingFields::SourceZoneorSpaceName);
ASSERT_TRUE(source_);
auto sch_ = zm.getTarget(ZoneMixingFields::ScheduleName);
ASSERT_TRUE(sch_);

if (name == zmZone.nameString()) {
EXPECT_EQ(receiving_->iddObject().type(), IddObjectType::Zone);
EXPECT_EQ(source_->iddObject().type(), IddObjectType::Zone);
EXPECT_EQ(z1.nameString(), receiving_->nameString());
EXPECT_EQ(mixingSch.nameString(), sch_->nameString());
EXPECT_EQ("Flow/Area", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DesignFlowRate));
EXPECT_EQ(0.008, zm.getDouble(ZoneMixingFields::FlowRateperFloorArea).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(z2.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
EXPECT_EQ("deltaTempSch", zm.getString(ZoneMixingFields::DeltaTemperatureScheduleName).get());
EXPECT_EQ("minimumReceivingTempSch", zm.getString(ZoneMixingFields::MinimumReceivingTemperatureScheduleName).get());
EXPECT_EQ("maximumReceivingTempSch", zm.getString(ZoneMixingFields::MaximumReceivingTemperatureScheduleName).get());
EXPECT_EQ("minimumSourceTempSch", zm.getString(ZoneMixingFields::MinimumSourceTemperatureScheduleName).get());
EXPECT_EQ("maximumSourceTempSch", zm.getString(ZoneMixingFields::MaximumSourceTemperatureScheduleName).get());
EXPECT_EQ("minimumOutdoorTempSch", zm.getString(ZoneMixingFields::MinimumOutdoorTemperatureScheduleName).get());
EXPECT_EQ("maximumOutdoorTempSch", zm.getString(ZoneMixingFields::MaximumOutdoorTemperatureScheduleName).get());
continue;
}

EXPECT_EQ(receiving_->iddObject().type(), IddObjectType::Space);
EXPECT_EQ(source_->iddObject().type(), IddObjectType::Space);
EXPECT_EQ(alwaysOnContinuousSchedule.nameString(), sch_->nameString());

EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumReceivingTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumReceivingTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumSourceTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumSourceTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumOutdoorTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumOutdoorTemperatureScheduleName));
if (name == zmAbs.nameString()) {
EXPECT_EQ(space3.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Zone", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_EQ(m3sAbs, zm.getDouble(ZoneMixingFields::DesignFlowRate).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(space2.nameString(), source_->nameString());
EXPECT_EQ(0.5, zm.getDouble(ZoneMixingFields::DeltaTemperature).get());
} else if (name == zmFloor.nameString()) {
EXPECT_EQ(space2.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Area", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DesignFlowRate));
EXPECT_EQ(0.001, zm.getDouble(ZoneMixingFields::FlowRateperFloorArea).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(space4.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
} else if (name == zmPerson.nameString()) {
EXPECT_EQ(space4.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Person", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DesignFlowRate));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_EQ(0.03, zm.getDouble(ZoneMixingFields::FlowRateperPerson).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(space1.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
} else if (name == zmACH.nameString()) {
EXPECT_EQ(space1.nameString(), receiving_->nameString());
EXPECT_EQ("AirChanges/Hour", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DesignFlowRate));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_EQ(0.5, zm.getDouble(ZoneMixingFields::AirChangesperHour).get());
EXPECT_EQ(space3.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
}
}
}

{
ft.setExcludeSpaceTranslation(true);

Workspace w = ft.translateModel(m);

auto zones = w.getObjectsByType(IddObjectType::Zone);
ASSERT_EQ(2, zones.size());
auto zone = zones[0];
EXPECT_EQ(0, w.getObjectsByType(IddObjectType::Space).size());

auto zms = w.getObjectsByType(IddObjectType::ZoneMixing);
ASSERT_EQ(5, zms.size());

for (const auto& zm : zms) {
auto name = zm.nameString();
auto receiving_ = zm.getTarget(ZoneMixingFields::ZoneorSpaceName);
ASSERT_TRUE(receiving_);
auto source_ = zm.getTarget(ZoneMixingFields::SourceZoneorSpaceName);
ASSERT_TRUE(source_);
auto sch_ = zm.getTarget(ZoneMixingFields::ScheduleName);
ASSERT_TRUE(sch_);

EXPECT_EQ(receiving_->iddObject().type(), IddObjectType::Zone);
EXPECT_EQ(source_->iddObject().type(), IddObjectType::Zone);

if (name == zmZone.nameString()) {
EXPECT_EQ(z1.nameString(), receiving_->nameString());
EXPECT_EQ(mixingSch.nameString(), sch_->nameString());
EXPECT_EQ("Flow/Area", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DesignFlowRate));
EXPECT_EQ(0.008, zm.getDouble(ZoneMixingFields::FlowRateperFloorArea).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(z2.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
EXPECT_EQ("deltaTempSch", zm.getString(ZoneMixingFields::DeltaTemperatureScheduleName).get());
EXPECT_EQ("minimumReceivingTempSch", zm.getString(ZoneMixingFields::MinimumReceivingTemperatureScheduleName).get());
EXPECT_EQ("maximumReceivingTempSch", zm.getString(ZoneMixingFields::MaximumReceivingTemperatureScheduleName).get());
EXPECT_EQ("minimumSourceTempSch", zm.getString(ZoneMixingFields::MinimumSourceTemperatureScheduleName).get());
EXPECT_EQ("maximumSourceTempSch", zm.getString(ZoneMixingFields::MaximumSourceTemperatureScheduleName).get());
EXPECT_EQ("minimumOutdoorTempSch", zm.getString(ZoneMixingFields::MinimumOutdoorTemperatureScheduleName).get());
EXPECT_EQ("maximumOutdoorTempSch", zm.getString(ZoneMixingFields::MaximumOutdoorTemperatureScheduleName).get());
continue;
}

EXPECT_EQ(alwaysOnContinuousSchedule.nameString(), sch_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumReceivingTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumReceivingTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumSourceTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumSourceTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumOutdoorTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumOutdoorTemperatureScheduleName));
if (name == zmAbs.nameString()) {
EXPECT_EQ(z2.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Zone", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_EQ(m3sAbs, zm.getDouble(ZoneMixingFields::DesignFlowRate).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(z1.nameString(), source_->nameString());
EXPECT_EQ(0.5, zm.getDouble(ZoneMixingFields::DeltaTemperature).get());
} else if (name == zmFloor.nameString()) {
EXPECT_EQ(z1.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Zone", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_EQ(equivm3s_Floor, zm.getDouble(ZoneMixingFields::DesignFlowRate).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(z2.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
} else if (name == zmPerson.nameString()) {
EXPECT_EQ(z2.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Zone", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_EQ(equivm3s_Person, zm.getDouble(ZoneMixingFields::DesignFlowRate).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(z1.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
} else if (name == zmACH.nameString()) {
EXPECT_EQ(z1.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Zone", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_EQ(equivm3s_ACH, zm.getDouble(ZoneMixingFields::DesignFlowRate).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(z2.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
}
}
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

FT Tests when different zone.

  • Test all combinations of Spaces one
  • Test one zone one
  • Do that when excluding spaces and when including them

Comment on lines +390 to +500
TEST_F(EnergyPlusFixture, ForwardTranslatorZoneMixing_SameZone) {

Model m;

constexpr double width = 10.0;
constexpr double height = 3.6; // It's convenient for ACH, since 3600 s/hr

// y (=North)
// ▲
// │ building height = 3m
// 10├────────┼────────┼
// │ │ │
// │ Zone 1 │ Zone 1 │
// │ Space 1│ Space 2│
// │ │ │ │ │
// └────────┴────────┴────────┴────────┴──► x
// 0 10 20 30 40

// Counterclockwise points
std::vector<Point3d> floorPointsSpace1{{0.0, 0.0, 0.0}, {0.0, width, 0.0}, {width, width, 0.0}, {width, 0.0, 0.0}};

auto space1 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
auto space2 = Space::fromFloorPrint(floorPointsSpace1, height, m).get();
space2.setXOrigin(width);

ThermalZone z1(m);
space1.setThermalZone(z1);
space2.setThermalZone(z1);

ZoneMixing zmAbs(space1);
zmAbs.setName("zmAbs");
EXPECT_TRUE(zmAbs.setSourceSpace(space2));
constexpr double m3sAbs = 0.6;
EXPECT_TRUE(zmAbs.setDesignFlowRate(m3sAbs));

// | Zone Mixing | Receiving | Source | Flow/Space | Per Floor Area | Per Person | ACH ) || Calculated Total |
// | Name | Entity | Entity | (m3/s) | (m3/s-m2) | (m3/s.p) | (1/hr) || For 1 Space (m3/s) |
// |------------------|-----------|---------|------------|----------------|------------|----------||--------------------|
// | zmAbs | space1 | space2 | 0.6 | | | || 0.6 |

auto alwaysOnContinuousSchedule = m.alwaysOnContinuousSchedule();

ForwardTranslator ft;
{
ft.setExcludeSpaceTranslation(false);

Workspace w = ft.translateModel(m);

auto zones = w.getObjectsByType(IddObjectType::Zone);
ASSERT_EQ(1, zones.size());
auto zone = zones[0];
EXPECT_EQ(2, w.getObjectsByType(IddObjectType::Space).size());

auto zms = w.getObjectsByType(IddObjectType::ZoneMixing);
ASSERT_EQ(1, zms.size());
auto& zm = zms.front();

auto name = zm.nameString();
auto receiving_ = zm.getTarget(ZoneMixingFields::ZoneorSpaceName);
ASSERT_TRUE(receiving_);
auto source_ = zm.getTarget(ZoneMixingFields::SourceZoneorSpaceName);
ASSERT_TRUE(source_);
auto sch_ = zm.getTarget(ZoneMixingFields::ScheduleName);
ASSERT_TRUE(sch_);

EXPECT_EQ(receiving_->iddObject().type(), IddObjectType::Space);
EXPECT_EQ(source_->iddObject().type(), IddObjectType::Space);

EXPECT_EQ(name, zmAbs.nameString());
EXPECT_EQ(alwaysOnContinuousSchedule.nameString(), sch_->nameString());
EXPECT_EQ(space1.nameString(), receiving_->nameString());
EXPECT_EQ("Flow/Zone", zm.getString(ZoneMixingFields::DesignFlowRateCalculationMethod).get());
EXPECT_EQ(m3sAbs, zm.getDouble(ZoneMixingFields::DesignFlowRate).get());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperFloorArea));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::FlowRateperPerson));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::AirChangesperHour));
EXPECT_EQ(space2.nameString(), source_->nameString());
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperature));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::DeltaTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumReceivingTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumReceivingTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumSourceTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumSourceTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MinimumOutdoorTemperatureScheduleName));
EXPECT_TRUE(zm.isEmpty(ZoneMixingFields::MaximumOutdoorTemperatureScheduleName));
}

{
ft.setExcludeSpaceTranslation(true);

Workspace w = ft.translateModel(m);

auto zones = w.getObjectsByType(IddObjectType::Zone);
ASSERT_EQ(1, zones.size());
auto zone = zones[0];
EXPECT_EQ(0, w.getObjectsByType(IddObjectType::Space).size());

auto zms = w.getObjectsByType(IddObjectType::ZoneMixing);
ASSERT_EQ(0, zms.size());

// This is problematic, we actually try to FT that object 4 TIMES
// EXPECT_EQ(1, ft.warnings().size());
EXPECT_GE(ft.warnings().size(), 1);
std::string expectedWarning =
"Object of type 'OS:ZoneMixing' and named 'zmAbs' doesn't have a Source Zone or Space, it will not be translated. As you were using "
"Space-Level ZoneMixing, and you are not translating to Spaces, it's possible it was pointing to two spaces inside the same zone";
auto warnings = ft.warnings();
EXPECT_NE(warnings.cend(),
std::find_if(warnings.cbegin(), warnings.cend(), [&expectedWarning](auto& l) { return l.logMessage() == expectedWarning; }));
}
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Make a Space Level ZoneMixing that points to two spaces inside the same zone:

  • When trnaslating to spaces: it should work fine
  • When excluding spaces: it shouldn't translate it and we should get a nice warning.

Note that I noticed we try to translate it 4 times and opened #4704

@shorowit
Copy link
Contributor

  • Verified that I get the same behavior as the previous OS 3.5 install (i.e., an EnergyPlus EMS error) when ZoneMixing is assigned to thermal zones
  • Verified that the EnergyPlus error goes away when ZoneMixing is assigned to spaces

I haven't tried a comprehensive set of scenarios but it works for my case. Thanks @jmarrec!

@jmarrec jmarrec merged commit 029d1c5 into develop Oct 3, 2022
@jmarrec jmarrec deleted the 4673_ZoneMixing_SpaceLevel branch October 3, 2022 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

APIChange A motivated non-backward compatible change that breaks the existing API and needs to be communicated component - HVAC component - IDF Translation 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.

E+ 22.2.0 - ZoneMixing is now allowed at space level

3 participants