Repository: NREL/hescore-hpxml

Issue:
Area weighted R-value fix for knee walls
Proposed changes:

- Get rid of the center of cavity effective R-value lookups for walls and attics. Do area weighted R-values with nominal cavity R-values only.
- If the continuous insulation is >= R-5, choose the appropriate construction type. Do not add additional continuous R-value to the cavity R-value (not sure if we're already doing this).
- For cases where one surface is close to R-0, set it to some small minimum R-value. @lirainer is going to come up with a very well thought out value to use here.

@lirainer @gdickey627

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 3ce39c42..58094f71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ docs/build
 hescore_hpxml.egg-info
 dist
 build
-
+.env
+.devcontainer
diff --git a/examples/house8.json b/examples/house8.json
index b87518e7..7f070bf4 100644
--- a/examples/house8.json
+++ b/examples/house8.json
@@ -27,7 +27,7 @@
           "roof_assembly_code": "rfwf00tg",
           "roof_color": "medium",
           "roof_type": "vented_attic",
-          "ceiling_assembly_code": "ecwf30",
+          "ceiling_assembly_code": "ecwf38",
           "zone_skylight": {
             "skylight_area": 0
           }
@@ -63,40 +63,40 @@
       "window_construction_same": false,
       "zone_wall": [
         {
-          "side": "back",
+          "side": "front",
           "wall_assembly_code": "ewwf00wo",
           "zone_window": {
-            "window_area": 220.0,
+            "window_area": 180.0,
             "window_method": "code",
             "window_code": "dpeaw",
             "solar_screen": false
           }
         },
         {
-          "side": "right",
+          "side": "left",
           "wall_assembly_code": "ewwf00wo",
           "zone_window": {
-            "window_area": 40.0,
+            "window_area": 70.0,
             "window_method": "code",
             "window_code": "dpeaw",
             "solar_screen": false
           }
         },
         {
-          "side": "front",
+          "side": "back",
           "wall_assembly_code": "ewwf00wo",
           "zone_window": {
-            "window_area": 180.0,
+            "window_area": 220.0,
             "window_method": "code",
             "window_code": "dpeaw",
             "solar_screen": false
           }
         },
         {
-          "side": "left",
+          "side": "right",
           "wall_assembly_code": "ewwf00wo",
           "zone_window": {
-            "window_area": 70.0,
+            "window_area": 40.0,
             "window_method": "code",
             "window_code": "dpeaw",
             "solar_screen": false
diff --git a/hescorehpxml/base.py b/hescorehpxml/base.py
index 084ab53b..3dc254f5 100644
--- a/hescorehpxml/base.py
+++ b/hescorehpxml/base.py
@@ -376,6 +376,32 @@ class HPXMLtoHEScoreTranslatorBase(object):
                           'ground-to-air': 'gchp',
                           'ground-to-water': 'gchp'}
 
+    def get_attic_knee_wall_rvalue_and_area(self, attic, b):
+        knee_walls = self.get_attic_knee_walls(attic, b)
+        if len(knee_walls) == 0:
+            return 0, 0
+        if len(knee_walls) == 1:
+            knee_wall_r = convert_to_type(float, self.xpath(knee_walls[0], 'sum(h:Insulation/h:Layer/h:NominalRValue)'))
+            knee_wall_area = convert_to_type(float, self.xpath(knee_walls[0], 'h:Area/text()'))
+            return knee_wall_r, knee_wall_area
+
+        knee_wall_dict_ls = []
+        for knee_wall in knee_walls:
+            wall_area = convert_to_type(float, self.xpath(knee_wall, 'h:Area/text()'))
+            if wall_area is None:
+                raise TranslationError('All attic knee walls need an Area specified')
+            rvalue = self.xpath(knee_wall, 'sum(h:Insulation/h:Layer/h:NominalRValue)')
+            knee_wall_dict_ls.append({'area': wall_area, 'rvalue': rvalue})
+        # Average
+        knee_wall_area = sum(x['area'] for x in knee_wall_dict_ls)
+        try:
+            knee_wall_r = knee_wall_area / \
+                          sum(x['area'] / x['rvalue'] for x in knee_wall_dict_ls)
+        except ZeroDivisionError:
+            knee_wall_r = 0
+
+        return knee_wall_r, knee_wall_area
+
     def add_fuel_type(self, fuel_type):
         # Some fuel types are not included in fuel_type_mapping, throw an error if not mapped.
         try:
@@ -1153,20 +1179,16 @@ class HPXMLtoHEScoreTranslatorBase(object):
             # attic floor center of cavity R-value
             attic_floor_rvalue = self.get_attic_floor_rvalue(attic, b)
             if knee_walls:
-                try:
-                    knee_wall_ua = sum(x['area'] / x['rvalue'] for x in knee_walls)
-                except ZeroDivisionError:
-                    knee_wall_ua = float('inf')
-                knee_wall_area = sum([x['area'] for x in knee_walls])
-                try:
-                    attic_floor_adj_ua = knee_wall_ua + atticd['roof_area'] / attic_floor_rvalue
-                except ZeroDivisionError:
-                    attic_floor_adj_ua = float('inf')
+                knee_wall_rvalue, knee_wall_area = self.get_attic_knee_wall_rvalue_and_area(attic, b)
+                knee_wall_coc_rvalue = knee_wall_rvalue + 0.5
+                knee_wall_ua = knee_wall_area / knee_wall_coc_rvalue
+
+                attic_floor_coc_rvalue = attic_floor_rvalue + 0.5
+                attic_floor_adj_ua = knee_wall_ua + atticd['roof_area'] / attic_floor_coc_rvalue
                 attic_floor_adj_area = knee_wall_area + atticd['roof_area']
-                attic_floor_rvalue = attic_floor_adj_area / attic_floor_adj_ua
+                attic_floor_rvalue = (attic_floor_adj_area / attic_floor_adj_ua) - 0.5
                 atticd['roof_area'] = attic_floor_adj_area
-            atticd['attic_floor_coc_rvalue'] = \
-                min(attic_floor_rvalues, key=lambda x: abs(x - attic_floor_rvalue)) + 0.5
+            atticd['attic_floor_coc_rvalue'] = attic_floor_rvalue + 0.5
 
         if len(atticds) == 0:
             raise TranslationError('There are no Attic elements in this building.')
diff --git a/hescorehpxml/hpxml2.py b/hescorehpxml/hpxml2.py
index 2cb15f7c..fc0345f9 100644
--- a/hescorehpxml/hpxml2.py
+++ b/hescorehpxml/hpxml2.py
@@ -54,12 +54,7 @@ class HPXML2toHEScoreTranslator(HPXMLtoHEScoreTranslatorBase):
                 raise_err=True,
                 kneewallid=kneewall_idref
             )
-            wall_rvalue = self.xpath(wall, 'sum(h:Insulation/h:Layer/h:NominalRValue)')
-            wall_area = self.xpath(wall, 'h:Area/text()')
-            if wall_area is None:
-                raise TranslationError('All attic knee walls need an Area specified')
-            wall_area = float(wall_area)
-            knee_walls.append({'area': wall_area, 'rvalue': wall_rvalue})
+            knee_walls.append(wall)
 
         return knee_walls
 
diff --git a/hescorehpxml/hpxml3.py b/hescorehpxml/hpxml3.py
index 44bf7a71..9a227e76 100644
--- a/hescorehpxml/hpxml3.py
+++ b/hescorehpxml/hpxml3.py
@@ -69,12 +69,7 @@ class HPXML3toHEScoreTranslator(HPXMLtoHEScoreTranslatorBase):
                 raise_err=True,
                 kneewallid=kneewall_idref
             )
-            wall_rvalue = self.xpath(wall, 'sum(h:Insulation/h:Layer/h:NominalRValue)')
-            wall_area = self.xpath(wall, 'h:Area/text()')
-            if wall_area is None:
-                raise TranslationError('All attic knee walls need an Area specified')
-            wall_area = float(wall_area)
-            knee_walls.append({'area': wall_area, 'rvalue': wall_rvalue})
+            knee_walls.append(wall)
 
         return knee_walls
 


Test patch:
diff --git a/tests/test_translation.py b/tests/test_translation.py
index 669ae14e..50571e69 100644
--- a/tests/test_translation.py
+++ b/tests/test_translation.py
@@ -928,7 +928,7 @@ class TestOtherHouses(unittest.TestCase, ComparatorBase):
         kneewall_rvalue_el.text = '0'
         # run translation
         resp = tr.hpxml_to_hescore()
-        self.assertEqual(resp['building']['zone']['zone_roof'][0]['ceiling_assembly_code'], 'ecwf00')
+        self.assertEqual(resp['building']['zone']['zone_roof'][0]['ceiling_assembly_code'], 'ecwf03')
         self.assertAlmostEqual(resp['building']['zone']['zone_roof'][0]['roof_area'], 1400.0)
         # Set kneewall R-value to 11 and attic floor R-value to zero
         kneewall_rvalue_el.text = '11'