diff --git a/energytool/base/parse_results.py b/energytool/base/parse_results.py index 0121889..00dbe7b 100644 --- a/energytool/base/parse_results.py +++ b/energytool/base/parse_results.py @@ -141,7 +141,9 @@ def get_output_variable( results = eplus_res.loc[:, mask] if drop_suffix: - new_columns = [re.sub(f":{variables}.+", "", col) for col in results.columns] - results.columns = new_columns + strip_pattern = re.compile( + ":(?:" + "|".join(re.escape(v) for v in variable_names_list) + ").+" + ) + results.columns = [strip_pattern.sub("", col) for col in results.columns] return results diff --git a/energytool/system.py b/energytool/system.py index 53cc435..fc8f29c 100644 --- a/energytool/system.py +++ b/energytool/system.py @@ -244,7 +244,9 @@ class Sensor(System): (list of strings). Default is '*' meaning all the available variables. """ - def __init__(self, name: str, variables: str, key_values: str | list[str] = "*"): + def __init__( + self, name: str, variables: str | list[str], key_values: str | list[str] = "*" + ): super().__init__(name=name, category=SystemCategories.SENSOR) self.variables = variables self.key_values = key_values @@ -257,13 +259,16 @@ def pre_process(self, idf: IDF): ) def post_process(self, idf: IDF = None, eplus_results: pd.DataFrame = None): - results = get_output_variable( - eplus_res=eplus_results, - key_values=self.key_values, - variables=self.variables, - ) - results.columns = results.columns + f"_{self.variables}" - return results + per_variable_results = [] + for variable in to_list(self.variables): + res = get_output_variable( + eplus_res=eplus_results, + key_values=self.key_values, + variables=variable, + ) + res.columns = res.columns + f"_{variable}" + per_variable_results.append(res) + return pd.concat(per_variable_results, axis=1) class SimplifiedChiller(System): diff --git a/tests/test_outputs.py b/tests/test_outputs.py index 202edd5..67b674e 100644 --- a/tests/test_outputs.py +++ b/tests/test_outputs.py @@ -127,14 +127,20 @@ def test_get_output_zone_variable(self): ), ) - pd.testing.assert_frame_equal( - toy_df.iloc[:, [0, 4]], - get_output_variable( - eplus_res=toy_df, - key_values="Zone1", - variables=[ - "Equipment Total Heating Energy", - "Ideal Loads Supply Air Total Heating Energy", - ], - ), + # Real Sensor usage passes variable names that themselves start with + # "Zone" (e.g. "Zone Ideal Loads Supply Air Total Heating Energy"), so + # this must mirror that instead of variable names with no relation to + # the column's own key prefix - otherwise the drop_suffix regex bug + # (corrupted char-class silently failing to match) goes unnoticed. + multi_variable_result = get_output_variable( + eplus_res=toy_df, + key_values="Zone1", + variables=[ + "Zone Other Equipment Total Heating Energy", + "Zone Ideal Loads Supply Air Total Heating Energy", + ], + ) + assert list(multi_variable_result.columns) == ["ZONE1", "ZONE1"] + assert list(multi_variable_result.iloc[0]) == list( + toy_df.iloc[0, [0, 4]] ) diff --git a/tests/test_system.py b/tests/test_system.py index 0439aec..9906bd4 100644 --- a/tests/test_system.py +++ b/tests/test_system.py @@ -133,6 +133,50 @@ def test_sensor(self): rel=0.05, ) + def test_sensor_multi_variable(self): + """A Sensor built with a list of several variables must return one + distinctly-named, non-collapsed column per variable (regression test + for the silent multi-variable column collapse bug).""" + test_build = Building(idf_path=RESOURCES_PATH / "test.idf") + test_build.add_system( + sys.Sensor( + name="IDEAL_LOADS", + variables=[ + "Zone Ideal Loads Supply Air Total Heating Energy", + "Zone Ideal Loads Supply Air Total Cooling Energy", + ], + key_values="*", + ) + ) + + result = test_build.simulate( + parameter_dict={}, + simulation_options={ + "epw_file": (RESOURCES_PATH / "Paris_2020.epw").as_posix(), + "outputs": "SENSOR", + "verbose": "v", + }, + ) + + heating_cols = [ + c + for c in result.columns + if c.endswith("_Zone Ideal Loads Supply Air Total Heating Energy") + ] + cooling_cols = [ + c + for c in result.columns + if c.endswith("_Zone Ideal Loads Supply Air Total Cooling Energy") + ] + + assert len(heating_cols) == 4 + assert len(cooling_cols) == 4 + assert not set(heating_cols) & set(cooling_cols) + # the two variables must carry distinct, non-collapsed values + assert not np.allclose( + result[heating_cols].sum().to_numpy(), result[cooling_cols].sum().to_numpy() + ) + def test_heater_simple(self, idf): gas_boiler = sys.HeaterSimple( name="Main_boiler",