Repository: NREL/OpenStudio (language: python)

Issue:
HeatExchangerAirToAirSensibleAndLatent normalization divisor error
Issue overview
--------------


@mdahlhausen  reported an issue where the Normalization Divisor is zero at https://github.com/NREL/OpenStudio/pull/5099#issuecomment-2050633394

Need to determine what's causing this and ensure it doesn't happen.

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->

## Expected Behavior
<!--- Tell us what should happen -->

## Steps to Reproduce

@mdahlhausen can you please help here?

<!-- Provide an unambiguous set of steps to reproduce this bug. -->
<!--- Provide a link to a model that has the bug (a minimum complete and verifiable example (MCVE) is preferred) and/or code to reproduce -->
1.
2.
3.
4.

## Possible Solution
<!--- Optional, but if you can, suggest a fix/reason for the bug, -->

## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version): all
 - Version of OpenStudio (if using an intermediate build, include SHA): 3.8.0 circa RC1

### Context
<!--- How has this issue affected you? What are you trying to accomplish? -->

HeatExchangerAirToAirSensibleAndLatent normalization divisor error
Issue overview
--------------


@mdahlhausen  reported an issue where the Normalization Divisor is zero at https://github.com/NREL/OpenStudio/pull/5099#issuecomment-2050633394

Need to determine what's causing this and ensure it doesn't happen.

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->

## Expected Behavior
<!--- Tell us what should happen -->

## Steps to Reproduce

@mdahlhausen can you please help here?

<!-- Provide an unambiguous set of steps to reproduce this bug. -->
<!--- Provide a link to a model that has the bug (a minimum complete and verifiable example (MCVE) is preferred) and/or code to reproduce -->
1.
2.
3.
4.

## Possible Solution
<!--- Optional, but if you can, suggest a fix/reason for the bug, -->

## Details

### Environment

Some additional details about your environment for this issue (if relevant):
 - Platform (Operating system, version): all
 - Version of OpenStudio (if using an intermediate build, include SHA): 3.8.0 circa RC1

### Context
<!--- How has this issue affected you? What are you trying to accomplish? -->



Discussion / hints:
I'm guessing the issue might be VT, or maybe a backward-compatible setter. I can't reproduce with model

```ruby
include OpenStudio::Model
m = Model.new

# Hx does not have curves assigned yet
hx = HeatExchangerAirToAirSensibleAndLatent.new(m)
puts hx
OS:HeatExchanger:AirToAir:SensibleAndLatent,
  {8c6f9457-55f0-4773-9e9a-7a90368cdbd9}, !- Handle
  Heat Exchanger Air To Air Sensible And Latent 2, !- Name
  {ace21dbd-41a6-4465-b0d2-1e263620a860}, !- Availability Schedule
  autosize,                               !- Nominal Supply Air Flow Rate {m3/s}
  0.76,                                   !- Sensible Effectiveness at 100% Heating Air Flow {dimensionless}
  0.68,                                   !- Latent Effectiveness at 100% Heating Air Flow {dimensionless}
  0.76,                                   !- Sensible Effectiveness at 100% Cooling Air Flow {dimensionless}
  0.68,                                   !- Latent Effectiveness at 100% Cooling Air Flow {dimensionless}
  ,                                       !- Supply Air Inlet Node
  ,                                       !- Supply Air Outlet Node
  ,                                       !- Exhaust Air Inlet Node
  ,                                       !- Exhaust Air Outlet Node
  0,                                      !- Nominal Electric Power {W}
  Yes,                                    !- Supply Air Outlet Temperature Control
  Plate,                                  !- Heat Exchanger Type
  None,                                   !- Frost Control Type
  1.7,                                    !- Threshold Temperature {C}
  ,                                       !- Initial Defrost Time Fraction {dimensionless}
  ,                                       !- Rate of Defrost Time Fraction Increase {1/K}
  Yes,                                    !- Economizer Lockout
  ,                                       !- Sensible Effectiveness of Heating Air Flow Curve Name
  ,                                       !- Latent Effectiveness of Heating Air Flow Curve Name
  ,                                       !- Sensible Effectiveness of Cooling Air Flow Curve Name
  ;                                       !- Latent Effectiveness of Cooling Air Flow Curve Name

hx.assignHistoricalEffectivenessCurves

m.getTableLookups.map{|t| [t.nameString, t.normalizationDivisor]}.to_h
=> {"Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensCoolEff"=>0.76,
 "Heat Exchanger Air To Air Sensible And Latent 1_LatHeatEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensHeatEff"=>0.76}
```

Can't do it either with the backward-compatible setters...

```ruby
m = Model.new
hx = HeatExchangerAirToAirSensibleAndLatent.new(m)
hx.setSensibleEffectivenessat75HeatingAirFlow(0.68)
hx.setLatentEffectivenessat75HeatingAirFlow(0.68)
hx.setSensibleEffectivenessat75CoolingAirFlow(0.68)
hx.setLatentEffectivenessat75CoolingAirFlow(0.68)
m.getTableLookups.map{|t| [t.nameString, t.normalizationDivisor]}.to_h
=> {"Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensCoolEff"=>0.76,
 "Heat Exchanger Air To Air Sensible And Latent 1_LatHeatEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensHeatEff"=>0.76}
```

Can't reproduce either via VT.

```shell
/usr/local/openstudio-3.7.0/bin/openstudio -e "m = OpenStudio::Model::Model.new; hx = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(m); m.save('test.osm')"
```

```ruby
[1] test(main)> OpenStudio.openStudioLongVersion
=> "3.8.0-beta+d2f0bdd21a"
[2] test(main)> m = osload('test.osm')
=> #<OpenStudio::Model::Model:0x00007f15f8477b00 @__swigtype__="_p_openstudio__model__Model">
[3] test(main)> m.getTableLookups.map{|t| [t.nameString, t.normalizationDivisor]}.to_h
=> {"Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_LatHeatEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensCoolEff"=>0.76,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensHeatEff"=>0.76}
```

Apparently it's going to be fixed on openstudio-standards.
Reopening this issue, because it is still possible to cause a divide by zero error in a common use case of the air to air HX:
```
model = OpenStudio::Model::Model.new
heat_exchanger = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(model)
heat_exchanger.setLatentEffectivenessat100HeatingAirFlow(0.0)
heat_exchanger.setLatentEffectivenessat75HeatingAirFlow(0.0)
heat_exchanger.setLatentEffectivenessat100CoolingAirFlow(0.0)
heat_exchanger.setLatentEffectivenessat75CoolingAirFlow(0.0)
model.save('hx_out.osm', true)
```

creates a model with a Table with a 0 normalization divisor:

```
OS:HeatExchanger:AirToAir:SensibleAndLatent,
  {5e0c8290-c998-417e-871a-5e1d81fdebca}, !- Handle
  Heat Exchanger Air To Air Sensible And Latent 1, !- Name
  {75a43233-8ab7-4b33-9d8e-e1ce660b3080}, !- Availability Schedule
  autosize,                               !- Nominal Supply Air Flow Rate {m3/s}
  0.76,                                   !- Sensible Effectiveness at 100% Heating Air Flow {dimensionless}
  0,                                      !- Latent Effectiveness at 100% Heating Air Flow {dimensionless}
  0.76,                                   !- Sensible Effectiveness at 100% Cooling Air Flow {dimensionless}
  0,                                      !- Latent Effectiveness at 100% Cooling Air Flow {dimensionless}
  ,                                       !- Supply Air Inlet Node
  ,                                       !- Supply Air Outlet Node
  ,                                       !- Exhaust Air Inlet Node
  ,                                       !- Exhaust Air Outlet Node
  0,                                      !- Nominal Electric Power {W}
  Yes,                                    !- Supply Air Outlet Temperature Control
  Plate,                                  !- Heat Exchanger Type
  None,                                   !- Frost Control Type
  1.7,                                    !- Threshold Temperature {C}
  ,                                       !- Initial Defrost Time Fraction {dimensionless}
  ,                                       !- Rate of Defrost Time Fraction Increase {1/K}
  Yes,                                    !- Economizer Lockout
  ,                                       !- Sensible Effectiveness of Heating Air Flow Curve Name
  {961209b4-3204-4900-a992-c77738ff3203}, !- Latent Effectiveness of Heating Air Flow Curve Name
  ,                                       !- Sensible Effectiveness of Cooling Air Flow Curve Name
  {ac21cf30-8b67-45c9-bac1-f7bee22148a3}; !- Latent Effectiveness of Cooling Air Flow Curve Name

OS:Table:Lookup,
  {ac21cf30-8b67-45c9-bac1-f7bee22148a3}, !- Handle
  Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff, !- Name
  {ec872863-15b9-4ae8-b55a-3854b2709453}, !- Independent Variable List Name
  DivisorOnly,                            !- Normalization Method
  0,                                      !- Normalization Divisor
  0,                                      !- Minimum Output {BasedOnField A5}
  10,                                     !- Maximum Output {BasedOnField A5}
  Dimensionless,                          !- Output Unit Type
  ,                                       !- External File Name
  ,                                       !- External File Column Number
  ,                                       !- External File Starting Row Number
  0,                                      !- Output Value 1 {BasedOnField A5}
  0;                                      !- Output Value 2 {BasedOnField A5}
```

A proposed solution is to check if the effectiveness is zero, and don't create the air flow curve if so.
```
t = TableLookup.new(m)
t.setNormalizationDivisor(0)
=> true
puts t

OS:Table:Lookup,
  {d7e6d211-cf84-4977-a37e-09c35bd1128a}, !- Handle
  Table Lookup 1,                         !- Name
  {d1a909ea-8356-411e-868f-c8f5962c4f51}, !- Independent Variable List Name
  None,                                   !- Normalization Method
  0,                                      !- Normalization Divisor
  ,                                       !- Minimum Output {BasedOnField A5}
  ,                                       !- Maximum Output {BasedOnField A5}
  Dimensionless;                          !- Output Unit Type
OS:Table:Lookup,
  {d7e6d211-cf84-4977-a37e-09c35bd1128a}, !- Handle
  Table Lookup 1,                         !- Name
  {d1a909ea-8356-411e-868f-c8f5962c4f51}, !- Independent Variable List Name
  None,                                   !- Normalization Method
  0,                                      !- Normalization Divisor
  ,                                       !- Minimum Output {BasedOnField A5}
  ,                                       !- Maximum Output {BasedOnField A5}
  Dimensionless;                          !- Output Unit Type
OS:Table:Lookup,
  {d7e6d211-cf84-4977-a37e-09c35bd1128a}, !- Handle
  Table Lookup 1,                         !- Name
  {d1a909ea-8356-411e-868f-c8f5962c4f51}, !- Independent Variable List Name
  None,                                   !- Normalization Method
  0,                                      !- Normalization Divisor
  ,                                       !- Minimum Output {BasedOnField A5}
  ,                                       !- Maximum Output {BasedOnField A5}
  Dimensionless;                          !- Output Unit Type
```

The problem is what we copied the E+ idd blindly, and it does not have any constraints on the value it accepts at all.

I will reject 0.
I'm guessing the issue might be VT, or maybe a backward-compatible setter. I can't reproduce with model

```ruby
include OpenStudio::Model
m = Model.new

# Hx does not have curves assigned yet
hx = HeatExchangerAirToAirSensibleAndLatent.new(m)
puts hx
OS:HeatExchanger:AirToAir:SensibleAndLatent,
  {8c6f9457-55f0-4773-9e9a-7a90368cdbd9}, !- Handle
  Heat Exchanger Air To Air Sensible And Latent 2, !- Name
  {ace21dbd-41a6-4465-b0d2-1e263620a860}, !- Availability Schedule
  autosize,                               !- Nominal Supply Air Flow Rate {m3/s}
  0.76,                                   !- Sensible Effectiveness at 100% Heating Air Flow {dimensionless}
  0.68,                                   !- Latent Effectiveness at 100% Heating Air Flow {dimensionless}
  0.76,                                   !- Sensible Effectiveness at 100% Cooling Air Flow {dimensionless}
  0.68,                                   !- Latent Effectiveness at 100% Cooling Air Flow {dimensionless}
  ,                                       !- Supply Air Inlet Node
  ,                                       !- Supply Air Outlet Node
  ,                                       !- Exhaust Air Inlet Node
  ,                                       !- Exhaust Air Outlet Node
  0,                                      !- Nominal Electric Power {W}
  Yes,                                    !- Supply Air Outlet Temperature Control
  Plate,                                  !- Heat Exchanger Type
  None,                                   !- Frost Control Type
  1.7,                                    !- Threshold Temperature {C}
  ,                                       !- Initial Defrost Time Fraction {dimensionless}
  ,                                       !- Rate of Defrost Time Fraction Increase {1/K}
  Yes,                                    !- Economizer Lockout
  ,                                       !- Sensible Effectiveness of Heating Air Flow Curve Name
  ,                                       !- Latent Effectiveness of Heating Air Flow Curve Name
  ,                                       !- Sensible Effectiveness of Cooling Air Flow Curve Name
  ;                                       !- Latent Effectiveness of Cooling Air Flow Curve Name

hx.assignHistoricalEffectivenessCurves

m.getTableLookups.map{|t| [t.nameString, t.normalizationDivisor]}.to_h
=> {"Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensCoolEff"=>0.76,
 "Heat Exchanger Air To Air Sensible And Latent 1_LatHeatEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensHeatEff"=>0.76}
```

Can't do it either with the backward-compatible setters...

```ruby
m = Model.new
hx = HeatExchangerAirToAirSensibleAndLatent.new(m)
hx.setSensibleEffectivenessat75HeatingAirFlow(0.68)
hx.setLatentEffectivenessat75HeatingAirFlow(0.68)
hx.setSensibleEffectivenessat75CoolingAirFlow(0.68)
hx.setLatentEffectivenessat75CoolingAirFlow(0.68)
m.getTableLookups.map{|t| [t.nameString, t.normalizationDivisor]}.to_h
=> {"Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensCoolEff"=>0.76,
 "Heat Exchanger Air To Air Sensible And Latent 1_LatHeatEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensHeatEff"=>0.76}
```

Can't reproduce either via VT.

```shell
/usr/local/openstudio-3.7.0/bin/openstudio -e "m = OpenStudio::Model::Model.new; hx = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(m); m.save('test.osm')"
```

```ruby
[1] test(main)> OpenStudio.openStudioLongVersion
=> "3.8.0-beta+d2f0bdd21a"
[2] test(main)> m = osload('test.osm')
=> #<OpenStudio::Model::Model:0x00007f15f8477b00 @__swigtype__="_p_openstudio__model__Model">
[3] test(main)> m.getTableLookups.map{|t| [t.nameString, t.normalizationDivisor]}.to_h
=> {"Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_LatHeatEff"=>0.68,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensCoolEff"=>0.76,
 "Heat Exchanger Air To Air Sensible And Latent 1_SensHeatEff"=>0.76}
```

Apparently it's going to be fixed on openstudio-standards.
Reopening this issue, because it is still possible to cause a divide by zero error in a common use case of the air to air HX:
```
model = OpenStudio::Model::Model.new
heat_exchanger = OpenStudio::Model::HeatExchangerAirToAirSensibleAndLatent.new(model)
heat_exchanger.setLatentEffectivenessat100HeatingAirFlow(0.0)
heat_exchanger.setLatentEffectivenessat75HeatingAirFlow(0.0)
heat_exchanger.setLatentEffectivenessat100CoolingAirFlow(0.0)
heat_exchanger.setLatentEffectivenessat75CoolingAirFlow(0.0)
model.save('hx_out.osm', true)
```

creates a model with a Table with a 0 normalization divisor:

```
OS:HeatExchanger:AirToAir:SensibleAndLatent,
  {5e0c8290-c998-417e-871a-5e1d81fdebca}, !- Handle
  Heat Exchanger Air To Air Sensible And Latent 1, !- Name
  {75a43233-8ab7-4b33-9d8e-e1ce660b3080}, !- Availability Schedule
  autosize,                               !- Nominal Supply Air Flow Rate {m3/s}
  0.76,                                   !- Sensible Effectiveness at 100% Heating Air Flow {dimensionless}
  0,                                      !- Latent Effectiveness at 100% Heating Air Flow {dimensionless}
  0.76,                                   !- Sensible Effectiveness at 100% Cooling Air Flow {dimensionless}
  0,                                      !- Latent Effectiveness at 100% Cooling Air Flow {dimensionless}
  ,                                       !- Supply Air Inlet Node
  ,                                       !- Supply Air Outlet Node
  ,                                       !- Exhaust Air Inlet Node
  ,                                       !- Exhaust Air Outlet Node
  0,                                      !- Nominal Electric Power {W}
  Yes,                                    !- Supply Air Outlet Temperature Control
  Plate,                                  !- Heat Exchanger Type
  None,                                   !- Frost Control Type
  1.7,                                    !- Threshold Temperature {C}
  ,                                       !- Initial Defrost Time Fraction {dimensionless}
  ,                                       !- Rate of Defrost Time Fraction Increase {1/K}
  Yes,                                    !- Economizer Lockout
  ,                                       !- Sensible Effectiveness of Heating Air Flow Curve Name
  {961209b4-3204-4900-a992-c77738ff3203}, !- Latent Effectiveness of Heating Air Flow Curve Name
  ,                                       !- Sensible Effectiveness of Cooling Air Flow Curve Name
  {ac21cf30-8b67-45c9-bac1-f7bee22148a3}; !- Latent Effectiveness of Cooling Air Flow Curve Name

OS:Table:Lookup,
  {ac21cf30-8b67-45c9-bac1-f7bee22148a3}, !- Handle
  Heat Exchanger Air To Air Sensible And Latent 1_LatCoolEff, !- Name
  {ec872863-15b9-4ae8-b55a-3854b2709453}, !- Independent Variable List Name
  DivisorOnly,                            !- Normalization Method
  0,                                      !- Normalization Divisor
  0,                                      !- Minimum Output {BasedOnField A5}
  10,                                     !- Maximum Output {BasedOnField A5}
  Dimensionless,                          !- Output Unit Type
  ,                                       !- External File Name
  ,                                       !- External File Column Number
  ,                                       !- External File Starting Row Number
  0,                                      !- Output Value 1 {BasedOnField A5}
  0;                                      !- Output Value 2 {BasedOnField A5}
```

A proposed solution is to check if the effectiveness is zero, and don't create the air flow curve if so.
```
t = TableLookup.new(m)
t.setNormalizationDivisor(0)
=> true
puts t

OS:Table:Lookup,
  {d7e6d211-cf84-4977-a37e-09c35bd1128a}, !- Handle
  Table Lookup 1,                         !- Name
  {d1a909ea-8356-411e-868f-c8f5962c4f51}, !- Independent Variable List Name
  None,                                   !- Normalization Method
  0,                                      !- Normalization Divisor
  ,                                       !- Minimum Output {BasedOnField A5}
  ,                                       !- Maximum Output {BasedOnField A5}
  Dimensionless;                          !- Output Unit Type
OS:Table:Lookup,
  {d7e6d211-cf84-4977-a37e-09c35bd1128a}, !- Handle
  Table Lookup 1,                         !- Name
  {d1a909ea-8356-411e-868f-c8f5962c4f51}, !- Independent Variable List Name
  None,                                   !- Normalization Method
  0,                                      !- Normalization Divisor
  ,                                       !- Minimum Output {BasedOnField A5}
  ,                                       !- Maximum Output {BasedOnField A5}
  Dimensionless;                          !- Output Unit Type
OS:Table:Lookup,
  {d7e6d211-cf84-4977-a37e-09c35bd1128a}, !- Handle
  Table Lookup 1,                         !- Name
  {d1a909ea-8356-411e-868f-c8f5962c4f51}, !- Independent Variable List Name
  None,                                   !- Normalization Method
  0,                                      !- Normalization Divisor
  ,                                       !- Minimum Output {BasedOnField A5}
  ,                                       !- Maximum Output {BasedOnField A5}
  Dimensionless;                          !- Output Unit Type
```

The problem is what we copied the E+ idd blindly, and it does not have any constraints on the value it accepts at all.

I will reject 0.

Fix patch:
diff --git a/src/model/HeatExchangerAirToAirSensibleAndLatent.cpp b/src/model/HeatExchangerAirToAirSensibleAndLatent.cpp
index 55209a07cb8..328ac5a9296 100644
--- a/src/model/HeatExchangerAirToAirSensibleAndLatent.cpp
+++ b/src/model/HeatExchangerAirToAirSensibleAndLatent.cpp
@@ -35,6 +35,8 @@
 // Remove when deprecated methods are removed
 #include "../utilities/core/DeprecatedHelpers.hpp"
 
+#include <limits>
+
 namespace openstudio {
 
 namespace model {
@@ -1026,8 +1028,13 @@ namespace model {
       return setCurveAt75(*curve_, sensibleEffectivenessat75HeatingAirFlow);
     }
 
-    auto tableLookup = makeTable(sensibleEffectivenessat75HeatingAirFlow, sensibleEffectivenessat100HeatingAirFlow(), model(),
-                                 fmt::format("{}_SensHeatEff", nameString()));
+    const auto val100 = sensibleEffectivenessat100HeatingAirFlow();
+    if (std::abs(val100) < std::numeric_limits<double>::min()) {
+      LOG_AND_THROW("Unable to set " << briefDescription()
+                                     << "'s Sensible Effectiveness at 75 Heating Air Flow because the value at 100 is zero and would lead to a "
+                                        "Normalization divisor equal to zero.");
+    }
+    auto tableLookup = makeTable(sensibleEffectivenessat75HeatingAirFlow, val100, model(), fmt::format("{}_SensHeatEff", nameString()));
     return setSensibleEffectivenessofHeatingAirFlowCurve(tableLookup);
   }
 
@@ -1038,8 +1045,13 @@ namespace model {
       return setCurveAt75(*curve_, latentEffectivenessat75HeatingAirFlow);
     }
 
-    auto tableLookup =
-      makeTable(latentEffectivenessat75HeatingAirFlow, latentEffectivenessat100HeatingAirFlow(), model(), fmt::format("{}_LatHeatEff", nameString()));
+    const auto val100 = latentEffectivenessat100HeatingAirFlow();
+    if (std::abs(val100) < std::numeric_limits<double>::min()) {
+      LOG_AND_THROW("Unable to set " << briefDescription()
+                                     << "'s Latent Effectiveness at 75 Heating Air Flow because the value at 100 is zero and would lead to a "
+                                        "Normalization divisor equal to zero.");
+    }
+    auto tableLookup = makeTable(latentEffectivenessat75HeatingAirFlow, val100, model(), fmt::format("{}_LatHeatEff", nameString()));
     return setLatentEffectivenessofHeatingAirFlowCurve(tableLookup);
   }
 
@@ -1050,8 +1062,13 @@ namespace model {
       return setCurveAt75(*curve_, sensibleEffectivenessat75CoolingAirFlow);
     }
 
-    auto tableLookup = makeTable(sensibleEffectivenessat75CoolingAirFlow, sensibleEffectivenessat100CoolingAirFlow(), model(),
-                                 fmt::format("{}_SensCoolEff", nameString()));
+    const auto val100 = sensibleEffectivenessat100CoolingAirFlow();
+    if (std::abs(val100) < std::numeric_limits<double>::min()) {
+      LOG_AND_THROW("Unable to set " << briefDescription()
+                                     << "'s Sensible Effectiveness at 75 Cooling Air Flow because the value at 100 is zero and would lead to a "
+                                        "Normalization divisor equal to zero.");
+    }
+    auto tableLookup = makeTable(sensibleEffectivenessat75CoolingAirFlow, val100, model(), fmt::format("{}_SensCoolEff", nameString()));
     return setSensibleEffectivenessofCoolingAirFlowCurve(tableLookup);
   }
 
@@ -1062,8 +1079,13 @@ namespace model {
       return setCurveAt75(*curve_, latentEffectivenessat75CoolingAirFlow);
     }
 
-    auto tableLookup =
-      makeTable(latentEffectivenessat75CoolingAirFlow, latentEffectivenessat100CoolingAirFlow(), model(), fmt::format("{}_LatCoolEff", nameString()));
+    const auto val100 = latentEffectivenessat100CoolingAirFlow();
+    if (std::abs(val100) < std::numeric_limits<double>::min()) {
+      LOG_AND_THROW("Unable to set " << briefDescription()
+                                     << "'s Latent Effectiveness at 75 Cooling Air Flow because the value at 100 is zero and would lead to a "
+                                        "Normalization divisor equal to zero.");
+    }
+    auto tableLookup = makeTable(latentEffectivenessat75CoolingAirFlow, val100, model(), fmt::format("{}_LatCoolEff", nameString()));
     return setLatentEffectivenessofCoolingAirFlowCurve(tableLookup);
   }
 
diff --git a/src/model/TableLookup.cpp b/src/model/TableLookup.cpp
index b85107ac973..0ace1a568d2 100644
--- a/src/model/TableLookup.cpp
+++ b/src/model/TableLookup.cpp
@@ -26,6 +26,7 @@
 
 #include <algorithm>
 #include <iomanip>
+#include <limits>
 
 namespace openstudio {
 namespace model {
@@ -165,8 +166,13 @@ namespace model {
       return result;
     }
 
-    bool TableLookup_Impl::setNormalizationDivisor(double normalizationDivisior) {
-      bool result = setDouble(OS_Table_LookupFields::NormalizationDivisor, normalizationDivisior);
+    bool TableLookup_Impl::setNormalizationDivisor(double normalizationDivisor) {
+      if (std::abs(normalizationDivisor) < std::numeric_limits<double>::min()) {
+        LOG(Warn, "Unable to set " << briefDescription() << "'s Normalization divisor to zero.");
+        return false;
+      }
+
+      bool result = setDouble(OS_Table_LookupFields::NormalizationDivisor, normalizationDivisor);
       return result;
     }
 
@@ -374,8 +380,8 @@ namespace model {
     return getImpl<detail::TableLookup_Impl>()->setNormalizationMethod(normalizationMethod);
   }
 
-  bool TableLookup::setNormalizationDivisor(double normalizationDivisior) {
-    return getImpl<detail::TableLookup_Impl>()->setNormalizationDivisor(normalizationDivisior);
+  bool TableLookup::setNormalizationDivisor(double normalizationDivisor) {
+    return getImpl<detail::TableLookup_Impl>()->setNormalizationDivisor(normalizationDivisor);
   }
 
   bool TableLookup::setMinimumOutput(double minimumOutput) {
diff --git a/src/model/TableLookup.hpp b/src/model/TableLookup.hpp
index 2d17a155b31..04ce8ec2a0c 100644
--- a/src/model/TableLookup.hpp
+++ b/src/model/TableLookup.hpp
@@ -69,7 +69,7 @@ namespace model {
 
     bool setNormalizationMethod(const std::string& normalizationMethod);
 
-    bool setNormalizationDivisor(double normalizationDivisior);
+    bool setNormalizationDivisor(double normalizationDivisor);
 
     bool setMinimumOutput(double minimumOutput);
     void resetMinimumOutput();
diff --git a/src/model/TableLookup_Impl.hpp b/src/model/TableLookup_Impl.hpp
index 24c4d075a98..43e913f9200 100644
--- a/src/model/TableLookup_Impl.hpp
+++ b/src/model/TableLookup_Impl.hpp
@@ -72,7 +72,7 @@ namespace model {
 
       bool setNormalizationMethod(const std::string& normalizationMethod);
 
-      bool setNormalizationDivisor(double normalizationDivisior);
+      bool setNormalizationDivisor(double normalizationDivisor);
 
       bool setMinimumOutput(double minimumOutput);
       void resetMinimumOutput();


Test patch:
diff --git a/src/model/test/HeatExchangerAirToAirSensibleAndLatent_GTest.cpp b/src/model/test/HeatExchangerAirToAirSensibleAndLatent_GTest.cpp
index be3ccabcfe2..9aea91c6a64 100644
--- a/src/model/test/HeatExchangerAirToAirSensibleAndLatent_GTest.cpp
+++ b/src/model/test/HeatExchangerAirToAirSensibleAndLatent_GTest.cpp
@@ -401,6 +401,26 @@ TEST_F(ModelFixture, HeatExchangerAirToAirSensibleAndLatent_Deprecated75Effectiv
     EXPECT_TRUE(hx.setLatentEffectivenessat75CoolingAirFlow(val2));
     EXPECT_DOUBLE_EQ(val2, hx.latentEffectivenessat75CoolingAirFlow());
   }
+
+  // Ensure we protect users against dumb issues that lead to a normalization divisor of zero
+  {
+    // Ensure no curves assigned for now
+    hx.resetSensibleEffectivenessofHeatingAirFlowCurve();
+    hx.resetLatentEffectivenessofHeatingAirFlowCurve();
+    hx.resetSensibleEffectivenessofCoolingAirFlowCurve();
+    hx.resetLatentEffectivenessofCoolingAirFlowCurve();
+
+    // IMHO this doesn't make sense (IDD should be `\minimum> 0.0`), but the E+ IDD and OS IDD have always allowed it, so leaving it untouched
+    EXPECT_TRUE(hx.setSensibleEffectivenessat100HeatingAirFlow(0.0));
+    EXPECT_TRUE(hx.setLatentEffectivenessat100HeatingAirFlow(0.0));
+    EXPECT_TRUE(hx.setSensibleEffectivenessat100CoolingAirFlow(0.0));
+    EXPECT_TRUE(hx.setLatentEffectivenessat100CoolingAirFlow(0.0));
+
+    EXPECT_ANY_THROW(hx.setSensibleEffectivenessat75HeatingAirFlow(0.5));
+    EXPECT_ANY_THROW(hx.setLatentEffectivenessat75HeatingAirFlow(0.5));
+    EXPECT_ANY_THROW(hx.setSensibleEffectivenessat75CoolingAirFlow(0.5));
+    EXPECT_ANY_THROW(hx.setLatentEffectivenessat75CoolingAirFlow(0.5));
+  }
 }
 
 TEST_F(ModelFixture, HeatExchangerAirToAirSensibleAndLatent_assignHistoricalEffectivenessCurves) {
diff --git a/src/model/test/TableLookup_GTest.cpp b/src/model/test/TableLookup_GTest.cpp
index 4156343639d..cb7d7d0152e 100644
--- a/src/model/test/TableLookup_GTest.cpp
+++ b/src/model/test/TableLookup_GTest.cpp
@@ -51,6 +51,10 @@ TEST_F(ModelFixture, TableLookup_GettersSetters) {
   EXPECT_TRUE(tableLookup.setNormalizationDivisor(0.5));
   EXPECT_EQ(0.5, tableLookup.normalizationDivisor());
 
+  // #5145 - We reject 0 as a normalization divisor
+  EXPECT_FALSE(tableLookup.setNormalizationDivisor(0.0));
+  EXPECT_EQ(0.5, tableLookup.normalizationDivisor());
+
   // Minimum Output: Optional Double
   EXPECT_TRUE(tableLookup.setMinimumOutput(0.6));
   ASSERT_TRUE(tableLookup.minimumOutput());