diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84cab6dd9f..7b96eba41c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -52,7 +52,7 @@ include: - export BIM2SIM_LOG_LEVEL=ERROR - | if [ "$COVERAGE" = "true" ]; then - coverage run -m unittest discover ~/bim2sim-coding/test + coverage run --omit="*/*_txt*" -m unittest discover ~/bim2sim-coding/test mkdir -p /builds/EBC/EBC_all/github_ci/bim2sim/$CI_COMMIT_REF_NAME/coverage coverage html -d /builds/EBC/EBC_all/github_ci/bim2sim/$CI_COMMIT_REF_NAME/coverage coverage-badge -o /builds/EBC/EBC_all/github_ci/bim2sim/$CI_COMMIT_REF_NAME/coverage/badge.svg @@ -84,11 +84,11 @@ include: if [ -d "${plugin_test_dir}/unit" ] || [ -d "${plugin_test_dir}/integration" ]; then if [ -d "${plugin_test_dir}/unit" ]; then echo "Running unit tests..." - coverage run --source=~/bim2sim-coding/bim2sim/plugins/${plugin} -m unittest discover -v ${plugin_test_dir}/unit + coverage run --omit="*/*_txt*" --source=~/bim2sim-coding/bim2sim/plugins/${plugin} -m unittest discover -v ${plugin_test_dir}/unit fi if [ -d "${plugin_test_dir}/integration" ]; then echo "Running integration tests..." - coverage run --append --source=~/bim2sim-coding/bim2sim/plugins/${plugin} -m unittest discover -v ${plugin_test_dir}/integration + coverage run --omit="*/*_txt*" --append --source=~/bim2sim-coding/bim2sim/plugins/${plugin} -m unittest discover -v ${plugin_test_dir}/integration fi else echo "No unit or integration test directories found." @@ -123,9 +123,9 @@ include: - | set +e if [[ "$CI_JOB_IMAGE" == *"dymola"* ]]; then - xvfb-run -n 77 coverage run -m unittest discover ~/bim2sim-coding/bim2sim/plugins/${plugin}/test/regression + xvfb-run -n 77 coverage run --omit="*/*_txt*" -m unittest discover ~/bim2sim-coding/bim2sim/plugins/${plugin}/test/regression else - coverage run -m unittest discover ~/bim2sim-coding/bim2sim/plugins/${plugin}/test/regression + coverage run --omit="*/*_txt*" -m unittest discover ~/bim2sim-coding/bim2sim/plugins/${plugin}/test/regression fi test_exit_code=$? set -e diff --git a/bim2sim/assets/finder/template_LuArtX_Carf.json b/bim2sim/assets/finder/template_LuArtX_Carf.json index 621eb8461d..23880bf00c 100644 --- a/bim2sim/assets/finder/template_LuArtX_Carf.json +++ b/bim2sim/assets/finder/template_LuArtX_Carf.json @@ -37,5 +37,946 @@ "internal_pump": ["VDI 710.05-Luft-Wasser-Wärmepumpe", "Heizkreispumpe intern"], "vdi_performance_data_table": ["VDI-Tables", "Leistungsdaten"] } + }, + "AirDuctRectangular": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "height_a": ["MyTab1", "[a] (a)"], + "width_b": ["MyTab1", "[b] (b)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "reference_edge": ["MyTab1", "[K] Bezugskante"], + "length": ["MyTab1", "[l] Länge (l)"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "cross_section_area": ["MyTab1", "[QUERSCHNITT] G"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirDuctOval": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_b": ["MyTab1", "[a] Breitendurchmesser B"], + "designation": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "height_h": ["MyTab1", "[b] Höhendurchmesser H"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "reference_edge": ["MyTab1", "[K] Bezugskante"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirDuctRound": { + "default_ps": { + "radius": ["Profile", "Radius"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "cross_section_d": ["MyTab1", "[d] Querschnitt"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "reference_edge": ["MyTab1", "[K] Bezugskante"], + "length": ["MyTab1", "[l] Länge"], + "list_round": ["MyTab1", "[LSTR] Auf Liste Rund"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "pipe_connection": ["MyTab1", "[RAHMEN] Verbindungsart"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirDuctRoundFlexible": { + "default_ps": { + "radius": ["Profile", "Radius"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "cross_section_d": ["MyTab1", "[d] Querschnitt"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "pipe_connection": ["MyTab1", "[RAHMEN] Rohrverbindung"], + "rings": ["MyTab1", "[RI] Ringe"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirDuctRectangularFitting": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "label_swap_ab": ["MyTab1", "[cab] Beschriftung a/b vertauschen"], + "insert": ["MyTab1", "[e] Einschub"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirDuctRoundFitting": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "pen": ["MyTab1", "[PEN] Schtift"] + } + }, + "AirDuctRoundTransition": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_b": ["MyTab1", "[a] Breitendurchmesser B"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "height_h": ["MyTab1", "[b] Höhendurchmesser H"], + "cross_section_output": ["MyTab1", "[d1] Querschnitt Ausgang"], + "e_offset": ["MyTab1", "[c] e-Versatz"], + "f_offset": ["MyTab1", "[f] f-Versatz"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirDuctRoundTransitionSymmetric": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_b": ["MyTab1", "[a] Breitendurchmesser B"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "height_h": ["MyTab1", "[b] Höhendurchmesser H"], + "cross_section_output": ["MyTab1", "[d1] Querschnitt Ausgang"], + "e_offset": ["MyTab1", "[e] Versatz e"], + "f_offset": ["MyTab1", "[f] Versatz f"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirDuctOvalBow": { + "default_ps": { + "radius": ["Profile", "Radius"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_b": ["MyTab1", "[a] Breitendurchmesser B"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "height_h": ["MyTab1", "[b] Höhendurchmesser H"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "angle": ["MyTab1", "[w] Winkel"] + } + }, + "AirDuctRoundBend": { + "default_ps": { + "radius": ["Profile", "Radius"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "neutral_axis": ["MyTab1", "[l] Neutrale Fase"], + "insertion_length": ["MyTab1", "[le2] Einstedekaenge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "connection_type": ["MyTab1", "[RAHMEN] Verbindungsart"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "angle": ["MyTab1", "[w] Winkel"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "edge": ["MyTab1", "[KANTE] Kante"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"] + } + }, + "AirDuctConcentricReduction": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN1"], + "nominal_diameter_d2": ["MyTab1", "[d2] DN2"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "insertion_length": ["MyTab1", "[le2] Einstedekaenge"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "connection_type": ["MyTab1", "[RAHMEN] Verbindungsart"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirDuctRectangularBow": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "height_a": ["MyTab1", "[a] Höhe (a)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "width_start_b": ["MyTab1", "[b] Breite Anfang (b)"], + "width_end_d": ["MyTab1", "[d] Breite Ende (d)"], + "insert_end": ["MyTab1", "[e] Einschub Ende"], + "insert_start": ["MyTab1", "[f] Einschub Anfang"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "guide_vanes": ["MyTab1", "[LBT] Leitbleche"], + "number_guide_vanes": ["MyTab1", "[LB] Anzahl Leitbleche"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "fitting_length": ["MyTab1", "[PE] Passlänge e"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "pipe_connection": ["MyTab1", "[RAHMEN] Rohrverbindung"], + "inner_radius_is_bend": ["MyTab1", "[RK] Innerradius ist Abkantung"], + "radius": ["MyTab1", "[r] Radius"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "angle": ["MyTab1", "[w] Winkel"], + "absolute_roughness_factor": ["Pset_DuctFittingTypeCommon", "AbsoluteRoughnessFactor"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "length": ["MyTab1", "[l] Länge"] + } + }, + "AirDuctEndCap": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_a": ["MyTab1", "[a] (a)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "height_b": ["MyTab1", "[b] (b)"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "manufacturer": ["MyTab1", "[HST] Hersteller"] + } + }, + "AirDuctTPiece": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_entrance_a": ["MyTab1", "[a] Breite Eingang (a)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "height_entrance_b": ["MyTab1", "[b] Höhe Eingang (b)"], + "width_outlet_d": ["MyTab1", "[d] Breite Ausgang (d)"], + "width_branch_g": ["MyTab1", "[g] Breite Abzweig (g)"], + "height_branch_h": ["MyTab1", "[h] Breite Abzweig (h)"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "cross_tee": ["MyTab1", "[KR] Kreuzstück"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "height_branch_m": ["MyTab1", "[m] Höhe Abzweig"], + "length_entrance_n": ["MyTab1", "[n] Länge Eingang"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "second_branch_length_o": ["MyTab1", "[o] 2. Verzweigung. Länge Eingang"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "second_branch_height_p": ["MyTab1", "[p] 2. Verzweigung. Höhe"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "radius": ["MyTab1", "[r] Radius"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "absolute_roughness_factor": ["Pset_DuctFittingTypeCommon", "AbsoluteRoughnessFactor"], + "manufacturer": ["MyTab1", "[HST] Hersteller"] + } + }, + "AirDuctTransitionRectangularAsymmetric": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "list_as_ua": ["MyTab1", "[AUA] Auf Liste als UA"], + "height_a": ["MyTab1", "[a] Höhe (a)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "width_start_b": ["MyTab1", "[b] Breite Anfang (b)"], + "height_end_c": ["MyTab1", "[c] Höhe Ende (c)"], + "width_end_d": ["MyTab1", "[d] Breite Ende (d)"], + "e_offset": ["MyTab1", "[e] e-Versatz"], + "insert_start": ["MyTab1", "[f] Einschub Anfang"], + "f_offset": ["MyTab1", "[f] f-Versatz"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "box_bottom_lbh": ["MyTab1", "[LBHU] BOX unten: L-B-H"], + "box_lbh": ["MyTab1", "[LBH] BOX: Länge Breite Höhe"], + "connection_length_height": ["MyTab1", "[LHA] Anschluss Länge Höhe"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "extension_m": ["MyTab1", "[m] Anlängung (m)"], + "extension_n": ["MyTab1", "[n] Anlängung (n)"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VOL] Vm3/h"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "absolute_roughness_factor": ["Pset_DuctFittingTypeCommon", "AbsoluteRoughnessFactor"] + } + }, + "AirDuctTransitionSymmetrical": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "height_a": ["MyTab1", "[a] Höhe (a)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "width_start_b": ["MyTab1", "[b] Breite Anfang (b)"], + "width_end_d": ["MyTab1", "[d] Breite Ende (d)"], + "insert_end": ["MyTab1", "[e] Einschub Ende"], + "insert_start": ["MyTab1", "[f] Einschub Anfang"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "guide_vanes": ["MyTab1", "[LBT] Leitbleche"], + "number_guide_vanes": ["MyTab1", "[LB] Anzahl Leitbleche"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "inner_radius_is_bend": ["MyTab1", "[RK] Innerradius ist Abkantung"], + "radius": ["MyTab1", "[r] Radius"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "angle": ["MyTab1", "[w] Winkel"], + "flow_rate": ["MyTab1", "[QS] Querschnitt"], + "absolute_roughness_factor": ["Pset_DuctFittingTypeCommon", "AbsoluteRoughnessFactor"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "length": ["MyTab1", "[l] Länge"] + } + }, + "AirDuctTransitionRectangularBow": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "width_b": ["MyTab1", "[a] Breitendurchmesser B"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "height_h": ["MyTab1", "[b] Höhendurchmesser H"], + "cross_section_output": ["MyTab1", "[d1] Querschnitt Ausgang"], + "e_offset": ["MyTab1", "[e] Versatz e"], + "f_offset": ["MyTab1", "[f] Versatz f"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."], + "flow_rate": ["MyTab1", "[QS] Querschnitt"], + "absolute_roughness_factor": ["Pset_DuctFittingTypeCommon", "AbsoluteRoughnessFactor"] + } + }, + "AirDuctRectangularPants": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "cross_section_entrance_a": ["MyTab1", "[a] Querschnitt Eingang"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "cross_section_entrance_b": ["MyTab1", "[b] Querschnitt Eingang"], + "cross_section_output_c": ["MyTab1", "[c] Querschnitt Ausgang"], + "cross_section_output_d": ["MyTab1", "[d] Querschnitt Ausgang (d)"], + "e_offset": ["MyTab1", "[e] Versatz e"], + "f_offset": ["MyTab1", "[fF] Versatz f"], + "cross_section_output_h": ["MyTab1", "[h] Querschnitt Ausgang (h)"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "center_distance": ["MyTab1", "[m] Mittelstück-m"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirDuctRectangularInspectionCover": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "length_a": ["MyTab1", "[a] Länge"], + "remark": ["MyTab1", "[BEM] Bemerkung"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "width_b": ["MyTab1", "[b] Breite"], + "nominal_diameter_dn": ["MyTab1", "[DN] DN Rohr"], + "height_h": ["MyTab1", "[H] Höhe"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "radius": ["MyTab1", "[R] Radius"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirDuctRoundStub": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirDuctRivetedEdge": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Medium"], + "dimension_a": ["MyTab1", "[a] (a)"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "dimension_b": ["MyTab1", "[b] (b)"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] Kurzbezeichnung"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirDuctRoundCoupling": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "cross_section_d1": ["MyTab1", "[d1] Querschnitt"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "connection_type": ["MyTab1", "[RAHMEN] Verbindungsart"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "FireDamper": { + "default_ps": { + "flow_velocity": ["Calc", "[ve] Strömungsgeschwindigkeit"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "drive_side": ["MyTab1", "[AS] Antriebseite"], + "remark": ["MyTab1", "[ATB] Bemerkung"], + "drive": ["MyTab1", "[ATR] Antrieb"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "label_swap_ab": ["MyTab1", "[cab] Beschriftung a/b vertauschen"], + "insert": ["MyTab1", "[e] Einschub"], + "pressure_loss_opening_1": ["MyTab1", "[DP] Druckverlust Öffnung 1"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "air_volume": ["MyTab1", "[VS] Luftvol"], + "zeta_opening_1": ["MyTab1", "[Z] Zeta Öffnung 1"] + } + }, + "AirVolumeFlowControllerConstant": { + "default_ps": { + "radius": ["Profile", "Radius"], + "flow_velocity": ["Calc", "[ve] Strömungsgeschwindigkeit"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "outer_diameter": ["MyTab1", "[D] Aussendurchmesser"], + "insert": ["MyTab1", "[e] Einschub"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "air_volume": ["MyTab1", "[VS] Luftvol"] + } + }, + "AirVolumeFlowControllerDynamic": { + "default_ps": { + "flow_velocity": ["Calc", "[ve] Strömungsgeschwindigkeit"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "label_swap_ab": ["MyTab1", "[cab] Beschriftung a/b vertauschen"], + "insert": ["MyTab1", "[e] Einschub"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "isolation": ["MyTab1", "[ISOL] Isolation"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate_range": ["MyTab1", "[VS] Vm3/h"] + } + }, + "AirSilencerRoundFlexible": { + "default_ps": { + "flow_velocity": ["Calc", "[ve] Strömungsgeschwindigkeit"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation_is": ["MyTab1", "[BEZ] Bezeichnung (IS)"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "outer_diameter": ["MyTab1", "[D] Aussendurchmesser"], + "insert_end": ["MyTab1", "[e] Einschub Ende"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirTerminal": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outlet_grille_graphic": ["MyTab1", "[CG] Auslass-Gitter-Grafik"], + "nominal_diameter_d1": ["MyTab1", "[d1] Anschluss DN"], + "cylinder_1": ["MyTab1", "[DL1] Zyl1: DN1 DN2 Länge"], + "cylinder_2": ["MyTab1", "[DL2] Zyl2: DN1 DN2 Länge"], + "cylinder_3": ["MyTab1", "[DL3] Zyl3: DN1 DN2 Länge"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "project": ["MyTab1", "[PRJ] Projekt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "series": ["MyTab1", "[SER] Serie"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VOL] Vm3/h"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AirSilencerTelephony": { + "default_ps": { + "radius": ["Profile", "Radius"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "nominal_diameter_d1": ["MyTab1", "[d1] DN"], + "outer_diameter": ["MyTab1", "[D] Aussendurchmesser"], + "insert": ["MyTab1", "[e] Einschub"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "list_round": ["MyTab1", "[LSTR] Auf Liste Rund"], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "rings": ["MyTab1", "[RI] Ringe"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"] + } + }, + "AirValve": { + "default_ps": { + "radius": ["Profile", "Radius"], + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outlet_grille_graphic": ["MyTab1", "[CG] Auslass-Gitter-Grafik"], + "nominal_diameter_connection": ["MyTab1", "[d1] Anschluss DN"], + "cylinder_1": ["MyTab1", "[DL1] Zyl1: DN1 DN2 Länge"], + "cylinder_2": ["MyTab1", "[DL2] Zyl2: DN1 DN2 Länge"], + "cylinder_3": ["MyTab1", "[DL3] Zyl3: DN1 DN2 Länge"], + "manufacturer": ["MyTab1", "[HST] Hersteller"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlüssel"], + "cross_tee": ["MyTab1", "[KR] Kreuzstück"], + "cost_group": ["MyTab1", "[KGR] Kostengruppe"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "outlet_type": ["MyTab1", "[OT] Typ"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "project": ["MyTab1", "[PRJ] Projekt"], + "connection_type": ["MyTab1", "[RAHMEN] Rahmen"], + "rings": ["MyTab1", "[RI] Ringe"], + "series": ["MyTab1", "[SER] Serie"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "type": ["MyTab1", "[TYP] Typ"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VOL] Vm3/h"], + "air_velocity": ["MyTab1", "[v] Luftgeschw."] + } + }, + "AHUFan": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "insert": ["MyTab1", "[e] Einschub"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlussel"], + "graphics": ["MyTab1", "[LKFB] Grafik Oben,Unten,Vorne, Hinten..."], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VS] Vm3/h"] + } + }, + "AHUAirChamber": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "insert": ["MyTab1", "[e] Einschub"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlussel"], + "graphics": ["MyTab1", "[LKFB] Grafik Oben,Unten,Vorne, Hinten..."], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VS] Vm3/h"] + } + }, + "AHUCooler": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "insert": ["MyTab1", "[e] Einschub"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlussel"], + "graphics": ["MyTab1", "[LKFB] Grafik Oben,Unten,Vorne, Hinten..."], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VS] Vm3/h"] + } + }, + "AHUSilencer": { + "default_ps": { + "air_type": ["MyTab1", "[ANLAGE] Luftart"], + "outer_dimension_a": ["MyTab1", "[A] Aussenabmessung A"], + "cross_section_a": ["MyTab1", "[a] Querschnitt a"], + "designation": ["MyTab1", "[BEZ] Bezeichnung"], + "outer_dimension_b": ["MyTab1", "[B] Aussenabmessung B"], + "cross_section_b": ["MyTab1", "[b] Querschnitt b"], + "insert": ["MyTab1", "[e] Einschub"], + "isolation_type": ["MyTab1", "[ISOL-TYPE] Isolationstyp"], + "edge": ["MyTab1", "[KANTE] Kante"], + "code": ["MyTab1", "[KBZ] KBZ"], + "key": ["MyTab1", "[KEY] Schlussel"], + "graphics": ["MyTab1", "[LKFB] Grafik Oben,Unten,Vorne, Hinten..."], + "length": ["MyTab1", "[l] Länge"], + "material": ["MyTab1", "[MAT] Material"], + "data_mask_number": ["MyTab1", "[MSKNR] Datenmaskennummer"], + "on_list": ["MyTab1", "[ON_LST] Auf Liste"], + "pen": ["MyTab1", "[PEN] Schtift"], + "system": ["MyTab1", "[PLANT] Anlage"], + "position_in_ifc": ["MyTab1", "[POS] Position"], + "cross_section": ["MyTab1", "[QS] Querschnitt"], + "floor": ["MyTab1", "[STOCK] Stockwerk"], + "strand": ["MyTab1", "[STR] Strang"], + "version": ["MyTab1", "[VERS] Version"], + "flow_rate": ["MyTab1", "[VS] Vm3/h"] + } } -} \ No newline at end of file +} + + diff --git a/bim2sim/assets/templates/modelica/package.txt b/bim2sim/assets/templates/modelica/package.txt new file mode 100644 index 0000000000..afbf594abb --- /dev/null +++ b/bim2sim/assets/templates/modelica/package.txt @@ -0,0 +1,14 @@ +%if within is not None: +within ${within}; +%endif +package ${name} + extends Modelica.Icons.Package; + + %if uses is not None: + annotation (uses( + %for use in uses: + ${use}${',' if not loop.last else '),'}\ + %endfor + version="1"); + %endif +end ${name}; \ No newline at end of file diff --git a/bim2sim/assets/templates/modelica/package_order.txt b/bim2sim/assets/templates/modelica/package_order.txt new file mode 100644 index 0000000000..3536bb05ac --- /dev/null +++ b/bim2sim/assets/templates/modelica/package_order.txt @@ -0,0 +1,10 @@ +%if extra != None: +${extra} +%endif +%for i in list: +%if addition != None: +${addition}${i.replace(" ", "")} +%else: +${i.replace(" ", "")} +%endif +%endfor \ No newline at end of file diff --git a/bim2sim/assets/templates/modelica/tmplModel.txt b/bim2sim/assets/templates/modelica/tmplModel.txt index aec47c2b09..c3b4f1f51f 100644 --- a/bim2sim/assets/templates/modelica/tmplModel.txt +++ b/bim2sim/assets/templates/modelica/tmplModel.txt @@ -1,5 +1,7 @@ model ${model.name} "${model.comment}" +% if not model.connections_heat_ports_conv or not model.connections_heat_ports_rad: extends Modelica.Icons.Example; +% endif import SI = Modelica.Units.SI;\ %if unknowns: @@ -27,19 +29,39 @@ model ${model.name} "${model.comment}" {${instance.position[0]+10},${instance.position[1]+10}}}))); \ \ +// Only for SpawnOfEnergyPlus +% if len(model.connections_heat_ports_conv) > 0: +Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a heatPortOuterCon[${len(model.connections_heat_ports_conv)}] + annotation (Placement(transformation(extent={{-110,10},{-90,30}}))); +% endif +% if len(model.connections_heat_ports_rad) > 0: +Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a heatPortOuterRad[${len(model.connections_heat_ports_rad)}] + annotation (Placement(transformation(extent={{-110,-22},{-90,-2}}))); +% endif + +Buildings.BoundaryConditions.WeatherData.Bus weaBus "Weather data bus" + annotation (Placement(transformation(extent={{-110,-110},{-90,-90}}))); + equation % for con1, con2, pos1, pos2 in model.connections: connect(${con1}, ${con2}) - annotation (Line(points={{${pos1[0]},${pos1[1]}},{38,-6}}, color={0,0,127}));; -% endfor\ + annotation (Line(points={{${pos1[0]},${pos1[1]}},{38,-6}}, color={0,0,127})); +% endfor + +// heatport connections +% for cons in model.connections_heat_ports_conv: + connect${str(cons).replace("'","")} annotation (Line(points={{0,0},{0,0},{0,0},{0,0},{0,0}}, color={191,0,0})); +% endfor +% for cons in model.connections_heat_ports_rad: + connect${str(cons).replace("'","")} annotation (Line(points={{0,0},{0,0},{0,0},{0,0},{0,0}}, color={191,0,0})); +% endfor annotation( %if unknowns: Diagram(graphics={Text( extent={{-100,100},{100,60}}, lineColor={238,46,47}, - textString="${len(unknowns)} unknown parameters! - see comments for details.")}), + textString="${len(unknowns)} unknown parameters! See comments for details.")}), %endif experiment(StopTime=36000)); end ${model.name}; \ No newline at end of file diff --git a/bim2sim/assets/templates/modelica/tmplSpawnBuilding.txt b/bim2sim/assets/templates/modelica/tmplSpawnBuilding.txt new file mode 100644 index 0000000000..85c01913df --- /dev/null +++ b/bim2sim/assets/templates/modelica/tmplSpawnBuilding.txt @@ -0,0 +1,129 @@ +within ${within}; +model ${model_name} "${model_comment}" + import SI = Modelica.Units.SI; + + parameter Integer nPorts=2 + "Number of fluid ports (equals to 2 for one inlet and one outlet)" + annotation (Evaluate=true,Dialog(connectorSizing=true,tab="General",group="Ports")); + + parameter Integer nZones = ${n_zones}; + + final parameter Modelica.Units.SI.MassFlowRate mOut_flow_base[nZones]=0.1/3600 * fill(1, nZones) * 1.2 + "Outside air infiltration for each exterior room"; + + parameter String zoneNames[nZones] = {${', '.join('"' + z + '"' for z in ep_zone_lists.strip("{}").split(','))}} + "Name of the thermal zone as specified in the EnergyPlus input"; + + parameter Real baseInfiltration[nZones] = {${', '.join(z for z in base_infiltration.strip("{}").split(','))}} + "Base infiltration per thermal zone"; + + Modelica.Units.SI.MassFlowRate mOut_flow_infiltration[nZones] + "Outside air infiltration for each exterior room"; + + inner Buildings.ThermalZones.EnergyPlus_9_6_0.Building building( + idfName = ${idf_path}, + epwName = ${weather_path_ep}, + weaName = ${weather_path_mos}, + printUnits = true) "" + annotation (Placement(transformation(extent={{-100,60},{-80,80}}))); + + + Buildings.Fluid.Sources.MassFlowSource_WeatherData freshairsource[nZones]( + redeclare package Medium = Buildings.Media.Air, + use_m_flow_in=true, + each nPorts=1) "" + annotation (Placement(transformation(extent={{-43.1111,58},{-23.1111,78}}))); + + Buildings.ThermalZones.EnergyPlus_9_6_0.ThermalZone zon[nZones]( + zoneName=zoneNames, + redeclare package Medium = Buildings.Media.Air, + each use_C_flow=false, + each nPorts=nPorts) + annotation (Placement(transformation(extent={{-20,-20},{20,20}}))); + +// Infiltration + Buildings.Fluid.Sources.Outside out[nZones]( + redeclare package Medium = Buildings.Media.Air, + each nPorts=1) "Outside condition" + annotation (Placement(transformation(extent={{-44,32},{-24,52}}))); + + Buildings.Fluid.FixedResistances.PressureDrop resOut[nZones]( + redeclare each package Medium = Buildings.Media.Air, + each m_flow_nominal=sum(mOut_flow_base), + each dp_nominal=10, + each linearized=true) "Small flow resistance for inlet" + annotation (Placement(transformation(extent={{-6,58},{14,78}}))); + + Buildings.Fluid.FixedResistances.PressureDrop resIn[nZones]( + redeclare package Medium = Buildings.Media.Air, + each m_flow_nominal=sum(mOut_flow_base), + each dp_nominal=10, + each linearized=true) "Small flow resistance for outlet" + annotation (Placement(transformation(extent={{-6,32},{14,52}}))); + +// Interfaces + Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a heaPorCon[nZones] + "Convective heat port to air volume for each zone" + annotation (Placement(transformation(extent={{-110,-10},{-90,10}}))); + Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a heaPorRad[nZones] + "Radiative heat port to air volume for each zone" + annotation (Placement(transformation(extent={{-110,-28},{-90,-8}}))); + + Modelica.Blocks.Sources.Constant const[nZones,3](each k=0) "TODO" + annotation (Placement(transformation(extent={{-100,16},{-80,36}}))); + + Buildings.BoundaryConditions.WeatherData.Bus weaBus "Weather data bus" + annotation (Placement(transformation(extent={{-110,90},{-90,110}}))); + + Modelica.Blocks.Sources.RealExpression Infiltration_m_flow[nZones](y= + mOut_flow_infiltration) + annotation (Placement(transformation(extent={{0,80},{-20,100}}))); + +equation + + for i in 1:nZones loop + mOut_flow_infiltration[i] = baseInfiltration[i] * zon[i].V / 3600 * 1.2; + + connect(building.weaBus, freshairsource[i].weaBus) annotation (Line( + points={{-80,68},{-52,68},{-52,68.2},{-43.1111,68.2}}, + color={255,204,51}, + thickness=0.5)); + connect(building.weaBus, out[i].weaBus) annotation (Line( + points={{-80,68},{-52,68},{-52,42.2},{-44,42.2}}, + color={255,204,51}, + thickness=0.5)); + connect(resOut[i].port_b, zon[i].ports[1]) annotation (Line(points={{14,68}, + {98,68},{98,-28},{-1,-28},{-1,-19.1}}, color={0,127,255})); + connect(out[i].ports[1], resIn[i].port_a) annotation (Line(points={{-24,42}, + {-6,42}}, color={0,127,255})); + connect(resIn[i].port_b, zon[i].ports[2]) annotation (Line(points={{14,42},{ + 98,42},{98,-28},{1,-28},{1,-19.1}}, color={0,127,255})); + end for; + + + connect(freshairsource[:].ports[1], resOut[:].port_a) + annotation (Line(points={{-23.1111,68},{-14,68},{-14,68},{-6,68}}, + color={0,127,255})); + + connect(heaPorCon, zon.heaPorAir) annotation (Line(points={{-100,0},{0,0}}, + color={191,0,0})); + connect(zon.heaPorRad, heaPorRad) annotation (Line(points={{0,-6},{-82,-6},{-82, + -18},{-100,-18}}, color={191,0,0})); + + connect(const.y, zon.qGai_flow) annotation (Line(points={{-79,26},{-32,26},{-32, + 10},{-22,10}}, color={0,0,127})); + + connect(building.weaBus, weaBus) annotation (Line( + points={{-80,70},{-52,70},{-52,100},{-100,100}}, + color={255,204,51})); + + connect(Infiltration_m_flow.y, freshairsource.m_flow_in) annotation (Line( + points={{-21,90},{-60,90},{-60,76},{-43.1111,76}}, color={0,0,127})); + + annotation ( + experiment(StopTime=36000), uses( + Modelica(version="4.0.0"), + Buildings(version="11.1.0"), + AixLib(version="3.0.0"))); + +end ${model_name}; diff --git a/bim2sim/assets/templates/modelica/tmplSpawnTotalModel.txt b/bim2sim/assets/templates/modelica/tmplSpawnTotalModel.txt new file mode 100644 index 0000000000..ecc4254d65 --- /dev/null +++ b/bim2sim/assets/templates/modelica/tmplSpawnTotalModel.txt @@ -0,0 +1,30 @@ +within ${within}; +model ${model_name} "${model_comment}" + import SI = Modelica.Units.SI; + ${model_name_building} ${model_name_building.lower()} + annotation (Placement(transformation(extent={{-14,28},{6,48}}))); + ${model_name_hydraulic} ${model_name_hydraulic.lower()} + annotation (Placement(transformation(extent={{-14,-62},{6,-42}}))); +equation + + // heatport connections + % for cons in cons_heat_ports_conv_building_hvac: + connect${str(cons).replace("'","")} annotation (Line(points={{0,0},{0,0},{0,0},{0,0},{0,0}}, color={191,0,0})); + % endfor + % for cons in cons_heat_ports_rad_building_hvac: + connect${str(cons).replace("'","")} annotation (Line(points={{0,0},{0,0},{0,0},{0,0},{0,0}}, color={191,0,0})); + % endfor + + connect(hydraulic.weaBus, buildingmodel.weaBus) annotation (Line( + points={{-14,-62},{-22,-62},{-22,48},{-14,48}}, + color={255,204,51}, + thickness=0.5)); + + annotation (Icon(coordinateSystem(preserveAspectRatio=false)), Diagram( + coordinateSystem(preserveAspectRatio=false)), + experiment( + StopTime=31536000, + Interval=3600, + __Dymola_Algorithm="Dassl")); +end ${model_name}; + diff --git a/bim2sim/elements/aggregation/hvac_aggregations.py b/bim2sim/elements/aggregation/hvac_aggregations.py index 23bb2ebbc3..90820c2925 100644 --- a/bim2sim/elements/aggregation/hvac_aggregations.py +++ b/bim2sim/elements/aggregation/hvac_aggregations.py @@ -657,10 +657,9 @@ def pump_elements(self) -> list: def _calc_rated_power(self, name) -> ureg.Quantity: """Calculate the rated power adding the rated power of the pump-like elements""" - if all(ele.rated_power for ele in self.pump_elements): - return sum([ele.rated_power for ele in self.pump_elements]) - else: - return None + value = sum(ele.rated_power for ele in self.pump_elements) + if value: + return value rated_power = attribute.Attribute( unit=ureg.kilowatt, @@ -684,7 +683,9 @@ def _calc_rated_height(self, name) -> ureg.Quantity: def _calc_volume_flow(self, name) -> ureg.Quantity: """Calculate the volume flow, adding the volume flow of the pump-like elements""" - return sum([ele.rated_volume_flow for ele in self.pump_elements]) + value = sum([ele.rated_volume_flow for ele in self.pump_elements]) + if value: + return value rated_volume_flow = attribute.Attribute( description='rated volume flow', @@ -695,7 +696,9 @@ def _calc_volume_flow(self, name) -> ureg.Quantity: def _calc_diameter(self, name) -> ureg.Quantity: """Calculate the diameter, using the pump-like elements diameter""" - return sum(item.diameter ** 2 for item in self.pump_elements) ** 0.5 + value = sum(item.diameter ** 2 for item in self.pump_elements) ** 0.5 + if value: + return value diameter = attribute.Attribute( description='diameter', @@ -821,7 +824,9 @@ def _calc_rated_power(self, name) -> ureg.Quantity: """ Calculate the rated power adding the rated power of the whitelist_classes elements. """ - return sum([ele.rated_power for ele in self.whitelist_elements]) + value = sum([ele.rated_power for ele in self.whitelist_elements]) + if value: + return value rated_power = attribute.Attribute( description="rated power", @@ -839,7 +844,9 @@ def _calc_rated_pump_power(self, name) -> ureg.Quantity: """ Calculate the rated pump power adding the rated power of the pump-like elements. """ - return sum([ele.rated_power for ele in self.pump_elements]) + value = sum([ele.rated_power for ele in self.pump_elements]) + if value: + return value rated_pump_power = attribute.Attribute( description="rated pump power", @@ -852,7 +859,9 @@ def _calc_volume_flow(self, name) -> ureg.Quantity: """ Calculate the volume flow, adding the volume flow of the pump-like elements. """ - return sum([ele.rated_volume_flow for ele in self.pump_elements]) + value = sum([ele.rated_volume_flow for ele in self.pump_elements]) + if value: + return value rated_volume_flow = attribute.Attribute( description="rated volume flow", @@ -865,8 +874,19 @@ def _calc_flow_temperature(self, name) -> ureg.Quantity: """ Calculate the flow temperature, using the flow temperature of the whitelist_classes elements. """ - return sum(ele.flow_temperature.to_base_units() for ele - in self.whitelist_elements) / len(self.whitelist_elements) + # TODO the following would work, but only if we want a medium + # temperature for the consumer. If we want a list, this needs to look + # different + value = (sum(ele.flow_temperature.to_base_units() for ele + in self.whitelist_elements if + ele.flow_temperature is not None) + / len([ele for ele in self.whitelist_elements if + ele.flow_temperature is not None])) + # value = (sum(ele.flow_temperature.to_base_units() for ele + # in self.whitelist_elements if ele.flow_temperature) + # / len(self.whitelist_elements)) + if value: + return value flow_temperature = attribute.Attribute( description="temperature inlet", @@ -879,8 +899,13 @@ def _calc_return_temperature(self, name) -> ureg.Quantity: """ Calculate the return temperature, using the return temperature of the whitelist_classes elements. """ - return sum(ele.return_temperature.to_base_units() for ele - in self.whitelist_elements) / len(self.whitelist_elements) + value = (sum(ele.return_temperature.to_base_units() for ele + in self.whitelist_elements if + ele.return_temperature is not None) + / len([ele for ele in self.whitelist_elements if + ele.return_temperature is not None])) + if value: + return value return_temperature = attribute.Attribute( description="temperature outlet", @@ -891,7 +916,8 @@ def _calc_return_temperature(self, name) -> ureg.Quantity: def _calc_dT_water(self, name): """ Water dt of consumer.""" - return self.flow_temperature - self.return_temperature + if self.flow_temperature and self.return_temperature: + return self.flow_temperature - self.return_temperature dT_water = attribute.Attribute( description="Nominal temperature difference", @@ -901,7 +927,9 @@ def _calc_dT_water(self, name): def _calc_body_mass(self, name): """ Body mass of consumer.""" - return sum(ele.body_mass for ele in self.whitelist_elements) + value = sum(ele.body_mass for ele in self.whitelist_elements) + if value: + return value body_mass = attribute.Attribute( description="Body mass of Consumer", @@ -911,8 +939,9 @@ def _calc_body_mass(self, name): def _calc_heat_capacity(self, name): """ Heat capacity of consumer.""" - return sum(ele.heat_capacity for ele in - self.whitelist_elements) + value = sum(ele.heat_capacity for ele in self.whitelist_elements) + if value: + return value heat_capacity = attribute.Attribute( description="Heat capacity of Consumer", @@ -1112,12 +1141,6 @@ def whitelist_elements(self) -> list: """list of whitelist_classes elements present on the aggregation""" return [ele for ele in self.elements if type(ele) in self.whitelist_classes] - def _calc_flow_temperature(self, name) -> list: - """Calculate the flow temperature, using the flow temperature of the - whitelist_classes elements""" - return [ele.flow_temperature.to_base_units() for ele - in self.whitelist_elements] - def _calc_has_pump(self, name) -> list[bool]: """Returns a list with boolean for every consumer if it has a pump.""" return [con.has_pump for con in self.whitelist_elements] @@ -1125,7 +1148,7 @@ def _calc_has_pump(self, name) -> list[bool]: flow_temperature = attribute.Attribute( description="temperature inlet", unit=ureg.kelvin, - functions=[_calc_flow_temperature], + functions=[Consumer._calc_flow_temperature], dependant_elements='whitelist_elements' ) @@ -1134,16 +1157,10 @@ def _calc_has_pump(self, name) -> list[bool]: functions=[_calc_has_pump] ) - def _calc_return_temperature(self, name) -> list: - """Calculate the return temperature, using the return temperature of the - whitelist_classes elements""" - return [ele.return_temperature.to_base_units() for ele - in self.whitelist_elements] - return_temperature = attribute.Attribute( description="temperature outlet", unit=ureg.kelvin, - functions=[_calc_return_temperature], + functions=[Consumer._calc_return_temperature], dependant_elements='whitelist_elements' ) @@ -1443,7 +1460,10 @@ def not_whitelist_elements(self) -> list: def _calc_rated_power(self, name) -> ureg.Quantity: """ Calculate the rated power adding the rated power of the whitelist_classes elements.""" - return sum([ele.rated_power for ele in self.whitelist_elements]) + value = sum([ele.rated_power for ele in self.whitelist_elements + if ele.rated_power]) + if value: + return value rated_power = attribute.Attribute( unit=ureg.kilowatt, @@ -1455,7 +1475,10 @@ def _calc_rated_power(self, name) -> ureg.Quantity: def _calc_min_power(self, name): """ Calculates the min power, adding the min power of the whitelist_elements.""" - return sum([ele.min_power for ele in self.whitelist_elements]) + min_powers = [ele.min_power for ele in self.whitelist_elements + if ele.min_power] + if min_powers: + return min(min_powers) min_power = attribute.Attribute( unit=ureg.kilowatt, @@ -1466,7 +1489,8 @@ def _calc_min_power(self, name): def _calc_min_PLR(self, name): """ Calculates the min PLR, using the min power and rated power.""" - return self.min_power / self.rated_power + if self.min_power and self.rated_power: + return self.min_power / self.rated_power min_PLR = attribute.Attribute( description="Minimum part load ratio", @@ -1474,35 +1498,33 @@ def _calc_min_PLR(self, name): functions=[_calc_min_PLR], ) - def _calc_flow_temperature(self, name) -> ureg.Quantity: - """ Calculate the flow temperature, using the flow temperature of the - whitelist_classes elements.""" - return sum(ele.flow_temperature.to_base_units() for ele - in self.whitelist_elements) / len(self.whitelist_elements) - flow_temperature = attribute.Attribute( - description="Nominal inlet temperature", - unit=ureg.kelvin, - functions=[_calc_flow_temperature], + description="Nominal flow temperature", + unit=ureg.celsius, + functions=[Consumer._calc_flow_temperature], dependant_elements='whitelist_elements' ) def _calc_return_temperature(self, name) -> ureg.Quantity: """ Calculate the return temperature, using the return temperature of the whitelist_classes elements.""" - return sum(ele.return_temperature.to_base_units() for ele - in self.whitelist_elements) / len(self.whitelist_elements) + value = (sum(ele.return_temperature.to_base_units() for ele + in self.whitelist_elements if ele.return_temperature) + / len(self.whitelist_elements)) + if value: + return value return_temperature = attribute.Attribute( - description="Nominal outlet temperature", - unit=ureg.kelvin, + description="Nominal return temperature", + unit=ureg.celsius, functions=[_calc_return_temperature], dependant_elements='whitelist_elements' ) def _calc_dT_water(self, name): """ Rated power of boiler.""" - return abs(self.return_temperature - self.flow_temperature) + if self.return_temperature and self.flow_temperature: + return abs(self.return_temperature - self.flow_temperature) dT_water = attribute.Attribute( description="Nominal temperature difference", @@ -1513,8 +1535,10 @@ def _calc_dT_water(self, name): def _calc_diameter(self, name) -> ureg.Quantity: """ Calculate the diameter, using the whitelist_classes elements diameter.""" - return sum( + value = sum( item.diameter ** 2 for item in self.whitelist_elements) ** 0.5 + if value: + return value diameter = attribute.Attribute( description='diameter', @@ -1548,10 +1572,9 @@ def pump_elements(self) -> list: def _calc_rated_pump_power(self, name) -> ureg.Quantity: """ Calculate the rated pump power adding the rated power of the pump-like elements.""" - if all(ele.rated_power for ele in self.pump_elements): - return sum([ele.rated_power for ele in self.pump_elements]) - else: - return None + value = all(ele.rated_power for ele in self.pump_elements) + if value: + return value rated_pump_power = attribute.Attribute( description="rated pump power", @@ -1563,7 +1586,9 @@ def _calc_rated_pump_power(self, name) -> ureg.Quantity: def _calc_volume_flow(self, name) -> ureg.Quantity: """ Calculate the volume flow, adding the volume flow of the pump-like elements.""" - return sum([ele.rated_volume_flow for ele in self.pump_elements]) + value = sum([ele.rated_volume_flow for ele in self.pump_elements]) + if value: + return value rated_volume_flow = attribute.Attribute( description="rated volume flow", diff --git a/bim2sim/elements/base_elements.py b/bim2sim/elements/base_elements.py index 8700f45cec..488c434a20 100644 --- a/bim2sim/elements/base_elements.py +++ b/bim2sim/elements/base_elements.py @@ -178,11 +178,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.ifc = ifc self.predefined_type = ifc2python.get_predefined_type(ifc) self.ifc_domain = ifc_domain self.finder = finder - self.ifc_units = ifc_units + self.ifc_units = ifc_units # ToDo Attribute enrichment in here self.source_tool: SourceTool = None # TBD @@ -203,6 +204,7 @@ def from_ifc(cls, ifc, *args, **kwargs): """Factory method to create instance from ifc""" ifc_args, ifc_kwargs = cls.ifc2args(ifc) kwargs.update(ifc_kwargs) + test = cls(*(args + ifc_args), **kwargs) return cls(*(args + ifc_args), **kwargs) @property @@ -210,11 +212,6 @@ def ifc_type(self): if self.ifc: return self.ifc.is_a() - @classmethod - def pre_validate(cls, ifc) -> bool: - """Check if ifc meets conditions to create element from it""" - raise NotImplementedError - def _calc_position(self, name): """returns absolute position""" if hasattr(self.ifc, 'ObjectPlacement'): @@ -451,9 +448,9 @@ def __str__(self): return "%s" % self.__class__.__name__ -class RelationBased(IFCBased): - - pass +# class RelationBased(IFCBased): +# +# pass class ProductBased(IFCBased): @@ -474,6 +471,7 @@ def __init__(self, *args, **kwargs): self.ports = self.get_ports() self.material = None self.material_set = {} + self.storeys = [] self.cost_group = self.calc_cost_group() def __init_subclass__(cls, **kwargs): @@ -753,6 +751,8 @@ def __init__( ifc_domain: IFCDomain, finder: Union[TemplateFinder, None] = None, dummy=Dummy): + + self.relevant_elements = relevant_elements self.mapping, self.blacklist, self.defaults = self.create_ifc_mapping(relevant_elements) self.dummy_cls = dummy self.ifc_domain = ifc_domain @@ -799,7 +799,31 @@ def __call__(self, ifc_entity, *args, ifc_type: str = None, use_dummy=True, f" will only be created for IFC files of domain " f"{element_cls.from_ifc_domains}") + # descriptions = ifc2python.get_descriptions(ifc_entity) + # + # if descriptions and not any( + # any(p.search(desc) for p in element_cls.pattern_ifc_type) + # for desc in descriptions if desc): + # ele_matches = [] + # for ele in self.relevant_elements: + # match_count = 0 + # for p in ele.pattern_ifc_type: + # for desc in descriptions: + # if desc: + # if p.search(desc): + # match_count += 1 + # if match_count > 0: + # ele_matches.append((ele, match_count)) + # if not ele_matches: + # raise LookupError(f"No element found for {ifc_entity}") + # elif len(ele_matches) == 1: + # element_cls = ele_matches[0][0] + # else: + # best_ele, _ = max(ele_matches, key=lambda x: x[1]) + # element_cls = best_ele + element = self.create(element_cls, ifc_entity, *args, **kwargs) + return element def create(self, element_cls, ifc_entity, *args, **kwargs): diff --git a/bim2sim/elements/bps_elements.py b/bim2sim/elements/bps_elements.py index 1a0684b172..4657ad10e8 100644 --- a/bim2sim/elements/bps_elements.py +++ b/bim2sim/elements/bps_elements.py @@ -5,11 +5,13 @@ import re import sys from datetime import date +from pathlib import Path from typing import Set, List, Union import ifcopenshell import ifcopenshell.geom from OCC.Core.BRepBndLib import brepbndlib +import numpy as np from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape from OCC.Core.BRepGProp import brepgprop @@ -23,7 +25,7 @@ from ifcopenshell import guid from bim2sim.elements.mapping import condition, attribute -from bim2sim.elements.base_elements import ProductBased, RelationBased +from bim2sim.elements.base_elements import ProductBased, RelationBased, Element from bim2sim.elements.mapping.units import ureg from bim2sim.tasks.common.inner_loop_remover import remove_inner_loops from bim2sim.utilities.common_functions import vector_angle, angle_equivalent @@ -40,7 +42,6 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.thermal_zones = [] self.space_boundaries = [] - self.storeys = [] self.material = None self.disaggregations = [] self.building = None @@ -704,10 +705,6 @@ def _calc_position(self, name): return position - @classmethod - def pre_validate(cls, ifc) -> bool: - return True - def validate_creation(self) -> bool: if self.bound_area and self.bound_area < 1e-2 * ureg.meter ** 2: return True @@ -1320,10 +1317,6 @@ def get_id(prefix=""): ifcopenshell_guid = guid.new()[prefix_length + 1:] return f"{prefix}{ifcopenshell_guid}" - @classmethod - def pre_validate(cls, ifc) -> bool: - return True - def validate_creation(self) -> bool: return True @@ -1804,7 +1797,7 @@ def __init__(self, *args, **kwargs): self.elements = [] ifc_types = {"IfcBuilding": ['*']} - from_ifc_domains = [IFCDomain.arch] + from_ifc_domains = [IFCDomain.arch, IFCDomain.mixed] conditions = [ condition.RangeCondition('year_of_construction', @@ -1918,7 +1911,7 @@ def _check_tz_ahu(self, name): class Storey(BPSProduct): ifc_types = {'IfcBuildingStorey': ['*']} - from_ifc_domains = [IFCDomain.arch] + from_ifc_domains = [IFCDomain.arch, IFCDomain.mixed] def __init__(self, *args, **kwargs): """storey __init__ function""" diff --git a/bim2sim/elements/graphs/hvac_graph.py b/bim2sim/elements/graphs/hvac_graph.py index 62be7e0e71..25a45ad203 100644 --- a/bim2sim/elements/graphs/hvac_graph.py +++ b/bim2sim/elements/graphs/hvac_graph.py @@ -15,6 +15,7 @@ from networkx import json_graph from bim2sim.elements.base_elements import ProductBased, ElementEncoder +from bim2sim.utilities.types import FlowSide, FlowDirection logger = logging.getLogger(__name__) @@ -35,7 +36,9 @@ def _update_from_elements(self, elements): """ nodes = [port for instance in elements for port in instance.ports - if port.connection] + # TODO #1 add this again, this is just for testing purpose + # if port.connection + ] inner_edges = [connection for instance in elements for connection in instance.inner_connections] edges = [(port, port.connection) for port in nodes if port.connection] @@ -118,7 +121,7 @@ def get_type_chains( types: Iterable[Type[ProductBased]], include_singles: bool = False): """Get lists of consecutive elements of the given types. Elements are - ordered in the same way as the are connected. + ordered in the same way as they are connected. Args: element_graph: Graph object with elements as nodes. @@ -181,9 +184,6 @@ def get_connections(self): return [edge for edge in self.edges if not edge[0].parent is edge[1].parent] - # def get_nodes(self): - # """Returns list of nodes represented by graph""" - # return list(self.nodes) def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, use_pyvis=False): @@ -203,16 +203,15 @@ def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, # https://plot.ly/python/network-graphs/ edge_colors_flow_side = { - 1: dict(edge_color='red'), - -1: dict(edge_color='blue'), - 0: dict(edge_color='grey'), - None: dict(edge_color='grey'), + FlowSide.supply_flow: dict(edge_color='red'), + FlowSide.return_flow: dict(edge_color='blue'), + FlowSide.unknown: dict(edge_color='grey'), } node_colors_flow_direction = { - 1: dict(node_color='white', edgecolors='blue'), - -1: dict(node_color='blue', edgecolors='black'), - 0: dict(node_color='grey', edgecolors='black'), - None: dict(node_color='grey', edgecolors='black'), + FlowDirection.source: dict(node_color='white', edgecolors='blue'), + FlowDirection.sink: dict(node_color='blue', edgecolors='black'), + FlowDirection.sink_and_source: dict(node_color='grey', edgecolors='black'), + FlowDirection.unknown: dict(node_color='grey', edgecolors='black'), } kwargs = {} @@ -293,6 +292,7 @@ def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, node['label'] = node['label'].split('<')[1] except: pass + # TODO #733 use is_generator(), is_consumer() etc. node['label'] = node['label'].split('(ports')[0] if 'agg' in node['label'].lower(): node['label'] = node['label'].split('Agg0')[0] @@ -654,88 +654,7 @@ def group_parallels(graph, group_attr, cond, threshold=None): graphs.append(_graph) return graphs - def recurse_set_side(self, port, side, known: dict = None, - raise_error=True): - """Recursive set flow_side to connected ports""" - if known is None: - known = {} - - # set side suggestion - is_known = port in known - current_side = known.get(port, port.flow_side) - if not is_known: - known[port] = side - elif is_known and current_side == side: - return known - else: - # conflict - if raise_error: - raise AssertionError("Conflicting flow_side in %r" % port) - else: - logger.error("Conflicting flow_side in %r", port) - known[port] = None - return known - - # call neighbours - for neigh in self.neighbors(port): - if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ - and port.parent is neigh.parent: - # switch flag over consumers / generators - self.recurse_set_side(neigh, -side, known, raise_error) - else: - self.recurse_set_side(neigh, side, known, raise_error) - - return known - - def recurse_set_unknown_sides(self, port, visited: list = None, - masters: list = None): - """Recursive checks neighbours flow_side. - :returns tuple of - common flow_side (None if conflict) - list of checked ports - list of ports on which flow_side s are determined""" - - if visited is None: - visited = [] - if masters is None: - masters = [] - - # mark as visited to prevent deadloops - visited.append(port) - - if port.flow_side in (-1, 1): - # use port with known flow_side as master - masters.append(port) - return port.flow_side, visited, masters - - # call neighbours - neighbour_sides = {} - for neigh in self.neighbors(port): - if neigh not in visited: - if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ - and port.parent is neigh.parent: - # switch flag over consumers / generators - side, _, _ = self.recurse_set_unknown_sides( - neigh, visited, masters) - side = -side - else: - side, _, _ = self.recurse_set_unknown_sides( - neigh, visited, masters) - neighbour_sides[neigh] = side - - sides = set(neighbour_sides.values()) - if not sides: - return port.flow_side, visited, masters - elif len(sides) == 1: - # all neighbours have same site - side = sides.pop() - return side, visited, masters - elif len(sides) == 2 and 0 in sides: - side = (sides - {0}).pop() - return side, visited, masters - else: - # conflict - return None, visited, masters + @staticmethod def get_dir_paths_between(graph, nodes, include_edges=False): diff --git a/bim2sim/elements/hvac_elements.py b/bim2sim/elements/hvac_elements.py index db2849007a..613307ecf1 100644 --- a/bim2sim/elements/hvac_elements.py +++ b/bim2sim/elements/hvac_elements.py @@ -8,6 +8,7 @@ from typing import Set, List, Tuple, Generator, Union, Type import numpy as np +import ifcopenshell.geom from bim2sim.kernel.decision import ListDecision, DecisionBunch from bim2sim.elements.mapping import condition, attribute @@ -15,6 +16,8 @@ from bim2sim.elements.mapping.ifc2python import get_ports as ifc2py_get_ports from bim2sim.elements.mapping.ifc2python import get_predefined_type from bim2sim.elements.mapping.units import ureg +from bim2sim.utilities.types import FlowDirection, FlowSide + logger = logging.getLogger(__name__) quality_logger = logging.getLogger('bim2sim.QualityReport') @@ -33,19 +36,32 @@ def length_post_processing(value): class HVACPort(Port): - """Port of HVACProduct.""" - vl_pattern = re.compile('.*vorlauf.*', - re.IGNORECASE) # TODO: extend pattern - rl_pattern = re.compile('.*rücklauf.*', re.IGNORECASE) - + """Port of HVACProduct. + + Definitions: + flow_direction: is the direction of the port which can be sink, source, + sink_and_source or unknown depending on the IFC data. + groups: based on IFC assignment this might be "vorlauf" or something else. + flow_side: defines if the port is part of the supply or return network. + E.g. the radiator is a splitter where one port is part of the supply and + the other port is part of the return network + + """ + vl_pattern = re.compile('.*(vorlauf|supply|feed|forward).*', re.IGNORECASE) + rl_pattern = re.compile('.*(rücklauf|return|recirculation|back).*', + re.IGNORECASE) + + # TODO #733 Clean port flow side setup def __init__( self, *args, groups: Set = None, - flow_direction: int = 0, **kwargs): + flow_direction: FlowDirection = FlowDirection.unknown, **kwargs): super().__init__(*args, **kwargs) self._flow_master = False - self._flow_direction = None + # self._flow_direction = None + self._flow_side = None + # groups and flow_direction coming from ifc2args kwargs self.groups = groups or set() self.flow_direction = flow_direction @@ -54,13 +70,16 @@ def ifc2args(cls, ifc) -> Tuple[tuple, dict]: args, kwargs = super().ifc2args(ifc) groups = {assg.RelatingGroup.ObjectType for assg in ifc.HasAssignments} - flow_direction = None if ifc.FlowDirection == 'SOURCE': - flow_direction = 1 + flow_direction = FlowDirection.source elif ifc.FlowDirection == 'SINK': - flow_direction = -1 + flow_direction = FlowDirection.sink elif ifc.FlowDirection in ['SINKANDSOURCE', 'SOURCEANDSINK']: - flow_direction = 0 + flow_direction = FlowDirection.sink_and_source + elif ifc.FlowDirection == 'NOTDEFINED': + flow_direction = FlowDirection.unknown + else: + flow_direction = FlowDirection.unknown kwargs['groups'] = groups kwargs['flow_direction'] = flow_direction @@ -89,10 +108,6 @@ def _calc_position(self, name) -> np.array: quality_logger.info("Suspect position [0, 0, 0] for %s", self) return coordinates - @classmethod - def pre_validate(cls, ifc) -> bool: - return True - def validate_creation(self) -> bool: return True @@ -105,34 +120,23 @@ def flow_master(self): def flow_master(self, value: bool): self._flow_master = value - @property - def flow_direction(self): - """Flow direction of port - - -1 = medium flows into port - 1 = medium flows out of port - 0 = medium flow undirected - None = flow direction unknown""" - return self._flow_direction - - @flow_direction.setter - def flow_direction(self, value): - if self._flow_master: - raise AttributeError("Can't set flow direction for flow master.") - if value not in (-1, 0, 1, None): - raise AttributeError("Invalid value. Use one of (-1, 0, 1, None).") - self._flow_direction = value - - @property - def verbose_flow_direction(self): - """Flow direction of port""" - if self.flow_direction == -1: - return 'SINK' - if self.flow_direction == 0: - return 'SINKANDSOURCE' - if self.flow_direction == 1: - return 'SOURCE' - return 'UNKNOWN' + # @property + # def flow_direction(self): + # """Flow direction of port + # + # -1 = medium flows into port + # 1 = medium flows out of port + # 0 = medium flow undirected + # None = flow direction unknown""" + # return self._flow_direction + + # @flow_direction.setter + # def flow_direction(self, value): + # if self._flow_master: + # raise AttributeError("Can't set flow direction for flow master.") + # if value not in (-1, 0, 1, None): + # raise AttributeError("Invalid value. Use one of (-1, 0, 1, None).") + # self._flow_direction = value @property def flow_side(self): @@ -149,39 +153,39 @@ def flow_side(self): @flow_side.setter def flow_side(self, value): - if value not in (-1, 0, 1): - raise ValueError("allowed values for flow_side are 1, 0, -1") previous = self._flow_side self._flow_side = value if previous: if previous != value: - logger.info("Overwriting flow_side for %r with %s" % ( - self, self.verbose_flow_side)) + logger.info( + f"Overwriting flow_side for {self} with {value.name}") else: - logger.debug( - "Set flow_side for %r to %s" % (self, self.verbose_flow_side)) - - @property - def verbose_flow_side(self): - if self.flow_side == 1: - return "VL" - if self.flow_side == -1: - return "RL" - return "UNKNOWN" + logger.debug(f"Set flow_side for {self} to {value.name}") def determine_flow_side(self): - """Check groups for hints of flow_side and returns flow_side if hints are definitely""" + """Check groups for hints of flow_side and returns flow_side if hints + are definitely. + + First the flow_direction and the type of the element + (generator/consumer) is checked for clear information. If no + information can be obtained the pattern matches are evaluated based on + the groups from IFC, that come from RelatingGroup assignment. + If there are mismatching information from flow_direction and patterns + the flow_side is set to unknown, otherwise it's set to supply_flow or + supply_flow. + """ vl = None rl = None + if self.parent.is_generator(): - if self.flow_direction == 1: + if self.flow_direction.name == "source": vl = True - elif self.flow_direction == -1: + elif self.flow_direction.name == "sink": rl = True elif self.parent.is_consumer(): - if self.flow_direction == 1: + if self.flow_direction.name == "source": rl = True - elif self.flow_direction == -1: + elif self.flow_direction.name == "sink": vl = True if not vl: vl = any(filter(self.vl_pattern.match, self.groups)) @@ -189,10 +193,10 @@ def determine_flow_side(self): rl = any(filter(self.rl_pattern.match, self.groups)) if vl and not rl: - return 1 + return FlowSide.supply_flow if rl and not vl: - return -1 - return 0 + return FlowSide.return_flow + return FlowSide.unknown class HVACProduct(ProductBased): @@ -317,8 +321,8 @@ def decide_inner_connections(self) -> Generator[DecisionBunch, None, None]: vl = port_dict[decision_vl.value] rl = port_dict[decision_rl.value] # set flow correct side - vl.flow_side = 1 - rl.flow_side = -1 + vl.flow_side = FlowSide.supply_flow + rl.flow_side = FlowSide.return_flow self.inner_connections.append((vl, rl)) def validate_ports(self): @@ -361,6 +365,9 @@ class HeatPump(HVACProduct): re.compile('W(ä|ae)rme.?pumpe', flags=re.IGNORECASE), ] + def is_generator(self): + return True + min_power = attribute.Attribute( description='Minimum power that heat pump operates at.', unit=ureg.kilowatt, @@ -484,6 +491,10 @@ class CoolingTower(HVACProduct): re.compile('RKA', flags=re.IGNORECASE), ] + def is_consumer(self): + # TODO #733 check this + return True + min_power = attribute.Attribute( description='Minimum power that CoolingTower operates at.', unit=ureg.kilowatt, @@ -683,7 +694,8 @@ def _calc_partial_load_efficiency(self, name): def _calc_min_power(self, name) -> ureg.Quantity: """Function to calculate the minimum power that boiler operates at, using the partial load efficiency and the nominal power consumption""" - return self.partial_load_efficiency * self.nominal_power_consumption + if self.partial_load_efficiency and self.nominal_power_consumption: + return self.partial_load_efficiency * self.nominal_power_consumption min_power = attribute.Attribute( description="Minimum power that boiler operates at", @@ -701,13 +713,13 @@ def _calc_min_PLR(self, name) -> ureg.Quantity: unit=ureg.dimensionless, functions=[_calc_min_PLR], ) - flow_temperature = attribute.Attribute( - description="Nominal inlet temperature", + return_temperature = attribute.Attribute( + description="Nominal return temperature", default_ps=('Pset_BoilerTypeCommon', 'WaterInletTemperatureRange'), unit=ureg.celsius, ) - return_temperature = attribute.Attribute( - description="Nominal outlet temperature", + flow_temperature = attribute.Attribute( + description="Nominal flow temperature", default_ps=('Pset_BoilerTypeCommon', 'OutletTemperatureRange'), unit=ureg.celsius, ) @@ -935,6 +947,24 @@ def expected_hvac_ports(self): def is_consumer(self): return True + def _get_radiator_shape(self, name): + """returns topods shape of the radiator""" + settings = ifcopenshell.geom.settings() + settings.set(settings.USE_PYTHON_OPENCASCADE, True) + settings.set(settings.USE_WORLD_COORDS, True) + settings.set(settings.PRECISION, 1e-6) + settings.set( + "dimensionality", + ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) # 2 + # settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False) + # settings.set(settings.INCLUDE_CURVES, True) + return ifcopenshell.geom.create_shape(settings, self.ifc).geometry + + shape = attribute.Attribute( + description="Returns topods shape of the radiator.", + functions=[_get_radiator_shape] + ) + number_of_panels = attribute.Attribute( description="Number of panels of heater", default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfPanels'), @@ -1284,10 +1314,63 @@ class ThreeWayValve(Valve): def expected_hvac_ports(self): return 3 +class Medium(HVACProduct): + # is deprecated? + ifc_types = {"IfcDistributionSystem": ['*']} + pattern_ifc_type = [ + re.compile('Medium', flags=re.IGNORECASE) + ] + + @property + def expected_hvac_ports(self): + return 0 + + +class CHP(HVACProduct): + ifc_types = {'IfcElectricGenerator': ['CHP']} + + @property + def expected_hvac_ports(self): + return 2 + + def is_generator(self): + return True + + rated_power = attribute.Attribute( + default_ps=('Pset_ElectricGeneratorTypeCommon', 'MaximumPowerOutput'), + description="Rated power of CHP", + patterns=[ + re.compile('.*Nennleistung', flags=re.IGNORECASE), + re.compile('.*capacity', flags=re.IGNORECASE), + ], + unit=ureg.kilowatt, + ) + + efficiency = attribute.Attribute( + default_ps=( + 'Pset_ElectricGeneratorTypeCommon', 'ElectricGeneratorEfficiency'), + description="Electric efficiency of CHP", + patterns=[ + re.compile('.*electric.*efficiency', flags=re.IGNORECASE), + re.compile('.*el.*efficiency', flags=re.IGNORECASE), + ], + unit=ureg.dimensionless, + ) + + # water_volume = attribute.Attribute( + # description="Water volume CHP chp", + # unit=ureg.meter ** 3, + # ) +# Old ventilation classes +""" class Duct(HVACProduct): ifc_types = {"IfcDuctSegment": ['*', 'RIGIDSEGMENT', 'FLEXIBLESEGMENT']} + @property + def expected_hvac_ports(self): + return (2, float('inf')) + pattern_ifc_type = [ re.compile('Duct.?segment', flags=re.IGNORECASE) ] @@ -1309,6 +1392,10 @@ class DuctFitting(HVACProduct): 'OBSTRUCTION', 'TRANSITION'] } + @property + def expected_hvac_ports(self): + return (0, float('inf')) + pattern_ifc_type = [ re.compile('Duct.?fitting', flags=re.IGNORECASE) ] @@ -1329,6 +1416,10 @@ class AirTerminal(HVACProduct): ['*', 'DIFFUSER', 'GRILLE', 'LOUVRE', 'REGISTER'] } + @property + def expected_hvac_ports(self): + return (1, 2) + pattern_ifc_type = [ re.compile('Air.?terminal', flags=re.IGNORECASE) ] @@ -1338,59 +1429,1540 @@ class AirTerminal(HVACProduct): unit=ureg.millimeter, ) + def is_consumer(self): + return True +""" -class Medium(HVACProduct): - # is deprecated? - ifc_types = {"IfcDistributionSystem": ['*']} - pattern_ifc_type = [ - re.compile('Medium', flags=re.IGNORECASE) - ] + +class VentilationElement(HVACProduct): + """Common properties for all ventilation elements""" @property def expected_hvac_ports(self): - return 0 + return (2, float('inf')) + air_type = attribute.Attribute( + description='Air type', + ) + system = attribute.Attribute( + description='System', + ) + position_in_ifc = attribute.Attribute( + description='Position out of ifc entity', + ) + floor = attribute.Attribute( + description='Floor', + ) + version = attribute.Attribute( + description='Version', + ) + isolation_type = attribute.Attribute( + description='Insulation type', + ) + edge = attribute.Attribute( + description='Edge', + ) + data_mask_number = attribute.Attribute( + description='Data mask number', + ) + on_list = attribute.Attribute( + description='On list', + ) + pen = attribute.Attribute( + description='Pen', + ) + material = attribute.Attribute( + description='Material', + ) + designation = attribute.Attribute( + description='Designation', + ) + code = attribute.Attribute( + description='Code/KBZ', + ) + key = attribute.Attribute( + description='Key', + ) -class CHP(HVACProduct): - ifc_types = {'IfcElectricGenerator': ['CHP']} - @property - def expected_hvac_ports(self): - return 2 +# ============================================================================ +# AIR DUCT FAMILY +# ============================================================================ - rated_power = attribute.Attribute( - default_ps=('Pset_ElectricGeneratorTypeCommon', 'MaximumPowerOutput'), - description="Rated power of CHP", +class AirDuct(VentilationElement): + """Base class for all air ducts""" + + ifc_types = { + 'IfcDuctSegment': ['*'] + } + + length = attribute.Attribute( + description='Length', + unit=ureg.millimeter, patterns=[ - re.compile('.*Nennleistung', flags=re.IGNORECASE), - re.compile('.*capacity', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge \(l\)', flags=re.IGNORECASE), ], - unit=ureg.kilowatt, + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + reference_edge = attribute.Attribute( + description='Reference edge', ) - efficiency = attribute.Attribute( - default_ps=( - 'Pset_ElectricGeneratorTypeCommon', 'ElectricGeneratorEfficiency'), - description="Electric efficiency of CHP", + +class AirDuctRectangular(AirDuct): + """Rectangular air ducts""" + + pattern_ifc_type = [ + re.compile(r'Gerader\s+Kanal\s*\(\s*K\s*:\s*\d+\s*\)', flags=re.IGNORECASE), + re.compile(r'Kanal\s+\d+\s*x\s*\d+', flags=re.IGNORECASE), + re.compile(r'Kanal-Teil\s+\d+\s*x\s*\d+', flags=re.IGNORECASE) + ] + + height_a = attribute.Attribute( + description='Height a', + unit=ureg.millimeter, + ) + width_b = attribute.Attribute( + description='Width b', + unit=ureg.millimeter, + ) + cross_section_area = attribute.Attribute( + description='Cross section area', + unit=ureg.millimeter ** 2, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctOval(AirDuct): + """Oval air ducts (Ovalrohr)""" + + pattern_ifc_type = [ + re.compile(r'Ovalrohr.*\(.*170.*\)', flags=re.IGNORECASE), + ] + + width_b = attribute.Attribute( + description='Width diameter B', + unit=ureg.millimeter, + ) + height_h = attribute.Attribute( + description='Height diameter H', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctRound(AirDuct): + """Round/circular air ducts""" + + pattern_ifc_type = [ + re.compile(r'Rohr.*\(.*R.*,.*115.*\)', flags=re.IGNORECASE), + re.compile(r'^Rohr\s+DN\s*\d+', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + cross_section_d = attribute.Attribute( + description='Cross section diameter', + unit=ureg.millimeter, + ) + pipe_connection = attribute.Attribute( + description='Pipe connection type', + ) + list_round = attribute.Attribute( + description='On round list', + ) + + + +# ============================================================================ +# AIR DUCT FITTINGS +# ============================================================================ + +class AirDuctFitting(VentilationElement): + """Base class for all air duct fittings""" + + ifc_types = { + 'IfcDuctFitting': ['*'] + } + + length = attribute.Attribute( + description='Length', + unit=ureg.millimeter, patterns=[ - re.compile('.*electric.*efficiency', flags=re.IGNORECASE), - re.compile('.*el.*efficiency', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge \(l\)', flags=re.IGNORECASE), ], - unit=ureg.dimensionless, + ) + manufacturer = attribute.Attribute( + description='Manufacturer', + ) + type = attribute.Attribute( + description='Type', ) - # water_volume = attribute.Attribute( - # description="Water volume CHP chp", - # unit=ureg.meter ** 3, - # ) +class AirDuctRectangularFitting(AirDuctFitting): + """Rectangular duct fittings (Komponente eckig)""" -# collect all domain classes -items: Set[HVACProduct] = set() -for name, cls in inspect.getmembers( - sys.modules[__name__], - lambda member: inspect.isclass(member) # class at all - and issubclass(member, HVACProduct) # domain subclass - and member is not HVACProduct # but not base class - and member.__module__ == __name__): # declared here - items.add(cls) + pattern_ifc_type = [ + re.compile(r'Komponente\s+eckig.*\(.*122.*\)', flags=re.IGNORECASE), + re.compile(r'\d+\s*x\s*\d+', flags=re.IGNORECASE), + ] + + outer_dimension_a = attribute.Attribute( + description='Outer dimension A', + unit=ureg.millimeter, + ) + cross_section_a = attribute.Attribute( + description='Cross section a', + unit=ureg.millimeter, + ) + outer_dimension_b = attribute.Attribute( + description='Outer dimension B', + unit=ureg.millimeter, + ) + cross_section_b = attribute.Attribute( + description='Cross section b', + unit=ureg.millimeter, + ) + label_swap_ab = attribute.Attribute( + description='Swap label a/b', + ) + insert = attribute.Attribute( + description='Insert dimension', + unit=ureg.millimeter, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctRoundFitting(AirDuctFitting): + """Round duct components (Komponente rund)""" + + pattern_ifc_type = [ + re.compile(r'Komponente\s+rund.*\(.*123.*\)', flags=re.IGNORECASE), + re.compile(r'BEZ', flags=re.IGNORECASE), + ] + +class AirDuctRoundFlexible(AirDuctFitting): + """Flexible round air ducts""" + + pattern_ifc_type = [ + re.compile(r'Flexibles\s+Rohr.*\(.*RF.*,.*121.*\)', flags=re.IGNORECASE), + re.compile(r'Flexibles[_\s]Rohr', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + cross_section_d = attribute.Attribute( + description='Cross section diameter', + unit=ureg.millimeter, + ) + pipe_connection = attribute.Attribute( + description='Pipe connection type', + ) + +class AirDuctRoundTransition(AirDuctFitting): + """Round/Oval transition ducts (Rohrübergang Asym.)""" + + pattern_ifc_type = [ + re.compile(r'Übergang\s+Oval\s*-\s*Rund/Oval.*\(.*172.*\)', flags=re.IGNORECASE), + re.compile(r'Rohrübergang\s+Asym', flags=re.IGNORECASE), + ] + + width_b = attribute.Attribute( + description='Width diameter B', + unit=ureg.millimeter, + ) + height_h = attribute.Attribute( + description='Height diameter H', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Connection type/frame', + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + cross_section_output = attribute.Attribute( + description='Cross section output', + unit=ureg.millimeter, + ) + e_offset = attribute.Attribute( + description='E-offset', + unit=ureg.millimeter, + ) + f_offset = attribute.Attribute( + description='F-offset', + unit=ureg.millimeter, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + + +class AirDuctRoundTransitionSymmetric(AirDuctFitting): + """Round transition symmetric (Rohrübergang RS, RA)""" + + pattern_ifc_type = [ + re.compile(r'Rohrübergang.*\(.*RS.*,.*RA.*:.*109.*\)', flags=re.IGNORECASE), + ] + + width_b = attribute.Attribute( + description='Width diameter B', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + height_h = attribute.Attribute( + description='Height diameter H', + unit=ureg.millimeter, + ) + cross_section_output = attribute.Attribute( + description='Cross section output', + unit=ureg.millimeter, + ) + e_offset = attribute.Attribute( + description='E-offset', + unit=ureg.millimeter, + ) + f_offset = attribute.Attribute( + description='F-offset', + unit=ureg.millimeter, + ) + isolation_type = attribute.Attribute( + description='Isolation type', + ) + isolation = attribute.Attribute( + description='Insulation', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctOvalBow(AirDuctFitting): + """Oval duct bows/bends (Ovalrohr Bogen / Bogen Sym.)""" + + pattern_ifc_type = [ + re.compile(r'Ovalrohr\s+Bogen.*\(.*171.*\)', flags=re.IGNORECASE), + re.compile(r'Bogen\s+Sym.*0/G2R', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + width_b = attribute.Attribute( + description='Width diameter B', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + height_h = attribute.Attribute( + description='Height diameter H', + unit=ureg.millimeter, + ) + angle = attribute.Attribute( + description='Angle', + unit=ureg.degree, + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctRoundBend(AirDuctFitting): + """Round duct bends (Bogen (rund))""" + + pattern_ifc_type = [ + re.compile(r'Bogen.*\(.*rund.*,.*116.*\)', flags=re.IGNORECASE), + re.compile(r'^Bogen\s+\(rund', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + neutral_axis = attribute.Attribute( + description='Neutral axis', + unit=ureg.millimeter, + ) + insertion_length = attribute.Attribute( + description='Insertion length', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Bend radius connection', + ) + angle = attribute.Attribute( + description='Angle', + unit=ureg.degree, + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + + +class AirDuctConcentricReduction(AirDuctFitting): + """Concentric reductions (Reduktion_konzentrisch)""" + + pattern_ifc_type = [ + re.compile(r'Reduktion.*\(.*rund.*,.*117.*\)', flags=re.IGNORECASE), + re.compile(r'Reduktion[_\s]konzentrisch', flags=re.IGNORECASE), + ] + + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN1', + unit=ureg.millimeter, + ) + nominal_diameter_d2 = attribute.Attribute( + description='Nominal diameter DN2', + unit=ureg.millimeter, + ) + insertion_length = attribute.Attribute( + description='Insertion length', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + + +class AirDuctRectangularBow(AirDuctFitting): + """Rectangular bows/bends (Winkel)""" + + pattern_ifc_type = [ + re.compile(r'Bogen.*\(.*eckig.*WS.*,.*WA.*,.*111.*\)', flags=re.IGNORECASE), + re.compile(r'Winkel', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + height_a = attribute.Attribute( + description='Height a', + unit=ureg.millimeter, + ) + width_start_b = attribute.Attribute( + description='Width start b', + unit=ureg.millimeter, + ) + width_end_d = attribute.Attribute( + description='Width end d', + unit=ureg.millimeter, + ) + insert_end = attribute.Attribute( + description='Insert end', + unit=ureg.millimeter, + ) + insert_start = attribute.Attribute( + description='Insert start', + unit=ureg.millimeter, + ) + guide_vanes = attribute.Attribute( + description='Guide vanes', + ) + number_guide_vanes = attribute.Attribute( + description='Number of guide vanes', + ) + inner_radius_is_bend = attribute.Attribute( + description='Inner radius is bend', + ) + angle = attribute.Attribute( + description='Angle', + unit=ureg.degree, + ) + fitting_length = attribute.Attribute( + description='Fitting length', + unit=ureg.millimeter, + ) + pipe_connection = attribute.Attribute( + description='Pipe connection', + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + absolute_roughness_factor = attribute.Attribute( + description='Absolute roughness factor', + unit=ureg.millimeter, + ) + + +class AirDuctEndCap(AirDuctFitting): + """Duct end caps (Endboden)""" + + pattern_ifc_type = [ + re.compile(r'Endboden.*\(.*eckig.*,.*138.*\)', flags=re.IGNORECASE), + re.compile(r'^Endboden', flags=re.IGNORECASE), + ] + + width_a = attribute.Attribute( + description='Width a', + unit=ureg.millimeter, + ) + height_b = attribute.Attribute( + description='Height b', + unit=ureg.millimeter, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + + +class AirDuctTPiece(AirDuctFitting): + """T-pieces and branches (T-Stück)""" + + pattern_ifc_type = [ + re.compile(r'T-Stück.*\(.*TS.*,.*TA.*:.*114.*\)', flags=re.IGNORECASE), + re.compile(r'^T-Stück', flags=re.IGNORECASE), + ] + + width_entrance_a = attribute.Attribute( + description='Width entrance a', + unit=ureg.millimeter, + ) + height_entrance_b = attribute.Attribute( + description='Height entrance b', + unit=ureg.millimeter, + ) + width_outlet_d = attribute.Attribute( + description='Width outlet d', + unit=ureg.millimeter, + ) + width_branch_g = attribute.Attribute( + description='Width branch g', + unit=ureg.millimeter, + ) + height_branch_h = attribute.Attribute( + description='Height branch h', + unit=ureg.millimeter, + ) + height_branch_m = attribute.Attribute( + description='Height branch m', + unit=ureg.millimeter, + ) + length_entrance_n = attribute.Attribute( + description='Length entrance n', + unit=ureg.millimeter, + ) + second_branch_length_o = attribute.Attribute( + description='Second branch length o', + unit=ureg.millimeter, + ) + second_branch_height_p = attribute.Attribute( + description='Second branch height p', + unit=ureg.millimeter, + ) + cross_tee = attribute.Attribute( + description='Cross tee', + ) + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + absolute_roughness_factor = attribute.Attribute( + description='Absolute roughness factor', + unit=ureg.millimeter, + ) + + +class AirDuctTransitionRectangularAsymmetric(AirDuctFitting): + """Rectangular duct transitions (Übergang Asym.)""" + + pattern_ifc_type = [ + re.compile(r'Etage.*,.*Übergang.*\(.*ES.*,.*US.*,.*UA.*:.*105.*\)', flags=re.IGNORECASE), + re.compile(r'^Übergang\s+Asym', flags=re.IGNORECASE), + re.compile(r'^Übergang\s+Sym', flags=re.IGNORECASE), + re.compile(r'Etage', flags=re.IGNORECASE), + ] + + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + flow_rate = attribute.Attribute( + description='Flow rate', + unit=ureg.meter ** 3 / ureg.hour, + ) + strand = attribute.Attribute( + description='Strand', + ) + height_a = attribute.Attribute( + description='Height a', + unit=ureg.millimeter, + ) + width_start_b = attribute.Attribute( + description='Width start b', + unit=ureg.millimeter, + ) + height_end_c = attribute.Attribute( + description='Height end c', + unit=ureg.millimeter, + ) + width_end_d = attribute.Attribute( + description='Width end d', + unit=ureg.millimeter, + ) + e_offset = attribute.Attribute( + description='E-offset', + unit=ureg.millimeter, + ) + f_offset = attribute.Attribute( + description='F-offset', + unit=ureg.millimeter, + ) + insert_start = attribute.Attribute( + description='Insert start', + unit=ureg.millimeter, + ) + extension_m = attribute.Attribute( + description='Extension m', + unit=ureg.millimeter, + ) + extension_n = attribute.Attribute( + description='Extension n', + unit=ureg.millimeter, + ) + box_lbh = attribute.Attribute( + description='Box length-width-height', + ) + box_bottom_lbh = attribute.Attribute( + description='Box bottom length-width-height', + ) + connection_length_height = attribute.Attribute( + description='Connection length height', + ) + list_as_ua = attribute.Attribute( + description='List as UA', + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + absolute_roughness_factor = attribute.Attribute( + description='Absolute roughness factor', + unit=ureg.millimeter, + ) + + +class AirDuctTransitionSymmetrical(AirDuctFitting): + """Symmetrical duct transitions (Bogen Sym.)""" + + # @property + # def expected_hvac_ports(self): + # return 0 + + pattern_ifc_type = [ + re.compile(r'Bogen.*\(.*eckig\s+BS.*,.*BA.*,.*107.*\)', flags=re.IGNORECASE), + re.compile(r'^Bogen\s+Sym.*eckig', flags=re.IGNORECASE), + re.compile(r'Kanalbogen\s+\d+\s*x\s*\d+\s*x\s*\w+', flags=re.IGNORECASE) + ] + + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + flow_rate = attribute.Attribute( + description='Flow rate', + unit=ureg.meter ** 3 / ureg.hour, + ) + strand = attribute.Attribute( + description='Strand', + ) + height_a = attribute.Attribute( + description='Height a', + unit=ureg.millimeter, + ) + width_start_b = attribute.Attribute( + description='Width start b', + unit=ureg.millimeter, + ) + width_end_d = attribute.Attribute( + description='Width end d', + unit=ureg.millimeter, + ) + insert_end = attribute.Attribute( + description='Insert end', + unit=ureg.millimeter, + ) + insert_start = attribute.Attribute( + description='Insert start', + unit=ureg.millimeter, + ) + guide_vanes = attribute.Attribute( + description='Guide vanes', + ) + number_guide_vanes = attribute.Attribute( + description='Number of guide vanes', + ) + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + inner_radius_is_bend = attribute.Attribute( + description='Inner radius is bend', + ) + angle = attribute.Attribute( + description='Angle', + unit=ureg.degree, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + absolute_roughness_factor = attribute.Attribute( + description='Absolute roughness factor', + unit=ureg.millimeter, + ) + + +class AirDuctTransitionRectangularBow(AirDuctFitting): + """Rectangular duct transition bows (Übergangsbogen)""" + + pattern_ifc_type = [ + re.compile(r'^Übergangsbogen', flags=re.IGNORECASE), + ] + + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + flow_rate = attribute.Attribute( + description='Flow rate', + unit=ureg.meter ** 3 / ureg.hour, + ) + strand = attribute.Attribute( + description='Strand', + ) + width_b = attribute.Attribute( + description='Width diameter B', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + height_h = attribute.Attribute( + description='Height diameter H', + unit=ureg.millimeter, + ) + cross_section_output = attribute.Attribute( + description='Cross section output', + unit=ureg.millimeter, + ) + e_offset = attribute.Attribute( + description='E-offset', + unit=ureg.millimeter, + ) + f_offset = attribute.Attribute( + description='F-offset', + unit=ureg.millimeter, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + absolute_roughness_factor = attribute.Attribute( + description='Absolute roughness factor', + unit=ureg.millimeter, + ) + +class AirDuctRectangularPants(AirDuctFitting): + """Rectangular pants/Y-piece (Hosenstück eckig)""" + + pattern_ifc_type = [ + re.compile(r'Hosenstück.*\(.*HSE.*:.*131.*\)', flags=re.IGNORECASE), + re.compile(r'^Hosenstück\s+eckig', flags=re.IGNORECASE), + re.compile(r'Hosenstück', flags=re.IGNORECASE), + ] + + cross_section_entrance_a = attribute.Attribute( + description='Cross section entrance a', + unit=ureg.millimeter, + ) + cross_section_entrance_b = attribute.Attribute( + description='Cross section entrance b', + unit=ureg.millimeter, + ) + cross_section_output_c = attribute.Attribute( + description='Cross section output c', + unit=ureg.millimeter, + ) + cross_section_output_d = attribute.Attribute( + description='Cross section output d', + unit=ureg.millimeter, + ) + e_offset = attribute.Attribute( + description='E-offset', + unit=ureg.millimeter, + ) + f_offset = attribute.Attribute( + description='F-offset', + unit=ureg.millimeter, + ) + cross_section_output_h = attribute.Attribute( + description='Cross section output h', + unit=ureg.millimeter, + ) + center_distance = attribute.Attribute( + description='Center distance m', + unit=ureg.millimeter, + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctRectangularInspectionCover(AirDuctFitting): + """Rectangular inspection cover (Revisionsdeckel)""" + + pattern_ifc_type = [ + re.compile(r'Revisionsdeckel.*\(.*REV.*,.*163.*\)', flags=re.IGNORECASE), + re.compile(r'^Revisionsdeckel.*Rohr', flags=re.IGNORECASE), + ] + + length_a = attribute.Attribute( + description='Length a', + unit=ureg.millimeter, + ) + remark = attribute.Attribute( + description='Remark', + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + width_b = attribute.Attribute( + description='Width b', + unit=ureg.millimeter, + ) + nominal_diameter_dn = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + height_h = attribute.Attribute( + description='Height', + unit=ureg.millimeter, + ) + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + + +class AirDuctRoundStub(AirDuctFitting): + """Round stub/nozzle (Stutzen rund)""" + + pattern_ifc_type = [ + re.compile(r'Stutzen.*rund.*\(.*SR.*:.*124.*\)', flags=re.IGNORECASE), + re.compile(r'^Stutzen[_\s]rund', flags=re.IGNORECASE), + ] + + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + + +class AirDuctRivetedEdge(AirDuctFitting): + """Riveted edge/flange (Nietbord)""" + + @property + def expected_hvac_ports(self): + return 0 + + pattern_ifc_type = [ + re.compile(r'Nietbord.*\(.*K.*:.*125.*\)', flags=re.IGNORECASE), + re.compile(r'^Nietbord', flags=re.IGNORECASE), + ] + + dimension_a = attribute.Attribute( + description='Dimension a', + unit=ureg.millimeter, + ) + dimension_b = attribute.Attribute( + description='Dimension b', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + type = attribute.Attribute( + description='Type (Round/Rectangular)', + ) + +class AirDuctRoundCoupling(AirDuctFitting): + """Round duct coupling/sleeve (Muffe)""" + + pattern_ifc_type = [ + re.compile(r'Muffe.*\(.*R.*,.*145.*\)', flags=re.IGNORECASE), + re.compile(r'^MUFFE\s+DN\d+', flags=re.IGNORECASE), + re.compile(r'^Muffe.*rund', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + cross_section_d1 = attribute.Attribute( + description='Cross section diameter', + unit=ureg.millimeter, + ) + connection_type = attribute.Attribute( + description='Connection type', + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + + + +# ============================================================================ +# DAMPERS AND CONTROLLERS +# ============================================================================ + + +class FireDamper(VentilationElement): + """Fire dampers (Brandschutzklappe)""" + + ifc_types = { + 'IfcDamper': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'Brandschutzklappe', flags=re.IGNORECASE), + re.compile(r'Komponente\s+eckig\s*\(\s*(\d+)\s*\)', flags=re.IGNORECASE), + ] + + manufacturer = attribute.Attribute( + description='Manufacturer', + ) + type = attribute.Attribute( + description='Type', + ) + insert = attribute.Attribute( + description='Insert dimension', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + flow_velocity = attribute.Attribute( + description='Flow velocity', + unit=ureg.meter / ureg.second, + ) + drive_side = attribute.Attribute( + description='Drive side', + ) + remark = attribute.Attribute( + description='Remark', + ) + drive = attribute.Attribute( + description='Drive/actuator', + ) + outer_dimension_a = attribute.Attribute( + description='Outer dimension A', + unit=ureg.millimeter, + ) + cross_section_a = attribute.Attribute( + description='Cross section a', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + outer_dimension_b = attribute.Attribute( + description='Outer dimension B', + unit=ureg.millimeter, + ) + cross_section_b = attribute.Attribute( + description='Cross section b', + unit=ureg.millimeter, + ) + label_swap_ab = attribute.Attribute( + description='Swap label a/b', + ) + pressure_loss_opening_1 = attribute.Attribute( + description='Pressure loss opening 1', + unit=ureg.pascal, + ) + length = attribute.Attribute( + description='Length', + unit=ureg.millimeter, + patterns=[ + re.compile(r'\[l\] Länge', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge \(l\)', flags=re.IGNORECASE), + ], + ) + air_volume = attribute.Attribute( + description='Air volume', + unit=ureg.meter ** 3 / ureg.hour, + ) + zeta_opening_1 = attribute.Attribute( + description='Zeta opening 1', + ) + + +class AirVolumeFlowController(VentilationElement): + """Base class for volume flow controllers""" + + flow_velocity = attribute.Attribute( + description='Flow velocity', + unit=ureg.meter / ureg.second, + ) + manufacturer = attribute.Attribute( + description='Manufacturer', + ) + type = attribute.Attribute( + description='Type', + ) + insert = attribute.Attribute( + description='Insert dimension', + unit=ureg.millimeter, + ) + length = attribute.Attribute( + description='Length', + unit=ureg.millimeter, + patterns=[ + re.compile(r'\[l\] Länge', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge \(l\)', flags=re.IGNORECASE), + ], + ) + + +class AirVolumeFlowControllerConstant(AirVolumeFlowController): + """Constant volume flow controllers (Konstant VSR)""" + + ifc_types = { + 'IfcFlowController': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Konstant\s+VSR', flags=re.IGNORECASE), + re.compile(r'Komponente\s+rund\s*\(\s*(\d+)\s*\)', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + outer_diameter = attribute.Attribute( + description='Outer diameter', + unit=ureg.millimeter, + ) + air_volume = attribute.Attribute( + description='Air volume', + unit=ureg.meter ** 3 / ureg.hour, + ) + + +class AirVolumeFlowControllerDynamic(AirVolumeFlowController): + """Dynamic/variable volume flow controllers (Variabel VSR)""" + + ifc_types = { + 'IfcDuctFitting': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Variabel\s+VSR', flags=re.IGNORECASE), + re.compile(r'Komponente\s+eckig\s*\(\s*(\d+)\s*\)', flags=re.IGNORECASE), + ] + + outer_dimension_a = attribute.Attribute( + description='Outer dimension A', + unit=ureg.millimeter, + ) + cross_section_a = attribute.Attribute( + description='Cross section a', + unit=ureg.millimeter, + ) + outer_dimension_b = attribute.Attribute( + description='Outer dimension B', + unit=ureg.millimeter, + ) + cross_section_b = attribute.Attribute( + description='Cross section b', + unit=ureg.millimeter, + ) + label_swap_ab = attribute.Attribute( + description='Swap label a/b', + ) + isolation = attribute.Attribute( + description='Insulation thickness', + unit=ureg.millimeter, + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + flow_rate_range = attribute.Attribute( + description='Flow rate range', + unit=ureg.meter ** 3 / ureg.hour, + ) + + +# ============================================================================ +# SILENCERS +# ============================================================================ + +class AirSilencer(VentilationElement): + """Base class for silencers/sound dampeners""" + + manufacturer = attribute.Attribute( + description='Manufacturer', + ) + type = attribute.Attribute( + description='Type', + ) + length = attribute.Attribute( + description='Length', + unit=ureg.millimeter, + patterns=[ + re.compile(r'\[l\] Länge', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge \(l\)', flags=re.IGNORECASE), + ], + ) + + +class AirSilencerRoundFlexible(AirSilencer): + """Flexible round silencers (Flexibler Rohrschalldämpfer)""" + + ifc_types = { + 'IfcDuctFitting': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Flexibler\s+Rohrschalld(?:a|ä|ae)mpfer', flags=re.IGNORECASE), + re.compile(r'Komponente\s+rund\s*\(\s*(\d+)\s*\)', flags=re.IGNORECASE), + ] + + flow_velocity = attribute.Attribute( + description='Flow velocity', + unit=ureg.meter / ureg.second, + ) + designation_is = attribute.Attribute( + description='Designation (IS)', + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + outer_diameter = attribute.Attribute( + description='Outer diameter', + unit=ureg.millimeter, + ) + insert_end = attribute.Attribute( + description='Insert end', + unit=ureg.millimeter, + ) + + +class AirSilencerTelephony(AirSilencer): + """Telephony silencers (Telefonie-Schalldämpfer)""" + + ifc_types = { + 'IfcDamper': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Telefonie-Schalld[aä]mpfer', flags=re.IGNORECASE), + re.compile(r'^Telefonie-Schalld(?:a|ä|ae)mpfer', flags=re.IGNORECASE), + re.compile(r'Komponente\s+rund\s*\(\s*(\d+)\s*\)', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + outer_diameter = attribute.Attribute( + description='Outer diameter', + unit=ureg.millimeter, + ) + insert = attribute.Attribute( + description='Insert dimension', + unit=ureg.millimeter, + ) + list_round = attribute.Attribute( + description='On round list', + ) + rings = attribute.Attribute( + description='Number of rings', + ) + + +# ============================================================================ +# AIR TERMINALS +# ============================================================================ + +class AirTerminal(VentilationElement): + """Air terminals/outlets (Luftdurchlasse)""" + + ifc_types = { + 'IfcAirTerminal': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'Drallauslass.*\(.*M[_\s]20011.*\)', flags=re.IGNORECASE), + re.compile(r'^Luftdurchl[aä]sse', flags=re.IGNORECASE), + re.compile(r'^Auslass.*\(.*M[_\s]20011.*\)', flags=re.IGNORECASE), + ] + + outlet_grille_graphic = attribute.Attribute( + description='Outlet grille graphic', + ) + nominal_diameter_d1 = attribute.Attribute( + description='Nominal diameter DN', + unit=ureg.millimeter, + ) + cylinder_1 = attribute.Attribute( + description='Cylinder 1: DN1 DN2 Length', + ) + cylinder_2 = attribute.Attribute( + description='Cylinder 2: DN1 DN2 Length', + ) + cylinder_3 = attribute.Attribute( + description='Cylinder 3: DN1 DN2 Length', + ) + manufacturer = attribute.Attribute( + description='Manufacturer', + ) + project = attribute.Attribute( + description='Project', + ) + series = attribute.Attribute( + description='Series', + ) + type = attribute.Attribute( + description='Type', + ) + flow_rate = attribute.Attribute( + description='Flow rate', + unit=ureg.meter ** 3 / ureg.hour, + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + + +class AirValve(VentilationElement): + """Air valves/disc valves (Tellerventile)""" + + ifc_types = { + 'IfcDuctFitting': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'Auslass.*\(.*M[_\s]20111.*\)', flags=re.IGNORECASE), + re.compile(r'^Tellerventile', flags=re.IGNORECASE), + ] + + radius = attribute.Attribute( + description='Radius', + unit=ureg.millimeter, + ) + outlet_grille_graphic = attribute.Attribute( + description='Outlet grille graphic', + ) + nominal_diameter_connection = attribute.Attribute( + description='Nominal diameter connection', + unit=ureg.millimeter, + ) + cylinder_1 = attribute.Attribute( + description='Cylinder 1: DN1 DN2 Length', + ) + cylinder_2 = attribute.Attribute( + description='Cylinder 2: DN1 DN2 Length', + ) + cylinder_3 = attribute.Attribute( + description='Cylinder 3: DN1 DN2 Length', + ) + manufacturer = attribute.Attribute( + description='Manufacturer', + ) + cross_tee = attribute.Attribute( + description='Cross tee', + ) + cost_group = attribute.Attribute( + description='Cost group', + ) + outlet_type = attribute.Attribute( + description='Outlet type', + ) + project = attribute.Attribute( + description='Project', + ) + rings = attribute.Attribute( + description='Number of rings', + ) + series = attribute.Attribute( + description='Series', + ) + type = attribute.Attribute( + description='Type', + ) + flow_rate = attribute.Attribute( + description='Flow rate', + unit=ureg.meter ** 3 / ureg.hour, + ) + air_velocity = attribute.Attribute( + description='Air velocity', + unit=ureg.meter / ureg.second, + ) + + +# ============================================================================ +# AIR HANDLING UNIT COMPONENTS +# ============================================================================ + +class AHUComponent(VentilationElement): + """Base class for air handling unit components""" + + outer_dimension_a = attribute.Attribute( + description='Outer dimension A', + unit=ureg.millimeter, + ) + cross_section_a = attribute.Attribute( + description='Cross section a', + unit=ureg.millimeter, + ) + outer_dimension_b = attribute.Attribute( + description='Outer dimension B', + unit=ureg.millimeter, + ) + cross_section_b = attribute.Attribute( + description='Cross section b', + unit=ureg.millimeter, + ) + insert = attribute.Attribute( + description='Insert dimension', + unit=ureg.millimeter, + ) + graphics = attribute.Attribute( + description='Graphics (top, bottom, front, back)', + ) + length = attribute.Attribute( + description='Length', + unit=ureg.millimeter, + patterns=[ + re.compile(r'\[l\] Länge', flags=re.IGNORECASE), + re.compile(r'\[l\] Länge \(l\)', flags=re.IGNORECASE), + ], + ) + cross_section = attribute.Attribute( + description='Cross section', + ) + strand = attribute.Attribute( + description='Strand', + ) + flow_rate = attribute.Attribute( + description='Flow rate', + unit=ureg.meter ** 3 / ureg.hour, + ) + + +class AHUFan(AHUComponent): + """Air handling unit fan""" + + ifc_types = { + 'IfcFan': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Ventilator\s+allgemein', flags=re.IGNORECASE), + ] + + +class AHUAirChamber(AHUComponent): + """Air handling unit air chamber""" + + ifc_types = { + 'IfcDistributionChamberElement': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Leerkammer', flags=re.IGNORECASE), + ] + + +class AHUCooler(AHUComponent): + """Air handling unit cooler""" + + ifc_types = { + 'IfcDuctFitting': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Luftk[uü]hler', flags=re.IGNORECASE), + ] + + +class AHUSilencer(AHUComponent): + """Air handling unit silencer""" + + ifc_types = { + 'IfcDamper': ['*'] + } + + pattern_ifc_type = [ + re.compile(r'^Schalld(?:a|ä|ae)mpfer\s+Allgemein', flags=re.IGNORECASE), + re.compile(r'^Schalld(?:a|ä|ae)mpfer', flags=re.IGNORECASE), + ] + +# collect all domain classes +items: Set[HVACProduct] = set() +for name, cls in inspect.getmembers( + sys.modules[__name__], + lambda member: inspect.isclass(member) # class at all + and issubclass(member, HVACProduct) # domain subclass + and member is not HVACProduct # but not base class + and member not in (VentilationElement, AirSilencer, AirVolumeFlowController, AHUComponent) # but not sub base class + and member.__module__ == __name__): # declared here + items.add(cls) + +hydraulic_items: Set[HVACProduct] = set() +for name, cls in inspect.getmembers( + sys.modules[__name__], + lambda member: inspect.isclass(member) # class at all + and issubclass(member, HVACProduct) # domain subclass + and member is not HVACProduct # but not base class + and not issubclass(member, VentilationElement) # but not ventilation element + and member.__module__ == __name__): # declared here + hydraulic_items.add(cls) + +# collect all domain classes +ventilation_items: Set[VentilationElement] = set() +for name, cls in inspect.getmembers( + sys.modules[__name__], + lambda member: inspect.isclass(member) # class at all + and issubclass(member, VentilationElement) # domain subclass + and member is not VentilationElement # but not base class + and member not in (AirSilencer, AirVolumeFlowController, AHUComponent) # but not sub base class + and member.__module__ == __name__): # declared here + ventilation_items.add(cls) diff --git a/bim2sim/elements/mapping/attribute.py b/bim2sim/elements/mapping/attribute.py index 1590177437..facf4f2cc4 100644 --- a/bim2sim/elements/mapping/attribute.py +++ b/bim2sim/elements/mapping/attribute.py @@ -241,27 +241,63 @@ def get_from_patterns(bind, patterns, name): def get_from_functions(bind, functions: list, name: str): """Get value from functions. - First successful function calls return value is used. As we want to - allow to overwrite functions in inherited classes, we use - getattr(bind, func.__name__) to get the function from the bind. + First successful function call's return value is used. Functions can be + 1. Methods from the bind object's class hierarchy (use inheritance) + 2. Methods from external classes (call directly with bind as first arg) Args: - bind: the bind object - functions: a list of functions - name: the name of the attribute + bind: The bind object + functions: List of function objects + name: The attribute name to process """ value = None for func in functions: - func_inherited = getattr(bind, func.__name__) - try: - value = func_inherited(name) - except Exception as ex: - logger.error("Function '%s' of %s.%s raised %s", - func.__name__, bind, name, ex) - pass + # Check if the function's class is in the bind object's class + # hierarchy + if hasattr(func, '__qualname__') and '.' in func.__qualname__: + func_class_name = func.__qualname__.split('.')[0] + + # Check if the function's class is in the bind's class + # hierarchy + is_in_hierarchy = False + for cls in bind.__class__.__mro__: + if cls.__name__ == func_class_name: + is_in_hierarchy = True + break + + if is_in_hierarchy: + # Function is from bind's class hierarchy, + # use inheritance + try: + func_to_call = getattr(bind, func.__name__) + value = func_to_call(name) + except Exception as ex: + logger.error("Function '%s' of %s.%s raised %s", + func.__name__, bind, name, ex) + pass + else: + # Function is from an external class, call directly with + # bind as first arg + try: + value = func(bind, name) + except Exception as ex: + logger.error("Function '%s' of %s.%s raised %s", + func.__name__, bind, name, ex) + pass else: - if value is not None: - break + # Fallback for functions without __qualname__, use inheritance + try: + func_to_call = getattr(bind, func.__name__) + value = func_to_call(name) + except Exception as ex: + logger.error("Function '%s' of %s.%s raised %s", + func.__name__, bind, name, ex) + pass + + # Break the loop if we got a non-None value + if value is not None: + break + return value @staticmethod diff --git a/bim2sim/elements/mapping/ifc2python.py b/bim2sim/elements/mapping/ifc2python.py index f212dc9947..aad247a5e6 100644 --- a/bim2sim/elements/mapping/ifc2python.py +++ b/bim2sim/elements/mapping/ifc2python.py @@ -414,6 +414,12 @@ def get_predefined_type(ifcElement) -> Union[str, None]: except AttributeError: return None +def get_descriptions(ifcElement): + """Return the object type of the IFC element""" + try: + return [ifcElement.ObjectType, ifcElement.Name, ifcElement.Description] + except TypeError: + pass def getElementType(ifcElement): """Return the ifctype of the IFC element""" diff --git a/bim2sim/elements/mapping/units.py b/bim2sim/elements/mapping/units.py index 48aa69bc19..05104000b4 100644 --- a/bim2sim/elements/mapping/units.py +++ b/bim2sim/elements/mapping/units.py @@ -64,7 +64,7 @@ def parse_ifc(unit_entity): unit_part = ureg.parse_units('{}{}'.format(prefix_string, ifc_pint_unitmap[ element.Unit.Name])) - if element.Unit.Dimensions: + if hasattr(element, "Dimensions"): unit_part = unit_part ** element.Dimensions unit = unit * unit_part ** element.Exponent return unit diff --git a/bim2sim/examples/e1_template_plugin.py b/bim2sim/examples/e1_template_plugin.py index b52d8f715c..963dec1304 100644 --- a/bim2sim/examples/e1_template_plugin.py +++ b/bim2sim/examples/e1_template_plugin.py @@ -63,7 +63,7 @@ def run_simple_project(): # Let's assign a weather file first. This is currently needed, even if no # simulation is performed - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') diff --git a/bim2sim/plugins/PluginAixLib/test/unit/kernel/task/__init__.py b/bim2sim/examples/e2_compare_two_simulations.py similarity index 100% rename from bim2sim/plugins/PluginAixLib/test/unit/kernel/task/__init__.py rename to bim2sim/examples/e2_compare_two_simulations.py diff --git a/bim2sim/examples/e2_interactive_project.py b/bim2sim/examples/e2_interactive_project.py index 31447105e6..0dc9d7b93b 100644 --- a/bim2sim/examples/e2_interactive_project.py +++ b/bim2sim/examples/e2_interactive_project.py @@ -38,7 +38,7 @@ def run_interactive_example(): project_path, ifc_paths, 'template', open_conf=True) # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') diff --git a/bim2sim/export/modelica/__init__.py b/bim2sim/export/modelica/__init__.py index d01fc5d274..f2fdbf790a 100644 --- a/bim2sim/export/modelica/__init__.py +++ b/bim2sim/export/modelica/__init__.py @@ -1,10 +1,10 @@ """Package for Modelica export""" -import codecs import logging -import os +from enum import Enum from pathlib import Path from threading import Lock -from typing import Union, Type, Dict, Container, Callable, List, Any, Iterable +from typing import (Union, Type, Dict, Container, Callable, List, Any, + Iterable) import numpy as np import pint @@ -42,6 +42,70 @@ def clean_string(string: str) -> str: return string.replace('$', '_') +def help_package(path: Path, name: str, uses: str = None, within: str = None): + """Creates a package.mo file. + + Parameters + ---------- + + path : Path + path of where the package.mo should be placed + name : string + name of the Modelica package + uses : + within : string + path of Modelica package containing this package + """ + + # Create the directory if it doesn't exist + path.mkdir(parents=True, exist_ok=True) + + # Define the path to the template and render the package.mo + template_path_package = (Path(bim2sim.__file__).parent / + "assets/templates/modelica/package.txt") + package_template = Template(filename=str(template_path_package)) + # Write the rendered template to 'package.mo' in the specified path + with open(path / 'package.mo', 'w') as out_file: + out_file.write(package_template.render_unicode( + name=name, + within=within, + uses=uses)) + # out_file.close() + + +def help_package_order(path: Path, package_list: List[str], addition=None, + extra=None): + """Creates a package.order file. + + Parameters + ---------- + + path : Path + path of where the package.mo should be placed + package_list : string + name of all models or packages contained in the package + addition : string + if there should be a suffix in front of package_list.string it can + be specified + extra : string + an extra package or model not contained in package_list can be + specified + """ + + template_package_order_path = Path(bim2sim.__file__).parent / \ + "assets/templates/modelica/package_order.txt" + package_order_template = Template(filename=str( + template_package_order_path)) + + rendered_content = package_order_template.render_unicode( + list=package_list, + addition=addition, + extra=extra + ) + final_output = rendered_content.rstrip() + with open(path / 'package.order', 'w', newline='\n') as out_file: + out_file.write(final_output) + class ModelicaModel: """Modelica model""" @@ -49,7 +113,10 @@ def __init__(self, name: str, comment: str, modelica_elements: List['ModelicaElement'], - connections: list): + connections: list, + connections_heat_ports_conv: list, + connections_heat_ports_rad: list + ): """ Args: name: The name of the model. @@ -66,6 +133,9 @@ def __init__(self, self.connections = self.set_positions(modelica_elements, connections) + self.connections_heat_ports_conv = connections_heat_ports_conv + self.connections_heat_ports_rad = connections_heat_ports_rad + def set_positions(self, elements: list, connections: list) -> list: """ Sets the position of elements relative to min/max positions of instance.element.position @@ -108,7 +178,7 @@ def set_positions(self, elements: list, connections: list) -> list: ) return connections_positions - def code(self) -> str: + def render_modelica_code(self) -> str: """ Returns the Modelica code for the model.The mako template is used to render the Modelica code based on the model's elements, connections, and unknown parameters. @@ -136,25 +206,34 @@ def unknown_params(self) -> list: unknown_parameters.extend(unknown_parameter) return unknown_parameters - def save(self, path: str): + def save(self, path: Path): """ Save the model as Modelica file. Args: - path (str): The path where the Modelica file should be saved. + path (Path): The path where the Modelica file should be saved. """ - _path = os.path.normpath(path) - if os.path.isdir(_path): - _path = os.path.join(_path, self.name) + _path = path.resolve() - if not _path.endswith(".mo"): - _path += ".mo" + if _path.is_dir(): + _path = _path / self.name - data = self.code() + if not str(_path).endswith(".mo"): + _path = _path.with_suffix(".mo") + + data = self.render_modelica_code() user_logger.info("Saving '%s' to '%s'", self.name, _path) - with codecs.open(_path, "w", "utf-8") as file: + with _path.open("w", encoding="utf-8") as file: file.write(data) + def save_pkg(self, pkg_path: Path): + + pkg_name = pkg_path.stem + help_package(path=pkg_path, name=pkg_name, + within=pkg_path.parent.stem) + help_package_order(path=pkg_path, package_list=[pkg_name]) + self.save(pkg_path / pkg_name) + class ModelicaElement: """ Modelica model element @@ -200,6 +279,7 @@ def __init__(self, element: HVACProduct): self.guid = self._get_clean_guid() self.name = self._get_name() self.comment = self.get_comment() + self.heat_ports = [] def _get_clean_guid(self) -> str: """ Gets a clean GUID of the element. @@ -369,6 +449,10 @@ def get_full_port_name(self, port: HVACPort) -> str: """ return "%s.%s" % (self.name, self.get_port_name(port)) + def get_heat_port_names(self): + """Returns names of heat ports if existing""" + return {} + def __repr__(self): return "<%s %s>" % (self.path, self.name) @@ -508,7 +592,8 @@ def collect(self): self.value = self._answers[self.name] elif self.attributes: attribute_value = self.get_attribute_value() - self.value = self.convert_parameter(attribute_value) + if attribute_value is not None: + self.value = self.convert_parameter(attribute_value) elif self.value is not None: self.value = self.convert_parameter(self.value) else: @@ -533,7 +618,10 @@ def value(self, value): self._value = value else: logger.warning("Parameter check failed for '%s' with value: " - "%s", self.name, self._value) + "%s of element %s with GUID %s", + self.name, self._value, + self.element.__class__.__name__, + self.element.guid) self._value = None else: self._value = value @@ -695,6 +783,58 @@ def inner_check(value): return inner_check +class HeatTransferType(Enum): + CONVECTIVE = "convective" + RADIATIVE = "radiative" + GENERIC = "generic" + + +class HeatPort: + """Simplified representation of a heat port in Modelica. + + This does not represent a bim2sim element, as IFC doesn't have the concept + of heat ports. This class is just for better differentiation between + radiative, convective and generic heat ports. + + Args: + heat_transfer_type (HeatTransferType): The type of heat transfer. + name (str): name of the heat port in the parent modelica element + parent (Instance): Modelica Instance that holds this heat port + """ + + def __init__(self, + heat_transfer_type: Union[HeatTransferType, str], + name: str, + parent: ModelicaElement): + self.heat_transfer_type = heat_transfer_type + self.name = name + self.parent = parent + + @property + def heat_transfer_type(self): + return self._heat_transfer_type + + @heat_transfer_type.setter + def heat_transfer_type(self, value: Union[HeatTransferType, str]): + if isinstance(value, HeatTransferType): + self._heat_transfer_type = value + elif isinstance(value, str): + try: + self._heat_transfer_type = HeatTransferType[value.upper()] + except KeyError: + raise AttributeError(f'Cannot set heat_transfer_type to {value}, ' + f'only "convective", "radiative", and ' + f'"generic" are allowed') + else: + raise AttributeError(f'Cannot set heat_transfer_type to {value}, ' + f'only instances of HeatTransferType or ' + f'strings "convective", "radiative", and ' + f'"generic" are allowed') + + def get_full_name(self): + return f"{self.parent.name}.{self.name}" + + class Dummy(ModelicaElement): path = "Path.to.Dummy" represents = elem.Dummy diff --git a/bim2sim/export/modelica/standardlibrary.py b/bim2sim/export/modelica/standardlibrary.py index b699976fcc..1fa52f4010 100644 --- a/bim2sim/export/modelica/standardlibrary.py +++ b/bim2sim/export/modelica/standardlibrary.py @@ -51,10 +51,12 @@ def __init__(self, element: Union[hvac.Pipe]): attributes=['diameter']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' + # TODO #733 find port if sourceandsink or sinkdansource + # if port.flow_direction == 0. # SOURCEANDSINK and SINKANDSOURCE else: return super().get_port_name(port) @@ -81,9 +83,9 @@ def __init__(self, element): attributes=['nominal_mass_flow_rate']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) diff --git a/bim2sim/kernel/__init__.py b/bim2sim/kernel/__init__.py index 6a3b5d61ad..26cbbf3659 100644 --- a/bim2sim/kernel/__init__.py +++ b/bim2sim/kernel/__init__.py @@ -6,3 +6,4 @@ class IFCDomainError(Exception): """Exception raised if IFCDomain of file and element do not fit.""" + diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py index ac1767d968..d1900cf31b 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py @@ -26,15 +26,18 @@ class PluginAixLib(Plugin): tasks = {LoadLibrariesAixLib} default_tasks = [ common.LoadIFC, - common.CheckIfc, + # common.CheckIfc, common.CreateElementsOnIfcTypes, hvac.ConnectElements, hvac.MakeGraph, + hvac.EnrichFlowDirection, hvac.ExpansionTanks, hvac.Reduce, hvac.DeadEnds, LoadLibrariesAixLib, + hvac.CreateModelicaModel, hvac.Export, + ] def create_modelica_table_from_list(self, curve): diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib.py index 488cc0decb..7daee6e491 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib.py @@ -34,7 +34,7 @@ def run_example_simple_hvac_aixlib(): project = Project.create(project_path, ifc_paths, 'aixlib') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib_testing.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib_testing.py new file mode 100644 index 0000000000..542ee4b33a --- /dev/null +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e1_simple_project_hvac_aixlib_testing.py @@ -0,0 +1,84 @@ +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project, run_project, ConsoleDecisionHandler +from bim2sim.utilities.types import IFCDomain +from bim2sim.elements.base_elements import Material +from bim2sim.elements import bps_elements as bps_elements, \ + hvac_elements as hvac_elements + +def run_example_simple_hvac_aixlib(): + """Run an HVAC simulation with the AixLib backend. + + This example runs an HVAC with the aixlib backend. Specifies project + directory and location of the HVAC IFC file. Then, it creates a bim2sim + project with the aixlib backend. Simulation settings are specified (here, + the aggregations are specified), before the project is executed with the + previously specified settings.""" + + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path( + tempfile.TemporaryDirectory( + prefix='bim2sim_example_simple_aixlib').name) + + + # Set path of ifc for hydraulic domain with the fresh downloaded test models + ifc_paths = { + IFCDomain.ventilation: + Path(r"D:\03_Cloud\Sciebo\BIM2Praxis\IFC-Modelle\EDGE\ueberarbeitet_2025-10\BIM2PRAXIS_RLT-2025-09-11_cleaned_jho.ifc") + } + + # Create a project including the folder structure for the project with + # teaser as backend and no specified workflow (default workflow is taken) + project = Project.create(project_path, ifc_paths, 'aixlib') + + project.sim_settings.relevant_elements = {*hvac_elements.ventilation_items, Material} + + # set weather file data + project.sim_settings.weather_file_path_modelica = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + # specify simulation settings + project.sim_settings.aggregations = [ + 'UnderfloorHeating', + 'Consumer', + 'PipeStrand', + 'ParallelPump', + 'ConsumerHeatingDistributorModule', + 'GeneratorOneFluid' + ] + project.sim_settings.group_unidentified = 'name_and_description' + + # Run the project with the ConsoleDecisionHandler. This allows interactive + # input to answer upcoming questions regarding the imported IFC. + # Correct decision for identification of elements and useful parameters for + # missing attributes are written below + run_project(project, ConsoleDecisionHandler()) + + +# Documentation of Decision answers: +# IfcBuildingElementProxy: skip + +# IfcPipeFitting, Name: Heizungsarmatur, Description: Ventilgehäuse DG mit +# Fühler, GUID: 0$QIFdmTARhKaMBTfJUvWD: 'HVAC-PipeFitting' + +# IfcPipeFitting, Name: Heizungsarmatur, Description: Rücklaufverschraubung +# DG, GUID: 03TbBCNszVXaBWMuR55Ezt: 'HVAC-PipeFitting' + +# IfcPipeFitting, Name: Apparate (M_606), Description: Apparate (M_606), +# GUID: 1259naiEpIkasmH4NcC8DL: 'HVAC-Distributor', + +# IfcValve, Name: Armatur/Flansch/Dichtung M_600, Description: +# 3-Wege-Regelventil PN16, GUID: 0A4aE_Sb7Wa4OrZ9Zwd6P3: 'HVAC-ThreeWayValve' + +# IfcValve, Name: Armatur/Flansch/Dichtung M_600, Description: Hubventil, +# GUID: 1CczU6h2kUYa3KDyi1bJj6: 'HVAC-Valve' + +# True * 6 + + +if __name__ == '__main__': + run_example_simple_hvac_aixlib() diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e2_complex_project_hvac_aixlib.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e2_complex_project_hvac_aixlib.py index 24b3b3df92..472fb8f3e1 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e2_complex_project_hvac_aixlib.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e2_complex_project_hvac_aixlib.py @@ -4,6 +4,9 @@ import bim2sim from bim2sim import Project, run_project, ConsoleDecisionHandler from bim2sim.utilities.types import IFCDomain +from bim2sim.elements.base_elements import Material +from bim2sim.elements import bps_elements as bps_elements, \ + hvac_elements as hvac_elements def run_example_complex_hvac_aixlib(): @@ -19,25 +22,24 @@ def run_example_complex_hvac_aixlib(): # Create a temp directory for the project, feel free to use a "normal" # directory - project_path = Path( - tempfile.TemporaryDirectory( - prefix='bim2sim_example_complex_aixlib').name) + project_path = Path(r"D:\02_Daten\Testing\PluginAixLib\Test") # Set path of ifc for hydraulic domain with the fresh downloaded test models ifc_paths = { - IFCDomain.hydraulic: - Path(bim2sim.__file__).parent.parent / - 'test/resources/hydraulic/ifc/' - 'DigitalHub_Gebaeudetechnik-HEIZUNG_v2.ifc', + IFCDomain.ventilation: + Path( + r"D:\03_Cloud\Sciebo\BIM2Praxis\IFC-Modelle\EDGE\ueberarbeitet_2025-10\BIM2PRAXIS_RLT-2025-09-11_cleaned_jho.ifc") } # Create a project including the folder structure for the project with project = Project.create(project_path, ifc_paths, 'aixlib') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + project.sim_settings.relevant_elements = {*hvac_elements.ventilation_items, Material} + # Set fuzzy threshold to 0.5 to reduce the number of decisions (this is # IFC-file specific and needs to be evaluated by the user project.sim_settings.fuzzy_threshold = 0.5 diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e3_single_radiator_in_building.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e3_single_radiator_in_building.py new file mode 100644 index 0000000000..840b11a70d --- /dev/null +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/examples/e3_single_radiator_in_building.py @@ -0,0 +1,102 @@ +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project, run_project, ConsoleDecisionHandler +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +from bim2sim.kernel.log import default_logging_setup +from bim2sim.utilities.common_functions import download_test_resources +from bim2sim.utilities.types import IFCDomain +from bim2sim_aixlib import LoadLibrariesAixLib + + +def run_example_simple_hvac_aixlib(): + """Run an HVAC simulation with the AixLib backend. + """ + + # Create the default logging to for quality log and bim2sim main log ( + # see logging documentation for more information + default_logging_setup() + + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path( + tempfile.TemporaryDirectory( + prefix='bim2sim_example_simple_aixlib_3').name) + + # download additional test resources for arch domain, you might want to set + # force_new to True to update your test resources + download_test_resources(IFCDomain.hydraulic, force_new=False) + + # Set path of ifc for hydraulic domain with the fresh downloaded test models + ifc_paths = { + IFCDomain.hydraulic: + Path(bim2sim.__file__).parent.parent / + 'test/resources/mixed/ifc/' + 'b03_heating_with_building.ifc' + } + # Create a project including the folder structure for the project with + # teaser as backend and no specified workflow (default workflow is taken) + project = Project.create(project_path, ifc_paths, 'aixlib') + + # set weather file data + project.sim_settings.weather_file_path_modelica = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + # specify simulation settings + # project.sim_settings.aggregations = [ + # # 'UnderfloorHeating', + # # 'Consumer', + # # 'PipeStrand', + # # 'ParallelPump', + # # 'ConsumerHeatingDistributorModule', + # # 'GeneratorOneFluid' + # ] + project.sim_settings.group_unidentified = 'name' + from bim2sim.tasks import base, common, hvac + # project.plugin_cls.default_tasks = [ + # common.LoadIFC, + # common.CheckIfc, + # common.CreateElements, + # hvac.ConnectElements, + # hvac.MakeGraph, + # LoadLibrariesAixLib, + # hvac.Export, + # ] + answers = ('HVAC-PipeFitting', 'HVAC-Distributor', + 'HVAC-ThreeWayValve', + # 8 dead ends + *(True,) * 6, + # boiler efficiency, flow temp, power consumption, + # return temp + # 0.95, 70, 79, 50, + # *(500, 50,) * 7, + # rated_mass_flow for distributor, rated of boiler pump + # 1, + # rated_mass_flow for boiler pump, rated dp of boiler pump + # 0.9, 4500, + # body mass and heat capacity for all space heaters + + ) + # Run the project with the ConsoleDecisionHandler. This allows interactive + # input to answer upcoming questions regarding the imported IFC. + # Correct decision for identification of elements and useful parameters for + # missing attributes are written below + # run_project(project, ConsoleDecisionHandler()) + run_project(project, DebugDecisionHandler(answers)) + +# IfcBuildingElementProxy: skip +# Rücklaufverschraubung: 'HVAC-PipeFitting', +# Apparate (M_606) 'HVAC-Distributor', +# 3-Wege-Regelventil PN16: 'HVAC-ThreeWayValve', +# True * 6 +# efficiency: 0.95 +# flow_temperature: 70 +# nominal_power_consumption: 200 +# return_temperature: 50 +# heat_capacity: 10 * 7 + + +if __name__ == '__main__': + run_example_simple_hvac_aixlib() diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py index 15a1690a2b..4a3580ecff 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py @@ -4,6 +4,7 @@ from bim2sim.elements import hvac_elements as hvac from bim2sim.elements.mapping.units import ureg from bim2sim.export.modelica import check_numeric +from bim2sim.export.modelica import HeatPort MEDIUM_WATER = 'AixLib.Media.Water' @@ -43,12 +44,14 @@ def __init__(self, element): required=True, attributes=['min_PLR'], check=check_numeric( - min_value=0 * ureg.dimensionless)) + min_value=0 * ureg.dimensionless, + max_value=1 * ureg.dimensionless) + ) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) # ToDo: Gas connection @@ -60,6 +63,14 @@ class Radiator(AixLib): def __init__(self, element): super().__init__(element) + self.heat_ports = [ + HeatPort(name='heatPortCon', + heat_transfer_type='convective', + parent=self), + HeatPort(name='heatPortRad', + heat_transfer_type='radiative', + parent=self) + ] self._set_parameter(name='redeclare package Medium', unit=None, required=False, @@ -70,20 +81,20 @@ def __init__(self, element): attributes=['rated_power'], check=check_numeric(min_value=0 * ureg.watt)) self._set_parameter(name='T_a_nominal', - unit=ureg.celsius, + unit=ureg.kelvin, required=True, - check=check_numeric(min_value=0 * ureg.celsius), + check=check_numeric(min_value=0 * ureg.kelvin), attributes=['flow_temperature']) self._set_parameter(name='T_b_nominal', - unit=ureg.celsius, + unit=ureg.kelvin, required=True, - check=check_numeric(min_value=0 * ureg.celsius), + check=check_numeric(min_value=0 * ureg.kelvin), attributes=['return_temperature']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -123,13 +134,18 @@ def __init__(self, element): 'dp': self.parameters['dp']}}) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) + def get_heat_port_names(self): + return { + "con": "heatPortCon", + } + class Consumer(AixLib): path = "AixLib.Systems.HydraulicModules.SimpleConsumer" @@ -172,9 +188,9 @@ def __init__(self, element): * ureg.kg / ureg.meter ** 3)) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -283,19 +299,17 @@ def __init__(self, value=10000 * n_consumers) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) class BoilerAggregation(AixLib): - # TODO: the model does not exists in AiLib """Modelica AixLib representation of the GeneratorOneFluid aggregation.""" - path = "AixLib.Systems.ModularEnergySystems.Modules.ModularBoiler." \ - "ModularBoiler" + path = "AixLib.Systems.ScalableGenerationModules.ScalableBoiler.ScalableBoiler" represents = [hvac_aggregations.GeneratorOneFluid] def __init__(self, element): @@ -304,45 +318,56 @@ def __init__(self, element): unit=None, required=False, value=MEDIUM_WATER) - self._set_parameter(name='hasPump', + self._set_parameter(name='hasPum', unit=None, required=False, attributes=['has_pump']) - self._set_parameter(name='hasFeedback', + self._set_parameter(name='hasFedBac', unit=None, required=False, attributes=['has_bypass']) - self._set_parameter(name='QNom', + self._set_parameter(name='Q_flow_nominal', unit=ureg.watt, - required=False, + required=True, check=check_numeric(min_value=0 * ureg.watt), attributes=['rated_power']) - self._set_parameter(name='PLRMin', + self._set_parameter(name='FirRatMin', unit=ureg.dimensionless, - required=False, + required=True, check=check_numeric( - min_value=0 * ureg.dimensionless), + min_value=0 * ureg.dimensionless, + max_value=1 * ureg.dimensionless), attributes=['min_PLR']) - self._set_parameter(name='TRetNom', + self._set_parameter(name='TRet_nominal', unit=ureg.kelvin, - required=False, + required=True, check=check_numeric( - min_value=0 * ureg.kelvin), + min_value=20 * ureg.celsius), attributes=['return_temperature']) - self._set_parameter(name='dTWaterNom', + self._set_parameter(name='TSup_nominal', unit=ureg.kelvin, - required=False, + required=True, + check=check_numeric( + min_value=40 * ureg.celsius), + attributes=['flow_temperature']) + self._set_parameter(name='dT_nominal', + unit=ureg.kelvin, + required=True, check=check_numeric(min_value=0 * ureg.kelvin), attributes=['dT_water']) self._set_parameter(name='dp_Valve', unit=ureg.pascal, required=False, - value=10000) + value=10000*ureg.pascal) + self._set_parameter(name='Kv', + unit=ureg.m**3/ureg.hour / ureg.bar**0.5, + required=False, + value=0.7*ureg.m**3/ureg.hour / ureg.bar**0.5) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -362,17 +387,18 @@ def __init__(self, element: hvac.Distributor): self._set_parameter(name='n', unit=None, required=False, - value=n_ports) + value=int(n_ports)) self._set_parameter(name='m_flow_nominal', unit=ureg.kg / ureg.s, required=False, check=check_numeric(min_value=0 * ureg.kg / ureg.s), + value=0.05 * ureg.kg / ureg.s, # ToDo this is a random value, since no specific information has been found in ifc yet attributes=['rated_mass_flow']) def get_n_ports(self): ports = {port.guid: port for port in self.element.ports if port.connection} - return len(ports) / 2 - 1 + return len(ports) / 2 - 2 def get_port_name(self, port): try: @@ -416,7 +442,7 @@ class ThreeWayValve(AixLib): def __init__(self, element): super().__init__(element) - self._set_parameter(name='redeclare package Medium_con', + self._set_parameter(name='redeclare package Medium', unit=None, required=False, value=MEDIUM_WATER) @@ -428,8 +454,9 @@ def __init__(self, element): attributes=['nominal_mass_flow_rate']) self._set_parameter(name='dpValve_nominal', unit=ureg.pascal, - required=True, + required=False, check=check_numeric(min_value=0 * ureg.pascal), + value=1000*ureg.pascal, attributes=['nominal_pressure_difference']) def get_port_name(self, port): @@ -470,9 +497,9 @@ def __init__(self, element): def get_port_name(self, port): # TODO: heat pumps might have 4 ports (if source is modeled in BIM) - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -500,9 +527,9 @@ def __init__(self, element): def get_port_name(self, port): # TODO heat pumps might have 4 ports (if source is modeld in BIM) - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -543,7 +570,26 @@ def __init__(self, element): check=check_numeric(min_value=0 * ureg.meter), attributes=['diameter']) self._set_parameter(name='data', - unit=None, + unit=None, required=False, value={'hTank': self.parameters['hTank'], - "dTank": self.parameters['dTank']}) + "dTank": self.parameters['dTank']}) + + # def get_port_name(self, port): + # TODO #733 + # TODO function to determine ports. One Idea would be to use the + # geometric positions of ports to determine input and output. + # Top port with input: fluidportTop1 + # Bottom port with input: fluidportBottom1 + # Top port with output: fluidportTop2 + # Bottom port with input: fluidportBottom2 + + # Additionally, the number of ports can be used to determine if its a + # direct loaded or indirect loaded storage: + # 4 ports -> direct, + # 6 or any other even number > 4 -> indirect load + # with n=n_ports /2 -4 -> number of heating coils + # Then again used height of port to determine port name + # top port of first heating coil: portHC1In + # bottom port of first heating coil: portHC1Out + # etc. diff --git a/bim2sim/plugins/PluginAixLib/test/integration/test_aixlib.py b/bim2sim/plugins/PluginAixLib/test/integration/test_aixlib.py index c0f84425cd..b937f8406b 100644 --- a/bim2sim/plugins/PluginAixLib/test/integration/test_aixlib.py +++ b/bim2sim/plugins/PluginAixLib/test/integration/test_aixlib.py @@ -6,11 +6,11 @@ from bim2sim.export.modelica import ModelicaElement from bim2sim.elements.aggregation.hvac_aggregations import \ ConsumerHeatingDistributorModule -from bim2sim.utilities.test import IntegrationBase +from bim2sim.utilities.test import IntegrationWeatherBase from bim2sim.utilities.types import IFCDomain -class IntegrationBaseAixLib(IntegrationBase): +class IntegrationBaseAixLib(IntegrationWeatherBase): def tearDown(self): ModelicaElement.lookup = {} super().tearDown() @@ -18,6 +18,12 @@ def tearDown(self): def model_domain_path(self) -> str: return 'hydraulic' + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_modelica = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') + class TestIntegrationAixLib(IntegrationBaseAixLib, unittest.TestCase): @@ -29,8 +35,9 @@ def test_vereinshaus1_aixlib(self): project = self.create_project(ifc_names, 'aixlib') answers = ('HVAC-HeatPump', 'HVAC-Storage', 'HVAC-Storage', '2lU4kSSzH16v7KPrwcL7KZ', '0t2j$jKmf74PQpOI0ZmPCc', - # 1x expansion tank and 17x dead end - *(True,) * 18, + # TODO #733 + # 1x expansion tank and 20x dead end + *(True,) * 21, # boiler efficiency 0.9, # boiler flow temperature diff --git a/bim2sim/plugins/PluginAixLib/test/unit/tasks/__init__.py b/bim2sim/plugins/PluginAixLib/test/unit/tasks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginAixLib/test/unit/kernel/task/test_export.py b/bim2sim/plugins/PluginAixLib/test/unit/tasks/test_export.py similarity index 56% rename from bim2sim/plugins/PluginAixLib/test/unit/kernel/task/test_export.py rename to bim2sim/plugins/PluginAixLib/test/unit/tasks/test_export.py index e5a5b98471..acb70fb191 100644 --- a/bim2sim/plugins/PluginAixLib/test/unit/kernel/task/test_export.py +++ b/bim2sim/plugins/PluginAixLib/test/unit/tasks/test_export.py @@ -1,8 +1,12 @@ import unittest +from bim2sim import ConsoleDecisionHandler +from bim2sim.export.modelica import HeatTransferType +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler from bim2sim.plugins.PluginAixLib.bim2sim_aixlib import LoadLibrariesAixLib from bim2sim.elements.mapping.units import ureg from test.unit.tasks.hvac.test_export import TestStandardLibraryExports +from test.unit.elements.aggregation.test_consumer import ConsumerHelper class TestAixLibExport(TestStandardLibraryExports): @@ -19,21 +23,17 @@ def test_boiler_export(self): def test_radiator_export(self): graph = self.helper.get_simple_radiator() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Q_flow_nominal'), ('flow_temperature', 'T_a_nominal'), ('return_temperature', 'T_b_nominal')] - expected_units = [ureg.watt, ureg.celsius, ureg.celsius] + expected_units = [ureg.watt, ureg.kelvin, ureg.kelvin] self.run_parameter_test(graph, modelica_model, parameters, expected_units) def test_pump_export(self): graph, _ = self.helper.get_simple_pump() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) element = graph.elements[0] V_flow = element.rated_volume_flow.to(ureg.m ** 3 / ureg.s).magnitude dp = element.rated_pressure_difference.to(ureg.pascal).magnitude @@ -41,13 +41,11 @@ def test_pump_export(self): f"V_flow={{{0 * V_flow},{1 * V_flow},{2 * V_flow}}}," f"dp={{{2 * dp},{1 * dp},{0 * dp}}}" f"))") - self.assertIn(expected_string, modelica_model[0].code()) + self.assertIn(expected_string, modelica_model[0].render_modelica_code()) def test_consumer_export(self): graph, _ = self.helper.get_simple_consumer() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Q_flow_fixed')] expected_units = [ureg.watt] self.run_parameter_test(graph, modelica_model, parameters, @@ -68,8 +66,7 @@ def test_consumer_distributor_export(self): def test_three_way_valve_export(self): graph = self.helper.get_simple_three_way_valve() answers = (1 * ureg.kg / ureg.s,) - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph, answers) parameters = [('nominal_pressure_difference', 'dpValve_nominal'), ('nominal_mass_flow_rate', 'm_flow_nominal')] expected_units = [ureg.pascal, ureg.kg / ureg.s] @@ -78,9 +75,7 @@ def test_three_way_valve_export(self): def test_heat_pump_export(self): graph = self.helper.get_simple_heat_pump() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Q_useNominal')] expected_units = [ureg.watt] self.run_parameter_test(graph, modelica_model, parameters, @@ -88,9 +83,7 @@ def test_heat_pump_export(self): def test_chiller_export(self): graph = self.helper.get_simple_chiller() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Q_useNominal')] expected_units = [ureg.watt] self.run_parameter_test(graph, modelica_model, parameters, @@ -102,9 +95,7 @@ def test_consumer_CHP_export(self): def test_storage_export(self): graph = self.helper.get_simple_storage() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('height', 'hTank'), ('diameter', 'dTank')] expected_units = [ureg.meter, ureg.meter] element = graph.elements[0] @@ -114,4 +105,51 @@ def test_storage_export(self): param_value_pairs = [f"{param[1]}={value}" for param, value in zip(parameters, expected_values)] expected_string = f"data({','.join(param_value_pairs)})" - self.assertIn(expected_string, modelica_model[0].code()) + self.assertIn(expected_string, modelica_model[0].render_modelica_code()) + + def test_radiator_export_with_heat_ports(self): + """Test export of two radiators, focus on correct heat port export.""" + graph = self.helper.get_two_radiators() + + # export outer heat ports + self.test_task.playground.sim_settings.outer_heat_ports = True + + modelica_model = self.run_export(graph) + # ToDo: as elements are unsorted, testing with names is not robust + # connections_heat_ports_conv_expected = [ + # ('heatPortOuterCon[1]', + # 'spaceheater_0000000000000000000001.heatPortCon'), + # ('heatPortOuterCon[2]', + # 'spaceheater_0000000000000000000004.heatPortCon')] + + # connections_heat_ports_rad_expected = [ + # ('heatPortOuterRad[1]', + # 'spaceheater_0000000000000000000001.heatPortRad'), + # ('heatPortOuterRad[2]', + # 'spaceheater_0000000000000000000004.heatPortRad')] + + # check existence of heat ports + self.assertEqual( + 2, len(modelica_model[0].modelica_elements[0].heat_ports)) + self.assertEqual( + 2, len(modelica_model[0].modelica_elements[1].heat_ports)) + + # check types of heat ports + self.assertEqual( + modelica_model[0].modelica_elements[0].heat_ports[0].heat_transfer_type, + HeatTransferType.CONVECTIVE) + self.assertEqual( + modelica_model[0].modelica_elements[0].heat_ports[1].heat_transfer_type, + HeatTransferType.RADIATIVE) + self.assertEqual( + modelica_model[0].modelica_elements[1].heat_ports[0].heat_transfer_type, + HeatTransferType.CONVECTIVE) + self.assertEqual( + modelica_model[0].modelica_elements[1].heat_ports[1].heat_transfer_type, + HeatTransferType.RADIATIVE) + + # check number of heat port connections + self.assertEqual( + 2, len(modelica_model[0].connections_heat_ports_conv)) + self.assertEqual( + 2, len(modelica_model[0].connections_heat_ports_rad)) diff --git a/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e1_simple_project_comfort_energyplus.py b/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e1_simple_project_comfort_energyplus.py index 547400f227..8afe32ec5c 100644 --- a/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e1_simple_project_comfort_energyplus.py +++ b/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e1_simple_project_comfort_energyplus.py @@ -34,7 +34,7 @@ def run_example_1(): project = Project.create(project_path, ifc_paths, 'comfort') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_ep = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.epw') diff --git a/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e3_load_comfort_simulation_results.py b/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e3_load_comfort_simulation_results.py index 1fa3cc8f38..c0ef7d7f61 100644 --- a/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e3_load_comfort_simulation_results.py +++ b/bim2sim/plugins/PluginComfort/bim2sim_comfort/examples/e3_load_comfort_simulation_results.py @@ -35,7 +35,7 @@ def run_example_load_existing_project(): project = Project.create(project_path, plugin='comfort') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_ep = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginComfort/test/integration/test_comfort.py b/bim2sim/plugins/PluginComfort/test/integration/test_comfort.py index 811442fbf9..9d37ea3528 100644 --- a/bim2sim/plugins/PluginComfort/test/integration/test_comfort.py +++ b/bim2sim/plugins/PluginComfort/test/integration/test_comfort.py @@ -7,7 +7,7 @@ import bim2sim from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler -from bim2sim.utilities.test import IntegrationBase +from bim2sim.utilities.test import IntegrationWeatherBase from bim2sim.utilities.types import IFCDomain # raise unittest.SkipTest("Integration tests not reliable for automated use") @@ -15,7 +15,7 @@ DEBUG_COMFORT = False -class IntegrationBaseComfort(IntegrationBase): +class IntegrationBaseComfort(IntegrationWeatherBase): # HACK: We have to remember stderr because eppy resets it currently. def setUp(self): self.old_stderr = sys.stderr @@ -52,8 +52,10 @@ def tearDown(self): def model_domain_path(self) -> str: return 'arch' - def weather_file_path(self) -> Path: - return (self.test_resources_path() / + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_ep = ( + self.test_resources_path() / 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') diff --git a/bim2sim/plugins/PluginComfort/test/regression/test_comfort.py b/bim2sim/plugins/PluginComfort/test/regression/test_comfort.py index 4c87df7295..1aef2cd191 100644 --- a/bim2sim/plugins/PluginComfort/test/regression/test_comfort.py +++ b/bim2sim/plugins/PluginComfort/test/regression/test_comfort.py @@ -33,8 +33,10 @@ def tearDown(self): sys.stderr = self.old_stderr super().tearDown() - def weather_file_path(self) -> Path: - return (self.test_resources_path() / + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_ep = ( + self.test_resources_path() / 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') def create_regression_setup(self): diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e1_simple_project_energyplus.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e1_simple_project_energyplus.py index d3f58bc580..4e65886889 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e1_simple_project_energyplus.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e1_simple_project_energyplus.py @@ -33,7 +33,7 @@ def run_example_1(): project = Project.create(project_path, ifc_paths, 'energyplus') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_ep = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.epw') # Set the install path to your EnergyPlus installation according to your diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e2_complex_project_energyplus.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e2_complex_project_energyplus.py index 234f0f8e8c..84f845c1b5 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e2_complex_project_energyplus.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e2_complex_project_energyplus.py @@ -50,7 +50,7 @@ def run_example_complex_building_energyplus(): 'Alu- oder Stahlfenster, Waermeschutzverglasung, zweifach' # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_ep = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.epw') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e3_load_energyplus_simulation_results.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e3_load_energyplus_simulation_results.py index 7ccfe15682..7c60de8d49 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e3_load_energyplus_simulation_results.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e3_load_energyplus_simulation_results.py @@ -33,7 +33,7 @@ def run_example_load_existing_project(): project = Project.create(project_path, plugin='energyplus') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_ep = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e4_simple_rotated_project_energyplus.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e4_simple_rotated_project_energyplus.py index 153656c918..42713683f4 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e4_simple_rotated_project_energyplus.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/examples/e4_simple_rotated_project_energyplus.py @@ -31,7 +31,7 @@ def run_example_4(): project = Project.create(project_path, ifc_paths, 'energyplus') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_ep = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.epw') # Set the install path to your EnergyPlus installation according to your diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/sim_settings.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/sim_settings.py index d10c66ad09..857b3d6fdf 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/sim_settings.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/sim_settings.py @@ -50,6 +50,7 @@ class EnergyPlusSimSettings(BuildingSimSettings): choices={ '9-2-0': 'EnergyPlus Version 9-2-0', '9-4-0': 'EnergyPlus Version 9-4-0', + # '9-6-0': 'EnergyPlus Version 9-6-0', # todo #743 '22-2-0': 'EnergyPlus Version 22-2-0' # todo: Test latest version }, description='Choose EnergyPlus Version', diff --git a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py index cf47c57480..43666ae9b0 100644 --- a/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py +++ b/bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py @@ -4,7 +4,7 @@ import math import os from pathlib import Path, PosixPath -from typing import Union, TYPE_CHECKING +from typing import Union, TYPE_CHECKING, List import pandas as pd from OCC.Core.BRep import BRep_Tool @@ -49,15 +49,16 @@ class CreateIdf(ITask): function below. """ - reads = ('elements', 'weather_file',) - touches = ('idf', 'sim_results_path') + reads = ('elements', 'weather_file_ep',) + touches = ('idf', 'sim_results_path', 'ep_zone_lists') def __init__(self, playground): super().__init__(playground) self.idf = None self.hash_line = None - def run(self, elements: dict, weather_file: Path) -> tuple[IDF, Path]: + def run(self, elements: dict, weather_file_ep: Path) -> ( + tuple)[IDF, Path, List]: """Execute all methods to export an IDF from BIM2SIM. This task includes all functions for exporting EnergyPlus Input files @@ -78,17 +79,21 @@ def run(self, elements: dict, weather_file: Path) -> tuple[IDF, Path]: Args: elements (dict): dictionary in the format dict[guid: element], holds preprocessed elements including space boundaries. - weather_file (Path): path to weather file in .epw data format + weather_file_ep (Path): path to weather file in .epw data format Returns: idf (IDF): EnergyPlus input file sim_results_path (Path): path to the simulation results. + ep_zone_lists (List): List of thermal zone EP items """ logger.info("IDF generation started ...") - idf, sim_results_path = self.init_idf(self.playground.sim_settings, - self.paths, weather_file, - self.prj_name) + idf, sim_results_path = self.init_idf( + self.playground.sim_settings, + self.paths, + weather_file_ep, + self.prj_name) self.init_zone(self.playground.sim_settings, elements, idf) self.init_zonegroups(elements, idf) + ep_zone_lists = [z.Name for z in idf.idfobjects['ZONE']] self.get_preprocessed_materials_and_constructions( self.playground.sim_settings, elements, idf) if self.playground.sim_settings.add_shadings: @@ -118,14 +123,14 @@ def run(self, elements: dict, weather_file: Path) -> tuple[IDF, Path]: weather_file_sizing = ( self.playground.sim_settings.weather_file_for_sizing) else: - weather_file_sizing = str(weather_file) + weather_file_sizing = str(weather_file_ep) self.apply_system_sizing( idf, weather_file_sizing, sim_results_path) logger.info("Idf has been updated with limits from weather file " "sizing.") - return idf, sim_results_path + return idf, sim_results_path, ep_zone_lists def apply_system_sizing(self, idf, sizing_weather_file, sim_results_path): """ @@ -208,6 +213,8 @@ def init_idf(sim_settings: EnergyPlusSimSettings, paths: FolderStructure, # initialize the idf with a minimal idf setup idf = IDF(plugin_ep_path + '/data/Minimal.idf') # remove location and design days + idf.idfobjects['VERSION'][0].Version_Identifier = sim_settings.ep_version.replace("_", ".") + idf.removeallidfobjects('SIZINGPERIOD:DESIGNDAY') idf.removeallidfobjects('SITE:LOCATION') if sim_settings.system_weather_sizing != 'DesignDay': @@ -365,6 +372,9 @@ def init_zonegroups(self, elements: dict, idf: IDF): Args: elements: dict[guid: element] idf: idf file object + Returns + zone_lists: list of all zones with their name in EP + @Veronika Richter, correct? """ spaces = get_spaces_with_bounds(elements) space_usage_dict = {} @@ -390,6 +400,7 @@ def init_zonegroups(self, elements: dict, idf: IDF): Zone_List_Name=zlist.Name, Zone_List_Multiplier=1 ) + @staticmethod def check_preprocessed_materials_and_constructions(rel_elem, layers): """Check if preprocessed materials and constructions are valid.""" @@ -1015,7 +1026,7 @@ def set_infiltration(idf: IDF, idf.newidfobject( "ZONEINFILTRATION:DESIGNFLOWRATE", Name=name, - Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, + Zone_or_ZoneList_Name=zone_name, Schedule_Name="Continuous", Design_Flow_Rate_Calculation_Method="AirChanges/Hour", Air_Changes_per_Hour=space.base_infiltration @@ -1241,7 +1252,7 @@ def set_natural_ventilation(idf: IDF, name: str, zone_name: str, idf.newidfobject( "ZONEVENTILATION:DESIGNFLOWRATE", Name=name + '_winter', - Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, + Zone_or_ZoneList_Name=zone_name, Schedule_Name="Continuous", Ventilation_Type="Natural", Design_Flow_Rate_Calculation_Method="AirChanges/Hour", @@ -1255,7 +1266,7 @@ def set_natural_ventilation(idf: IDF, name: str, zone_name: str, idf.newidfobject( "ZONEVENTILATION:DESIGNFLOWRATE", Name=name + '_summer', - Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, + Zone_or_ZoneList_Name=zone_name, Schedule_Name="Continuous", Ventilation_Type="Natural", Design_Flow_Rate_Calculation_Method="AirChanges/Hour", @@ -1269,7 +1280,7 @@ def set_natural_ventilation(idf: IDF, name: str, zone_name: str, idf.newidfobject( "ZONEVENTILATION:DESIGNFLOWRATE", Name=name + '_overheating', - Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, + Zone_or_ZoneList_Name=zone_name, Schedule_Name="Continuous", Ventilation_Type="Natural", Design_Flow_Rate_Calculation_Method="AirChanges/Hour", diff --git a/bim2sim/plugins/PluginEnergyPlus/test/integration/test_energyplus.py b/bim2sim/plugins/PluginEnergyPlus/test/integration/test_energyplus.py index 48074a2e7c..5960c125bd 100644 --- a/bim2sim/plugins/PluginEnergyPlus/test/integration/test_energyplus.py +++ b/bim2sim/plugins/PluginEnergyPlus/test/integration/test_energyplus.py @@ -1,16 +1,12 @@ +import os import sys import unittest -from shutil import copyfile, copytree, rmtree from pathlib import Path - -import os - -from energyplus_regressions.diffs import math_diff, table_diff -from energyplus_regressions.diffs.thresh_dict import ThreshDict +from shutil import copyfile, copytree, rmtree import bim2sim from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler -from bim2sim.utilities.test import IntegrationBase +from bim2sim.utilities.test import IntegrationWeatherBase from bim2sim.utilities.types import IFCDomain # raise unittest.SkipTest("Integration tests not reliable for automated use") @@ -18,7 +14,7 @@ DEBUG_ENERGYPLUS = False -class IntegrationBaseEP(IntegrationBase): +class IntegrationBaseEP(IntegrationWeatherBase): # HACK: We have to remember stderr because eppy resets it currently. def setUp(self): self.old_stderr = sys.stderr @@ -51,12 +47,13 @@ def tearDown(self): sys.stderr = self.old_stderr super().tearDown() - def model_domain_path(self) -> str: return 'arch' - def weather_file_path(self) -> Path: - return (self.test_resources_path() / + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_ep = ( + self.test_resources_path() / 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') diff --git a/bim2sim/plugins/PluginEnergyPlus/test/regression/test_energyplus.py b/bim2sim/plugins/PluginEnergyPlus/test/regression/test_energyplus.py index 12aad96057..9727ccaa98 100644 --- a/bim2sim/plugins/PluginEnergyPlus/test/regression/test_energyplus.py +++ b/bim2sim/plugins/PluginEnergyPlus/test/regression/test_energyplus.py @@ -33,8 +33,10 @@ def tearDown(self): sys.stderr = self.old_stderr super().tearDown() - def weather_file_path(self) -> Path: - return (self.test_resources_path() / + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_ep = ( + self.test_resources_path() / 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') def create_regression_setup(self): diff --git a/bim2sim/plugins/PluginEnergyPlus/test/unit/task/test_weather.py b/bim2sim/plugins/PluginEnergyPlus/test/unit/task/test_weather.py index 5fb3cee14e..1edcc17a4a 100644 --- a/bim2sim/plugins/PluginEnergyPlus/test/unit/task/test_weather.py +++ b/bim2sim/plugins/PluginEnergyPlus/test/unit/task/test_weather.py @@ -41,14 +41,14 @@ def test_weather_energyplus(self): IFCDomain.arch: test_rsrc_path / 'arch/ifc/AC20-FZK-Haus.ifc'} self.project = Project.create(self.test_dir.name, ifc_paths, plugin=PluginWeatherDummyEP) - self.project.sim_settings.weather_file_path = ( + self.project.sim_settings.weather_file_path_ep = ( test_rsrc_path / 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') handler = DebugDecisionHandler([]) handler.handle(self.project.run(cleanup=False)) try: - weather_file = self.project.playground.state['weather_file'] + weather_file = self.project.playground.state['weather_file_ep'] except Exception: raise ValueError(f"No weather file set through Weather task. An" f"error occurred.") self.assertEquals(weather_file, - self.project.sim_settings.weather_file_path) + self.project.sim_settings.weather_file_path_ep) diff --git a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py index 9e2efb6460..14917c2643 100644 --- a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py +++ b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/__init__.py @@ -31,5 +31,6 @@ class PluginHKESim(Plugin): hvac.Reduce, hvac.DeadEnds, LoadLibrariesHKESim, + hvac.CreateModelicaModel, hvac.Export, ] diff --git a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/__init__.py b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/__init__.py index e69de29bb2..13c43d7f81 100644 --- a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/__init__.py +++ b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/__init__.py @@ -0,0 +1 @@ +from .export_co_sim import CoSimExport, GetZoneConnections diff --git a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/e1_simple_project_hvac_hkesim.py b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/e1_simple_project_hvac_hkesim.py index 512a12c6d4..86ea232f27 100644 --- a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/e1_simple_project_hvac_hkesim.py +++ b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/examples/e1_simple_project_hvac_hkesim.py @@ -31,7 +31,7 @@ def run_example_simple_hvac_hkesim(): project = Project.create(project_path, ifc_paths, 'HKESim') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') diff --git a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/models/__init__.py b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/models/__init__.py index 152cfabdfb..4b3745b36c 100644 --- a/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/models/__init__.py +++ b/bim2sim/plugins/PluginHKESim/bim2sim_hkesim/models/__init__.py @@ -33,9 +33,9 @@ def __init__(self, element): attributes=['return_temperature']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -63,9 +63,9 @@ def __init__(self, element): attributes=['return_temperature']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -99,9 +99,9 @@ def __init__(self, element): check=check_numeric(min_value=0 * ureg.watt)) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -149,7 +149,7 @@ def __init__(self, element): required=False, function=lambda flow_temperature, return_temperature: - (flow_temperature[0], return_temperature[0])) + (flow_temperature, return_temperature)) self._set_parameter(name='useHydraulicSeparator', unit=None, required=False, @@ -188,9 +188,9 @@ def get_port_name(self, port): except ValueError: # unknown port index = -1 - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return "port_a_consumer" - elif port.verbose_flow_direction == 'SOURCE': + elif port.flow_direction.name == 'source': return "port_b_consumer" elif (index % 2) == 0: return "port_a_consumer{}".format( @@ -233,9 +233,9 @@ def __init__(self, element): attributes=['has_bypass']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -263,9 +263,9 @@ def __init__(self, element): def get_port_name(self, port): # TODO: heat pump might have 4 ports (if source is modeled in BIM) - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a_con' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b_con' else: return super().get_port_name(port) @@ -299,9 +299,9 @@ def __init__(self, element): def get_port_name(self, port): # TODO: chiller might have 4 ports (if source is modeled in BIM) - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a_con' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b_con' else: return super().get_port_name(port) @@ -324,9 +324,9 @@ def __init__(self, element): attributes=['rated_power']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -349,9 +349,9 @@ def __init__(self, element): attributes=['rated_power']) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) diff --git a/bim2sim/plugins/PluginHKESim/test/integration/test_hkesim.py b/bim2sim/plugins/PluginHKESim/test/integration/test_hkesim.py index 158a2e198d..c9391ae7be 100644 --- a/bim2sim/plugins/PluginHKESim/test/integration/test_hkesim.py +++ b/bim2sim/plugins/PluginHKESim/test/integration/test_hkesim.py @@ -17,6 +17,12 @@ def tearDown(self): def model_domain_path(self) -> str: return 'hydraulic' + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_modelica = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') + class TestIntegrationHKESIM(IntegrationBaseHKESIM, unittest.TestCase): @@ -29,8 +35,8 @@ def test_run_vereinshaus1(self): project = self.create_project(ifc_names, 'hkesim') answers = ('HVAC-HeatPump', 'HVAC-Storage', 'HVAC-Storage', '2lU4kSSzH16v7KPrwcL7KZ', '0t2j$jKmf74PQpOI0ZmPCc', - # 1x expansion tank and 17x dead end - *(True,) * 18, + # 1x expansion tank and 20x dead end + *(True,) * 21, # boiler efficiency 0.9, # boiler power, @@ -105,12 +111,17 @@ def test_run_b03_heating_with_all_aggregations(self): 'HVAC-ThreeWayValve', # 6x dead ends *(True,) * 6, + # flow temperature consumer + 70, # boiler: nominal flow temperature 70, - # boiler: rated power consumption + # boiler: rated power 150, # boiler: nominal return temperature 50) + # from bim2sim import ConsoleDecisionHandler, run_project + # handler = ConsoleDecisionHandler() + # run_project(project, handler) handler = DebugDecisionHandler(answers) for decision, answer in handler.decision_answer_mapping(project.run()): decision.value = answer diff --git a/bim2sim/plugins/PluginHKESim/test/unit/kernel/task/test_export.py b/bim2sim/plugins/PluginHKESim/test/unit/kernel/task/test_export.py index d093757ac4..68f4910397 100644 --- a/bim2sim/plugins/PluginHKESim/test/unit/kernel/task/test_export.py +++ b/bim2sim/plugins/PluginHKESim/test/unit/kernel/task/test_export.py @@ -4,7 +4,6 @@ from bim2sim.elements.aggregation.hvac_aggregations import \ ConsumerHeatingDistributorModule from bim2sim.elements.mapping.units import ureg -from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler from bim2sim.plugins.PluginHKESim.bim2sim_hkesim import LoadLibrariesHKESim from test.unit.tasks.hvac.test_export import TestStandardLibraryExports @@ -20,9 +19,7 @@ def setUpClass(cls) -> None: def test_boiler_export(self): graph = self.helper.get_simple_boiler() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Q_nom'), ('return_temperature', 'T_set')] expected_units = [ureg.watt, ureg.kelvin] @@ -31,9 +28,7 @@ def test_boiler_export(self): def test_radiator_export(self): graph = self.helper.get_simple_radiator() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Q_flow_nominal'), ('return_temperature', 'Tout_max')] expected_units = [ureg.watt, ureg.kelvin] @@ -42,9 +37,7 @@ def test_radiator_export(self): def test_pump_export(self): graph, _ = self.helper.get_simple_pump() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_height', 'head_set'), ('rated_volume_flow', 'Vflow_set'), ('rated_power', 'P_nom')] @@ -60,21 +53,20 @@ def test_three_way_valve_export(self): def test_consumer_heating_distributor_module_export(self): # Set up the test graph and model graph = self.helper.get_simple_consumer_heating_distributor_module() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) # Get the ConsumerHeatingDistributorModule element element = next(element for element in graph.elements if isinstance(element, ConsumerHeatingDistributorModule)) # Get parameter values from element - flow_temp_0 = element.flow_temperature[0].magnitude - return_temp_0 = element.return_temperature[0].magnitude - flow_temp_1 = element.flow_temperature[1].magnitude - return_temp_1 = element.return_temperature[1].magnitude - rated_power_0 = element.rated_power[0].to(ureg.watt).magnitude - rated_power_1 = element.rated_power[1].to(ureg.watt).magnitude + flow_temp_0 = element.flow_temperature.magnitude + return_temp_0 = element.return_temperature.magnitude + flow_temp_1 = element.flow_temperature.magnitude + return_temp_1 = element.return_temperature.magnitude + consumers = iter(element.consumers) + rated_power_0 = next(consumers).rated_power.to(ureg.watt).magnitude + rated_power_1 = next(consumers).rated_power.to(ureg.watt).magnitude # Define the expected parameter strings in modelica model code expected_strings = [ @@ -92,29 +84,27 @@ def test_consumer_heating_distributor_module_export(self): # Assert that each expected string is in the modelica_model code for expected_string in expected_strings: - self.assertIn(expected_string, modelica_model[0].code()) + self.assertIn(expected_string, + modelica_model[0].render_modelica_code()) def test_boiler_module_export(self): graph = self.helper.get_simple_generator_one_fluid() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) element = graph.elements[0] rated_power = element.rated_power.to(ureg.watt).magnitude - flow_temp = element.flow_temperature.magnitude - return_temp = element.return_temperature.magnitude + flow_temp = element.flow_temperature.to(ureg.kelvin).magnitude + return_temp = element.return_temperature.to(ureg.kelvin).magnitude expected_strings = [ f"Theating={{{flow_temp},{return_temp}}}", f"Qflow_nom={rated_power}", ] for expected_string in expected_strings: - self.assertIn(expected_string, modelica_model[0].code()) + self.assertIn(expected_string, + modelica_model[0].render_modelica_code()) def test_heat_pump_export(self): graph = self.helper.get_simple_heat_pump() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Qcon_nom')] expected_units = [ureg.watt] self.run_parameter_test(graph, modelica_model, parameters, @@ -122,9 +112,7 @@ def test_heat_pump_export(self): def test_chiller_export(self): graph = self.helper.get_simple_chiller() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Qev_nom'), ('nominal_COP', 'EER_nom')] expected_units = [ureg.watt, ureg.dimensionless] @@ -133,9 +121,7 @@ def test_chiller_export(self): def test_chp_export(self): graph = self.helper.get_simple_chp() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'P_nom')] expected_units = [ureg.watt] self.run_parameter_test(graph, modelica_model, parameters, @@ -143,9 +129,7 @@ def test_chp_export(self): def test_cooling_tower_export(self): graph = self.helper.get_simple_cooling_tower() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) parameters = [('rated_power', 'Qflow_nom')] expected_units = [ureg.watt] self.run_parameter_test(graph, modelica_model, parameters, diff --git a/bim2sim/plugins/PluginLCA/bim2sim_lca/examples/e1_export_quantities_for_lca.py b/bim2sim/plugins/PluginLCA/bim2sim_lca/examples/e1_export_quantities_for_lca.py index 30cdb27d7a..9e1641919e 100644 --- a/bim2sim/plugins/PluginLCA/bim2sim_lca/examples/e1_export_quantities_for_lca.py +++ b/bim2sim/plugins/PluginLCA/bim2sim_lca/examples/e1_export_quantities_for_lca.py @@ -39,7 +39,7 @@ def run_example_complex_building_lca(): project = Project.create(project_path, ifc_paths, 'lca') # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') diff --git a/bim2sim/plugins/PluginLCA/test/integration/test_lca.py b/bim2sim/plugins/PluginLCA/test/integration/test_lca.py index d41a49512e..125531b1dc 100644 --- a/bim2sim/plugins/PluginLCA/test/integration/test_lca.py +++ b/bim2sim/plugins/PluginLCA/test/integration/test_lca.py @@ -9,6 +9,12 @@ class IntegrationBaseLCA(IntegrationBase): def model_domain_path(self) -> str: return 'arch' + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_modelica = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') + class TestIntegrationLCA(IntegrationBaseLCA, unittest.TestCase): def test_run_kitinstitute_lca(self): diff --git a/bim2sim/plugins/PluginSpawn/__init__.py b/bim2sim/plugins/PluginSpawn/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/__init__.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/__init__.py new file mode 100644 index 0000000000..dab0962caa --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/__init__.py @@ -0,0 +1,48 @@ +import bim2sim.plugins.PluginSpawn.bim2sim_spawn.tasks as spawn_tasks +from bim2sim.plugins import Plugin +from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus import \ + task as ep_tasks +from bim2sim.plugins.PluginSpawn.bim2sim_spawn.sim_settings import \ + SpawnOfEnergyPlusSimSettings +from bim2sim.tasks import common, hvac, bps +from bim2sim_aixlib import LoadLibrariesAixLib + + +class PluginSpawnOfEP(Plugin): + """Plugin for SpawnOfEnergyPlus. + + This is the first plugin that uses tasks from different plugins together. + We first execute the EnergyPlus related tasks to create an IDF file. + Afterwards, we execute the HVAC tasks, using PluginAixLib (PluginHKESim + would also work) and then execute the PluginSpawn tasks to put + EnergyPlus IDF and Modelica model together. + """ + name = 'spawn' + sim_settings = SpawnOfEnergyPlusSimSettings + default_tasks = [ + common.LoadIFC, + # common.CheckIfc, + common.CreateElementsOnIfcTypes, + + bps.CreateSpaceBoundaries, + bps.AddSpaceBoundaries2B, + bps.CorrectSpaceBoundaries, + common.CreateRelations, + bps.DisaggregationCreationAndTypeCheck, + bps.EnrichMaterial, + bps.EnrichUseConditions, + common.Weather, + ep_tasks.CreateIdf, + + hvac.ConnectElements, + hvac.MakeGraph, + hvac.ExpansionTanks, + hvac.Reduce, + hvac.DeadEnds, + LoadLibrariesAixLib, + hvac.CreateModelicaModel, + hvac.Export, + + spawn_tasks.ExportSpawnBuilding, + spawn_tasks.ExportSpawnTotal, + ] diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/examples/__init__.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/examples/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/examples/e1_simple_project_bps_spawn.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/examples/e1_simple_project_bps_spawn.py new file mode 100644 index 0000000000..e063c8be84 --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/examples/e1_simple_project_bps_spawn.py @@ -0,0 +1,121 @@ +import tempfile +from pathlib import Path + +import bim2sim +from bim2sim import Project, ConsoleDecisionHandler, run_project +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +# from bim2sim.kernel.log import default_logging_setup +from bim2sim.utilities.types import IFCDomain +from bim2sim.elements.base_elements import Material +from bim2sim.elements import bps_elements as bps_elements, \ + hvac_elements as hvac_elements + +def run_example_spawn_1(): + """Export a SpawnOfEnergyPlus simulation model. + + This example exports a SpawnOfEnergyPlus Co-Simulation model. The HVAC + model is generated via the PluginAixLib using the AixLib Modelica library. + The building model is generated using PluginEnergyPlus. The used IFC file + holds both, HVAC and building in one file. + + Currently required versions: + EnergyPlus >= 9.6.0 + AixLib branch: issue1147_GenericBoiler (future: main) + Modelica Buildings: 11.1.0 + """ + # Create the default logging to for quality log and bim2sim main log ( + # see logging documentation for more information + # default_logging_setup() + + # Create a temp directory for the project, feel free to use a "normal" + # directory + project_path = Path(r"D:\00_Temp\Testing\bim2sim\Spawn1") + + # Set the ifc path to use and define which domain the IFC belongs to + ifc_paths = { + IFCDomain.mixed: + Path(bim2sim.__file__).parent.parent / + 'test/resources/mixed/ifc/' + 'b03_heating_with_building_blenderBIM.ifc' + } + + # Create a project including the folder structure for the project with + # energyplus as backend + project = Project.create(project_path, ifc_paths, 'spawn') + + # Set the install path to your EnergyPlus installation according to your + # system requirements + project.sim_settings.ep_install_path = Path(r"C:\EnergyPlusV9-6-0") + project.sim_settings.ep_version = "9-6-0" + project.sim_settings.weather_file_path_ep = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.epw') + project.sim_settings.weather_file_path_modelica = ( + Path(bim2sim.__file__).parent.parent / + 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') + # Generate outer heat ports for spawn HVAC sub model + project.sim_settings.outer_heat_ports = True + + project.sim_settings.hvac_modelica_library = "AixLib" + + project.sim_settings.relevant_elements = {*bps_elements.items, *hvac_elements.hydraulic_items, Material} + + # Set other simulation settings, otherwise all settings are set to default + project.sim_settings.aggregations = [ + 'PipeStrand', + 'ParallelPump', + 'GeneratorOneFluid' + ] + + project.sim_settings.group_unidentified = 'name' + + # Run the project with the DebugDecisionHandler with pre-filled answers. + answers = ( + 'HVAC-PipeFitting', # Identify PipeFitting + 'HVAC-Distributor', # Identify Distributor + 'HVAC-ThreeWayValve', # Identify ThreeWayValve + 2010, # year of construction of building + *(True,) * 7, # 7 real dead ends found + *(0.001,)*13, # volume of junctions + 2000, 175, # rated_pressure_difference + rated_volume_flow pump of 1st storey (big) + 4000, 200, # rated_pressure_difference + rated_volume_flow for 2nd storey + *(70, 50,)*7, # flow and return temp for 7 space heaters + 0.056, # nominal_mass_flow_rate 2nd storey TRV (kg/s), + 20, # dT water of boiler + 70, # nominal flow temperature of boiler + 0.3, # minimal part load range of boiler + 8.5, # nominal power of boiler (in kW) + 50, # nominal return temperature of boiler + ) + + # answers = ( + # 'HVAC-PipeFitting', # Identify PipeFitting 03TbBCNszVXaBWMuR55Ezt + # 'HVAC-PipeFitting', # Identify PipeFitting 05GeK0Vqi$b4sUg10dylS4 + # 'HVAC-PipeFitting', # Identify PipeFitting 0LhPEcsRAfWKRlvc$odfB3 + # 'HVAC-Distributor', # Identify Distributor 1259naiEpIkasmH4NcC8DL + # 'HVAC-PipeFitting', # Identify PipeFitting 1fX98DWWkmb4_lxVNY2CYM + # 'HVAC-Pipe', # Identify PipeFitting 05lPOiNJdAe41k1zmv0NgS + # 'HVAC-ThreeWayValve', # Identify ThreeWayValve 05lPOiNJdAe41k1zmv0NgS + # 2010, # year of construction of building + # *(True,) * 7, # 7 real dead ends found + # *(0.001,) * 13, # volume of junctions + # 2000, 175, # rated_pressure_difference + rated_volume_flow pump of 1st storey (big) + # 4000, 200, # rated_pressure_difference + rated_volume_flow for 2nd storey + # *(70, 50,) * 7, # flow and return temp for 7 space heaters + # 0.056, # nominal_mass_flow_rate 2nd storey TRV (kg/s), + # 20, # dT water of boiler + # 70, # nominal flow temperature of boiler + # 0.3, # minimal part load range of boiler + # 8.5, # nominal power of boiler (in kW) + # 50, # nominal return temperature of boiler + # ) + + # handler = ConsoleDecisionHandler() + # handler = DebugDecisionHandler(answers) + # handler.handle(project.run()) + + run_project(project, ConsoleDecisionHandler()) + + +if __name__ == '__main__': + run_example_spawn_1() diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/sim_settings.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/sim_settings.py new file mode 100644 index 0000000000..13859d46d9 --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/sim_settings.py @@ -0,0 +1,24 @@ +from bim2sim.elements import bps_elements, hvac_elements +from bim2sim.elements.base_elements import Material +from bim2sim.sim_settings import PlantSimSettings, ChoiceSetting +from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.sim_settings import \ + EnergyPlusSimSettings + + +class SpawnOfEnergyPlusSimSettings(EnergyPlusSimSettings, PlantSimSettings): + def __init__(self): + super().__init__() + self.relevant_elements = {*bps_elements.items, *hvac_elements.items, + Material} + # change defaults + self.outer_heat_ports = True + + hvac_modelica_library = ChoiceSetting( + value='AixLib', + choices={ + 'AixLib': 'Using AixLib for HVAC simulation', + 'HKESim': 'Using HKESim for HVAC simulation' + }, + description='Choose Modelica library for HVAC simulation.', + for_frontend=True + ) diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/__init__.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/__init__.py new file mode 100644 index 0000000000..af7e1ff5c2 --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/__init__.py @@ -0,0 +1,2 @@ +from .export_spawn_building import ExportSpawnBuilding +from .export_spawn_total import ExportSpawnTotal diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/export_spawn_building.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/export_spawn_building.py new file mode 100644 index 0000000000..7955533b74 --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/export_spawn_building.py @@ -0,0 +1,142 @@ +import codecs +from pathlib import Path +from typing import List, Tuple, Optional, Dict +from collections import defaultdict + +from mako.template import Template + +import bim2sim +from bim2sim.export.modelica import parse_to_modelica +from bim2sim.utilities.common_functions import filter_elements +from bim2sim.tasks.base import ITask + + +class ExportSpawnBuilding(ITask): + """Export building for SpawnOfEnergyPlus model to Modelica""" + + reads = ('elements', 'weather_file_modelica', 'weather_file_ep', + 'package_name', 'ep_zone_lists') + touches = ('model_name_building',) + + def run(self, elements: dict, weather_file_modelica: Path, + weather_file_ep: Path, package_name: str, ep_zone_lists: List): + """Run the export process for a building Spawn model in Modelica. + + This method prepares the Modelica file for a building model by gathering + necessary elements, generating the template data, and writing the + Modelica code to file. + + Args: + elements: Dictionary containing building elements. + weather_file_modelica: Path to the Modelica weather file. + weather_file_ep: Path to the EnergyPlus weather file. + package_name: Name of the Modelica package for exporting. + ep_zone_lists: List of thermal zone EP items + + Returns: + model_name_building: the name of the building model. + """ + self.logger.info("Export building of Spawn model to Modelica code") + model_name_building = 'BuildingModel' + + tz_elements = filter_elements(elements, 'ThermalZone') + base_infiltration = self._get_base_infiltration_per_zone(tz_elements, ep_zone_lists) + + # Setup export paths + export_package_path = self.paths.export / Path(package_name) + + # Generate building template data + building_template_data = self._render_building_template( + package_path=export_package_path, + model_name=model_name_building, + weather_file_ep=weather_file_ep, + weather_file_modelica=weather_file_modelica, + ep_zone_lists=ep_zone_lists, + base_infiltration=base_infiltration + ) + + # Write the generated Modelica code to file + self._write_to_file( + Path(export_package_path / f"{model_name_building}.mo"), + building_template_data) + + return model_name_building, + + @staticmethod + def _get_base_infiltration_per_zone(tz_elements: List, + ep_zone_lists: List) -> List[float]: + """Get base infiltration per zones according to the zone's UseConditions + + Args: + tz_elements: List of thermal zone elements. + ep_zone_lists: List of zones in energy plus idf file + + Returns: + base_infiltration: Dict mapping base infiltration to zones. + """ + base_infiltration = [] + for ep_zone in ep_zone_lists: + for tz in tz_elements: + if tz.guid == ep_zone: + base_infiltration.append(tz.base_infiltration) + return base_infiltration + + @staticmethod + def _load_template() -> Template: + """Loads the building template for rendering. + + Returns: + Template: Mako template object for the building Modelica file. + """ + template_path = (Path(bim2sim.__file__).parent / + 'assets/templates/modelica/tmplSpawnBuilding.txt') + with open(template_path, 'r', encoding='utf-8') as f: + template_str = f.read() + return Template(template_str) + + def _render_building_template(self, + package_path: Path, + model_name: str, + weather_file_ep: Path, + weather_file_modelica: Path, + ep_zone_lists: list, + base_infiltration: list) -> str: + """Render the building Modelica template using provided data. + + Args: + package_path: The path to the Modelica package. + model_name: The name of the building model. + weather_file_ep: The EnergyPlus weather file path. + weather_file_modelica: The Modelica weather file path. + ep_zone_lists: List of zone names to be used in the model. + + Returns: + Rendered Modelica code as a string. + """ + template_bldg = self._load_template() + idf_path = (self.paths.export / "EnergyPlus/SimResults" / self.prj_name + / f"{self.prj_name}.idf") + return template_bldg.render( + within=package_path.stem, + model_name=model_name, + model_comment='Building model for Spawn of EnergyPlus', + weather_path_ep=parse_to_modelica( + name=None, value=weather_file_ep), + weather_path_mos=parse_to_modelica( + name=None, value=weather_file_modelica), + ep_zone_lists=parse_to_modelica(name=None, value=ep_zone_lists), + base_infiltration=parse_to_modelica(name=None, value=base_infiltration), + idf_path=parse_to_modelica(name=None, value=idf_path), + n_zones=len(ep_zone_lists) + ) + + def _write_to_file(self, file_path: Path, content: str): + """Write the generated content to a Modelica file. + + Args: + file_path: The path to the output file. + content: The content to write into the file. + """ + with codecs.open(file_path, 'w', 'utf-8') as f: + f.write(content) + self.logger.info(f"Successfully saved Modelica file to {file_path}") diff --git a/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/export_spawn_total.py b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/export_spawn_total.py new file mode 100644 index 0000000000..f8f9c14860 --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/bim2sim_spawn/tasks/export_spawn_total.py @@ -0,0 +1,242 @@ +import codecs +from collections import defaultdict +from pathlib import Path +from threading import Lock +from typing import List, Tuple, Optional, Dict + +from mako.template import Template + +import bim2sim +from bim2sim.elements.base_elements import ProductBased +from bim2sim.elements.hvac_elements import HVACProduct +from bim2sim.export.modelica import help_package, help_package_order, \ + ModelicaElement, parse_to_modelica +from bim2sim.tasks.base import ITask +from bim2sim.utilities.common_functions import filter_elements +from bim2sim.utilities.pyocc_tools import PyOCCTools + +TEMPLATE_PATH_TOTAL = Path(bim2sim.__file__).parent / \ + 'assets/templates/modelica/tmplSpawnTotalModel.txt' +TEMPLATE_TOTAL_STR = TEMPLATE_PATH_TOTAL.read_text() +TEMPLATE_TOTAL = Template(TEMPLATE_TOTAL_STR) +LOCK = Lock() + + +class ExportSpawnTotal(ITask): + """Export total model for SpawnOfEnergyPlus model to Modelica""" + + reads = ( + 'elements', 'weather_file_modelica', 'weather_file_ep', + 'model_name_hydraulic', 'model_name_building', + 'export_elements', 'connections', 'cons_heat_ports_conv', + 'cons_heat_ports_rad', 'package_name', 'ep_zone_lists', + ) + final = True + + def run(self, + elements: Dict[str, ProductBased], + weather_file_modelica: Path, + weather_file_ep: Path, + model_name_hydraulic: str, + model_name_building: str, + export_elements: Dict[HVACProduct, ModelicaElement], + connections: List[Tuple[str, str]], + cons_heat_ports_conv: List[Tuple[str, str]], + cons_heat_ports_rad: List[Tuple[str, str]], + package_name: str, + ep_zone_lists: List[str]): + """Run the export process to generate the Modelica code. + + Args: + elements: The elements' data. + weather_file_modelica: Path to the Modelica weather file. + weather_file_ep: Path to the EnergyPlus weather file. + model_name_hydraulic: The name of the hydraulic model. + model_name_building: The name of the building model. + export_elements: HVAC elements to export. + connections: List of fluid port connections. + cons_heat_ports_conv: List of convective heat port connections. + cons_heat_ports_rad: List of radiative heat port connections. + package_name: The package name of the modelica package. + ep_zone_lists: List of zones in energy plus idf file + """ + + # Exports the total model + self.logger.info("Export total Spawn model to Modelica code") + model_name_total = 'TotalModel' + + # Filter elements by type + tz_elements = filter_elements(elements, 'ThermalZone') + space_heater_elements = filter_elements(elements, 'SpaceHeater') + + # Group heaters by their corresponding zones + zone_to_heaters = self._group_space_heaters_by_zone( + tz_elements, ep_zone_lists, space_heater_elements + ) + + # Map heat ports between building and HVAC models + cons_heat_ports_conv_building_hvac = self.get_port_mapping( + cons_heat_ports_conv, "Con", model_name_building, + model_name_hydraulic, zone_to_heaters + ) + cons_heat_ports_rad_building_hvac = self.get_port_mapping( + cons_heat_ports_rad, "Rad", model_name_building, + model_name_hydraulic, zone_to_heaters + ) + + # Define package path + package_path = self.paths.export / Path(package_name) + # Save the rendered Modelica total model + self._save_total_modelica_model( + model_name_total, model_name_building, model_name_hydraulic, + cons_heat_ports_conv_building_hvac, + cons_heat_ports_rad_building_hvac, + weather_file_modelica, package_path + ) + + # Creates the package help files + self._create_modelica_help_package( + package_path, model_name_total, model_name_building, + model_name_hydraulic + ) + + @staticmethod + def _create_modelica_help_package( + package_path: Path, model_name_total: str, + model_name_building: str, model_name_hydraulic: str): + """Create the Modelica help package files. + + Args: + package_path: The path to the package directory. + model_name_total: The name of the total model. + model_name_building: The name of the building model. + model_name_hydraulic: The name of the hydraulic model. + """ + help_package(path=package_path, name=package_path.stem, within="") + help_package_order(path=package_path, package_list=[ + model_name_total, model_name_building, model_name_hydraulic]) + + @staticmethod + def _group_space_heaters_by_zone(tz_elements: List, + ep_zone_lists: List, + space_heater_elements: List) \ + -> Dict[str, List[str]]: + """Group space heaters by their respective zones according to the zone list order in the idf file + + Args: + tz_elements: List of thermal zone elements. + ep_zone_lists: List of zones in energy plus idf file + space_heater_elements: List of space heater elements. + + Returns: + A dictionary mapping zone GUIDs to lists of heater GUIDs. + """ + zone_to_heaters = defaultdict(list) + for ep_zone in ep_zone_lists: + zone_to_heaters[ep_zone] = [] + for tz in tz_elements: + if tz.guid == ep_zone: + for space_heater in space_heater_elements: + if PyOCCTools.obj2_in_obj1( + obj1=tz.space_shape, obj2=space_heater.shape): + zone_to_heaters[tz.guid].append(space_heater.guid) + return zone_to_heaters + + def _save_total_modelica_model(self, + model_name_total: str, + model_name_building: str, + model_name_hydraulic: str, + cons_heat_ports_conv_building_hvac: List[Tuple[str, str]], + cons_heat_ports_rad_building_hvac: List[Tuple[str, str]], + weather_path_mos: Path, + package_path: Path): + + """Render and save the total Modelica model file using a template. + + Args: + model_name_total: The name of the total model. + model_name_building: The name of the building model. + model_name_hydraulic: The name of the hydraulic model. + cons_heat_ports_conv_building_hvac: List of convective heat port + mappings. + cons_heat_ports_rad_building_hvac: List of radiative heat port + mappings. + weather_path_mos: Path to the Modelica weather file. + package_path: The path to the package directory. + """ + with LOCK: + total_template_data = TEMPLATE_TOTAL.render( + within=package_path.stem, + model_name=model_name_total, + model_comment='test2', + model_name_building=model_name_building, + model_name_hydraulic=model_name_hydraulic, + cons_heat_ports_conv_building_hvac= + cons_heat_ports_conv_building_hvac, + cons_heat_ports_rad_building_hvac= + cons_heat_ports_rad_building_hvac + ) + + export_path = package_path / f"{model_name_total}.mo" + self.logger.info(f"Saving {model_name_total} Modelica model to " + f"{export_path}") + with codecs.open(export_path, "w", "utf-8") as file: + file.write(total_template_data) + + + @classmethod + def get_port_mapping( + cls, cons_heat_ports: List[Tuple[str, str]], port_type: str, + model_name_building: str, model_name_hydraulic: str, + zone_to_heaters: Dict[str, List[str]]) -> List[Tuple[str, str]]: + """Mapping between building heat ports and HVAC heat ports. + + Args: + cons_heat_ports: A list of tuples where each tuple contains the HVAC + outer port name and the corresponding space heater name. + port_type: The type of port to map, e.g., "Con" for convective or + "Rad" for radiative. + model_name_building: The name of the building model. + model_name_hydraulic: The name of the hydraulic model. + zone_to_heaters: A dictionary mapping zone GUIDs to lists of heater + GUIDs. + + Returns: + A list of tuples where each tuple contains the mapped building port + and the corresponding hydraulic port in the HVAC model. + """ + mapping = [] + for hvac_outer_port, space_heater_name in cons_heat_ports: + heater_guid = space_heater_name.split('.')[0].replace( + 'spaceheater_', '').replace('_', '$') + building_index = cls.get_building_index( + zone_to_heaters, heater_guid) + building_port = (f"{model_name_building.lower()}." + f"heaPor{port_type}[{building_index}]") + hydraulic_port = (f"{model_name_hydraulic.lower()}." + f"{hvac_outer_port}") + mapping.append((building_port, hydraulic_port)) + return mapping + + + @staticmethod + def get_building_index( + zone_to_heaters: Dict[str, List[str]], + heater_guid: str) -> Optional[int]: + """Get the index of the building in the zone_to_heaters dictionary. + + Args: + zone_to_heaters: A dictionary mapping zone GUIDs to lists of heater + GUIDs. + heater_guid: The GUID of the heater to search for. + + Returns: + Optional: The index (1-based) of the zone that contains the heater + with the specified GUID. Returns None if the heater GUID is not + found in any zone. + """ + for index, (zone_guid, heater_list) in enumerate( + zone_to_heaters.items(), start=1): + if heater_guid in heater_list: + return index + return None diff --git a/bim2sim/plugins/PluginSpawn/test/__init__.py b/bim2sim/plugins/PluginSpawn/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginSpawn/test/integration/__init__.py b/bim2sim/plugins/PluginSpawn/test/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bim2sim/plugins/PluginSpawn/test/integration/test_spawn.py b/bim2sim/plugins/PluginSpawn/test/integration/test_spawn.py new file mode 100644 index 0000000000..0520443b5c --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/test/integration/test_spawn.py @@ -0,0 +1,101 @@ +import unittest +from pathlib import Path + +from bim2sim import ConsoleDecisionHandler, run_project +from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler +from bim2sim.export.modelica import ModelicaElement +from bim2sim.utilities.test import IntegrationWeatherBase +from bim2sim.utilities.types import IFCDomain +from bim2sim.tasks.hvac.export import Export + + +class IntegrationBaseSpawn(IntegrationWeatherBase): + def tearDown(self): + ModelicaElement.lookup = {} + super().tearDown() + + def model_domain_path(self) -> str: + return 'mixed' + + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_modelica = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') + + self.project.sim_settings.weather_file_path_ep = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') + + @staticmethod + def assertIsFile(path): + if not Path(path).resolve().is_file(): + raise AssertionError("File does not exist: %s" % str(path)) + +class TestIntegrationAixLib(IntegrationBaseSpawn, unittest.TestCase): + + def test_mixed_ifc_spawn(self): + """Run project with + KM_DPM_Vereinshaus_Gruppe62_Heizung_with_pumps.ifc""" + ifc_names = {IFCDomain.mixed: + 'b03_heating_with_building_blenderBIM.ifc'} + project = self.create_project(ifc_names, 'spawn') + + # HVAC/AixLib sim_settings + # Generate outer heat ports for spawn HVAC sub model + # Set other simulation settings, otherwise all settings are set to default + project.sim_settings.aggregations = [ + 'PipeStrand', + 'ParallelPump', + 'GeneratorOneFluid' + ] + project.sim_settings.group_unidentified = 'name' + + # EnergyPlus sim settings + project.sim_settings.ep_install_path = Path( + 'C:/EnergyPlusV9-6-0/') + project.sim_settings.ep_version = "9-6-0" + answers = ( + 'HVAC-PipeFitting', # Identify PipeFitting + 'HVAC-Distributor', # Identify Distributor + 'HVAC-ThreeWayValve', # Identify ThreeWayValve + 2010, # year of construction of building + *(True,) * 7, # 7 real dead ends found + *(0.001,) * 13, # volume of junctions + 2000, 175, + # rated_pressure_difference + rated_volume_flow pump of 1st storey (big) + 4000, 200, + # rated_pressure_difference + rated_volume_flow for 2nd storey + *(70, 50,) * 7, # flow and return temp for 7 space heaters + 0.056, # nominal_mass_flow_rate 2nd storey TRV (kg/s), + 20, # dT water of boiler + 70, # nominal flow temperature of boiler + 0.3, # minimal part load range of boiler + 8.5, # nominal power of boiler (in kW) + 50, # nominal return temperature of boiler + ) + handler = DebugDecisionHandler(answers) + for decision, answer in handler.decision_answer_mapping(project.run()): + decision.value = answer + + self.assertEqual(355, len(self.project.playground.elements)) + # check temperature values of exported boiler + self.assertEqual(70, project.playground.elements[ + '01ZLkLzum6a4lxl_1XXMh0'].aggregation.flow_temperature.m) + self.assertEqual(20, project.playground.elements[ + '01ZLkLzum6a4lxl_1XXMh0'].aggregation.dT_water.m) + # check number of thermal zones of building + self.assertEqual(5, len( + project.playground.elements[ + '0N8f6_N2WXb4$EWzeVdEh5'].thermal_zones)) + # check file structure + file_names = [ + "BuildingModel.mo", + "Hydraulic.mo", + "package.mo", + "package.order", + "TotalModel.mo" + ] + for file_name in file_names: + self.assertIsFile(project.paths.export / Export.get_package_name( + self.project.name) / file_name) diff --git a/bim2sim/plugins/PluginSpawn/test/integration/test_usage.py b/bim2sim/plugins/PluginSpawn/test/integration/test_usage.py new file mode 100644 index 0000000000..71c17acc52 --- /dev/null +++ b/bim2sim/plugins/PluginSpawn/test/integration/test_usage.py @@ -0,0 +1,18 @@ +import unittest + + +class TestUsage(unittest.TestCase): + """Tests for general use of library""" + + def test_import_plugin(self): + """Test importing Spawn plugin in python script""" + try: + from bim2sim.plugins import load_plugin, Plugin + plugin = load_plugin('bim2sim_spawn') + assert issubclass(plugin, Plugin) + except ImportError as err: + self.fail("Unable to import plugin\nreason: %s"%(err)) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e1_simple_project_bps_teaser.py b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e1_simple_project_bps_teaser.py index 83cfeb5a32..11d5323a7f 100644 --- a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e1_simple_project_bps_teaser.py +++ b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e1_simple_project_bps_teaser.py @@ -47,7 +47,7 @@ def run_example_simple_building_teaser(): 'Alu- oder Stahlfenster, Waermeschutzverglasung, zweifach' project.sim_settings.construction_class_doors = 'kfw_40' # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e2_complex_project_teaser.py b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e2_complex_project_teaser.py index 4be40c3ddf..e1a54658f1 100644 --- a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e2_complex_project_teaser.py +++ b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e2_complex_project_teaser.py @@ -50,7 +50,7 @@ def run_example_complex_building_teaser(): 'Alu- oder Stahlfenster, Waermeschutzverglasung, zweifach' # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e3_load_teaser_simulation_results.py b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e3_load_teaser_simulation_results.py index b83002f124..2e7706d33a 100644 --- a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e3_load_teaser_simulation_results.py +++ b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e3_load_teaser_simulation_results.py @@ -35,7 +35,7 @@ def run_example_load_existing_project(): # TODO those 2 are not used but are needed currently as otherwise the # plotting tasks will be executed and weather file is mandatory # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e5_serialize_teaser_prj.py b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e5_serialize_teaser_prj.py index 382fc9b749..36226b4f74 100644 --- a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e5_serialize_teaser_prj.py +++ b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/examples/e5_serialize_teaser_prj.py @@ -52,7 +52,7 @@ def run_serialize_teaser_project_example(): 'Alu- oder Stahlfenster, Waermeschutzverglasung, zweifach' # set weather file data - project.sim_settings.weather_file_path = ( + project.sim_settings.weather_file_path_modelica = ( Path(bim2sim.__file__).parent.parent / 'test/resources/weather_files/DEU_NW_Aachen.105010_TMYx.mos') # Run a simulation directly with dymola after model creation diff --git a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_result_df.py b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_result_df.py index f75ac5b857..6f9f7c17cc 100644 --- a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_result_df.py +++ b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_result_df.py @@ -79,7 +79,7 @@ def run(self, sim_results_path, bldg_names, elements): defined by the sim_settings, are exported to the dataframe. Args: - teaser_mat_result_paths: path to simulation result file + sim_results_path: path to simulation result file bldg_names (list): list of all buildings elements: bim2sim elements created based on ifc data Returns: diff --git a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_teaser_prj.py b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_teaser_prj.py index 117ea71250..4d895e3c28 100644 --- a/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_teaser_prj.py +++ b/bim2sim/plugins/PluginTEASER/bim2sim_teaser/task/create_teaser_prj.py @@ -13,11 +13,11 @@ class CreateTEASER(ITask): """Creates the TEASER project, run() method holds detailed information.""" - reads = ('libraries', 'elements', 'weather_file') + reads = ('libraries', 'elements', 'weather_file_modelica') touches = ('teaser_prj', 'bldg_names', 'orig_heat_loads', 'orig_cool_loads') - def run(self, libraries, elements, weather_file): + def run(self, libraries, elements, weather_file_modelica): """Creates the TEASER project based on `bim2sim` elements. The previous created and enriched `bim2sim` elements are used to @@ -28,7 +28,7 @@ def run(self, libraries, elements, weather_file): libraries: previous loaded libraries. In the case this is the TEASER library elements: dict[guid: element] with `bim2sim` elements - weather_file: path to weather file + weather_file_modelica: path to weather file Returns: teaser_prj: teaser project instance @@ -71,7 +71,7 @@ def run(self, libraries, elements, weather_file): teaser_prj.calc_all_buildings() orig_heat_loads, orig_cool_loads =\ self.overwrite_heatloads(exported_buildings) - teaser_prj.weather_file_path = weather_file + teaser_prj.weather_file_path = weather_file_modelica bldg_names = [] for bldg in exported_buildings: diff --git a/bim2sim/plugins/PluginTEASER/test/integration/test_teaser.py b/bim2sim/plugins/PluginTEASER/test/integration/test_teaser.py index 09adf4d594..029cb9d2bf 100644 --- a/bim2sim/plugins/PluginTEASER/test/integration/test_teaser.py +++ b/bim2sim/plugins/PluginTEASER/test/integration/test_teaser.py @@ -4,14 +4,20 @@ import bim2sim from bim2sim.kernel.decision.console import ConsoleDecisionHandler from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler -from bim2sim.utilities.test import IntegrationBase +from bim2sim.utilities.test import IntegrationWeatherBase from bim2sim.utilities.types import LOD, IFCDomain, ZoningCriteria -class IntegrationBaseTEASER(IntegrationBase): +class IntegrationBaseTEASER(IntegrationWeatherBase): def model_domain_path(self) -> str: return 'arch' + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_modelica = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') + class TestIntegrationTEASER(IntegrationBaseTEASER, unittest.TestCase): def test_run_kitoffice_spaces_medium_layers_low(self): diff --git a/bim2sim/plugins/PluginTEASER/test/regression/test_teaser.py b/bim2sim/plugins/PluginTEASER/test/regression/test_teaser.py index fa32283c7d..4ab9423f4d 100644 --- a/bim2sim/plugins/PluginTEASER/test/regression/test_teaser.py +++ b/bim2sim/plugins/PluginTEASER/test/regression/test_teaser.py @@ -45,6 +45,12 @@ def tearDown(self): super().tearDown() + def set_test_weather_file(self): + """Set the weather file path.""" + self.project.sim_settings.weather_file_path_modelica = ( + self.test_resources_path() / + 'weather_files/DEU_NW_Aachen.105010_TMYx.modelica') + def create_regression_setup( self, tolerance: float = 1E-3, diff --git a/bim2sim/plugins/PluginTEASER/test/unit/task/test_weather.py b/bim2sim/plugins/PluginTEASER/test/unit/task/test_weather.py index e418f4130e..75318a701d 100644 --- a/bim2sim/plugins/PluginTEASER/test/unit/task/test_weather.py +++ b/bim2sim/plugins/PluginTEASER/test/unit/task/test_weather.py @@ -41,14 +41,14 @@ def test_weather_modelica(self): IFCDomain.arch: test_rsrc_path / 'arch/ifc/AC20-FZK-Haus.ifc'} self.project = Project.create(self.test_dir.name, ifc_paths, plugin=PluginWeatherDummyTEASER) - self.project.sim_settings.weather_file_path = ( + self.project.sim_settings.weather_file_path_modelica = ( test_rsrc_path / 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') handler = DebugDecisionHandler([]) handler.handle(self.project.run(cleanup=False)) try: - weather_file = self.project.playground.state['weather_file'] + weather_file = self.project.playground.state['weather_file_modelica'] except Exception: raise ValueError(f"No weather file set through Weather task. An" f"error occurred.") self.assertEquals(weather_file, - self.project.sim_settings.weather_file_path) + self.project.sim_settings.weather_file_path_modelica) diff --git a/bim2sim/sim_settings.py b/bim2sim/sim_settings.py index ffaba75a2e..d04bc08301 100644 --- a/bim2sim/sim_settings.py +++ b/bim2sim/sim_settings.py @@ -318,6 +318,7 @@ def update_from_config(self, config): # handle all other strings val = set_from_cfg else: + # handle all other data types val = set_from_cfg setattr(self, setting, val) @@ -426,13 +427,12 @@ def check_mandatory(self): value=None, description='Path to the weather file that should be used for the ' 'simulation. If no path is provided, we will try to get ' - 'the' - 'location from the IFC and download a fitting weather' + 'the location from the IFC and download a fitting weather' ' file. For Modelica provide .mos files, for EnergyPlus ' '.epw files. If the format does not fit, we will try to ' 'convert.', for_frontend=True, - mandatory=True + mandatory=False ) building_rotation_overwrite = NumberSetting( @@ -450,11 +450,13 @@ def check_mandatory(self): 'performance simulation and co-simulations.', for_frontend=True ) + correct_space_boundaries = BooleanSetting( value=False, description='Apply geometric correction to space boundaries.', for_frontend=True ) + close_space_boundary_gaps = BooleanSetting( value=False, description='Close gaps in the set of space boundaries by adding ' @@ -525,6 +527,13 @@ def __init__(self): " should be validated by the geometric position of the " "ports." ) + outer_heat_ports = BooleanSetting( + value=False, + description='Add outer heat ports to allow connections to other ' + 'models.', + for_frontend=True + ) + class BuildingSimSettings(BaseSimSettings): diff --git a/bim2sim/tasks/base.py b/bim2sim/tasks/base.py index ba3b82d248..0541f4fd3c 100644 --- a/bim2sim/tasks/base.py +++ b/bim2sim/tasks/base.py @@ -149,8 +149,11 @@ def run_task(self, task: ITask) -> Generator[DecisionBunch, None, None]: # normal case n_res = len(result) if result is not None else 0 if len(task.touches) != n_res: - raise TaskFailed("Mismatch in '%s' result. Required items: %d (%s). Please make sure that required" - " inputs (reads) are created in previous tasks." % (task, n_res, task.touches)) + raise TaskFailed( + f"Mismatch in results of '{task}'." + f"Requiring {len(task.touches)} results, but only " + f"{n_res} are available. " + f"Required results are: ({task.touches}).") # assign results to state if n_res: diff --git a/bim2sim/tasks/common/create_elements.py b/bim2sim/tasks/common/create_elements.py index 0116f86e78..362b817d3a 100644 --- a/bim2sim/tasks/common/create_elements.py +++ b/bim2sim/tasks/common/create_elements.py @@ -239,7 +239,10 @@ def create_with_validation(self, entities_dict: dict, warn=True, bps.LayerSet, bps.Layer, Material ] - for entity, ifc_type_or_element_cls in entities_dict.items(): + entities_dict = sorted(entities_dict) + + for entity in entities_dict: + ifc_type_or_element_cls = entity.is_a() try: if isinstance(ifc_type_or_element_cls, str): if ifc_type_or_element_cls in blacklist: diff --git a/bim2sim/tasks/common/load_ifc.py b/bim2sim/tasks/common/load_ifc.py index d401658b6a..b666ac7682 100644 --- a/bim2sim/tasks/common/load_ifc.py +++ b/bim2sim/tasks/common/load_ifc.py @@ -36,7 +36,7 @@ def load_ifc_files(self, base_path: Path): if not base_path.is_dir(): raise AssertionError(f"Given base_path {base_path} is not a" f" directory. Please provide a directory.") - ifc_files_dict = {k: [] for k in ['arch', 'hydraulic', 'ventilation']} + ifc_files_dict = {k: [] for k in ['arch', 'hydraulic', 'ventilation', 'mixed']} ifc_files_unsorted = [] ifc_files = [] ifc_files_paths = list(base_path.glob("**/*.ifc")) + list( @@ -64,7 +64,7 @@ def load_ifc_files(self, base_path: Path): f"This took {t_loading} seconds") for file in ifc_files_unsorted: ifc_files_dict[file.domain.name].append(file) - for domain in ('arch', 'hydraulic', 'ventilation'): + for domain in ('arch', 'hydraulic', 'ventilation', 'mixed'): ifc_files.extend(ifc_files_dict[domain]) if not ifc_files: self.logger.error("No ifc found in project folder.") diff --git a/bim2sim/tasks/common/weather.py b/bim2sim/tasks/common/weather.py index 215639f501..2c8722c9e7 100644 --- a/bim2sim/tasks/common/weather.py +++ b/bim2sim/tasks/common/weather.py @@ -3,28 +3,38 @@ from typing import Any from pathlib import WindowsPath, Path from typing import Optional + class Weather(ITask): """Task to get the weather file for later simulation""" reads = ('elements',) touches = ('weather_file',) + """Task to get the weather file for later simulation""" + reads = ('elements',) + touches = ('weather_file_modelica', 'weather_file_ep') def run(self, elements: dict): self.logger.info("Setting weather file.") - weather_file: Optional[WindowsPath] = None - # try to get weather file from settings - if self.playground.sim_settings.weather_file_path: - weather_file = self.playground.sim_settings.weather_file_path + weather_file_modelica = None + weather_file_ep = None + # try to get weather file from settings for modelica and energyplus + if self.playground.sim_settings.weather_file_path_modelica: + weather_file_modelica = ( + self.playground.sim_settings.weather_file_path_modelica) + if self.playground.sim_settings.weather_file_path_ep: + weather_file_ep = self.playground.sim_settings.weather_file_path_ep + # try to get TRY weather file for location of IFC - if not weather_file: + if not weather_file_ep and not weather_file_modelica: raise NotImplementedError("Waiting for response from DWD if we can" "implement this") # lat, long = self.get_location_lat_long_from_ifc(elements) # weather_file = self.get_weatherfile_from_dwd(lat, long) - self.check_file_ending(weather_file) - if not weather_file: + # self.check_weather_file(weather_file_modelica, weather_file_ep) + + if not weather_file_ep and not weather_file_modelica: raise ValueError("No weather file provided for the simulation, " "can't continue model generation.") - return weather_file, + return weather_file_modelica, weather_file_ep def check_file_ending(self, weather_file: WindowsPath): """Check if the file ending fits the simulation model type.""" @@ -41,35 +51,34 @@ def check_file_ending(self, weather_file: WindowsPath): f"Modelica simulation model should be created, but " f"instead .mos a {weather_file.suffix} file was provided.") - def get_location_lat_long_from_ifc(self, elements: dict) -> [float]: - """ - Returns the location in form of latitude and longitude based on IfcSite. + # Get the expected endings for the plugin_name + if plugin_name not in expected_endings: + raise ValueError(f"Unknown plugin_name '{plugin_name}'") - The location of the site and therefore the building are taken from the - IfcSite in form of latitude and longitude. Latitude and Longitude each - are a tuple of (degrees, minutes, seconds) and, optionally, - millionths of seconds. See IfcSite Documentation for further - information. - Args: - elements: dict with bim2sim elements + required_endings = expected_endings[plugin_name] - Returns: - latitude, longitude: two float values for latitude and longitude - """ - site = filter_elements(elements, 'Site') - if len(site) > 1: - self.logger.warning( - "More than one IfcSite in the provided IFC file(s). We are" - "using the location of the first IfcSite found for weather " - "file definition.") - latitude = site[0].location_latitude - longitude = site[0].location_longitude - return latitude, longitude - - def get_weatherfile_from_dwd(self, lat: tuple, long: tuple): - # TODO implement scraper, if DWD allows it - raise NotImplementedError("Waiting for response from DWD if we can" - "implement this") + # If both are required, ensure both files are provided + if '.epw' in required_endings and '.mos' in required_endings: + if not weather_file_ep or not weather_file_modelica: + raise ValueError( + f"{plugin_name} requires both '.epw' and '.mos' " + f"weather files.") + + # Check if the correct weather file is provided + if '.epw' in required_endings: + if (not weather_file_ep or not isinstance(weather_file_ep, Path) or + not weather_file_ep.suffix == '.epw'): + raise ValueError( + f"{plugin_name} requires a weather file with '.epw' " + f"extension.") + + if '.mos' in required_endings: + if not weather_file_modelica or not isinstance( + weather_file_modelica, + Path) or not weather_file_modelica.suffix == '.mos': + raise ValueError( + f"{plugin_name} requires a weather file with '.mos'" + f" extension.") def get_location_name(self, latitude: tuple, longitude: tuple) -> str: """Returns the name of the location based on latitude and longitude. diff --git a/bim2sim/tasks/hvac/__init__.py b/bim2sim/tasks/hvac/__init__.py index 8bc04bb3fb..de2b83ee22 100644 --- a/bim2sim/tasks/hvac/__init__.py +++ b/bim2sim/tasks/hvac/__init__.py @@ -3,6 +3,8 @@ from .fixports import FixPorts from .make_graph import MakeGraph from .reduce import Reduce +from .export import CreateModelicaModel from .export import Export from .connect_elements import ConnectElements from .load_standardlibrary import LoadLibrariesStandardLibrary +from .enrich_flow_direction import EnrichFlowDirection diff --git a/bim2sim/tasks/hvac/connect_elements.py b/bim2sim/tasks/hvac/connect_elements.py index cc437b4f0e..e1a285a38b 100644 --- a/bim2sim/tasks/hvac/connect_elements.py +++ b/bim2sim/tasks/hvac/connect_elements.py @@ -9,7 +9,7 @@ from bim2sim.elements.base_elements import Port, ProductBased from bim2sim.kernel.decision import DecisionBunch from bim2sim.tasks.base import ITask, Playground - +from bim2sim.utilities.common_functions import filter_elements, all_subclasses quality_logger = logging.getLogger('bim2sim.QualityReport') @@ -48,11 +48,15 @@ def run(self, elements: dict) -> dict: # Check ports self.logger.info("Checking ports of elements ...") - self.check_element_ports(elements) + + hvac_elements = filter_elements( + elements, hvac.HVACProduct, create_dict=True, + include_sub_classes=True) + self.remove_duplicate_ports(hvac_elements) # Make connections by relations self.logger.info("Connecting the relevant elements") self.logger.info(" - Connecting by relations ...") - all_ports = [port for item in elements.values() for port in item.ports] + all_ports = [port for item in hvac_elements.values() for port in item.ports] rel_connections = self.connections_by_relation(all_ports) self.logger.info(" - Found %d potential connections.", len(rel_connections)) @@ -111,17 +115,44 @@ def run(self, elements: dict) -> dict: self.logger.warning( "Connecting by bounding box is not implemented.") # Check inner connections - yield from self.check_inner_connections(elements.values()) + yield from self.check_inner_connections(hvac_elements.values()) # TODO: manually add / modify connections return elements, @staticmethod - def check_element_ports(elements: dict): - """Checks position of all ports for each element. + def remove_duplicate_ports(elements: dict): + """Checks position of all ports for each element and handles + overlapping ports. + + This method analyzes port positions within building elements + (e.g. pipes, fittings) and identifies overlapping ports that may + indicate data quality issues. When two ports of the same element + overlap and both connect to the same third port, they are merged into + a single bidirectional port. Args: - elements: dictionary of elements to be checked with GUID as key. + elements: Dictionary mapping GUIDs to element objects that should + be checked. + Each element must have a 'ports' attribute containing its + ports. + + Quality Checks: + - Warns if ports of the same element are closer than 1 unit + (atol=1) + - Identifies overlapping ports using numpy.allclose with rtol=1e-7 + + Port Merging: + If overlapping ports A and B are found that both connect to the + same port C: + - Port B is removed from the element + - Port A is set as bidirectional (SINKANDSOURCE) + - Port A becomes the flow master + - The change is logged for documentation + + WARNING: Poor quality of elements : Overlapping ports (port1 + and port2 @[x,y,z]) + INFO: Removing and set as SINKANDSOURCE. """ for ele in elements.values(): for port_a, port_b in itertools.combinations(ele.ports, 2): @@ -139,13 +170,14 @@ def check_element_ports(elements: dict): port not in [port_a, port_b]] if port_a in all_ports and port_b in all_ports and len( set(other_ports)) == 1: - # Both ports connected to same other port -> merge ports + # Both ports connected to same other port -> + # merge ports quality_logger.info( "Removing %s and set %s as SINKANDSOURCE.", port_b.ifc, port_a.ifc) ele.ports.remove(port_b) port_b.parent = None - port_a.flow_direction = 0 + port_a.flow_direction.value = 0 port_a.flow_master = True @staticmethod diff --git a/bim2sim/tasks/hvac/dead_ends.py b/bim2sim/tasks/hvac/dead_ends.py index 3f14822c1b..d9674f0b4b 100644 --- a/bim2sim/tasks/hvac/dead_ends.py +++ b/bim2sim/tasks/hvac/dead_ends.py @@ -136,7 +136,7 @@ def decide_dead_ends(graph: HvacGraph, pot_dead_ends: list, cur_decision = BoolDecision( question="Found possible dead end at port %s in system, " "please check if it is a dead end" % dead_end, - console_identifier="GUID: %s" % dead_end.guid, + console_identifier="GUID: %s, Parent GUID: %s" % (dead_end.guid, related_guid), key=dead_end, global_key="deadEnd.%s" % dead_end.guid, allow_skip=False, diff --git a/bim2sim/tasks/hvac/enrich_flow_direction.py b/bim2sim/tasks/hvac/enrich_flow_direction.py new file mode 100644 index 0000000000..7ab264ff51 --- /dev/null +++ b/bim2sim/tasks/hvac/enrich_flow_direction.py @@ -0,0 +1,163 @@ +from bim2sim.elements.graphs.hvac_graph import HvacGraph +from bim2sim.elements.hvac_elements import HVACPort +from bim2sim.tasks.base import ITask +import logging +from bim2sim.kernel.decision import BoolDecision, DecisionBunch +from bim2sim.utilities.types import FlowSide + + +logger = logging.getLogger(__name__) + + +class EnrichFlowDirection(ITask): + + reads = ('graph', ) + + def run(self, graph: HvacGraph): + yield from self.set_flow_sides(graph) + print('test') + + #Todo Continue here #733 + def set_flow_sides(self, graph: HvacGraph): + """ Set flow sides for ports in HVAC graph based on known flow sides. + + This function iteratively sets flow sides for ports in the HVAC graph. + It uses a recursive method (`recurse_set_unknown_sides`) to determine + the flow side for each unset port. The function may prompt the user + for decisions in case of conflicts or unknown sides. + + Args: + graph: The HVAC graph. + + Yields: + DecisionBunch: A collection of decisions may be yielded during the + task. + """ + # TODO: needs testing! + # TODO: at least one master element required + print('test') + accepted = [] + while True: + unset_port = None + for port in list(graph.nodes): + if port.flow_side == FlowSide.unknown and graph[port] \ + and port not in accepted: + unset_port = port + break + if unset_port: + side, visited, masters = self.recurse_set_unknown_sides(graph, unset_port) + if side in (-1, 1): + # apply suggestions + for port in visited: + port.flow_side = side + elif side == 0: + # TODO: ask user? + accepted.extend(visited) + elif masters: + # ask user to fix conflicts (and retry in next while loop) + for port in masters: + decision = BoolDecision( + "Use %r as VL (y) or RL (n)?" % port, + global_key= "Use_port_%s" % port.guid) + yield DecisionBunch([decision]) + use = decision.value + if use: + port.flow_side = 1 + else: + port.flow_side = -1 + else: + # can not be solved (no conflicting masters) + # TODO: ask user? + accepted.extend(visited) + else: + # done + logging.info("Flow_side set") + break + + # TODO not used yet + def recurse_set_side(self, port, side, known: dict = None, + raise_error=True): + """Recursive set flow_side to connected ports""" + if known is None: + known = {} + + # set side suggestion + is_known = port in known + current_side = known.get(port, port.flow_side) + if not is_known: + known[port] = side + elif is_known and current_side == side: + return known + else: + # conflict + if raise_error: + raise AssertionError("Conflicting flow_side in %r" % port) + else: + logger.error("Conflicting flow_side in %r", port) + known[port] = None + return known + + # call neighbours + for neigh in self.neighbors(port): + if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ + and port.parent is neigh.parent: + # switch flag over consumers / generators + self.recurse_set_side(neigh, -side, known, raise_error) + else: + self.recurse_set_side(neigh, side, known, raise_error) + + return known + + def recurse_set_unknown_sides(self, + graph: HvacGraph, + port: HVACPort, + visited: list = None, + masters: list = None): + """Recursive checks neighbours flow_side. + :returns tuple of + common flow_side (None if conflict) + list of checked ports + list of ports on which flow_side s are determined""" + + if visited is None: + visited = [] + if masters is None: + masters = [] + + # mark as visited to prevent deadloops + visited.append(port) + + if port.flow_side in (-1, 1): + # use port with known flow_side as master + masters.append(port) + return port.flow_side, visited, masters + + # call neighbours + neighbour_sides = {} + for neigh in graph.neighbors(port): + if neigh not in visited: + if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ + and port.parent is neigh.parent: + # switch flag over consumers / generators + side, _, _ = self.recurse_set_unknown_sides(graph, neigh, visited, masters) + side = -side + else: + side, _, _ = self.recurse_set_unknown_sides(graph, neigh, visited, masters) + neighbour_sides[neigh] = side + + sides = set(neighbour_sides.values()) + if not sides: + return port.flow_side, visited, masters + elif len(sides) == 1: + # all neighbours have same site + side = sides.pop() + return side, visited, masters + elif len(sides) == 2 and 0 in sides: + side = (sides - {0}).pop() + return side, visited, masters + else: + # conflict + return None, visited, masters + + + diff --git a/bim2sim/tasks/hvac/export.py b/bim2sim/tasks/hvac/export.py index 6ec717d7f2..9ae50b9a14 100644 --- a/bim2sim/tasks/hvac/export.py +++ b/bim2sim/tasks/hvac/export.py @@ -1,19 +1,25 @@ +import re from datetime import datetime +from pathlib import Path +from typing import Tuple, List, Dict from bim2sim.elements import hvac_elements as hvac from bim2sim.elements.base_elements import ProductBased from bim2sim.elements.graphs.hvac_graph import HvacGraph +from bim2sim.elements.hvac_elements import HVACProduct from bim2sim.export import modelica -from bim2sim.export.modelica import ModelicaParameter +from bim2sim.export.modelica import HeatTransferType, ModelicaParameter, \ + ModelicaElement +from bim2sim.export.modelica import help_package, help_package_order from bim2sim.tasks.base import ITask -class Export(ITask): +class CreateModelicaModel(ITask): """Export to Dymola/Modelica""" reads = ('libraries', 'graph') - touches = ('modelica_model',) - final = True + touches = ('export_elements', 'connections', 'cons_heat_ports_conv', + 'cons_heat_ports_rad') def run(self, libraries: tuple, graph: HvacGraph): """Export HVAC graph to Modelica code. @@ -25,51 +31,69 @@ def run(self, libraries: tuple, graph: HvacGraph): 3. Collects and exports parameters for each Modelica instance. 4. Creates connections between Modelica instances based on the HVAC graph. - 5. Creates a Modelica model with the exported elements and connections. - 6. Saves the Modelica model to the specified export path. + 5. Creates heat port connections (both inner and outer). + 6. Returning the created elements and connections. Args: libraries: Tuple of libraries to be used in Modelica. graph: The HVAC graph to be exported. + + Returns: + export_elements: A mapping of HVAC elements to their corresponding + Modelica instances. + connections: A list of connections between the Modelica instances. + cons_heat_ports_conv: A list of convective heat port connections. + cons_heat_ports_rad: A list of radiative heat port connections. """ + # Initialize Modelica factory and create Modelica instances + self.logger.info("Exporting HVAC graph to Modelica code") + elements = sorted(graph.elements, key=lambda x: x.guid) + modelica.ModelicaElement.init_factory(libraries) + export_elements = self._create_export_elements(elements) - self.logger.info("Export to Modelica code") - elements = graph.elements + # Create connections based on HVAC graph + connections = self._create_connections(graph, export_elements) - connections = graph.get_connections() + # Handle pending attribute and parameter decisions + yield from self._handle_pending_decisions(elements) - modelica.ModelicaElement.init_factory(libraries) - export_elements = {inst: modelica.ModelicaElement.factory(inst) - for inst in elements} + # Collect parameters for each Modelica instance + self._collect_parameters(export_elements) + + # Create heat port connections + cons_heat_ports_conv, cons_heat_ports_rad = ( + self._create_heat_port_connections(export_elements)) + + # # TODO #1 integrate heat ports in connections + # connections.extend(cons_heat_ports_conv) + # connections.extend(cons_heat_ports_rad) + return (export_elements, connections, + cons_heat_ports_conv, cons_heat_ports_rad) + + @staticmethod + def _create_export_elements(elements: List[HVACProduct] + ) -> Dict[HVACProduct, ModelicaElement]: + """Create Modelica instances for each HVAC element.""" + return {inst: modelica.ModelicaElement.factory(inst) + for inst in elements} - # Perform decisions for requested but not existing attributes + @staticmethod + def _handle_pending_decisions(elements: List[HVACProduct]): + """Handle pending attribute and parameter decisions.""" yield from ProductBased.get_pending_attribute_decisions(elements) yield from ModelicaParameter.get_pending_parameter_decisions() - # All parameters are checked against the specified check function and - # exported with the correct unit + @staticmethod + def _collect_parameters( + export_elements: Dict[HVACProduct, ModelicaElement]): + """Collect and export parameters for each Modelica instance.""" for instance in export_elements.values(): instance.collect_params() - connection_port_names = self.create_connections(graph, export_elements) - - self.logger.info( - "Creating Modelica model with %d model elements " - "and %d connections.", - len(export_elements), len(connection_port_names)) - - modelica_model = modelica.ModelicaModel( - name="bim2sim_"+self.prj_name, - comment=f"Autogenerated by BIM2SIM on " - f"{datetime.now():%Y-%m-%d %H:%M:%S%z}", - modelica_elements=list(export_elements.values()), - connections=connection_port_names, - ) - modelica_model.save(self.paths.export) - return modelica_model, - @staticmethod - def create_connections(graph: HvacGraph, export_elements: dict) -> list: + def _create_connections(graph: HvacGraph, + export_elements: Dict[HVACProduct, ModelicaElement] + ) -> List[Tuple[str, str]]: """Creates a list of connections for the corresponding Modelica model. This method iterates over the edges of the HVAC graph and creates a @@ -81,8 +105,8 @@ def create_connections(graph: HvacGraph, export_elements: dict) -> list: export_elements: the Modelica elements Returns: - connection_port_names: list of tuple of port names that are - connected. + connection_port_names: list of tuple of port names that are + connected. """ connection_port_names = [] distributors_n = {} @@ -96,7 +120,8 @@ def create_connections(graph: HvacGraph, export_elements: dict) -> list: ports_name = {'a': elements['a'].get_full_port_name(port_a), 'b': elements['b'].get_full_port_name(port_b)} if any(isinstance(e.element, hvac.Distributor) - for e in elements.values()): + for e in + elements.values()): for key, inst in elements.items(): if type(inst.element) is hvac.Distributor: distributor = (key, inst) @@ -111,7 +136,178 @@ def create_connections(graph: HvacGraph, export_elements: dict) -> list: connection_port_names.append((ports_name['a'], ports_name['b'])) - for distributor in distributors_n: - distributor.export_parameters['n'] = int(distributors_n[distributor] / 2 - 1) + # for distributor in distributors_n: + # distributor.parameters['n'] = int( + # distributors_n[distributor] / 2 - 1) return connection_port_names + + def _create_heat_port_connections( + self, export_elements: Dict[HVACProduct, ModelicaElement] + ) -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]]]: + """Create inner and outer heat port connections.""" + inner_heat_port_cons_conv, inner_heat_port_cons_rad = ( + self.create_inner_heat_port_connections()) + + if self.playground.sim_settings.outer_heat_ports: + outer_heat_port_cons_conv, outer_heat_port_cons_rad = ( + self.create_outer_heat_port_connections( + list(export_elements.values()))) + else: + outer_heat_port_cons_conv, outer_heat_port_cons_rad = [], [] + + cons_heat_ports_conv = (outer_heat_port_cons_conv + + inner_heat_port_cons_conv) + cons_heat_ports_rad = (outer_heat_port_cons_rad + + inner_heat_port_cons_rad) + + return cons_heat_ports_conv, cons_heat_ports_rad + + @staticmethod + def create_outer_heat_port_connections( + export_elements: List[ModelicaElement] + ) -> Tuple[List[Tuple[str, str]], List[Tuple[str, str]]]: + """Creates connections to an outer heat port for further connections + + Connects heat ports of the export elements to outer heat ports + (convective and radiative). Returns two lists containing these + connections. + + Returns: + Two lists containing convective and radiative heat port connections. + """ + # ToDo only connect heat ports that might not be connected already by + # create_inner_heat_port_connections() function + + # ToDo annotations/placements are missing. Those needs to be added to + # visualize connections + convective_heat_port_connections = [] + radiative_heat_port_connections = [] + convective_ports_index = 1 + radiative_ports_index = 1 + + for ele in export_elements: + for heat_port in ele.heat_ports: + if heat_port.heat_transfer_type == HeatTransferType.CONVECTIVE: + convective_heat_port_connections.append( + (f"heatPortOuterCon[{convective_ports_index}]", + heat_port.get_full_name())) + convective_ports_index += 1 + if heat_port.heat_transfer_type == HeatTransferType.RADIATIVE: + radiative_heat_port_connections.append( + (f"heatPortOuterRad[{radiative_ports_index}]", + heat_port.get_full_name())) + radiative_ports_index += 1 + + return (convective_heat_port_connections, + radiative_heat_port_connections) + + def create_inner_heat_port_connections(self) -> [list, list]: + # TODO if this is needed + return [], [] + + +class Export(ITask): + """Export to Dymola/Modelica""" + + reads = ('export_elements', 'connections', 'cons_heat_ports_conv', + 'cons_heat_ports_rad') + touches = ('modelica_model', 'package_name', 'model_name_hydraulic') + + def run(self, export_elements: dict, connections: list, + cons_heat_ports_conv: list, cons_heat_ports_rad: list): + """Export Modelica elements and connections to Modelica code. + + This method creates a Modelica model, organizes the package structure, + and saves the model to the specified path. + + Args: + export_elements: Dictionary containing elements to export. + connections: List of fluid port connections. + cons_heat_ports_conv: List of convective heat port connections. + cons_heat_ports_rad: List of radiative heat port connections. + + Returns: + modelica_model: The modelica model as string. + package_name: The name of the package (i.e. the folder where the + modelica model is stored.) + model_name_hydraulic: The name of the model. + """ + self.logger.info("Creating Modelica model with %d model elements and %d" + " connections.", len(export_elements), + len(connections)) + model_name_hydraulic = 'Hydraulic' + package_name = self.get_package_name(self.prj_name) + + # Setup package directory structure + export_package_path = self._setup_package_structure( + package_name, model_name_hydraulic + ) + + # Save the Modelica model + modelica_model = save_modelica_model( + model_name=model_name_hydraulic, + package_path=export_package_path, + export_elements=list(export_elements.values()), + connections=connections, + cons_heat_ports_conv=cons_heat_ports_conv, + cons_heat_ports_rad=cons_heat_ports_rad + ) + return modelica_model, package_name, model_name_hydraulic + + @staticmethod + def get_package_name(prj_name) -> str: + """Generate a valid package name based on the project name. + + Returns: + str: The valid package name. + """ + regex = re.compile("[^a-zA-z0-9]") + return regex.sub("", prj_name) + + def _setup_package_structure(self, package_name: str, + model_name_hydraulic: str) -> Path: + """Set up the package directory structure for exporting Modelica models. + + Args: + package_name: The name of the package. + model_name_hydraulic: The name of the hydraulic model. + + Returns: + Path: The path to the export package directory. + """ + export_package_path = self.paths.export / Path(package_name) + + # Helper functions to structure the Modelica package + help_package(path=export_package_path, name=export_package_path.stem, + within="") + help_package_order(path=export_package_path, + package_list=[model_name_hydraulic]) + + return export_package_path + + +def save_modelica_model(model_name: str, package_path: Path, + export_elements, connections, cons_heat_ports_conv, + cons_heat_ports_rad): + """Saves the Modelica model file. + + Args: + model_name: The name of the model. + package_path: The directory/package path where the model will be saved. + export_elements: List of elements to export. + connections: List of connections data. + cons_heat_ports_conv: List of convective heat port connections. + cons_heat_ports_rad: List of radiative heat port connections. + """ + modelica_model = modelica.ModelicaModel( + name=model_name, + comment=f"Autogenerated by BIM2SIM on " + f"{datetime.now():%Y-%m-%d %H:%M:%S%z}", + modelica_elements=export_elements, + connections=connections, + connections_heat_ports_conv=cons_heat_ports_conv, + connections_heat_ports_rad=cons_heat_ports_rad + ) + modelica_model.save(package_path / f"{model_name}.mo") + return modelica_model diff --git a/bim2sim/tasks/hvac/make_graph.py b/bim2sim/tasks/hvac/make_graph.py index 986c65a54e..c290626693 100644 --- a/bim2sim/tasks/hvac/make_graph.py +++ b/bim2sim/tasks/hvac/make_graph.py @@ -1,6 +1,8 @@ from bim2sim.elements.base_elements import Material from bim2sim.elements.graphs.hvac_graph import HvacGraph from bim2sim.tasks.base import ITask +from bim2sim.utilities.common_functions import filter_elements +from bim2sim.elements.hvac_elements import HVACProduct class MakeGraph(ITask): @@ -21,7 +23,10 @@ def run(self, elements: dict): The created HVAC graph. """ self.logger.info("Creating graph from IFC elements") + hvac_elements = filter_elements( + elements, HVACProduct, create_dict=True, + include_sub_classes=True) not_mat_elements = \ - {k: v for k, v in elements.items() if not isinstance(v, Material)} + {k: v for k, v in hvac_elements.items() if not isinstance(v, Material)} graph = HvacGraph(not_mat_elements.values()) return graph, diff --git a/bim2sim/tasks/hvac/reduce.py b/bim2sim/tasks/hvac/reduce.py index 5f6bfe141b..c018cc609e 100644 --- a/bim2sim/tasks/hvac/reduce.py +++ b/bim2sim/tasks/hvac/reduce.py @@ -1,10 +1,7 @@ -import logging - from bim2sim.elements.aggregation.hvac_aggregations import UnderfloorHeating, \ Consumer, PipeStrand, ParallelPump, ConsumerHeatingDistributorModule, \ GeneratorOneFluid from bim2sim.elements.graphs.hvac_graph import HvacGraph -from bim2sim.kernel.decision import BoolDecision, DecisionBunch from bim2sim.tasks.base import ITask @@ -102,6 +99,7 @@ def set_flow_sides(graph: HvacGraph): DecisionBunch: A collection of decisions may be yielded during the task. """ + # TODO #733 # TODO: needs testing! # TODO: at least one master element required accepted = [] diff --git a/bim2sim/utilities/common_functions.py b/bim2sim/utilities/common_functions.py index 52dfa073f3..797dde7612 100644 --- a/bim2sim/utilities/common_functions.py +++ b/bim2sim/utilities/common_functions.py @@ -3,17 +3,20 @@ import logging import math import re -import shutil import zipfile -from urllib.request import urlopen from pathlib import Path -from typing import Union from time import sleep +from typing import TYPE_CHECKING, Type, Union +from urllib.request import urlopen + import git import bim2sim from bim2sim.utilities.types import IFCDomain +if TYPE_CHECKING: + from bim2sim.elements.base_elements import IFCBased + assets = Path(bim2sim.__file__).parent / 'assets' logger = logging.getLogger(__name__) @@ -251,24 +254,29 @@ def get_material_templates(): def filter_elements( - elements: Union[dict, list], type_name, create_dict=False)\ - -> Union[list, dict]: + elements: Union[dict, list], + type_name: Union[str, Type['IFCBased']], create_dict=False, + include_sub_classes=False) -> Union[list, dict]: """Filters the inspected elements by type name (e.g. Wall) and - returns them as list or dict if wanted + returns them as a list or dict if wanted Args: elements: dict or list with all bim2sim elements type_name: str or element type to filter for - create_dict (Boolean): True if a dict instead of a list should be - created + create_dict (Boolean): True if a dict instead of a list should be created + include_sub_classes (Boolean): True if all subclasses of the given + type_name are included as well Returns: elements_filtered: list of all bim2sim elements of type type_name """ from bim2sim.elements.base_elements import SerializedElement + elements_filtered = [] - list_elements = elements.values() if type(elements) is dict \ - else elements + list_elements = elements.values() if isinstance(elements, dict) else elements + + # Handle filtering based on type_name being a string or class if isinstance(type_name, str): + # Direct string comparison, subclasses are irrelevant for instance in list_elements: if isinstance(instance, SerializedElement): if instance.element_type == type_name: @@ -277,12 +285,25 @@ def filter_elements( if type_name in type(instance).__name__: elements_filtered.append(instance) else: + # type_name is a class type, include subclasses if requested + if include_sub_classes: + type_classes = all_subclasses(type_name, include_self=True) + else: + type_classes = [type_name] + for instance in list_elements: if isinstance(instance, SerializedElement): - if instance.element_type == type_name.__name__: + # Check the class type or subclass match + if any(isinstance(instance, cls) for cls in type_classes): elements_filtered.append(instance) if type_name is type(instance): elements_filtered.append(instance) + else: + # Direct instance check for non-SerializedElement instances + if any(isinstance(instance, cls) for cls in type_classes): + elements_filtered.append(instance) + + # Return the results in the desired format if not create_dict: return elements_filtered else: diff --git a/bim2sim/utilities/pyocc_tools.py b/bim2sim/utilities/pyocc_tools.py index 143745f214..0b1cc952f4 100644 --- a/bim2sim/utilities/pyocc_tools.py +++ b/bim2sim/utilities/pyocc_tools.py @@ -39,6 +39,7 @@ class PyOCCTools: + # TODO @Veronika: why is this a class and not just a file with functions? """Class for Tools handling and modifying Python OCC Shapes""" @staticmethod @@ -646,7 +647,6 @@ def obj2_in_obj1(obj1: TopoDS_Shape, obj2: TopoDS_Shape) -> bool: shell = PyOCCTools.make_shell_from_faces(faces) obj1_solid = PyOCCTools.make_solid_from_shell(shell) obj2_center = PyOCCTools.get_center_of_volume(obj2) - return PyOCCTools.check_pnt_in_solid(obj1_solid, obj2_center) @staticmethod diff --git a/bim2sim/utilities/test.py b/bim2sim/utilities/test.py index c0e622a9cb..a153a515e5 100644 --- a/bim2sim/utilities/test.py +++ b/bim2sim/utilities/test.py @@ -53,7 +53,7 @@ def create_project( ifc_paths=ifc_paths, plugin=plugin) # set weather file data - self.project.sim_settings.weather_file_path = self.weather_file_path() + self.set_test_weather_file() return self.project @staticmethod @@ -63,12 +63,15 @@ def test_resources_path() -> Path: def model_domain_path(self) -> Union[str, None]: return None - def weather_file_path(self) -> Path: - return (self.test_resources_path() / - 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') +class IntegrationWeatherBase(IntegrationBase): + """Base class for integration tests that need weather files.""" + def set_test_weather_file(self): + """Set the weather file path.""" + raise NotImplementedError("") -class RegressionTestBase(IntegrationBase): + +class RegressionTestBase(IntegrationWeatherBase): """Base class for regression tests.""" def setUp(self): self.results_src_dir = None diff --git a/bim2sim/utilities/types.py b/bim2sim/utilities/types.py index c67f7f7efc..a5191a398b 100644 --- a/bim2sim/utilities/types.py +++ b/bim2sim/utilities/types.py @@ -111,3 +111,17 @@ class BoundaryOrientation(Enum): top = auto() bottom = auto() vertical = auto() + + +class FlowDirection(Enum): + """Used to describe the flow direction of ports.""" + sink_and_source = 0 + sink = -1 + source = 1 + unknown = 2 + + +class FlowSide(Enum): + supply_flow = 1 + return_flow = -1 + unknown = 0 diff --git a/docs/source/user-guide/PluginAixLib.md b/docs/source/user-guide/PluginAixLib.md index 77f487c96e..1b061e8835 100644 --- a/docs/source/user-guide/PluginAixLib.md +++ b/docs/source/user-guide/PluginAixLib.md @@ -214,6 +214,24 @@ taskDeadEnds --> taskLoadLibrariesAixLib taskLoadLibrariesAixLib --> taskExport ``` +### Port handling +#### Port creation +1. Ports get created during product creation as they are relation based +2. `ProductBased` base class has `get_ports()` function that is overwritten in `HVACProduct` +3. Ports are stored under self.ports in every HVACElement +4. IFC offers [according to schema](https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/ifcsharedbldgserviceelements/lexical/ifcflowdirectionenum.htm) the `FlowDirection` enumeration, which can be either "SOURCE", "SINK", or "SOURCEANDSINK" but some IFC files also hold "SINKANDSOURCE". +5. Groups and port `flow_directions` are assigned via `HVACPort.ifc2args()` method +6. `flow_side` is assigned via `determine_flow_side()` function that uses patters matches for the names of "vorlauf" "rücklauf" etc. and the port `flow_direction` + +#### Port Connection +Task: `ConnectElements` +`check_element_ports()` +`connections_by_relation()` +`set_flow_sides()` +`recurse_set_side()` +`recurse_set_unknown_sides()` + + This figure is generated by the script template_mermaid.py (see [Visualization of bim2sim plugin structure](genVisPlugins)). ## How to create a project? @@ -238,3 +256,6 @@ This figure is generated by the script template_mermaid.py (see [Visualization o ### What kind of results exist? ### What programs/tools to use for further analysis? + + +# \ No newline at end of file diff --git a/test/unit/elements/helper.py b/test/unit/elements/helper.py index 18f395af4e..16399de692 100644 --- a/test/unit/elements/helper.py +++ b/test/unit/elements/helper.py @@ -136,6 +136,7 @@ def get_simple_consumer(self): consumer = self.element_generator( hvac_aggregations.Consumer, rated_power=20 * ureg.kilowatt, + rated_volume_flow=1 * ureg.meter**3 / ureg.second, base_graph=nx.Graph(), match_graph=nx.Graph() ) @@ -332,6 +333,33 @@ def elements_in_agg(agg): return False return True + def get_pipe(self): + pipe = self.element_generator( + hvac.Pipe, + diameter=0.02 * ureg.m, + length=1 * ureg.m + ) + return HvacGraph([pipe]) + + def get_pump(self): + pump = self.element_generator( + hvac.Pump, + rated_volume_flow=1 * ureg.m ** 3 / ureg.s, + rated_pressure_difference=10000 * ureg.N / (ureg.m ** 2)) + return HvacGraph([pump]) + + def get_two_radiators(self): + radiator_1 = self.element_generator( + hvac.SpaceHeater, rated_power=1 * ureg.kilowatt, + flow_temperature=70 * ureg.celsius, + return_temperature=50 * ureg.celsius + ) + radiator_2 = self.element_generator( + hvac.SpaceHeater, rated_power=1 * ureg.kilowatt, + flow_temperature=70 * ureg.celsius, + return_temperature=50 * ureg.celsius + ) + return HvacGraph([radiator_1, radiator_2]) class SetupHelperBPS(SetupHelper): def element_generator(self, element_cls, flags=None, **kwargs): diff --git a/test/unit/elements/mapping/test_attribute.py b/test/unit/elements/mapping/test_attribute.py index 565810c0f8..68fa77e7b1 100644 --- a/test/unit/elements/mapping/test_attribute.py +++ b/test/unit/elements/mapping/test_attribute.py @@ -9,7 +9,6 @@ from test.unit.elements.helper import SetupHelperHVAC from bim2sim.utilities.types import AttributeDataSource - class TestElement(ProductBased): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -32,10 +31,19 @@ def _func1(self, name): attr7 = Attribute(attr_type=bool) +class TestElementOther(ProductBased): + def _func2(self, name): + return 45 + + class TestElementInherited(TestElement): def _func1(self, name): return 43 + attr6 = Attribute( + functions=[TestElementOther._func2] + ) + class TestAttribute(unittest.TestCase): @@ -181,6 +189,9 @@ def test_attribute_reset(self): def test_from_function_inheritance(self): self.assertEqual(43, self.subject_inherited.attr5) + def test_function_from_another_element(self): + self.assertEqual(45, self.subject_inherited.attr6) + class TestAttributeDecisions(unittest.TestCase): diff --git a/test/unit/tasks/__init__.py b/test/unit/tasks/__init__.py index 6d266ec739..2f15dfb359 100644 --- a/test/unit/tasks/__init__.py +++ b/test/unit/tasks/__init__.py @@ -1,3 +1,4 @@ +import inspect import tempfile import unittest from typing import Union @@ -54,7 +55,10 @@ def tearDown(self) -> None: self.playground.sim_settings.load_default_settings() def run_task(self, answers, reads): - return DebugDecisionHandler(answers).handle(self.test_task.run(*reads)) + if inspect.isgeneratorfunction(self.test_task.run): + return DebugDecisionHandler(answers).handle(self.test_task.run(*reads)) + else: + return self.test_task.run(*reads) @classmethod def simSettingsClass(cls) -> Union[BaseSimSettings, None]: diff --git a/test/unit/tasks/common/test_inspect.py b/test/unit/tasks/common/test_inspect.py index 6561c7fa43..b732b35c4e 100644 --- a/test/unit/tasks/common/test_inspect.py +++ b/test/unit/tasks/common/test_inspect.py @@ -46,7 +46,7 @@ def test_case_1(self): 'hydraulic/ifc/B01_2_HeatExchanger_Pipes.ifc'} self.project = Project.create(self.test_dir.name, ifc_paths, plugin=PluginDummy, ) - self.project.sim_settings.weather_file_path = ( + self.project.sim_settings.weather_file_path_modelica = ( test_rsrc_path / 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') handler = DebugDecisionHandler([HeatExchanger.key]) handler.handle(self.project.run(cleanup=False)) @@ -65,7 +65,7 @@ def test_case_2(self): 'B01_3_HeatExchanger_noPorts.ifc'} self.project = Project.create(self.test_dir.name, ifc_paths, plugin=PluginDummy, ) - self.project.sim_settings.weather_file_path = ( + self.project.sim_settings.weather_file_path_modelica = ( test_rsrc_path / 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') handler = DebugDecisionHandler([HeatExchanger.key, *(Pipe.key,) * 4]) @@ -86,7 +86,7 @@ def test_case_3(self): 'B01_4_HeatExchanger_noConnection.ifc'} self.project = Project.create(self.test_dir.name, ifc_paths, plugin=PluginDummy, ) - self.project.sim_settings.weather_file_path = ( + self.project.sim_settings.weather_file_path_modelica = ( test_rsrc_path / 'weather_files/DEU_NW_Aachen.105010_TMYx.epw') handler = DebugDecisionHandler([HeatExchanger.key]) handler.handle(self.project.run(cleanup=False)) @@ -105,7 +105,7 @@ def test_case_4(self): 'B01_5_HeatExchanger_mixConnection.ifc'} self.project = Project.create(self.test_dir.name, ifc_paths, plugin=PluginDummy, ) - self.project.sim_settings.weather_file_path = ( + self.project.sim_settings.weather_file_path_modelica = ( test_rsrc_path / 'weather_files/DEU_NW_Aachen.105010_TMYx.mos') handler = DebugDecisionHandler([HeatExchanger.key]) handler.handle(self.project.run(cleanup=False)) diff --git a/test/unit/tasks/hvac/test_export.py b/test/unit/tasks/hvac/test_export.py index 8ced68559d..0bbc983ff8 100644 --- a/test/unit/tasks/hvac/test_export.py +++ b/test/unit/tasks/hvac/test_export.py @@ -8,11 +8,11 @@ from bim2sim.elements.hvac_elements import HVACProduct, Pump from bim2sim.elements.mapping.units import ureg -from bim2sim.export.modelica import ModelicaElement, ModelicaParameter, \ - parse_to_modelica +from bim2sim.export.modelica import ModelicaElement, parse_to_modelica from bim2sim.kernel.decision.decisionhandler import DebugDecisionHandler from bim2sim.sim_settings import PlantSimSettings -from bim2sim.tasks.hvac import Export, LoadLibrariesStandardLibrary +from bim2sim.tasks.hvac import Export, LoadLibrariesStandardLibrary, \ + CreateModelicaModel from test.unit.elements.helper import SetupHelperHVAC from test.unit.tasks import TestTask @@ -37,6 +37,22 @@ def setUpClass(cls) -> None: lib_msl = LoadLibrariesStandardLibrary(cls.playground) cls.loaded_libs = lib_msl.run()[0] + # Instantiate modelica create task and set required values via mocks + cls.create_modelica_model_task = CreateModelicaModel(cls.playground) + cls.create_modelica_model_task.prj_name = 'TestStandardLibrary' + cls.create_modelica_model_task.paths = cls.test_task.paths + + def run_export(self, graph, answers=()): + """Method to reduce redundant code. + + We need an extra method here, as the normal run_task method just runs + one task but in this case we need to run CreateModelicaModel model task + first and then Export. We use Export as testTask. + """ + reads_from_modelica_creation = DebugDecisionHandler(answers).handle( + self.create_modelica_model_task.run(self.loaded_libs, graph)) + modelica_model = self.run_task(answers, reads_from_modelica_creation) + return modelica_model def run_parameter_test(self, graph: HvacGraph, modelica_model: list, parameters: List[Tuple[str, str]], @@ -64,7 +80,8 @@ def run_parameter_test(self, graph: HvacGraph, modelica_model: list, expected_strings = [f'{param[1]}={values[index]}' for index, param in enumerate(parameters)] for expected_string in expected_strings: - self.assertIn(expected_string, modelica_model[0].code()) + self.assertIn(expected_string, + modelica_model[0].render_modelica_code()) def test_to_modelica(self): element = HVACProduct() @@ -116,10 +133,10 @@ def test_missing_required_parameter(self): """ Test if an AssertionError is raised if a required parameter is not provided.""" graph, pipe = self.helper.get_simple_pipe() - answers = () with self.assertRaises(AssertionError): - DebugDecisionHandler(answers).handle( - self.test_task.run(self.loaded_libs, graph)) + self.run_export(graph) + # DebugDecisionHandler(answers).handle( + # self.test_task.run(self.loaded_libs, graph)) def test_check_function(self): """ Test if the check function for a parameter works. The exported @@ -129,19 +146,27 @@ def test_check_function(self): graph, pipe = self.helper.get_simple_pipe() pipe.diameter = -1 * ureg.meter answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + # reads = (self.loaded_libs, graph) + modelica_model = self.run_export(graph) + # modelica_model = self.run_task(answers, reads) + # (export_elements, connections, cons_heat_ports_conv, + # cons_heat_ports_rad) = DebugDecisionHandler( + # answers).handle( + # self.create_modelica_model_task.run(self.loaded_libs, graph)) + # modelica_model = self.export_task.run( + # export_elements, connections, cons_heat_ports_conv, + # cons_heat_ports_rad) self.assertIsNone( - modelica_model[0].modelica_elements[0].parameters['diameter'].value) + modelica_model[0].modelica_elements[0].parameters[ + 'diameter'].value) self.assertIsNotNone( modelica_model[0].modelica_elements[0].parameters['length'].value) def test_pipe_export(self): graph, pipe = self.helper.get_simple_pipe() pipe.diameter = 0.2 * ureg.meter - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) + # Test for expected and exported parameters parameters = [('diameter', 'diameter'), ('length', 'length')] expected_units = [ureg.m, ureg.m] @@ -151,8 +176,7 @@ def test_pipe_export(self): def test_valve_export(self): graph = self.helper.get_simple_valve() answers = (1 * ureg.kg / ureg.h,) - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph, answers) parameters = [('nominal_pressure_difference', 'dp_nominal'), ('nominal_mass_flow_rate', 'm_flow_nominal')] expected_units = [ureg.bar, ureg.kg / ureg.s] @@ -161,9 +185,7 @@ def test_valve_export(self): def test_junction_export(self): graph = self.helper.get_simple_junction() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) # Test for expected and exported parameters parameters = [('volume', 'V')] expected_units = [ureg.m ** 3] @@ -172,9 +194,7 @@ def test_junction_export(self): def test_storage_export(self): graph = self.helper.get_simple_storage() - answers = () - reads = (self.loaded_libs, graph) - modelica_model = self.run_task(answers, reads) + modelica_model = self.run_export(graph) # Test for expected and exported parameters parameters = [('volume', 'V')] expected_units = [ureg.m ** 3]