diff --git a/requirements/release-3rd-party.in b/requirements/release-3rd-party.in index aa26c3db..8d0ed2e5 100644 --- a/requirements/release-3rd-party.in +++ b/requirements/release-3rd-party.in @@ -20,4 +20,6 @@ typing_extensions packaging pydantic -pyyaml \ No newline at end of file +pyyaml + +xmlschema diff --git a/requirements/release-3rd-party.txt b/requirements/release-3rd-party.txt index dec312db..50448898 100644 --- a/requirements/release-3rd-party.txt +++ b/requirements/release-3rd-party.txt @@ -1,4 +1,4 @@ -# SHA1:9d82ad464f04b0b0b3a59deb0d928af94dc2cd0a +# SHA1:c2d4f7c8829e3fe3cf7f2140842b3c64470bba1a # # This file is autogenerated by pip-compile-multi # To update, run: @@ -37,6 +37,8 @@ debugpy==1.8.0 # via ipykernel decorator==5.1.1 # via ipython +elementpath==4.4.0 + # via xmlschema executing==2.0.1 # via stack-data fonttools==4.46.0 @@ -201,5 +203,7 @@ tzdata==2023.3 # via pandas wcwidth==0.2.12 # via prompt-toolkit +xmlschema==3.3.1 + # via -r requirements\release-3rd-party.in xyzservices==2023.10.1 # via bokeh diff --git a/setup.py b/setup.py index 69491d9a..a58b991f 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ def _getInstallRequirements(): "dev_template": _MASTER_VERSION_TEMPLATE, "dirty_template": f"{_MASTER_VERSION_TEMPLATE}.dirty", }, - author_email="martin.neugebauer@ost.ch", + author_email="damian.birchler@ost.ch", description="A GUI for Trnsys", long_description=long_description, long_description_content_type="text/markdown", @@ -70,6 +70,7 @@ def _getInstallRequirements(): "templates/generic/*.ddck", ], "trnsysGUI.components.plugin": ["data/*/*.svg", "data/*/*.yaml"], + "trnsysGUI.proforma": ["templates/ddck.jinja", "xmltmf.xsd"], }, data_files=_getDataFilePairs(), install_requires=_getInstallRequirements(), diff --git a/tests/trnsysGUI/proforma/__init__.py b/tests/trnsysGUI/proforma/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/trnsysGUI/proforma/data/actual/.gitignore b/tests/trnsysGUI/proforma/data/actual/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/tests/trnsysGUI/proforma/data/expected/Type137.ddck b/tests/trnsysGUI/proforma/data/expected/Type137.ddck new file mode 100644 index 00000000..8ce159b1 --- /dev/null +++ b/tests/trnsysGUI/proforma/data/expected/Type137.ddck @@ -0,0 +1,141 @@ +******************************* +** BEGIN Type137.ddck +******************************* + +*************************************************************************** +** Description: +** 4-Pipe Fan Coil: Heating and Cooling +*************************************************************************** + +*************************************************************************** +** Details: +** This component models a fan coil where the air is heated or cooled as it passes across coils containing hot and cold +** liquid flow streams. This model relies on user-provided external data files which contain the performance of the +** coils as a function of the entering air and fluid conditions. Refer to the sample data files which accompany this +** model for the format of these external files. +*************************************************************************** + +*********************************** +** inputs from hydraulic solver +*********************************** + +*********************************** +** outputs to hydraulic solver +*********************************** + +*********************************** +** outputs to other ddck +*********************************** + + +****************************************************************************************** +** outputs to energy balance in kWh and ABSOLUTE value +****************************************************************************************** + + +*********************************** +** Dependencies with other ddck +*********************************** + + +*********************************** +** Begin CONSTANTS +*********************************** + + +*********************************** +** Begin TYPE +*********************************** +UNIT 1 TYPE 137 +PARAMETERS 20 +2.0 ! 1: Humidity Mode [-] ([2,2]) +4.19 ! 2: Cooling Fluid Specific Heat [kJ/kg.K] ([0.0,+Inf]) +4.19 ! 3: Heating Fluid Specific Heat [kJ/kg.K] ([0.0,+Inf]) +943.89 ! 4: Rated Volumetric Air Flow Rate [l/s] ([0.,+Inf]) +564.0 ! 5: Rated Fan Power [kJ/hr] ([0.,+Inf]) +10.0 ! 6: Logical Unit - Cooling Performance [-] ([10,+Inf]) +7.0 ! 7: Number of Drybulb Temperatures - Cooling [-] ([1,+Inf]) +7.0 ! 8: Number of Wetbulb Temperatures - Cooling [-] ([1,+Inf]) +6.0 ! 9: Number of Air Flows - Cooling [-] ([1,+Inf]) +8.0 ! 10: Number of Liquid Temperatures - Cooling [-] ([1,+Inf]) +2.0 ! 11: Number of Liquid Flow Rates - Cooling [-] ([1,+Inf]) +11.0 ! 12: Logical Unit - Heating Performance [-] ([10,+Inf]) +7.0 ! 13: Number of Air Temperatures - Heating [-] ([1,+Inf]) +6.0 ! 14: Number of Air Flows - Heating [-] ([1,+Inf]) +11.0 ! 15: Number of Liquid Temperatures - Heating [-] ([1,+Inf]) +2.0 ! 16: Number of Liquid Flow Rates - Heating [-] ([1,+Inf]) +12.0 ! 17: Logical Unit - Fan Corrections [-] ([10,+Inf]) +11.0 ! 18: Number of Fan Speeds [-] ([1,+Inf]) +0.9 ! 19: Efficiency of Fan Motor [-] ([0.,1.]) +1.0 ! 20: Fraction of Fan Heat to Air [-] ([0.,1.]) +INPUTS 17 +0,0 ! 1: Cooling Fluid Inlet Temperature [C] ([-Inf,+Inf]) +0,0 ! 2: Cooling Fluid Flow Rate [kg/hr] ([0.0,+Inf]) +0,0 ! 3: Heating Fluid Inlet Temperature [C] ([-Inf,+Inf]) +0,0 ! 4: Heating Fluid Flow Rate [kg/hr] ([0.0,+Inf]) +0,0 ! 5: Return Air Temperature [C] ([-Inf,+Inf]) +0,0 ! 6: Return Air Humidity Ratio [-] ([0.,+Inf]) +0,0 ! 7: Return Air % Relative Humidity [% (base 100)] ([0,100]) +0,0 ! 8: Return Air Pressure [atm] ([0.0,+Inf]) +0,0 ! 9: Air-Side Pressure Rise: Fan [atm] ([0.0,+Inf]) +0,0 ! 10: Air-Side Pressure Drop: Coils [atm] ([0.0,+Inf]) +0,0 ! 11: Fresh Air Temperature [C] ([-Inf,+Inf]) +0,0 ! 12: Fresh Air Humidity Ratio [-] ([0.,+Inf]) +0,0 ! 13: Fresh Air % Relative Humidity [% (base 100)] ([0,100]) +0,0 ! 14: Heating Control Signal [-] ([0.,1.]) +0,0 ! 15: Cooling Control Signal [-] ([0.,1.]) +0,0 ! 16: Fan Control Signal [-] ([0.,1.]) +0,0 ! 17: Fraction of Outside Air [-] ([0.,1.]) +** initial values +10.0 ! 1: Cooling Fluid Inlet Temperature initial value +0.0 ! 2: Cooling Fluid Flow Rate initial value +10.0 ! 3: Heating Fluid Inlet Temperature initial value +0.0 ! 4: Heating Fluid Flow Rate initial value +20.0 ! 5: Return Air Temperature initial value +0.002 ! 6: Return Air Humidity Ratio initial value +50.0 ! 7: Return Air % Relative Humidity initial value +1.0 ! 8: Return Air Pressure initial value +0.0 ! 9: Air-Side Pressure Rise: Fan initial value +0.0 ! 10: Air-Side Pressure Drop: Coils initial value +20.0 ! 11: Fresh Air Temperature initial value +0.002 ! 12: Fresh Air Humidity Ratio initial value +50.0 ! 13: Fresh Air % Relative Humidity initial value +0.0 ! 14: Heating Control Signal initial value +0.0 ! 15: Cooling Control Signal initial value +0.0 ! 16: Fan Control Signal initial value +0.0 ! 17: Fraction of Outside Air initial value + +! EQUATIONS 18 +! XXX = [1, 1] ! Cooling Fluid Outlet Temperature [C] ([-Inf,+Inf]) +! XXX = [1, 2] ! Outlet Cooling Fluid Flow Rate [kg/hr] ([0.0,+Inf]) +! XXX = [1, 3] ! Heating Fluid Outlet Temperature [C] ([-Inf,+Inf]) +! XXX = [1, 4] ! Outlet Heating Fluid Flow Rate [kg/hr] ([0.0,+Inf]) +! XXX = [1, 5] ! Outlet Air Temperature [C] ([-Inf,+Inf]) +! XXX = [1, 6] ! Outlet Air Humidity Ratio [-] ([-Inf,+Inf]) +! XXX = [1, 7] ! Outlet Air % Relative Humidity [% (base 100)] ([-Inf,+Inf]) +! XXX = [1, 8] ! Outlet Air Flow Rate [kg/hr] ([-Inf,+Inf]) +! XXX = [1, 9] ! Outlet Air Pressure [atm] ([-Inf,+Inf]) +! XXX = [1, 10] ! Total Cooling Rate [kJ/hr] ([-Inf,+Inf]) +! XXX = [1, 11] ! Sensible Cooling Rate [kJ/hr] ([-Inf,+Inf]) +! XXX = [1, 12] ! Total Heating Rate [kJ/hr] ([-Inf,+Inf]) +! XXX = [1, 13] ! Fan Power [kJ/hr] ([-Inf,+Inf]) +! XXX = [1, 14] ! Fan Heat to Air Stream [kJ/hr] ([-Inf,+Inf]) +! XXX = [1, 15] ! Fan Heat to Ambient [kJ/hr] ([-Inf,+Inf]) +! XXX = [1, 16] ! Condensate Temperature [C] ([-Inf,+Inf]) +! XXX = [1, 17] ! Condensate Flow Rate [kg/hr] ([0.0,+Inf]) +! XXX = [1, 18] ! Conditioning Energy Rate [kJ/hr] ([-Inf,+Inf]) + +*********************************** +** Monthly printer +*********************************** + + +*********************************** +** Hourly printer +*********************************** + + +*********************************** +** Online Plotter +*********************************** + diff --git a/tests/trnsysGUI/proforma/data/expected/Type71-no-hydraulic-connections.ddck b/tests/trnsysGUI/proforma/data/expected/Type71-no-hydraulic-connections.ddck new file mode 100644 index 00000000..93c25e2a --- /dev/null +++ b/tests/trnsysGUI/proforma/data/expected/Type71-no-hydraulic-connections.ddck @@ -0,0 +1,115 @@ +******************************* +** BEGIN Type71-no-hydraulic-connections.ddck +******************************* + +*************************************************************************** +** Description: +** Solar Collector; Evacuated Tube +*************************************************************************** + +*************************************************************************** +** Details: +** Because the Solar Ratings and Certification Commission (SRCC) defines the efficiency of an evacuated tube collector +** bank using the same equations as those for a flat plat, the main difference (from a modeling point of view) between +** an evacuated tube collector and a flat plate collector is in the treatment of incidence angle modifiers (IAMs). Type +** 71 is therefore based on the Type 1 code with the major difference being that Type 71 reads a text file containing a +** list of transverse and longitudinal IAMs. This component models the thermal performance of a variety of an evacuated +** tube collector types using theory. The total collector array may consist of collectors connected in series and in +** parallel. The thermal performance of the total collector array is determined by the number of modules in series and +** the characteristics of each module. The user must provide results from standard tests of efficiency versus a ratio of +** fluid temperature minus ambient temperature to radiation (DT/IT). The fluid temperature may be an inlet, average, or +** outlet temperature. The model assumes that the efficiency vs. DT/IT curve can be modeled as a quadratic equation. +** (Changed from version 13 where efficiency vs. DT/IT was assumed linear.) Corrections are applied to the slope, +** intercept, and curvature parameters to account for identical collectors in series, and flow rates other than those at +** test conditions. The effects of off-normal solar incidence are modeled by the provision of a bi-axial incidence angle +** modifier data file. +*************************************************************************** + +*********************************** +** inputs from hydraulic solver +*********************************** + +*********************************** +** outputs to hydraulic solver +*********************************** + +*********************************** +** outputs to other ddck +*********************************** + + +****************************************************************************************** +** outputs to energy balance in kWh and ABSOLUTE value +****************************************************************************************** + + +*********************************** +** Dependencies with other ddck +*********************************** + + +*********************************** +** Begin CONSTANTS +*********************************** + + +*********************************** +** Begin TYPE +*********************************** +UNIT 1 TYPE 71 +PARAMETERS 11 +1.0 ! 1: Number in series [-] ([1,+Inf]) +2.0 ! 2: Collector area [m^2] ([0.0,+Inf]) +4.19 ! 3: Fluid specific heat [kJ/kg.K] ([0.0,+Inf]) +1.0 ! 4: Efficiency mode [-] ([1,3]) +50.0 ! 5: Flow rate at test conditions [kg/hr.m^2] ([0.0,+Inf]) +0.7 ! 6: Intercept efficiency [-] ([0.0,1.0]) +10.0 ! 7: Negative of first order efficiency coeficient [kJ/hr.m^2.K] ([0.0,+Inf]) +0.03 ! 8: Negative of second order efficiency coeficient [kJ/hr.m^2.K^2] ([0.0,+Inf]) +13.0 ! 9: Logical unit of file containing biaxial IAM data [-] ([10,100]) +7.0 ! 10: Number of longitudinal angles for which IAMs are provided [-] ([1,+Inf]) +7.0 ! 11: Number of transverse angles for which IAMs are provided [-] ([1,+Inf]) +INPUTS 10 +0,0 ! 1: Inlet temperature [C] ([-Inf,+Inf]) +0,0 ! 2: Inlet flowrate [kg/hr] ([0.0,+Inf]) +0,0 ! 3: Ambient temperature [C] ([-Inf,+Inf]) +0,0 ! 4: Incident radiation [kJ/hr.m^2] ([0.0,+Inf]) +0,0 ! 5: Incident diffuse radiation [kJ/hr.m^2] ([0.0,+Inf]) +0,0 ! 6: Solar incidence angle [degrees] ([-360,+360]) +0,0 ! 7: Solar zenith angle [degrees] ([-360,+360]) +0,0 ! 8: Solar azimuth angle [degrees] ([-360,+360]) +0,0 ! 9: Collector slope [degrees] ([-360,+360]) +0,0 ! 10: Collector azimuth [degrees] ([-360,+360]) +** initial values +20.0 ! 1: Inlet temperature initial value +100.0 ! 2: Inlet flowrate initial value +10.0 ! 3: Ambient temperature initial value +0.0 ! 4: Incident radiation initial value +0.0 ! 5: Incident diffuse radiation initial value +0.0 ! 6: Solar incidence angle initial value +0.0 ! 7: Solar zenith angle initial value +0.0 ! 8: Solar azimuth angle initial value +45.0 ! 9: Collector slope initial value +0.0 ! 10: Collector azimuth initial value + +! EQUATIONS 5 +! XXX = [1, 1] ! Outlet temperature [C] ([-Inf,+Inf]) +! XXX = [1, 2] ! Outlet flowrate [kg/hr] ([0.0,+Inf]) +! XXX = [1, 3] ! Useful energy gain [kJ/hr] ([0.0,+Inf]) +! XXX = [1, 4] ! Collector efficiency [-] ([-Inf,+Inf]) +! XXX = [1, 5] ! Incidence angle modifier - overall [-] ([-Inf,+Inf]) + +*********************************** +** Monthly printer +*********************************** + + +*********************************** +** Hourly printer +*********************************** + + +*********************************** +** Online Plotter +*********************************** + diff --git a/tests/trnsysGUI/proforma/data/expected/Type71.ddck b/tests/trnsysGUI/proforma/data/expected/Type71.ddck new file mode 100644 index 00000000..d6614385 --- /dev/null +++ b/tests/trnsysGUI/proforma/data/expected/Type71.ddck @@ -0,0 +1,121 @@ +******************************* +** BEGIN Type71.ddck +******************************* + +*************************************************************************** +** Description: +** Solar Collector; Evacuated Tube +*************************************************************************** + +*************************************************************************** +** Details: +** Because the Solar Ratings and Certification Commission (SRCC) defines the efficiency of an evacuated tube collector +** bank using the same equations as those for a flat plat, the main difference (from a modeling point of view) between +** an evacuated tube collector and a flat plate collector is in the treatment of incidence angle modifiers (IAMs). Type +** 71 is therefore based on the Type 1 code with the major difference being that Type 71 reads a text file containing a +** list of transverse and longitudinal IAMs. This component models the thermal performance of a variety of an evacuated +** tube collector types using theory. The total collector array may consist of collectors connected in series and in +** parallel. The thermal performance of the total collector array is determined by the number of modules in series and +** the characteristics of each module. The user must provide results from standard tests of efficiency versus a ratio of +** fluid temperature minus ambient temperature to radiation (DT/IT). The fluid temperature may be an inlet, average, or +** outlet temperature. The model assumes that the efficiency vs. DT/IT curve can be modeled as a quadratic equation. +** (Changed from version 13 where efficiency vs. DT/IT was assumed linear.) Corrections are applied to the slope, +** intercept, and curvature parameters to account for identical collectors in series, and flow rates other than those at +** test conditions. The effects of off-normal solar incidence are modeled by the provision of a bi-axial incidence angle +** modifier data file. +*************************************************************************** + +*********************************** +** inputs from hydraulic solver +*********************************** +EQUATIONS 3 +:CpIn = @cp(In) +:TIn = @temp(In) +:MIn = @mfr(In) + +*********************************** +** outputs to hydraulic solver +*********************************** +EQUATIONS 1 +@temp(Out) = :TOut + +*********************************** +** outputs to other ddck +*********************************** + + +****************************************************************************************** +** outputs to energy balance in kWh and ABSOLUTE value +****************************************************************************************** + + +*********************************** +** Dependencies with other ddck +*********************************** + + +*********************************** +** Begin CONSTANTS +*********************************** + + +*********************************** +** Begin TYPE +*********************************** +UNIT 1 TYPE 71 +PARAMETERS 11 +1.0 ! 1: Number in series [-] ([1,+Inf]) +2.0 ! 2: Collector area [m^2] ([0.0,+Inf]) +:CpIn ! 3: Fluid specific heat [kJ/kg.K] ([0.0,+Inf]) +1.0 ! 4: Efficiency mode [-] ([1,3]) +50.0 ! 5: Flow rate at test conditions [kg/hr.m^2] ([0.0,+Inf]) +0.7 ! 6: Intercept efficiency [-] ([0.0,1.0]) +10.0 ! 7: Negative of first order efficiency coeficient [kJ/hr.m^2.K] ([0.0,+Inf]) +0.03 ! 8: Negative of second order efficiency coeficient [kJ/hr.m^2.K^2] ([0.0,+Inf]) +13.0 ! 9: Logical unit of file containing biaxial IAM data [-] ([10,100]) +7.0 ! 10: Number of longitudinal angles for which IAMs are provided [-] ([1,+Inf]) +7.0 ! 11: Number of transverse angles for which IAMs are provided [-] ([1,+Inf]) +INPUTS 10 +:TIn ! 1: Inlet temperature [C] ([-Inf,+Inf]) +:MIn ! 2: Inlet flowrate [kg/hr] ([0.0,+Inf]) +0,0 ! 3: Ambient temperature [C] ([-Inf,+Inf]) +0,0 ! 4: Incident radiation [kJ/hr.m^2] ([0.0,+Inf]) +0,0 ! 5: Incident diffuse radiation [kJ/hr.m^2] ([0.0,+Inf]) +0,0 ! 6: Solar incidence angle [degrees] ([-360,+360]) +0,0 ! 7: Solar zenith angle [degrees] ([-360,+360]) +0,0 ! 8: Solar azimuth angle [degrees] ([-360,+360]) +0,0 ! 9: Collector slope [degrees] ([-360,+360]) +0,0 ! 10: Collector azimuth [degrees] ([-360,+360]) +** initial values +20.0 ! 1: Inlet temperature initial value +100.0 ! 2: Inlet flowrate initial value +10.0 ! 3: Ambient temperature initial value +0.0 ! 4: Incident radiation initial value +0.0 ! 5: Incident diffuse radiation initial value +0.0 ! 6: Solar incidence angle initial value +0.0 ! 7: Solar zenith angle initial value +0.0 ! 8: Solar azimuth angle initial value +45.0 ! 9: Collector slope initial value +0.0 ! 10: Collector azimuth initial value + +EQUATIONS 1 ! 5 +:TOut = [1, 1] ! Outlet temperature [C] ([-Inf,+Inf]) +! :XXX = [1, 2] ! Outlet flowrate [kg/hr] ([0.0,+Inf]) +! :XXX = [1, 3] ! Useful energy gain [kJ/hr] ([0.0,+Inf]) +! :XXX = [1, 4] ! Collector efficiency [-] ([-Inf,+Inf]) +! :XXX = [1, 5] ! Incidence angle modifier - overall [-] ([-Inf,+Inf]) + +*********************************** +** Monthly printer +*********************************** + + +*********************************** +** Hourly printer +*********************************** + + +*********************************** +** Online Plotter +*********************************** + diff --git a/tests/trnsysGUI/proforma/data/input/Type137.xmltmf b/tests/trnsysGUI/proforma/data/input/Type137.xmltmf new file mode 100644 index 00000000..def6ee75 --- /dev/null +++ b/tests/trnsysGUI/proforma/data/input/Type137.xmltmf @@ -0,0 +1,2 @@ + +4-Pipe Fan Coil: Heating and CoolingTim McDowellThermal Energy System SpecialistsTPMv18June 2015434.\Studio\Proformas\HVAC\Fan Coil\Type137.bmp1379999Fan Coil4-Pipe Fan Coil
This component models a fan coil where the air is heated or cooled as it passes across coils containing hot and cold liquid flow streams. This model relies on user-provided external data files which contain the performance of the coils as a function of the entering air and fluid conditions. Refer to the sample data files which accompany this model for the format of these external files.
Performance Map 4-Pipe Fan Coil4-Pipe Fan Coil1Humidity ModeparameterDimensionless-integer22[ ; ]2SNThis parameter indicates whether the inputs for absolute humidity ratio (this parameter = 1) or percent relative humidity (this parameter = 2) should be used to set the inlet air conditions.2Cooling Fluid Specific HeatparameterSpecific HeatkJ/kg.Kreal0.0+Inf[ ; ]4.190SNThe specific heat of the liquid stream flowing through the fan coil cooling coils.3Heating Fluid Specific HeatparameterSpecific HeatkJ/kg.Kreal0.0+Inf[ ; ]4.190SNThe specific heat of the liquid stream flowing through the fan coil heating coils.4Rated Volumetric Air Flow RateparameterVolumetric Flow Ratel/sreal0.+Inf[ ; ]943.89SNThe volumetric flow rate of air through the device at its rated conditions.5Rated Fan PowerparameterPowerkJ/hrreal0.+Inf[ ; ]564.0SNThe fan power draw at its rated conditions.6Logical Unit - Cooling PerformanceparameterDimensionless-integer10+Inf[ ; ]10SNThe logical unit which wil be assigned to the external data file containing the total and sensible cooling load ratios as a function of the entering liquid temperature, entering liquid flow rate, the entering air dry bulb temperature, the entering air wet bulb temperature, and air flow rate. Logical units must be unique integers in each TRNSYS simulation.7Number of Drybulb Temperatures - CoolingparameterDimensionless-integer1+Inf[ ; ]7SNThe number of air drybulb temperatures for which cooling coil performance data will be provided in the user-provided external data file.8Number of Wetbulb Temperatures - CoolingparameterDimensionless-integer1+Inf[ ; ]7SNThe number of air wetbulb temperatures for which cooling coil performance data will be provided in the user-provided external data file.9Number of Air Flows - CoolingparameterDimensionless-integer1+Inf[ ; ]6SNThe number of normalized air flow rates for which cooling coil performance data will be provided in the user-provided external data file.10Number of Liquid Temperatures - CoolingparameterDimensionless-integer1+Inf[ ; ]8SNThe number of liquid (water typically) temperatures for which cooling coil performance data will be provided in the user-provided external data file.11Number of Liquid Flow Rates - CoolingparameterDimensionless-integer1+Inf[ ; ]2SNThe number of normalized liquid flow rates for which cooling coil performance data will be provided in the user-provided external data file.12Logical Unit - Heating PerformanceparameterDimensionless-integer10+Inf[ ; ]11SNThe logical unit which wil be assigned to the external data file containing the heating performance data as a function of the air inlet temperature and flow rate and the liquid entering temperature and flow rate. Logical units must be unique integers in each TRNSYS simulation.13Number of Air Temperatures - HeatingparameterDimensionless-integer1+Inf[ ; ]7SNThe number of air drybulb temperatures for which heating coil performance data will be provided in the user-provided external data file.14Number of Air Flows - HeatingparameterDimensionless-integer1+Inf[ ; ]6SNThe number of normalized air flow rates for which heating coil performance data will be provided in the user-provided external data file.15Number of Liquid Temperatures - HeatingparameterDimensionless-integer1+Inf[ ; ]11SNThe number of liquid (water typically) temperatures for which heating coil performance data will be provided in the user-provided external data file.16Number of Liquid Flow Rates - HeatingparameterDimensionless-integer1+Inf[ ; ]2SNThe number of normalized liquid flow rates for which heating coil performance data will be provided in the user-provided external data file.17Logical Unit - Fan CorrectionsparameterDimensionless-integer10+Inf[ ; ]12SNThe logical unit which wil be assigned to the external data file containing the fraction of fan full-load power as a function of the normalized fan speed.18Cooling Fluid Inlet TemperatureinputTemperatureCreal-Inf+Inf[ ; ]10.0SNThe temperature of the liquid stream fluid flowing into the fan coil unit's cooling coils.19Cooling Fluid Flow RateinputFlow Ratekg/hrreal0.0+Inf[ ; ]0.0SNThe flow rate of the cooling liquid stream fluid flowing into the fan coil unit's cooling coils.20Heating Fluid Inlet TemperatureinputTemperatureCreal-Inf+Inf[ ; ]10.0SNThe temperature of the liquid stream fluid flowing into the fan coil unit's heating coils.21Heating Fluid Flow RateinputFlow Ratekg/hrreal0.0+Inf[ ; ]0.0SNThe flow rate of the heating liquid stream fluid flowing into the fan coil unit's heating coils.22Return Air TemperatureinputTemperatureCreal-Inf+Inf[ ; ]20.0SNThe drybulb temperature of the return air entering the fan coil. This return air gets mixed with a user-specified fraction of outside air.23Return Air Humidity RatioinputDimensionless-real0.+Inf[ ; ]0.002SNThe absolute humidity ratio of the return air entering the fan coil. This return air gets mixed with a user-specified fraction of outside air.24Return Air % Relative HumidityinputPercentage% (base 100)real0100[ ; ]50.0SNThe percent relative humidity of the return air entering the fan coil. This return air gets mixed with a user-specified fraction of outside air.25Return Air PressureinputPressureatmreal0.0+Inf[ ; ]1.0SNThe absolute pressure of the air streams entering the fan coil.26Air-Side Pressure Rise: FaninputPressureatmreal0.0+Inf[ ; ]0.0SNThe pressure rise of the air stream as it flows across the fan.27Cooling Fluid Outlet TemperatureoutputTemperatureCreal-Inf+Inf[ ; ]0SNThe temperature of the liquid stream exiting the fan coil unit's cooling coils.28Outlet Cooling Fluid Flow RateoutputFlow Ratekg/hrreal0.0+Inf[ ; ]0SNThe flow rate of the liquid stream exiting the fan coil unit's cooling coils.29Heating Fluid Outlet TemperatureoutputTemperatureCreal-Inf+Inf[ ; ]0SNThe temperature of the liquid stream exiting the fan coil unit's heating coils.30Outlet Heating Fluid Flow RateoutputFlow Ratekg/hrreal0.0+Inf[ ; ]0SNThe flow rate of the liquid stream exiting the fan coil unit's heating coils.31Outlet Air TemperatureoutputTemperatureCreal-Inf+Inf[ ; ]0SNThe drybulb temperature of the air exiting the fan coil.32Outlet Air Humidity RatiooutputDimensionless-real-Inf+Inf[ ; ]0SNThe absolute humidity ratio of the air exiting the fan coil.33Outlet Air % Relative HumidityoutputPercentage% (base 100)real-Inf+Inf[ ; ]0SNThe percent relative humidity of the air exiting the fan coil.34Outlet Air Flow RateoutputFlow Ratekg/hrreal-Inf+Inf[ ; ]0SNThe flow rate of dry air exiting the fan coil.35Outlet Air PressureoutputPressureatmreal-Inf+Inf[ ; ]0SNThe absolute pressure of the air exiting the fan coil.36Total Cooling RateoutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which energy is removed from the air stream (sensible plus latent) across the cooling coil.37Sensible Cooling RateoutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which sensible energy is removed from the air stream across the cooling coil.38Total Heating RateoutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which energy is added to the air stream across the heating coil.39Fan PoweroutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which the fan consumes energy.40Fan Heat to Air StreamoutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which energy is added to the air stream by the fan.41Fan Heat to AmbientoutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which energy is rejected to the ambient by the fan.42Condensate TemperatureoutputTemperatureCreal-Inf+Inf[ ; ]20.0SNThe temperature of the condensed water from the air stream leaving the fan coil.43Condensate Flow RateoutputFlow Ratekg/hrreal0.0+Inf[ ; ]0SNThe rate at which condensed water from the air stream exits the fan coil.44Number of Fan SpeedsparameterDimensionless-integer1+Inf[ ; ]11SNThe number of normalized fan speeds for which fan performance data will be provided in the user-provided external data file.45Efficiency of Fan MotorparameterDimensionless-real0.1.[ ; ]0.9SNThe efficiency of the fan motor.46Fraction of Fan Heat to AirparameterDimensionless-real0.1.[ ; ]1.SNThe fraction of the fan power/heat that ends up in the air stream. Values are typically zero for fans motors mounted outside of the air stream and 1 for fan motors mounted within the air stream.47Air-Side Pressure Drop: CoilsinputPressureatmreal0.0+Inf[ ; ]0.0SNThe pressure drop of the air stream as it passes across the coils.48Fresh Air TemperatureinputTemperatureCreal-Inf+Inf[ ; ]20.0SNThe drybulb temperature of the ambient air entering the fan coil for mixing with the return air.49Fresh Air Humidity RatioinputDimensionless-real0.+Inf[ ; ]0.002SNThe absolute humidity ratio of the ambient air entering the fan coil for mixing with the return air.50Fresh Air % Relative HumidityinputPercentage% (base 100)real0100[ ; ]50.0SNThe percent relative humidity of the ambient air entering the fan coil for mixing with the return air.51Heating Control SignalinputDimensionless-real0.1.[ ; ]0.SNThe control signal for heating operation: 0 = Off and 1 = On.52Cooling Control SignalinputDimensionless-real0.1.[ ; ]0.SNThe control signal for cooling operation: 0 = Off and 1 = On.53Fan Control SignalinputDimensionless-real0.1.[ ; ]0.SNThe control signal for fan operation: 0 = Off, 1 = Full On, Values between 0 and 1 set the fraction of rated fan speed.54Fraction of Outside AirinputDimensionless-real0.1.[ ; ]0.SNThe control signal for outside air mixing: 0 = No outside air and 100% return air and 1 = 100% outside air and no return air. Values between 0 and 1 set the fraction of outside air.55Conditioning Energy RateoutputPowerkJ/hrreal-Inf+Inf[ ; ]0SNThe rate at which energy is transferred to the air stream by the coils; positive implies energy added to the air stream (heating). This term does not include energy added by the fan or energy associated with the condensate draining from the unit but is strictly a measure of the coil heat transfer.Which external file contains the cooling performance data?.\Examples\Sample Catalog Files\FanCoil_Cooling.dat.\Tess Models\SampleCatalogData\4-Pipe Performance Map Fan Coil\Normalized_FanCoil_Cooling.datLogical Unit - Cooling PerformancenoWhich external file contains the heating performance data?.\Examples\Sample Catalog Files\FanCoil_Heating.dat.\Tess Models\SampleCatalogData\4-Pipe Performance Map Fan Coil\Normalized_FanCoil_Heating.datLogical Unit - Heating PerformancenoWhich external file contains the fan performance data?.\Examples\Sample Catalog Files\FC_FanLawPerformance.dat.\Tess Models\SampleCatalogData\4-Pipe Performance Map Fan Coil\FanLawPerformance.datLogical Unit - Fan Correctionsno.\SourceCode\Types\Type137.f90
diff --git a/tests/trnsysGUI/proforma/data/input/Type71-missing-closing-bracket.xmltmf b/tests/trnsysGUI/proforma/data/input/Type71-missing-closing-bracket.xmltmf new file mode 100644 index 00000000..d9615781 --- /dev/null +++ b/tests/trnsysGUI/proforma/data/input/Type71-missing-closing-bracket.xmltmf @@ -0,0 +1,484 @@ + + + Solar Collector; Evacuated Tube + Contributors are listed in manuals + Solar Energy Laboratory, University of Wisconsin - Madison + TESS + TRNSYS v7.5 + May 2011 + 1 + 16 + C:\Users\damian.birchler\dev\pytrnsys\issues\gui\proforma-to-ddck\Type71.bmp + 71 + 20 + + + + + 1 + Inlet temperature + input + Temperature + C + real + -Inf + +Inf + [ ; ] + 20.0 + SN + The temperature of the fluid entering the the solar collector. + + + 2 + Inlet flowrate + input + Flow Rate + kg/hr + real + 0.0 + +Inf + [ ; ] + 100.0 + SN + The flow rate of the fluid entering the solar collector. + + + 3 + Ambient temperature + input + Temperature + C + real + -Inf + +Inf + [ ; ] + 10.0 + SN + The temperature of the environment in which the solar collector is located. + This temperature will be used for loss calculations. + + + 4 + Incident radiation + input + Flux + kJ/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 0. + SN + The total (beam + diffuse) radiation incident on the plane of the solar + collector per unit area. + This input is commonly hooked up to the TYPE 16 "total radiation on surface 1" + output. + + + 5 + Incident diffuse radiation + input + Flux + kJ/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 0.0 + SN + The incident diffuse solar radiation in the plane of the collector, per unit + area + + + 6 + Solar incidence angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + Incidence angle of beam radiation on the collector's surface + + + 7 + Solar zenith angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The solar zenith angle is the angle between the vertical and the line of + sight of the sun + + + 8 + Solar azimuth angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The solar azimuth angle is the angle between the local meridian and the + projection + of the line of sight of the sun onto the horizontal plane + + + 9 + Collector slope + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 45 + SN + The slope of the collector is the angle between the collector surface and + the horizontal + 0= horizontal, 90= vertical + The angle is positive when facing towards the collector surface azimuth + As a general rule, the performance of the collector is somewhat optimiszed when the + sollector slope is set to the latitude + + + 10 + Collector azimuth + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The collector surface azimuth is the angle between the local meridian and + the projection of the normal to the surface onto the horizontal plane + 0 = facing the equator + 90 = facing West + 180 = facing North in northern hemisphere, South in Southern hemisphere + 270 = facing East + + + 11 + Outlet temperature + output + Temperature + C + real + -Inf + +Inf + [ ; ] + 0 + SN + The temperature of the fluid exiting the solar collector array. + + + 12 + Outlet flowrate + output + Flow Rate + kg/hr + real + 0.0 + +Inf + [ ; ] + 0 + SN + The flowrate of the fluid exiting the solar collecor array. In this model: + mdot,in = mdot,out + + + 13 + Useful energy gain + output + Power + kJ/hr + real + 0.0 + +Inf + [ ; ] + 0 + SN + The rate of useful energy gain by the solar collector fluid: + Qu = mdot * Cp * (Tout - Tin) + + + 14 + Number in series + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 1 + SN + The solar collector model can simulate an array of identical solar + collectors hooked up in series. This parameter is used to specify how many + collectors are hooked up in a series arrangement where the output of the first + collector is the inlet to the second collector. NOTE: increasing this value does not + change the array area. Total array area is set by parameter 2 + + + 15 + Collector area + parameter + Area + m^2 + real + 0.0 + +Inf + [ ; ] + 2.0 + SN + The total area of the solar collector array consistent with the supplied + efficiency parameters (typically gross area and not net area). + + + 16 + Fluid specific heat + parameter + Specific Heat + kJ/kg.K + real + 0.0 + +Inf + [ ; ] + 4.190 + SN + The specific heat of the fluid flowing through the solar collector array. + + + 17 + Efficiency mode + parameter + Dimensionless + - + integer + 1 + 3 + [ ; ] + 1 + SN + The collector efficiency equation can be written as a function of the inlet, + average or outlet temperature. + Specify 1 if the collector efficiency parameters are given as a function of the + inlet temperature + Specify 2 for a function of the collector average temperature + Specify 3 for a function of the collector outlet temperature + + + 18 + Flow rate at test conditions + parameter + Flow/area + kg/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 50.0 + SN + Collector Flow rate per unit area for efficiency test conditions + + + 19 + Intercept efficiency + parameter + Dimensionless + - + real + 0.0 + 1.0 + [ ; ] + 0.7 + SN + This parameter is the y intercept of the collector efficiency curve versus + the temperature difference / radiation ratio + In equation form, this parameter is a0 in the following eq: + Eff = a0 - a1 * (Tc-Tamb)/Rad. - a2 * (Tc-Tamb)^2 / Rad. + where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 20 + Negative of first order efficiency coeficient + parameter + Heat Transfer Coeff. + kJ/hr.m^2.K + real + 0.0 + +Inf + [ ; ] + 10 + SN + This parameter is the slope of the collector efficiency curve versus the + temperature difference / radiation ratio + In equation form, this parameter is a1 in the following eq: + Eff. = a0 - a1 * (Tc-Tamb)/Rad. - a2 * (Tc-Tamb)^2 /Rad. + Where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 21 + Negative of second order efficiency coeficient + parameter + Temp. Dependent Loss Coeff. + kJ/hr.m^2.K^2 + real + 0.0 + +Inf + [ ; ] + 0.03 + SN + This parameter is the curvature of the efficiency curve versus the + temperature difference / radiation ratio + In equation form, this parameter is a2 in the following eq: + Eff. = a0 - a1 * (Tc-Tamb) /Rad.- a2 * (Tc-Tamb)^2/Rad + where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 22 + Logical unit of file containing biaxial IAM data + parameter + Dimensionless + - + integer + 10 + 100 + [ ; ] + 13 + SN + FORTRAN Logical unit for file containing biaxial IAM data Make sure that + each logical unit specified in an assembly is unique + + + 23 + Number of longitudinal angles for which IAMs are provided + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 7 + SN + Number of data points for the IAM (longitudinal direction) + + + 24 + Number of transverse angles for which IAMs are provided + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 7 + SN + Number of data points for the IAM (transverse direction) + + + 25 + Collector efficiency + output + dimensionless + - + real + -Inf + +Inf + [ ; ] + 0 + SN + + + 26 + Incidence angle modifier - overall + output + dimensionless + - + real + -Inf + +Inf + [ ; ] + 0 + SN + + + + + + + + 1 + + + + + 2 + + + + 16 + + + + + + 11 + + + + + + + + + What file contains the 2D IAM data? + .\Examples\Data Files\Type71-EvacuatedTubeSolarCollector-IAMData.dat + + .\Examples\Data Files\Type71-EvacuatedTubeSolarCollector-IAMData.dat + + Logical unit of file containing biaxial IAM data + no + + + df /c + .\SourceCode\Types\Type71.f90 + \ No newline at end of file diff --git a/tests/trnsysGUI/proforma/data/input/Type71-no-hydraulic-connections.xmltmf b/tests/trnsysGUI/proforma/data/input/Type71-no-hydraulic-connections.xmltmf new file mode 100644 index 00000000..d8b1b4f5 --- /dev/null +++ b/tests/trnsysGUI/proforma/data/input/Type71-no-hydraulic-connections.xmltmf @@ -0,0 +1,458 @@ + + + Solar Collector; Evacuated Tube + Contributors are listed in manuals + Solar Energy Laboratory, University of Wisconsin - Madison + TESS + TRNSYS v7.5 + May 2011 + 1 + 16 + C:\Users\damian.birchler\dev\pytrnsys\issues\gui\proforma-to-ddck\Type71.bmp + 71 + 20 + +
Because the Solar Ratings and Certification Commission (SRCC) defines the efficiency of + an evacuated tube collector bank using the same equations as those for a flat plat, the main + difference (from a modeling point of view) between an evacuated tube collector and a flat + plate collector is in the treatment of incidence angle modifiers (IAMs). Type 71 is + therefore based on the Type 1 code with the major difference being that Type 71 reads a text + file containing a list of transverse and longitudinal IAMs. This component models the + thermal performance of a variety of an evacuated tube collector types using theory. The + total collector array may consist of collectors connected in series and in parallel. The + thermal performance of the total collector array is determined by the number of modules in + series and the characteristics of each module. The user must provide results from standard + tests of efficiency versus a ratio of fluid temperature minus ambient temperature to + radiation (DT/IT). The fluid temperature may be an inlet, average, or outlet temperature. + The model assumes that the efficiency vs. DT/IT curve can be modeled as a quadratic + equation. (Changed from version 13 where efficiency vs. DT/IT was assumed linear.) + Corrections are applied to the slope, intercept, and curvature parameters to account for + identical collectors in series, and flow rates other than those at test conditions. + + The effects of off-normal solar incidence are modeled by the provision of a bi-axial + incidence angle modifier data file.
+ + + 1 + Inlet temperature + input + Temperature + C + real + -Inf + +Inf + [ ; ] + 20.0 + SN + The temperature of the fluid entering the the solar collector. + + + 2 + Inlet flowrate + input + Flow Rate + kg/hr + real + 0.0 + +Inf + [ ; ] + 100.0 + SN + The flow rate of the fluid entering the solar collector. + + + 3 + Ambient temperature + input + Temperature + C + real + -Inf + +Inf + [ ; ] + 10.0 + SN + The temperature of the environment in which the solar collector is located. + This temperature will be used for loss calculations. + + + 4 + Incident radiation + input + Flux + kJ/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 0. + SN + The total (beam + diffuse) radiation incident on the plane of the solar + collector per unit area. + This input is commonly hooked up to the TYPE 16 "total radiation on surface 1" + output. + + + 5 + Incident diffuse radiation + input + Flux + kJ/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 0.0 + SN + The incident diffuse solar radiation in the plane of the collector, per unit + area + + + 6 + Solar incidence angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + Incidence angle of beam radiation on the collector's surface + + + 7 + Solar zenith angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The solar zenith angle is the angle between the vertical and the line of + sight of the sun + + + 8 + Solar azimuth angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The solar azimuth angle is the angle between the local meridian and the + projection + of the line of sight of the sun onto the horizontal plane + + + 9 + Collector slope + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 45 + SN + The slope of the collector is the angle between the collector surface and + the horizontal + 0= horizontal, 90= vertical + The angle is positive when facing towards the collector surface azimuth + As a general rule, the performance of the collector is somewhat optimiszed when the + sollector slope is set to the latitude + + + 10 + Collector azimuth + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The collector surface azimuth is the angle between the local meridian and + the projection of the normal to the surface onto the horizontal plane + 0 = facing the equator + 90 = facing West + 180 = facing North in northern hemisphere, South in Southern hemisphere + 270 = facing East + + + 11 + Outlet temperature + output + Temperature + C + real + -Inf + +Inf + [ ; ] + 0 + SN + The temperature of the fluid exiting the solar collector array. + + + 12 + Outlet flowrate + output + Flow Rate + kg/hr + real + 0.0 + +Inf + [ ; ] + 0 + SN + The flowrate of the fluid exiting the solar collecor array. In this model: + mdot,in = mdot,out + + + 13 + Useful energy gain + output + Power + kJ/hr + real + 0.0 + +Inf + [ ; ] + 0 + SN + The rate of useful energy gain by the solar collector fluid: + Qu = mdot * Cp * (Tout - Tin) + + + 14 + Number in series + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 1 + SN + The solar collector model can simulate an array of identical solar + collectors hooked up in series. This parameter is used to specify how many + collectors are hooked up in a series arrangement where the output of the first + collector is the inlet to the second collector. NOTE: increasing this value does not + change the array area. Total array area is set by parameter 2 + + + 15 + Collector area + parameter + Area + m^2 + real + 0.0 + +Inf + [ ; ] + 2.0 + SN + The total area of the solar collector array consistent with the supplied + efficiency parameters (typically gross area and not net area). + + + 16 + Fluid specific heat + parameter + Specific Heat + kJ/kg.K + real + 0.0 + +Inf + [ ; ] + 4.190 + SN + The specific heat of the fluid flowing through the solar collector array. + + + 17 + Efficiency mode + parameter + Dimensionless + - + integer + 1 + 3 + [ ; ] + 1 + SN + The collector efficiency equation can be written as a function of the inlet, + average or outlet temperature. + Specify 1 if the collector efficiency parameters are given as a function of the + inlet temperature + Specify 2 for a function of the collector average temperature + Specify 3 for a function of the collector outlet temperature + + + 18 + Flow rate at test conditions + parameter + Flow/area + kg/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 50.0 + SN + Collector Flow rate per unit area for efficiency test conditions + + + 19 + Intercept efficiency + parameter + Dimensionless + - + real + 0.0 + 1.0 + [ ; ] + 0.7 + SN + This parameter is the y intercept of the collector efficiency curve versus + the temperature difference / radiation ratio + In equation form, this parameter is a0 in the following eq: + Eff = a0 - a1 * (Tc-Tamb)/Rad. - a2 * (Tc-Tamb)^2 / Rad. + where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 20 + Negative of first order efficiency coeficient + parameter + Heat Transfer Coeff. + kJ/hr.m^2.K + real + 0.0 + +Inf + [ ; ] + 10 + SN + This parameter is the slope of the collector efficiency curve versus the + temperature difference / radiation ratio + In equation form, this parameter is a1 in the following eq: + Eff. = a0 - a1 * (Tc-Tamb)/Rad. - a2 * (Tc-Tamb)^2 /Rad. + Where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 21 + Negative of second order efficiency coeficient + parameter + Temp. Dependent Loss Coeff. + kJ/hr.m^2.K^2 + real + 0.0 + +Inf + [ ; ] + 0.03 + SN + This parameter is the curvature of the efficiency curve versus the + temperature difference / radiation ratio + In equation form, this parameter is a2 in the following eq: + Eff. = a0 - a1 * (Tc-Tamb) /Rad.- a2 * (Tc-Tamb)^2/Rad + where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 22 + Logical unit of file containing biaxial IAM data + parameter + Dimensionless + - + integer + 10 + 100 + [ ; ] + 13 + SN + FORTRAN Logical unit for file containing biaxial IAM data Make sure that + each logical unit specified in an assembly is unique + + + 23 + Number of longitudinal angles for which IAMs are provided + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 7 + SN + Number of data points for the IAM (longitudinal direction) + + + 24 + Number of transverse angles for which IAMs are provided + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 7 + SN + Number of data points for the IAM (transverse direction) + + + 25 + Collector efficiency + output + dimensionless + - + real + -Inf + +Inf + [ ; ] + 0 + SN + + + 26 + Incidence angle modifier - overall + output + dimensionless + - + real + -Inf + +Inf + [ ; ] + 0 + SN + + + + + + What file contains the 2D IAM data? + .\Examples\Data Files\Type71-EvacuatedTubeSolarCollector-IAMData.dat + + .\Examples\Data Files\Type71-EvacuatedTubeSolarCollector-IAMData.dat + + Logical unit of file containing biaxial IAM data + no + + + df /c + .\SourceCode\Types\Type71.f90 +
\ No newline at end of file diff --git a/tests/trnsysGUI/proforma/data/input/Type71.xmltmf b/tests/trnsysGUI/proforma/data/input/Type71.xmltmf new file mode 100644 index 00000000..b19ddf56 --- /dev/null +++ b/tests/trnsysGUI/proforma/data/input/Type71.xmltmf @@ -0,0 +1,484 @@ + + + Solar Collector; Evacuated Tube + Contributors are listed in manuals + Solar Energy Laboratory, University of Wisconsin - Madison + TESS + TRNSYS v7.5 + May 2011 + 1 + 16 + C:\Users\damian.birchler\dev\pytrnsys\issues\gui\proforma-to-ddck\Type71.bmp + 71 + 20 + +
Because the Solar Ratings and Certification Commission (SRCC) defines the efficiency of + an evacuated tube collector bank using the same equations as those for a flat plat, the main + difference (from a modeling point of view) between an evacuated tube collector and a flat + plate collector is in the treatment of incidence angle modifiers (IAMs). Type 71 is + therefore based on the Type 1 code with the major difference being that Type 71 reads a text + file containing a list of transverse and longitudinal IAMs. This component models the + thermal performance of a variety of an evacuated tube collector types using theory. The + total collector array may consist of collectors connected in series and in parallel. The + thermal performance of the total collector array is determined by the number of modules in + series and the characteristics of each module. The user must provide results from standard + tests of efficiency versus a ratio of fluid temperature minus ambient temperature to + radiation (DT/IT). The fluid temperature may be an inlet, average, or outlet temperature. + The model assumes that the efficiency vs. DT/IT curve can be modeled as a quadratic + equation. (Changed from version 13 where efficiency vs. DT/IT was assumed linear.) + Corrections are applied to the slope, intercept, and curvature parameters to account for + identical collectors in series, and flow rates other than those at test conditions. + + The effects of off-normal solar incidence are modeled by the provision of a bi-axial + incidence angle modifier data file.
+ + + 1 + Inlet temperature + input + Temperature + C + real + -Inf + +Inf + [ ; ] + 20.0 + SN + The temperature of the fluid entering the the solar collector. + + + 2 + Inlet flowrate + input + Flow Rate + kg/hr + real + 0.0 + +Inf + [ ; ] + 100.0 + SN + The flow rate of the fluid entering the solar collector. + + + 3 + Ambient temperature + input + Temperature + C + real + -Inf + +Inf + [ ; ] + 10.0 + SN + The temperature of the environment in which the solar collector is located. + This temperature will be used for loss calculations. + + + 4 + Incident radiation + input + Flux + kJ/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 0. + SN + The total (beam + diffuse) radiation incident on the plane of the solar + collector per unit area. + This input is commonly hooked up to the TYPE 16 "total radiation on surface 1" + output. + + + 5 + Incident diffuse radiation + input + Flux + kJ/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 0.0 + SN + The incident diffuse solar radiation in the plane of the collector, per unit + area + + + 6 + Solar incidence angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + Incidence angle of beam radiation on the collector's surface + + + 7 + Solar zenith angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The solar zenith angle is the angle between the vertical and the line of + sight of the sun + + + 8 + Solar azimuth angle + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The solar azimuth angle is the angle between the local meridian and the + projection + of the line of sight of the sun onto the horizontal plane + + + 9 + Collector slope + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 45 + SN + The slope of the collector is the angle between the collector surface and + the horizontal + 0= horizontal, 90= vertical + The angle is positive when facing towards the collector surface azimuth + As a general rule, the performance of the collector is somewhat optimiszed when the + sollector slope is set to the latitude + + + 10 + Collector azimuth + input + Direction (Angle) + degrees + real + -360 + +360 + [ ; ] + 0.0 + SN + The collector surface azimuth is the angle between the local meridian and + the projection of the normal to the surface onto the horizontal plane + 0 = facing the equator + 90 = facing West + 180 = facing North in northern hemisphere, South in Southern hemisphere + 270 = facing East + + + 11 + Outlet temperature + output + Temperature + C + real + -Inf + +Inf + [ ; ] + 0 + SN + The temperature of the fluid exiting the solar collector array. + + + 12 + Outlet flowrate + output + Flow Rate + kg/hr + real + 0.0 + +Inf + [ ; ] + 0 + SN + The flowrate of the fluid exiting the solar collecor array. In this model: + mdot,in = mdot,out + + + 13 + Useful energy gain + output + Power + kJ/hr + real + 0.0 + +Inf + [ ; ] + 0 + SN + The rate of useful energy gain by the solar collector fluid: + Qu = mdot * Cp * (Tout - Tin) + + + 14 + Number in series + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 1 + SN + The solar collector model can simulate an array of identical solar + collectors hooked up in series. This parameter is used to specify how many + collectors are hooked up in a series arrangement where the output of the first + collector is the inlet to the second collector. NOTE: increasing this value does not + change the array area. Total array area is set by parameter 2 + + + 15 + Collector area + parameter + Area + m^2 + real + 0.0 + +Inf + [ ; ] + 2.0 + SN + The total area of the solar collector array consistent with the supplied + efficiency parameters (typically gross area and not net area). + + + 16 + Fluid specific heat + parameter + Specific Heat + kJ/kg.K + real + 0.0 + +Inf + [ ; ] + 4.190 + SN + The specific heat of the fluid flowing through the solar collector array. + + + 17 + Efficiency mode + parameter + Dimensionless + - + integer + 1 + 3 + [ ; ] + 1 + SN + The collector efficiency equation can be written as a function of the inlet, + average or outlet temperature. + Specify 1 if the collector efficiency parameters are given as a function of the + inlet temperature + Specify 2 for a function of the collector average temperature + Specify 3 for a function of the collector outlet temperature + + + 18 + Flow rate at test conditions + parameter + Flow/area + kg/hr.m^2 + real + 0.0 + +Inf + [ ; ] + 50.0 + SN + Collector Flow rate per unit area for efficiency test conditions + + + 19 + Intercept efficiency + parameter + Dimensionless + - + real + 0.0 + 1.0 + [ ; ] + 0.7 + SN + This parameter is the y intercept of the collector efficiency curve versus + the temperature difference / radiation ratio + In equation form, this parameter is a0 in the following eq: + Eff = a0 - a1 * (Tc-Tamb)/Rad. - a2 * (Tc-Tamb)^2 / Rad. + where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 20 + Negative of first order efficiency coeficient + parameter + Heat Transfer Coeff. + kJ/hr.m^2.K + real + 0.0 + +Inf + [ ; ] + 10 + SN + This parameter is the slope of the collector efficiency curve versus the + temperature difference / radiation ratio + In equation form, this parameter is a1 in the following eq: + Eff. = a0 - a1 * (Tc-Tamb)/Rad. - a2 * (Tc-Tamb)^2 /Rad. + Where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 21 + Negative of second order efficiency coeficient + parameter + Temp. Dependent Loss Coeff. + kJ/hr.m^2.K^2 + real + 0.0 + +Inf + [ ; ] + 0.03 + SN + This parameter is the curvature of the efficiency curve versus the + temperature difference / radiation ratio + In equation form, this parameter is a2 in the following eq: + Eff. = a0 - a1 * (Tc-Tamb) /Rad.- a2 * (Tc-Tamb)^2/Rad + where Tc is the collector inlet, average or outlet temperature according to + parameter 4 + + + 22 + Logical unit of file containing biaxial IAM data + parameter + Dimensionless + - + integer + 10 + 100 + [ ; ] + 13 + SN + FORTRAN Logical unit for file containing biaxial IAM data Make sure that + each logical unit specified in an assembly is unique + + + 23 + Number of longitudinal angles for which IAMs are provided + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 7 + SN + Number of data points for the IAM (longitudinal direction) + + + 24 + Number of transverse angles for which IAMs are provided + parameter + Dimensionless + - + integer + 1 + +Inf + [ ; ] + 7 + SN + Number of data points for the IAM (transverse direction) + + + 25 + Collector efficiency + output + dimensionless + - + real + -Inf + +Inf + [ ; ] + 0 + SN + + + 26 + Incidence angle modifier - overall + output + dimensionless + - + real + -Inf + +Inf + [ ; ] + 0 + SN + + + + + + + + 1 + + + + + 2 + + + + 16 + + + + + + 11 + + + + + + + + + What file contains the 2D IAM data? + .\Examples\Data Files\Type71-EvacuatedTubeSolarCollector-IAMData.dat + + .\Examples\Data Files\Type71-EvacuatedTubeSolarCollector-IAMData.dat + + Logical unit of file containing biaxial IAM data + no + + + df /c + .\SourceCode\Types\Type71.f90 +
\ No newline at end of file diff --git a/tests/trnsysGUI/proforma/testConvertXmlTmfToDdck.py b/tests/trnsysGUI/proforma/testConvertXmlTmfToDdck.py new file mode 100644 index 00000000..c4d3d572 --- /dev/null +++ b/tests/trnsysGUI/proforma/testConvertXmlTmfToDdck.py @@ -0,0 +1,96 @@ +import dataclasses as _dc +import pathlib as _pl +import pkgutil as _pu +import pprint as _pp +import typing as _tp + +import pytest as _pt +import xmlschema as _xml + +import pytrnsys.ddck.replaceTokens.defaultVisibility as _dv +import pytrnsys.utils.result as _res +import trnsysGUI.proforma.convertXmlTmfToDdck as _pc +import trnsysGUI.proforma.dialogs.editHydraulicConnectionsDialog as _ehcd + +_DATA_DIR_PATH = _pl.Path(__file__).parent / "data" +_INPUT_DIR_PATH = _DATA_DIR_PATH / "input" + + +def testValidateXmlTmf() -> None: + schema = _getSchema() + + xmlFilePath = _INPUT_DIR_PATH / "Type71.xmltmf" + schema.validate(xmlFilePath) + + +def testDecodeXmlTmf() -> None: + schema = _getSchema() + + xmlFilePath = _INPUT_DIR_PATH / "Type71.xmltmf" + deserializedData = schema.to_dict(xmlFilePath) + _pp.pprint(deserializedData, indent=4) + + +def _getSchema() -> _xml.XMLSchema11: + data = _pu.get_data(_pc.__name__, "xmltmf.xsd") + assert data + string = data.decode("UTF8") + schema = _xml.XMLSchema11(string) + return schema + + +@_dc.dataclass +class TestCase: + inputFilePath: _pl.Path + actualOutputFilePath: _pl.Path + expectedOutputFilePath: _pl.Path + + @property + def fileStem(self) -> str: + return self.inputFilePath.stem + + @staticmethod + def createForStem(fileNameStem: str) -> "TestCase": + inputFilePath = (_INPUT_DIR_PATH / fileNameStem).with_suffix(".xmltmf") + actualOutputFilePath = (_DATA_DIR_PATH / "actual" / fileNameStem).with_suffix(".ddck") + expectedOutputFilePath = (_DATA_DIR_PATH / "expected" / fileNameStem).with_suffix(".ddck") + testCase = TestCase(inputFilePath, actualOutputFilePath, expectedOutputFilePath) + return testCase + + +def _getTestCases() -> _tp.Iterable[TestCase]: + inputDirPath = _INPUT_DIR_PATH + for inputFilePath in inputDirPath.iterdir(): + assert inputFilePath.is_file() + + yield TestCase.createForStem(inputFilePath.stem) + + +@_pt.mark.parametrize("testCase", [_pt.param(tc, id=tc.fileStem) for tc in _getTestCases()]) +def testConvertXmlTmfStringToDdck(testCase: TestCase, monkeypatch) -> None: + xmlFileContent = testCase.inputFilePath.read_text(encoding="utf8") + + def returnConnectionsUnmodifiedWithGlobalDefaultVisibility(connections, _): + dialogResult = _ehcd.DialogResult(connections, _dv.DefaultVisibility.GLOBAL) + return dialogResult + + monkeypatch.setattr( + _ehcd.EditHydraulicConnectionsDialog, + _ehcd.EditHydraulicConnectionsDialog.showDialogAndGetResults.__name__, + returnConnectionsUnmodifiedWithGlobalDefaultVisibility, + ) + + fileName = testCase.inputFilePath.with_suffix(".ddck").name + result = _pc.convertXmlTmfStringToDdck(xmlFileContent, suggestedHydraulicConnections=None, fileName=fileName) + + if testCase.expectedOutputFilePath.is_file(): + assert isinstance(result, str) + actualDdckContent = _res.value(result) + + testCase.actualOutputFilePath.write_text(result) + expectedDdckContent = testCase.expectedOutputFilePath.read_text(encoding="utf8") + assert actualDdckContent == expectedDdckContent + else: + assert isinstance(result, _res.Error) + print(result.message) + assert not testCase.actualOutputFilePath.is_file() diff --git a/trnsysGUI/BlockItem.py b/trnsysGUI/BlockItem.py index c06748ae..3e65a03e 100644 --- a/trnsysGUI/BlockItem.py +++ b/trnsysGUI/BlockItem.py @@ -103,22 +103,25 @@ def setDisplayName(self, newName: str) -> None: def contextMenuEvent(self, event): menu = _qtw.QMenu() - rr = _img.ROTATE_TO_RIGHT_PNG.icon() - a2 = menu.addAction(rr, "Rotate Block clockwise") - a2.triggered.connect(self.rotateBlockCW) + rotateCwAction = menu.addAction(_img.ROTATE_TO_RIGHT_PNG.icon(), "Rotate Block clockwise") + rotateCwAction.triggered.connect(self.rotateBlockCW) - ll = _img.ROTATE_LEFT_PNG.icon() - a3 = menu.addAction(ll, "Rotate Block counter-clockwise") - a3.triggered.connect(self.rotateBlockCCW) + rotateCcwAction = menu.addAction(_img.ROTATE_LEFT_PNG.icon(), "Rotate Block counter-clockwise") + rotateCcwAction.triggered.connect(self.rotateBlockCCW) - a4 = menu.addAction("Reset Rotation") - a4.triggered.connect(self.resetRotation) + resetRotationAction = menu.addAction("Reset Rotation") + resetRotationAction.triggered.connect(self.resetRotation) - c1 = menu.addAction("Delete this Block") - c1.triggered.connect(self.deleteBlockCom) + deleteBlockAction = menu.addAction("Delete this Block") + deleteBlockAction.triggered.connect(self.deleteBlockCom) + + self._addChildContextMenuActions(menu) menu.exec_(event.screenPos()) + def _addChildContextMenuActions(self, contextMenu: _qtw.QMenu) -> None: + pass + def mouseDoubleClickEvent(self, event: _qtw.QGraphicsSceneMouseEvent) -> None: # pylint: disable=unused-argument self.editor.showBlockDlg(self) diff --git a/trnsysGUI/DeepInspector.py b/trnsysGUI/DeepInspector.py deleted file mode 100644 index 9512afd4..00000000 --- a/trnsysGUI/DeepInspector.py +++ /dev/null @@ -1,95 +0,0 @@ -# pylint: skip-file -# type: ignore - -import sys -import re - -from PyQt5.QtWidgets import QDialog, QLabel, QHBoxLayout, QPushButton, QLineEdit, QGridLayout, QListWidget, QRadioButton - - -class DeepInspector(QDialog): - def __init__(self, parent): - super(DeepInspector, self).__init__(parent) - - self.stdout = sys.stdout - self.messages = [] - - self.editor = parent - self.currentObj = parent - - funcLabel = QLabel("Enter function call:") - self.le = QLineEdit() - - self.okButton = QPushButton("Execute") - self.cancelButton = QPushButton("Cancel") - - self.listW = QListWidget() - - radioButtonLayout = QHBoxLayout() - self.rMeth = QRadioButton("Method") - self.rAttr = QRadioButton("Attr") - radioButtonLayout.addWidget(self.rMeth) - radioButtonLayout.addWidget(self.rAttr) - - buttonLayout = QHBoxLayout() - buttonLayout.addStretch() - buttonLayout.addWidget(self.okButton) - buttonLayout.addWidget(self.cancelButton) - layout = QGridLayout() - layout.addWidget(funcLabel, 0, 0) - layout.addWidget(self.le, 1, 0) - layout.addWidget(self.listW, 2, 0) - layout.addLayout(radioButtonLayout, 3, 0, 2, 0) - layout.addLayout(buttonLayout, 5, 0, 2, 0) - self.setLayout(layout) - - self.okButton.clicked.connect(self.acceptedEdit) - self.cancelButton.clicked.connect(self.cancel) - self.setWindowTitle("Diagram Properties") - self.show() - - def acceptedEdit(self): - if not (self.rMeth.isChecked() or self.rAttr.isChecked()): - print("No button checked") - else: - t = self.le.text() - # b = False - # if "(" in t: - # b = True - if self.rMeth.isChecked(): - funcStr = re.findall(r".{0,}\(", t)[0][:-1] - argStr = re.findall(r"\(.{0,}\)", t)[0][1:-1] - print(funcStr + ", " + argStr) - if argStr == "": - self.executeMeth((funcStr)) - else: - self.executeMeth(funcStr, argStr) - else: - funcStr = t - argStr = None - self.currentObj = self.getAtr(funcStr) - - # self.stopLog() - print("now in console should appear: " + str(self.messages)) - self.listW.addItem(funcStr) - - def cancel(self): - self.close() - - def executeMeth(self, meth, *args): - res = getattr(self.currentObj, meth)(*args) - # if type(res) is not int and type(res) is not list: - print(type(res)) - self.currentObj = res - - def getAtr(self, name): - return getattr(self.currentObj, name) - - def startLog(self): - sys.stdout = self - - def stopLog(self): - sys.stdout = self.stdout - - def write(self, text): - self.messages.append(text) diff --git a/trnsysGUI/TestDlg.py b/trnsysGUI/TestDlg.py deleted file mode 100644 index d71deb19..00000000 --- a/trnsysGUI/TestDlg.py +++ /dev/null @@ -1,37 +0,0 @@ -# pylint: skip-file -# type: ignore - -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel - - -class TestDlg(QDialog): - """ - This is the dialog box for testing. It will be called when an exported file cannot be found inside the reference - folder. (file of same name doesnt exist inside reference folder) - """ - - def __init__(self, exportedFile, *args, **kwargs): - super(TestDlg, self).__init__(*args, **kwargs) - - self.exportBool = False - - self.setWindowTitle("Error!") - self.dlgMessage = QLabel() - self.strMessage = "%s not found inside Reference folder, add the file into Reference folder?" % exportedFile - self.dlgMessage.setText(self.strMessage) - - QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel - - self.buttonBox = QDialogButtonBox(QBtn) - self.buttonBox.accepted.connect(self.acceptExport) - self.buttonBox.rejected.connect(self.reject) - - self.layout = QVBoxLayout() - self.layout.addWidget(self.dlgMessage) - self.layout.addWidget(self.buttonBox) - self.setLayout(self.layout) - self.exec_() - - def acceptExport(self): - self.exportBool = True - self.accept() diff --git a/trnsysGUI/Test_Export.py b/trnsysGUI/Test_Export.py deleted file mode 100644 index 15a7eec9..00000000 --- a/trnsysGUI/Test_Export.py +++ /dev/null @@ -1,207 +0,0 @@ -# pylint: skip-file -# type: ignore - -import filecmp -import os - - -class Test_Export(object): - """ - Just a class that contains the method for testing. - No need to declare or pass anything. - Parameters should be passed into the individual functions to avoid unnecessary instantiotion - of class. - """ - - testPassed = True - - def retrieveFiles(self, exportedFilepath, originalFilePath, exportedFileList, originalFileList): - """ - - Parameters - ---------- - exportedFilepath : Folder where files are exported to - originalFilePath : Folder containing the reference files - exportedFileList : List of files inside the exported folder - originalFileList : List of files inside the reference folder - - 1.Access the exported folder - 2.Retrieve all exported files and add into a list - 3.Access the reference folder - 4.Retrieve all reference files and add into a list - 5.Sort the two lists - - Returns - ------- - - """ - for efiles in os.listdir(exportedFilepath): - if efiles != ".keepMe": - # exportedFile = exportedFilepath + efiles - exportedFile = os.path.join(exportedFilepath, efiles) - if exportedFile not in exportedFileList: - exportedFileList.append(exportedFile) - - for ofiles in os.listdir(originalFilePath): - # originalFile = originalFilePath + ofiles - originalFile = os.path.join(originalFilePath, ofiles) - if originalFile not in originalFileList: - originalFileList.append(originalFile) - - self.sortList(exportedFileList, originalFileList) - - def checkFileExists(self, exportedFile, originalFileList): - """ - - Parameters - ---------- - exportedFile : ONE FILE from the export_test folder - originalFileList : list of files inside the reference folder - - 1.Check if the individual files inside the export_test folder can be found - inside the reference folder as well. - 2.If found, return true. - 3.Else, return false. - - Returns - ------- - - """ - i = 0 - exportedFile = exportedFile.split("\\") - while i < len(originalFileList): - referenceFile = originalFileList[i].split("\\") - if exportedFile[-1] == referenceFile[-1]: - return True - i += 1 - return False - - def checkFiles(self, exportedFileList, originalFileList): - """ - - Parameters - ---------- - exportedFileList : All files inside the export_test folder - originalFileList : All files inside the reference folder - - 1.Check each exported file against the reference file of the same name - 2.If two files of the same name from both folders are found, check their contents - 3.If the contents are not the same, append the file index(order inside the folder) into fileErrorList - and set testPassed to False - 4.Return fileErrorList and testPassed attribute - - Returns - ------- - - """ - - i = 0 - fileErrorList = [] - found = False - while i < len(exportedFileList): - j = 0 - while j < len(originalFileList): - exportedFile = exportedFileList[i].split("\\")[-1] - referencefile = originalFileList[j].split("\\")[-1] - # print('this is exported :' + exportedFile + '\n') - # print('this is reference :' + referencefile + '\n') - if exportedFile != referencefile: - j += 1 - else: - found = True - break - if found: - if not filecmp.cmp(exportedFileList[i], originalFileList[j], shallow=False): - # todo : can maybe simplify this part is not using random number for pump power - fileOne = open(exportedFileList[i]) - fileTwo = open(originalFileList[j]) - - fileOneLine = fileOne.readline() - fileTwoLine = fileTwo.readline() - - while fileOneLine: - if fileOneLine != fileTwoLine: - if fileOneLine.split(" = ")[0][:5] != "MfrPu": - self.testPassed = False - fileErrorList.append(i) - break - - fileOneLine = fileOne.readline() - fileTwoLine = fileTwo.readline() - # break - i += 1 - return fileErrorList, self.testPassed - # return i, self.testPassed - - def deleteFiles(self, fileDirectory): - """ - - Parameters - ---------- - fileDirectory : A specified file directory - - 1.Delete every files inside the specified file directory - - Returns - ------- - - """ - for dfiles in os.listdir(fileDirectory): - # deleteFiles = fileDirectory + dfiles - deleteFiles = os.path.join(fileDirectory, dfiles) - if dfiles != ".keepMe": - os.remove(deleteFiles) - - def showDifference(self, file1, file2): - """ - - Parameters - ---------- - file1 : exported file - file2 : reference file - - 1.Read a line from exported file and reference file - 2.Compare the two lines, if they are identical, go to next line. - 3.Else, Append the lines with their line number into two separate list. - 4.Close the files and return the two lists. - - Returns - ------- - - """ - line = 1 - fileOne = open(file1) - fileTwo = open(file2) - - file1List = [] - file2List = [] - - fileOneLine = fileOne.readline() - fileTwoLine = fileTwo.readline() - - while fileOneLine: - if fileOneLine != fileTwoLine: - print("Error found at line %d\n" % line) - print("Exported file: %s \nReference file: %s \n" % (fileOneLine, fileTwoLine)) - if fileOneLine.split(" = ")[0][:5] != "MfrPu": - file1Str = str(line) + ":" + fileOneLine - file2Str = str(line) + ":" + fileTwoLine - file1List.append(file1Str) - file2List.append(file2Str) - fileOneLine = fileOne.readline() - fileTwoLine = fileTwo.readline() - line += 1 - - # print("File 1 list:") - # print(file1List) - # print("\nFile 2 List:") - # print(file2List) - - fileOne.close() - fileTwo.close() - - return file1List, file2List - - def sortList(self, list1, list2): - list1.sort() - list2.sort() diff --git a/trnsysGUI/blockItemHasInternalPiping.py b/trnsysGUI/blockItemHasInternalPiping.py index 0359f118..970d28ce 100644 --- a/trnsysGUI/blockItemHasInternalPiping.py +++ b/trnsysGUI/blockItemHasInternalPiping.py @@ -1,7 +1,11 @@ +import pathlib as _pl import typing as _tp +from PyQt5 import QtWidgets as _qtw + import trnsysGUI.BlockItem as _bi import trnsysGUI.internalPiping as _ip +import trnsysGUI.loadDdckFile as _ld class BlockItemHasInternalPiping(_bi.BlockItem, _ip.HasInternalPiping): @@ -11,3 +15,17 @@ def getDisplayName(self) -> str: def getInternalPiping(self) -> _ip.InternalPiping: raise NotImplementedError() + + @_tp.override + def _addChildContextMenuActions(self, contextMenu: _qtw.QMenu) -> None: + if self.hasDdckDirectory(): + loadDdckAction = contextMenu.addAction("Load ddck file...") + loadDdckAction.triggered.connect(self._onLoadDdckActionTriggered) + + def _onLoadDdckActionTriggered(self) -> None: + ddckFileLoader = _ld.DdckFileLoader(self.editor) + + projectDirPath = _pl.Path(self.editor.projectFolder) + targetDirPath = projectDirPath / "ddck" / self.displayName + + ddckFileLoader.loadDdckFile(targetDirPath) diff --git a/trnsysGUI/blockItems/getBlockItem.py b/trnsysGUI/blockItems/getBlockItem.py index 65da71f7..a4f25aa6 100644 --- a/trnsysGUI/blockItems/getBlockItem.py +++ b/trnsysGUI/blockItems/getBlockItem.py @@ -141,7 +141,7 @@ def _addOrCreateAndAddDisplayName( if not displayName: createNamingHelper = _cname.CreateNamingHelper(namesManager) checkDdckFolder = ( - blockItemClass.hasDdckPlaceHolders() if issubclass(blockItemClass, _ip.HasInternalPiping) else False + blockItemClass.hasDdckDirectory() if issubclass(blockItemClass, _ip.HasInternalPiping) else False ) displayName = createNamingHelper.generateName( baseDisplayName, checkDdckFolder, firstGeneratedNameHasNumber=False diff --git a/trnsysGUI/closeDlg.py b/trnsysGUI/closeDlg.py deleted file mode 100644 index 0658ddb7..00000000 --- a/trnsysGUI/closeDlg.py +++ /dev/null @@ -1,37 +0,0 @@ -# pylint: skip-file -# type: ignore - -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel - - -class closeDlg(QDialog): - """ - Not used currently. Keep for future sake. - Used to prompt user whether to save their current template - """ - - def __init__(self, *args, **kwargs): - super(closeDlg, self).__init__(*args, **kwargs) - - self.closeBool = False - - self.setWindowTitle("Closing!") - self.dlgMessage = QLabel() - self.strMessage = "Do you want to retain the current template for when you next open the application?" - self.dlgMessage.setText(self.strMessage) - - QBtn = QDialogButtonBox.Ok | QDialogButtonBox.Cancel - - self.buttonBox = QDialogButtonBox(QBtn) - self.buttonBox.accepted.connect(self.acceptExport) - self.buttonBox.rejected.connect(self.reject) - - self.layout = QVBoxLayout() - self.layout.addWidget(self.dlgMessage) - self.layout.addWidget(self.buttonBox) - self.setLayout(self.layout) - self.exec_() - - def acceptExport(self): - self.closeBool = True - self.accept() diff --git a/trnsysGUI/common/cancelled.py b/trnsysGUI/common/cancelled.py index a3fcc24e..3ee83ba0 100644 --- a/trnsysGUI/common/cancelled.py +++ b/trnsysGUI/common/cancelled.py @@ -10,7 +10,7 @@ class Cancelled: MaybeCancelled: _tp.TypeAlias = _T | Cancelled -def isCancelled(maybeCancelled: MaybeCancelled[_T]) -> bool: +def isCancelled(maybeCancelled: MaybeCancelled[_T]) -> _tp.TypeGuard[Cancelled]: if maybeCancelled == CANCELLED: return True diff --git a/trnsysGUI/components/ddckFolderHelpers.py b/trnsysGUI/components/ddckFolderHelpers.py index a45f25c3..a5db61c1 100644 --- a/trnsysGUI/components/ddckFolderHelpers.py +++ b/trnsysGUI/components/ddckFolderHelpers.py @@ -33,7 +33,7 @@ def moveComponentDdckFolderIfNecessary( def hasComponentDdckFolder(blockItem: _bi.BlockItem) -> bool: - hasDdckFolder = blockItem.hasDdckPlaceHolders() if isinstance(blockItem, _ip.HasInternalPiping) else False + hasDdckFolder = blockItem.hasDdckDirectory() if isinstance(blockItem, _ip.HasInternalPiping) else False return hasDdckFolder diff --git a/trnsysGUI/deleteBlockCommand.py b/trnsysGUI/deleteBlockCommand.py index 69eb1252..fb7cdc6d 100644 --- a/trnsysGUI/deleteBlockCommand.py +++ b/trnsysGUI/deleteBlockCommand.py @@ -52,7 +52,7 @@ def redo(self): _dfh.maybeDeleteNonEmptyComponentDdckFolder(self._blockItem, _pl.Path(self._editor.projectFolder)) def undo(self): - checkDdckFolder = self._blockItem.hasDdckPlaceHolders() + checkDdckFolder = self._blockItem.hasDdckDirectory() oldName = self._blockItem.displayName generatedNamePrefix = self._blockItem.name diff --git a/trnsysGUI/diagram/Editor.py b/trnsysGUI/diagram/Editor.py index 7e7ea0ca..6f93031c 100644 --- a/trnsysGUI/diagram/Editor.py +++ b/trnsysGUI/diagram/Editor.py @@ -1,6 +1,7 @@ # pylint: skip-file # type: ignore +import collections.abc as _cabc import json import math as _math import os @@ -60,7 +61,7 @@ from . import fileSystemTreeView as _fst -class Editor(_qtw.QWidget): +class Editor(_qtw.QWidget, _ip.HasInternalPipingsProvider): def __init__(self, parent, projectFolder, jsonPath, loadValue, logger): super().__init__(parent) @@ -100,7 +101,7 @@ def __init__(self, parent, projectFolder, jsonPath, loadValue, logger): self.pathLayout.addWidget(self.projectPathLabel) self.pathLayout.addWidget(self.PPL) - treeView = _fst.FileSystemTreeView(_pl.Path(self.projectFolder)) + treeView = _fst.FileSystemTreeView(_pl.Path(self.projectFolder), self) self.fileBrowserLayout.addLayout(self.pathLayout) self.fileBrowserLayout.addWidget(treeView) @@ -857,45 +858,6 @@ def renameDiagram(self, newName): self.diagramName = newName self.parent().currentFile = newName - # fromPath = self.projectFolder - # destPath = os.path.dirname(__file__) - # destPath = os.path.join(destPath, 'default') - # destPath = os.path.join(destPath, newName) - # os.rename(fromPath, destPath) - - # print("Path is now: " + str(self.saveAsPath)) - # print("Diagram name is: " + self.diagramName) - - def saveAtClose(self): - self.logger.info("saveaspath is " + str(self.saveAsPath)) - - # closeDialog = closeDlg() - # if closeDialog.closeBool: - filepath = _pl.Path(_pl.Path(__file__).resolve().parent.joinpath("recent")) - self.encodeDiagram(str(filepath.joinpath(self.diagramName + ".json"))) - - # Mode related - def setAlignMode(self, b): - self.alignMode = True - - def setMoveDirectPorts(self, b): - """ - Sets the bool moveDirectPorts. When mouse released in diagramScene, moveDirectPorts is set to False again - Parameters - ---------- - b : bool - - Returns - ------- - - """ - self.moveDirectPorts = b - - def setSnapGrid(self, b): - self.snapGrid = b - - def setSnapSize(self, s): - self.snapSize = s def setConnLabelVis(self, isVisible: bool) -> None: for c in self.trnsysObj: @@ -1008,3 +970,7 @@ def editHydraulicLoop(self, singlePipeConnection: SinglePipeConnection): def _updateGradientsInHydraulicLoop(hydraulicLoop: _hlm.HydraulicLoop) -> None: for connection in hydraulicLoop.connections: connection.updateSegmentGradients() + + @_tp.override + def getInternalPipings(self) -> _cabc.Sequence[_ip.HasInternalPiping]: + return [o for o in self.trnsysObj if isinstance(o, _ip.HasInternalPiping)] diff --git a/trnsysGUI/diagram/fileSystemTreeView.py b/trnsysGUI/diagram/fileSystemTreeView.py index 0816c3db..beb83fd4 100644 --- a/trnsysGUI/diagram/fileSystemTreeView.py +++ b/trnsysGUI/diagram/fileSystemTreeView.py @@ -1,18 +1,21 @@ import os as _os import pathlib as _pl -import shutil as _su import PyQt5.QtCore as _qtc import PyQt5.QtGui as _qtg import PyQt5.QtWidgets as _qtw +import trnsysGUI.internalPiping as _ip +import trnsysGUI.loadDdckFile as _ld import trnsysGUI.warningsAndErrors as _warn class FileSystemTreeView(_qtw.QTreeView): - def __init__(self, rootDirPath: _pl.Path) -> None: + def __init__(self, rootDirPath: _pl.Path, hasInternalPipingsProvider: _ip.HasInternalPipingsProvider) -> None: super().__init__(parent=None) + self._ddckLoader = _ld.DdckFileLoader(hasInternalPipingsProvider) + rootFolder = str(rootDirPath) self._model = _qtw.QFileSystemModel() @@ -50,30 +53,7 @@ def _openCurrentFileOrNoOp(self) -> None: def _loadFileIntoFolder(self) -> None: currentPath = self._getCurrentPath() targetDirPath = currentPath if currentPath.is_dir() else currentPath.parent - - sourceFilePathString = _qtw.QFileDialog.getOpenFileName(self, "Load file")[0] - if not sourceFilePathString: - return - - sourceFilePath = _pl.Path(sourceFilePathString) - targetFilePath = targetDirPath / sourceFilePath.name - - if targetFilePath.is_dir(): - message = f"""\ -A directory of the name `{targetFilePath.name}` already exists. Please change the name of the file before -importing or remove the directory.""" - _warn.showMessageBox(message, _warn.Title.WARNING) - return - - if targetFilePath.is_file(): - message = f"""\ - A file of the name `{targetFilePath.name}` already exists. Do you want to overwrite it?""" - - standardButton = _qtw.QMessageBox.question(None, "Overwrite file?", message) - if standardButton != _qtw.QMessageBox.StandardButton.Yes: # pylint: disable=no-member - return - - _su.copy(sourceFilePath, targetFilePath) + self._ddckLoader.loadDdckFile(targetDirPath) def _deleteCurrentFile(self) -> None: path = self._getCurrentPath() diff --git a/trnsysGUI/internalPiping.py b/trnsysGUI/internalPiping.py index 2cf09f1c..e8feb25d 100644 --- a/trnsysGUI/internalPiping.py +++ b/trnsysGUI/internalPiping.py @@ -2,6 +2,7 @@ import dataclasses as _dc import typing as _tp +import collections.abc as _cabc import trnsysGUI.massFlowSolver.networkModel as _mfn @@ -14,10 +15,14 @@ class InternalPiping: nodes: _tp.Sequence[_mfn.Node] modelPortItemsToGraphicalPortItem: _tp.Mapping[_mfn.PortItem, _pi.PortItemBase] # type: ignore[name-defined] - def __post_init__(self): + def __post_init__(self) -> None: if not all(mpi in self.modelPortItemsToGraphicalPortItem for n in self.nodes for mpi in n.getPortItems()): raise ValueError("Error a port item of a node was not contained in `modelPortItemsToGraphicalPortItem`.") + uniqueNodeNames = {n.name for n in self.nodes} + if len(uniqueNodeNames) != len(self.nodes): + raise ValueError("Node names must be unique within one component.") + def getModelPortItem( self, graphicalPortItem: _pi.PortItemBase, portItemType: _mfn.PortItemType # type: ignore[name-defined] ) -> _mfn.PortItem: @@ -74,8 +79,17 @@ def hasDdckPlaceHolders(cls) -> bool: return True @classmethod - def shallRenameOutputTemperaturesInHydraulicFile(cls): + def hasDdckDirectory(cls) -> bool: + return cls.hasDdckPlaceHolders() + + @classmethod + def shallRenameOutputTemperaturesInHydraulicFile(cls) -> bool: return True def getInternalPiping(self) -> InternalPiping: raise NotImplementedError() + + +class HasInternalPipingsProvider: + def getInternalPipings(self) -> _cabc.Sequence[HasInternalPiping]: + raise NotImplementedError() diff --git a/trnsysGUI/loadDdckFile.py b/trnsysGUI/loadDdckFile.py new file mode 100644 index 00000000..39732596 --- /dev/null +++ b/trnsysGUI/loadDdckFile.py @@ -0,0 +1,89 @@ +import dataclasses as _dc +import pathlib as _pl +import shutil as _su + +import PyQt5.QtWidgets as _qtw + +import pytrnsys.utils.result as _res +import trnsysGUI.common.cancelled as _cancel +import trnsysGUI.internalPiping as _ip +import trnsysGUI.proforma.convertXmlTmfToDdck as _pro +import trnsysGUI.proforma.createModelConnections as _pcmc +import trnsysGUI.proforma.dialogs.convertDialog as _pcd +import trnsysGUI.warningsAndErrors as _warn + + +@_dc.dataclass +class DdckFileLoader: + hasInternalPipingsProvider: _ip.HasInternalPipingsProvider + + def loadDdckFile(self, targetDirPath: _pl.Path) -> None: + if not targetDirPath.is_dir(): + raise ValueError("Not a directory.", targetDirPath) + + sourceFilePathString, _ = _qtw.QFileDialog.getOpenFileName(None, "Load file") + if not sourceFilePathString: + return + + sourceFilePath = _pl.Path(sourceFilePathString) + + isSourceProformaFile = self._isProformaFilePath(sourceFilePath) + targetSuffix = ".ddck" if isSourceProformaFile else sourceFilePath.suffix + + targetFilePathStem = targetDirPath / sourceFilePath.stem + targetFilePath = targetFilePathStem.with_suffix(targetSuffix) + + if targetFilePath.is_dir(): + message = f"""\ + A directory of the name `{targetFilePath.name}` already exists. Please change the name of the file before + importing or remove the directory.""" + _warn.showMessageBox(message, _warn.Title.WARNING) + return + + if targetFilePath.is_file(): + message = f"""\ + A file of the name `{targetFilePath.name}` already exists. Do you want to overwrite it?""" + + standardButton = _qtw.QMessageBox.question(None, "Overwrite file?", message) + if standardButton != _qtw.QMessageBox.StandardButton.Yes: # pylint: disable=no-member + return + + if not isSourceProformaFile: + _su.copy(sourceFilePath, targetFilePath) + return + + self._convertAndLoadProformaFileIntoFolder(sourceFilePath, targetFilePath) + + def _convertAndLoadProformaFileIntoFolder(self, sourceFilePath: _pl.Path, targetFilePath: _pl.Path) -> None: + hasInternalPipingsWithDdckPlaceholders = [ + hip for hip in self.hasInternalPipingsProvider.getInternalPipings() if hip.hasDdckPlaceHolders() + ] + + dialogMaybeCancelled = _pcd.ConvertDialog.showDialogAndGetResults( + hasInternalPipingsWithDdckPlaceholders, targetFilePath + ) + if _cancel.isCancelled(dialogMaybeCancelled): + return + dialogResult = _cancel.value(dialogMaybeCancelled) + + createConnectionsResult = _pcmc.createModelConnectionsFromInternalPiping(dialogResult.internalPiping) + if _res.isError(createConnectionsResult): + _warn.showMessageBox(_res.error(createConnectionsResult).message) + return + suggestedHydraulicConnections = _res.value(createConnectionsResult) + + maybeCancelled = _pro.convertXmltmfToDdck( + sourceFilePath, + suggestedHydraulicConnections, + dialogResult.outputFilePath, + ) + if _cancel.isCancelled(maybeCancelled): + return + result = _cancel.value(maybeCancelled) + if _res.isError(result): + _warn.showMessageBox(_res.error(result).message) + return + + @staticmethod + def _isProformaFilePath(filePath: _pl.Path) -> bool: + return filePath.suffix == ".xmltmf" diff --git a/trnsysGUI/names/dialog.py b/trnsysGUI/names/dialog.py index 914a2cb2..b363ee59 100644 --- a/trnsysGUI/names/dialog.py +++ b/trnsysGUI/names/dialog.py @@ -5,7 +5,6 @@ import pytrnsys.utils.result as _res import trnsysGUI.BlockItem as _bi import trnsysGUI.components.ddckFolderHelpers as _dfh -import trnsysGUI.internalPiping as _ip import trnsysGUI.names.rename as _rename import trnsysGUI.warningsAndErrors as _werrors @@ -26,9 +25,7 @@ def acceptedEdit(self) -> None: newName = self._displayNameLineEdit.text() oldName = self._blockItem.displayName - hasDdckFolder = ( - self._blockItem.hasDdckPlaceHolders() if isinstance(self._blockItem, _ip.HasInternalPiping) else False - ) + hasDdckFolder = _dfh.hasComponentDdckFolder(self._blockItem) result = self._renameHelper.canRename(oldName, newName, hasDdckFolder) if _res.isError(result): diff --git a/trnsysGUI/proforma/README.md b/trnsysGUI/proforma/README.md new file mode 100644 index 00000000..3f30e0ff --- /dev/null +++ b/trnsysGUI/proforma/README.md @@ -0,0 +1,28 @@ +### Introduction ### +A prototype script to convert a TRNSYS simulation studio Proforma in the XMLTMF format to a ddck: +`Type71.xmltmf` is converted to `Type71.ddck`. + +The additional information needed for the conversion is contained within the `` +element. This element does not exist in Proformas yet and would have to be added. + +### Installation ### + +1. Install Python 3.9 64bit from here https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe +2. Create a virtual environment: + ```commandline + py -3.9 -m venv venv + ``` +3. Activate the environment: + ```commandline + venv\Scripts\activate + ``` +4. Install the dependencies: + ```commandline + python -m pip install -r dev-requirements.txt + ``` + +### Run tests ### +Run +```commandline +pytest -rA . +``` diff --git a/trnsysGUI/proforma/__init__.py b/trnsysGUI/proforma/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/trnsysGUI/proforma/convertXmlTmfToDdck.py b/trnsysGUI/proforma/convertXmlTmfToDdck.py new file mode 100644 index 00000000..c781ffd7 --- /dev/null +++ b/trnsysGUI/proforma/convertXmlTmfToDdck.py @@ -0,0 +1,338 @@ +import collections.abc as _cabc +import dataclasses as _dc +import pathlib as _pl +import re as _re +import textwrap as _tw +import typing as _tp +import xml.etree.ElementTree as _etree + +import jinja2 as _jj +import xmlschema as _xs + +import pytrnsys.ddck.replaceTokens.defaultVisibility as _dv +import pytrnsys.utils.result as _res +import trnsysGUI.common.cancelled as _cancel +import trnsysGUI.proforma.dialogs.editHydraulicConnectionsDialog as _ehcd + +from . import createModelConnections as _cmcs +from . import models as _models + +_CONTAINING_DIR_PATH = _pl.Path(__file__).parent +_SCHEMA_FILE_PATH = _CONTAINING_DIR_PATH / "xmltmf.xsd" + + +def _createDdckJinjaTemplate() -> _jj.Template: + environment = _jj.Environment( + loader=_jj.PackageLoader(__name__), + autoescape=_jj.select_autoescape(), + lstrip_blocks=True, + trim_blocks=True, + keep_trailing_newline=True, + undefined=_jj.StrictUndefined, + ) + template = environment.get_template("ddck.jinja") + return template + + +_JINJA_TEMPLATE = _createDdckJinjaTemplate() + + +def convertXmltmfToDdck( + xmlTmfFilePath: _pl.Path, + suggestedHydraulicConnections: _cabc.Sequence[_models.Connection] | None, + ddckFilePath: _pl.Path, +) -> _cancel.MaybeCancelled[_res.Result[None]]: + xmlTmfContent = xmlTmfFilePath.read_text(encoding="utf8") + maybeCancelled = convertXmlTmfStringToDdck(xmlTmfContent, suggestedHydraulicConnections, ddckFilePath.name) + if _cancel.isCancelled(maybeCancelled): + return _cancel.CANCELLED + result = _cancel.value(maybeCancelled) + if _res.isError(result): + return _res.error(result).withContext(f"Converting proforma `{xmlTmfFilePath}`") + ddckContent = _res.value(result) + + assert isinstance(ddckContent, str) + ddckFilePath.write_text(ddckContent, encoding="utf8") + + return None + + +_StringMapping = _tp.Mapping[str, _tp.Any] + + +@_dc.dataclass +class _HydraulicConnectionsData: + name: str | None + portName: str + stringConstants: _models.VariableStringConstants + + @property + def variableName(self) -> str: + capitalizedPortName = self.portName.capitalize() + return f"{self.stringConstants.variableNamePrefix}{capitalizedPortName}" + + @property + def rhs(self) -> str: + return f"@{self.stringConstants.propertyName}({self.portName})" + + @staticmethod + def createForTemperature(connectionName: str | None, portName: str) -> "_HydraulicConnectionsData": + return _HydraulicConnectionsData(connectionName, portName, _models.AllVariableStringConstants.TEMPERATURE) + + @staticmethod + def createForReverseTemperature(connectionName: str | None, portName: str): + return _HydraulicConnectionsData( + connectionName, portName, _models.AllVariableStringConstants.REVERSE_TEMPERATURE + ) + + @staticmethod + def createForMassFlowRate(connectionName: str | None, portName: str) -> "_HydraulicConnectionsData": + return _HydraulicConnectionsData(connectionName, portName, _models.AllVariableStringConstants.MASS_FLOW_RATE) + + @staticmethod + def createForFluidHeatCapacity(connectionName: str | None, portName: str) -> "_HydraulicConnectionsData": + return _HydraulicConnectionsData(connectionName, portName, _models.AllVariableStringConstants.HEAT_CAPACITY) + + @staticmethod + def createForFluidDensity(connectionName: str | None, portName: str) -> "_HydraulicConnectionsData": + return _HydraulicConnectionsData(connectionName, portName, _models.AllVariableStringConstants.DENSITY) + + +@_dc.dataclass +class _VariableWithHydraulicConnectionsData(_models.Variable): + hydraulicConnectionsData: _HydraulicConnectionsData | None + + @staticmethod + def fromVariable( + variable: _models.Variable, connectionsDataByOrder: _tp.Mapping[int, _HydraulicConnectionsData] + ) -> "_VariableWithHydraulicConnectionsData": + hydraulicConnectionsData = connectionsDataByOrder.get(variable.order) + variableWithData = _VariableWithHydraulicConnectionsData( + **vars(variable), hydraulicConnectionsData=hydraulicConnectionsData + ) + return variableWithData + + +def _createProcessedVariables( + variables: _cabc.Sequence[_models.Variable], + connectionsDataByOrder: _tp.Mapping[int, _HydraulicConnectionsData], +) -> _cabc.Sequence[_VariableWithHydraulicConnectionsData]: + + processedVariables = [ + _VariableWithHydraulicConnectionsData.fromVariable(v, connectionsDataByOrder) for v in variables + ] + + return processedVariables + + +def _createVariablesByRole( + serializedVariables: _cabc.Sequence[_StringMapping], +) -> _models.VariablesByRole: + def getOrder(serializedVariable: _StringMapping) -> int: + return serializedVariable["order"] + + sortedSerializedVariables = sorted(serializedVariables, key=getOrder) + + parameterVariables = _createVariablesForRole("parameter", sortedSerializedVariables) + inputVariables = _createVariablesForRole("input", sortedSerializedVariables) + outputVariables = _createVariablesForRole("output", sortedSerializedVariables) + + return _models.VariablesByRole(parameterVariables, inputVariables, outputVariables) + + +_Role = _tp.Literal["parameter", "input", "output"] + + +def _createVariablesForRole( + role: _Role, sortedSerializedVariables: _cabc.Sequence[_StringMapping] +) -> _cabc.Sequence[_models.Variable]: + sortedSerializedVariablesForRole = _getSerializedVariablesWithRole(role, sortedSerializedVariables) + + variables = [] + for roleOrder, serializedVariable in enumerate(sortedSerializedVariablesForRole, start=1): + order = serializedVariable["order"] + bounds = _getBounds(serializedVariable) + variable = _models.Variable( + serializedVariable["name"], + order, + role, + roleOrder, + serializedVariable["unit"], + bounds, + serializedVariable["default"], + ) + variables.append(variable) + + return variables + + +def convertXmlTmfStringToDdck( + xmlTmfContent: str, suggestedHydraulicConnections: _cabc.Sequence[_models.Connection] | None, fileName: str +) -> _cancel.MaybeCancelled[_res.Result[str]]: + schema = _xs.XMLSchema11(_SCHEMA_FILE_PATH) + + try: + schema.validate(xmlTmfContent) + except (_etree.ParseError, _xs.XMLSchemaException) as error: + return _res.Error(f"Error reading or validating the proforma file: {error}") + + proforma: _tp.Any = schema.to_dict(xmlTmfContent) + + serializedVariables = proforma["variables"]["variable"] + variablesByRole = _createVariablesByRole(serializedVariables) + + if "hydraulicConnections" in proforma: + serializedHydraulicConnections = proforma["hydraulicConnections"]["connection"] + result = _cmcs.createModelConnectionsFromProforma(serializedHydraulicConnections, variablesByRole.allVariables) + if _res.isError(result): + return _res.error(result) + hydraulicConnections = _res.value(result) + elif suggestedHydraulicConnections is not None: + hydraulicConnections = suggestedHydraulicConnections + else: + hydraulicConnections = [] + + defaultVisibility = _dv.DefaultVisibility.LOCAL + + if hydraulicConnections: + maybeCancelled = _ehcd.EditHydraulicConnectionsDialog.showDialogAndGetResults( + hydraulicConnections, variablesByRole + ) + if _cancel.isCancelled(maybeCancelled): + return _cancel.CANCELLED + dialogResult = _cancel.value(maybeCancelled) + + hydraulicConnections = dialogResult.hydraulicConnections + defaultVisibility = dialogResult.defaultVisibility + + otherJinjaVariables = { + "fileName": fileName, + "type": proforma["type"], + "description": _makeMultilineComment(proforma["object"]), + "details": _makeMultilineComment(proforma["details"]), + "visibilityModifier": _getVisibilityModifier(defaultVisibility), + } + + return _convertXmlTmfStringToDdck(hydraulicConnections, variablesByRole, otherJinjaVariables) + + +def _getVisibilityModifier( # pylint: disable=inconsistent-return-statements + defaultVisibility: _dv.DefaultVisibility, +) -> str: + if defaultVisibility == _dv.DefaultVisibility.LOCAL: + return "" + if defaultVisibility == _dv.DefaultVisibility.GLOBAL: + return ":" + + _tp.assert_never(defaultVisibility) + + +def _makeMultilineComment(text: str) -> str: + textWithCollapsedWhitespace = _re.sub(r"\s+", " ", text.strip(), count=0, flags=_re.MULTILINE) + linePrefix = "** " + maxWidth = 120 - len(linePrefix) + wrappedLines = _tw.wrap(textWithCollapsedWhitespace, maxWidth) + newText = "\n".join(f"** {l}" for l in wrappedLines) + return newText + + +def _convertXmlTmfStringToDdck( + hydraulicConnections: _cabc.Sequence[_models.Connection], + variablesByRole: _models.VariablesByRole, + otherJinjaVariables: _cabc.Mapping[str, _tp.Any], +) -> str: + connectionsDataByOrder = _createHydraulicConnectionDataByOrder(hydraulicConnections) + parameters = _createProcessedVariables(variablesByRole.parameters, connectionsDataByOrder) + inputs = _createProcessedVariables(variablesByRole.inputs, connectionsDataByOrder) + outputs = _createProcessedVariables(variablesByRole.outputs, connectionsDataByOrder) + ddckContent = _JINJA_TEMPLATE.render(parameters=parameters, inputs=inputs, outputs=outputs, **otherJinjaVariables) + return ddckContent + + +def _createHydraulicConnectionDataByOrder( + hydraulicConnections: _cabc.Sequence[_models.Connection], +) -> _cabc.Mapping[int, _HydraulicConnectionsData]: + hydraulicConnectionDataByOrder: dict[int, _HydraulicConnectionsData] = {} + for hydraulicConnection in hydraulicConnections: + connectionName = hydraulicConnection.name + inputPort = hydraulicConnection.inputPort + dataByOrderForFluid = _getHydraulicConnectionDataByOrderForFluid(connectionName, hydraulicConnection, inputPort) + + dataByOrderForInput = _getHydraulicConnectionDataByOrderForInput(connectionName, inputPort) + dataByOrderForOutput = _getHydraulicConnectionDataByOrderForOutput( + connectionName, hydraulicConnection.outputPort + ) + hydraulicConnectionDataByOrder |= dataByOrderForFluid | dataByOrderForInput | dataByOrderForOutput + + return hydraulicConnectionDataByOrder + + +def _getHydraulicConnectionDataByOrderForFluid( + connectionName: str | None, hydraulicConnection: _models.Connection, inputPort: _models.InputPort +) -> dict[int, _HydraulicConnectionsData]: + dataByOrderForFluid = {} + heatCapacityVariable = hydraulicConnection.fluid.heatCapacity + if heatCapacityVariable: + dataByOrderForFluid[heatCapacityVariable.order] = _HydraulicConnectionsData.createForFluidHeatCapacity( + connectionName, inputPort.name + ) + densityVariable = hydraulicConnection.fluid.density + if densityVariable: + dataByOrderForFluid[densityVariable.order] = _HydraulicConnectionsData.createForFluidDensity( + connectionName, inputPort.name + ) + return dataByOrderForFluid + + +def _getHydraulicConnectionDataByOrderForInput( + connectionName: str | None, inputPort: _models.InputPort +) -> dict[int, _HydraulicConnectionsData]: + portName = inputPort.name + + mfrOrder = inputPort.massFlowRateSet.order + tempOrder = inputPort.temperatureSet.order + + hydraulicConnectionData = { + mfrOrder: _HydraulicConnectionsData.createForMassFlowRate(connectionName, portName), + tempOrder: _HydraulicConnectionsData.createForTemperature(connectionName, portName), + } + + return hydraulicConnectionData + + +def _getHydraulicConnectionDataByOrderForOutput( + connectionName: str | None, + outputPort: _models.OutputPort, +) -> dict[int, _HydraulicConnectionsData]: + portName = outputPort.name + tempOrder = outputPort.temperatureSet.order + revTempOrder = outputPort.reverseTemperature.order if outputPort.reverseTemperature else None + + hydraulicConnectionData = { + tempOrder: _HydraulicConnectionsData.createForTemperature(connectionName, portName), + } + + if revTempOrder is not None: + hydraulicConnectionData[revTempOrder] = _HydraulicConnectionsData.createForReverseTemperature( + connectionName, portName + ) + + return hydraulicConnectionData + + +def _getOrder(variableReference: _StringMapping) -> int: + return variableReference["variableReference"]["order"] + + +def _getSerializedVariablesWithRole( + role: _Role, variables: _tp.Sequence[_StringMapping] +) -> _tp.Sequence[_StringMapping]: + return [v for v in variables if v["role"] == role] + + +def _getBounds(variable: _StringMapping) -> str: + leftBracket, rightBracket = [b.strip() for b in variable["boundaries"].split(";")] + minimum = variable["min"] + maximum = variable["max"] + bounds = f"{leftBracket}{minimum},{maximum}{rightBracket}" + return bounds diff --git a/trnsysGUI/proforma/createModelConnections.py b/trnsysGUI/proforma/createModelConnections.py new file mode 100644 index 00000000..0b7b35b4 --- /dev/null +++ b/trnsysGUI/proforma/createModelConnections.py @@ -0,0 +1,102 @@ +import collections.abc as _cabc +import typing as _tp + +import pytrnsys.utils.result as _res + +import trnsysGUI.internalPiping as _ip +import trnsysGUI.massFlowSolver.networkModel as _mfn + +from . import models as _models + + +def createModelConnectionsFromInternalPiping( + internalPiping: _ip.InternalPiping, +) -> _res.Result[_cabc.Sequence[_models.Connection]]: + connections = [] + for node in internalPiping.nodes: + if not isinstance(node, _mfn.Pipe): + return _res.Error(f"`{node.name}` is a `{node.__class__.__name__}`, but only direct pipes are supported.") + pipe = node + + inputPort = _models.InputPort(pipe.fromPort.name) + outputPort = _models.OutputPort(pipe.toPort.name) + + connection = _models.Connection(pipe.name, inputPort, outputPort) + + connections.append(connection) + + return connections + + +_StringMapping = _tp.Mapping[str, _tp.Any] + + +def createModelConnectionsFromProforma( + serializedConnections: _cabc.Sequence[_StringMapping], variables: _cabc.Sequence[_models.Variable] +) -> _res.Result[_cabc.Sequence[_models.Connection]]: + variablesByOrder = {v.order: v for v in variables} + + connections = [_createConnection(s, variablesByOrder) for s in serializedConnections] + + return connections + + +def _createConnection( + serializedConnection: _StringMapping, variablesByOrder: _cabc.Mapping[int, _models.Variable] +) -> _models.Connection: + serializedInputPort = serializedConnection["input"] + inputPort = _createInputPort(serializedInputPort, variablesByOrder) + + outputPort = _createOutputPort(serializedConnection, variablesByOrder) + + name = serializedConnection.get("@name") + + fluidDensityVariable = _getOptionalVariable(serializedInputPort, "fluidDensity", variablesByOrder) + fluidHeatCapacityVariable = _getOptionalVariable(serializedInputPort, "fluidHeatCapacity", variablesByOrder) + fluid = _models.Fluid(fluidDensityVariable, fluidHeatCapacityVariable) + + connection = _models.Connection(name, inputPort, outputPort, fluid) + + return connection + + +def _createInputPort( + serializedInputPort: _StringMapping, variablesByOrder: _cabc.Mapping[int, _models.Variable] +) -> _models.InputPort: + name = serializedInputPort["@name"] + massFlowRateVariable = _getVariable(serializedInputPort, "massFlowRate", variablesByOrder) + temperatureVariable = _getVariable(serializedInputPort, "temperature", variablesByOrder) + inputPort = _models.InputPort(name, temperatureVariable, massFlowRateVariable) + return inputPort + + +def _createOutputPort( + serializedConnection: _StringMapping, variablesByOrder: _cabc.Mapping[int, _models.Variable] +) -> _models.OutputPort: + serializedOutputPort = serializedConnection["output"] + name = serializedOutputPort["@name"] + temperature = _getVariable(serializedOutputPort, "temperature", variablesByOrder) + reverseTemperature = _getOptionalVariable(serializedOutputPort, "reverseTemperature", variablesByOrder) + outputPort = _models.OutputPort(name, temperature, reverseTemperature) + return outputPort + + +def _getVariable( + serializedPort: _StringMapping, variableName: str, variablesByOrder: _cabc.Mapping[int, _models.Variable] +) -> _models.Variable: + variable = _getOptionalVariable(serializedPort, variableName, variablesByOrder) + portName = serializedPort["@name"] + assert variable, f"No associated `{variableName}` variable for port {portName}" + return variable + + +def _getOptionalVariable( + serializedPort: _StringMapping, variableName: str, variablesByOrder: _cabc.Mapping[int, _models.Variable] +) -> _tp.Optional[_models.Variable]: + child = serializedPort.get(variableName) + if not child: + return None + + order = child["variableReference"]["order"] + variable = variablesByOrder[order] + return variable diff --git a/trnsysGUI/proforma/dialogs/__init__.py b/trnsysGUI/proforma/dialogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/trnsysGUI/proforma/dialogs/_convert.ui b/trnsysGUI/proforma/dialogs/_convert.ui new file mode 100644 index 00000000..dbcb228a --- /dev/null +++ b/trnsysGUI/proforma/dialogs/_convert.ui @@ -0,0 +1,127 @@ + + + Convert + + + + 0 + 0 + 644 + 83 + + + + Convert Proforma to ddck file... + + + + + + + + + false + + + + Component + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + + false + + + + Exported file path + + + + + + + 0 + + + + + + + + + 0 + 0 + + + + + 20 + 0 + + + + + 20 + 16777215 + + + + ... + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + diff --git a/trnsysGUI/proforma/dialogs/_hydraulicConnections.ui b/trnsysGUI/proforma/dialogs/_hydraulicConnections.ui new file mode 100644 index 00000000..78e1333d --- /dev/null +++ b/trnsysGUI/proforma/dialogs/_hydraulicConnections.ui @@ -0,0 +1,297 @@ + + + HydraulicConnections + + + + 0 + 0 + 792 + 746 + + + + Convert Proforma to ddck file... + + + + + + + + <html><head/><body><p>Note: Fields in <span style=" font-weight:700;">bold</span> are required. You can hover over light bulbs (<img src=":/tango-icons/status/tango-icon-theme-0.8.90/scalable/status/dialog-information.svg" height="20" widht="20"/>) and disabled buttons to get more information.</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Hydraulic connections + + + + + + + + + + + + + Hydraulic connection "Evap" + + + + + + + + Fluid + + + + + + Density + + + + + + + + + + Heat capacity + + + + + + + + + + + + + Inlet port "In" + + + + + + + true + + + + Mass flow rate + + + + + + + + + + + true + + + + Temperature + + + + + + + + + + + + + Outlet port "Out" + + + + + + Reverse flow temperature + + + + + + + + + + + + + + true + + + + Temperature + + + + + + + + 0 + 0 + + + + + 20 + 20 + + + + + 0 + 0 + + + + <html><head/><body><p>The inlet temperature at the nominal output when the direction of the mass flow is the reverse of the nominal direction. Not all types support reversing the mass flow. If your type doesn't support it, you can leave this field empty.</p></body></html> + + + image: url(:/tango-icons/status/tango-icon-theme-0.8.90/scalable/status/dialog-information.svg); + + + + + + + + + + + + Qt::Horizontal + + + + 91 + 20 + + + + + + + + + + + Advanced + + + false + + + true + + + false + + + + + + Default visibility + + + + + + + + + + + + + Summary + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + diff --git a/trnsysGUI/proforma/dialogs/convertDialog.py b/trnsysGUI/proforma/dialogs/convertDialog.py new file mode 100644 index 00000000..e1be7d21 --- /dev/null +++ b/trnsysGUI/proforma/dialogs/convertDialog.py @@ -0,0 +1,89 @@ +import dataclasses as _dc +import pathlib as _pl +import collections.abc as _cabc + +import PyQt5.QtWidgets as _qtw +import PyQt5.QtCore as _qtc + +import trnsysGUI.common.cancelled as _cancel +import trnsysGUI.dialogs as _dlgs +import trnsysGUI.internalPiping as _ip + +_dlgs.assertThatLocalGeneratedUIModuleAndResourcesExist(__name__, moduleName="_UI_convert_generated") + +from . import _UI_convert_generated as _uigen # type: ignore[import] # pylint: disable=wrong-import-position + + +@_dc.dataclass +class DialogResult: + internalPiping: _ip.InternalPiping + outputFilePath: _pl.Path + + +class ConvertDialog(_qtw.QDialog, _uigen.Ui_Convert): + def __init__(self, hasInternalPipings: _cabc.Sequence[_ip.HasInternalPiping], outputFilePath: _pl.Path) -> None: + super().__init__() + self.setupUi(self) + + if not hasInternalPipings: + raise ValueError("Must have at least one internal piping.") + + self.outputFilePathLineEdit.setText(str(outputFilePath)) + self.outputFilePathLineEdit.textChanged.connect(self._onOutputFilePathLineEditTextChanged) + + self._configureComponentComboBox(hasInternalPipings) + + componentName = outputFilePath.parent.name + currentInternalPiping = self._setAndGetCurrentInternalPiping(componentName) + + self.dialogResult = DialogResult(currentInternalPiping, outputFilePath) + + self.chooseOutputFilePathPushButton.pressed.connect(self._onChooseOutputFilePathPushButtonPressed) + + self.okCancelButtonBox.accepted.connect(self.accept) + self.okCancelButtonBox.rejected.connect(self.reject) + + def _configureComponentComboBox(self, hasInternalPipings: _cabc.Sequence[_ip.HasInternalPiping]) -> None: + def getDisplayName(hip: _ip.HasInternalPiping) -> str: + return hip.getDisplayName() + + sortedHasInternalPiping = sorted(hasInternalPipings, key=getDisplayName) + for hasInternalPiping in sortedHasInternalPiping: + displayName = hasInternalPiping.getDisplayName() + internalPiping = hasInternalPiping.getInternalPiping() + + self.componentComboBox.addItem(displayName, internalPiping) + + def _setAndGetCurrentInternalPiping(self, componentName: str) -> _ip.InternalPiping: + currentIndex = self.componentComboBox.findText(componentName, _qtc.Qt.MatchContains) + currentIndex = max(0, currentIndex) + self.componentComboBox.setCurrentIndex(currentIndex) + currentInternalPiping = self.componentComboBox.currentData() + return currentInternalPiping + + def _onOutputFilePathLineEditTextChanged(self, newText: str) -> None: + self.dialogResult.outputFilePath = _pl.Path(newText) + + def _onChooseOutputFilePathPushButtonPressed(self) -> None: + currentOutputFilePathString = self.outputFilePathLineEdit.text() + + newOutputFilePathString, _ = _qtw.QFileDialog.getSaveFileName( + None, "Select ouput file path...", currentOutputFilePathString + ) + + if not newOutputFilePathString: + return + + self.outputFilePathLineEdit.setText(newOutputFilePathString) + + @staticmethod + def showDialogAndGetResults( + hasInternalPipings: _cabc.Sequence[_ip.HasInternalPiping], outputFilePath: _pl.Path + ) -> _cancel.MaybeCancelled[DialogResult]: + dialog = ConvertDialog(hasInternalPipings, outputFilePath) + returnValue = dialog.exec() + + if returnValue == _qtw.QDialog.Rejected: + return _cancel.CANCELLED + + return dialog.dialogResult diff --git a/trnsysGUI/proforma/dialogs/editHydraulicConnectionsDialog.py b/trnsysGUI/proforma/dialogs/editHydraulicConnectionsDialog.py new file mode 100644 index 00000000..90465a84 --- /dev/null +++ b/trnsysGUI/proforma/dialogs/editHydraulicConnectionsDialog.py @@ -0,0 +1,306 @@ +import collections.abc as _cabc +import copy as _copy +import dataclasses as _dc + +import PyQt5.QtCore as _qtc +import PyQt5.QtWidgets as _qtw + +import pytrnsys.ddck.replaceTokens.defaultVisibility as _dv +import trnsysGUI.common as _com +import trnsysGUI.common.cancelled as _cancel +import trnsysGUI.dialogs as _dlgs +from .. import models as _models + +_dlgs.assertThatLocalGeneratedUIModuleAndResourcesExist(__name__, moduleName="_UI_hydraulicConnections_generated") + +from . import _UI_hydraulicConnections_generated as _uigen # type: ignore[import] # pylint: disable=wrong-import-position + + +class _OnActivatedCallBack: + def __init__(self, dialog: "EditHydraulicConnectionsDialog", comboBox: _qtw.QComboBox) -> None: + self._dialog = dialog + self._comboBox = comboBox + + @staticmethod + def createAndConnect(dialog: "EditHydraulicConnectionsDialog", comboBox: _qtw.QComboBox) -> "_OnActivatedCallBack": + callback = _OnActivatedCallBack(dialog, comboBox) + comboBox.activated.connect(callback.onActivated) + return callback + + def onActivated(self, newIndex: int) -> None: + self._dialog.onVariableComboBoxActivated(self._comboBox, newIndex) + + +@_dc.dataclass +class DialogResult: + hydraulicConnections: _cabc.Sequence[_models.Connection] + defaultVisibility: _dv.DefaultVisibility + + +class EditHydraulicConnectionsDialog(_qtw.QDialog, _uigen.Ui_HydraulicConnections): + _DEFAULT_CONNECTION_LABEL = "" + + def __init__( + self, + suggestedHydraulicConnections: _cabc.Sequence[_models.Connection], + variablesByRole: _models.VariablesByRole, + ) -> None: + super().__init__() + self.setupUi(self) + + self._variablesByRole = variablesByRole + self.hydraulicConnections = self._getDeepCopiesSortedByName(suggestedHydraulicConnections) + + self._onActivatedCallbacks = list[_OnActivatedCallBack]() + + self.summaryTextEdit.setReadOnly(True) + + self._configureButtonBox() + self._configureHydraulicConnections() + self._configureVariableComboBoxes() + + self._configureDefaultVisibilityGroupBox() + + self._reloadConnections() + + def _configureButtonBox(self) -> None: + self.okCancelButtonBox.accepted.connect(self.accept) + self.okCancelButtonBox.rejected.connect(self.reject) + + @staticmethod + def _getDeepCopiesSortedByName(suggestedHydraulicConnections): + hydraulicConnections = [_copy.deepcopy(c) for c in suggestedHydraulicConnections] + + def getConnectionName(connection: _models.Connection) -> str: + return connection.name or "" + + sortedHydraulicConnections = sorted(hydraulicConnections, key=getConnectionName) + return sortedHydraulicConnections + + def _configureHydraulicConnections(self): + self.connectionsListWidget.setSelectionMode(_qtw.QAbstractItemView.SingleSelection) + + for hydraulicConnection in self.hydraulicConnections: + name = hydraulicConnection.name if hydraulicConnection.name else self._DEFAULT_CONNECTION_LABEL + item = _qtw.QListWidgetItem(name) + item.setData(_qtc.Qt.UserRole, hydraulicConnection) + self.connectionsListWidget.addItem(item) + + self.connectionsListWidget.setCurrentRow(0) + + self.connectionsListWidget.itemSelectionChanged.connect(self._onSelectedConnectionChanged) + + def _onSelectedConnectionChanged(self) -> None: + self._reconfigureGroupBoxLabels() + self._reloadSelectedComboBoxItems() + + def _reconfigureGroupBoxLabels(self) -> None: + selectedConnection = self._getSelectedConnection() + + connectionGroupBoxLabel = ( + f'"{selectedConnection.name}"' if selectedConnection.name else self._DEFAULT_CONNECTION_LABEL + ) + connectionGroupBoxTitle = f"Hydraulic connection {connectionGroupBoxLabel}" + self.hydraulicConnectionGroupBox.setTitle(connectionGroupBoxTitle) + + self.inletPortGroupBox.setTitle(f'Inlet port "{selectedConnection.inputPort.name}"') + + self.outletPortGroupBox.setTitle(f'Outlet port "{selectedConnection.outputPort.name}"') + + def _reconfigureComboBoxOptions(self) -> None: + self._reconfigureFluid() + self._reconfigureInputs() + self._reconfigureOutputs() + + def _reconfigureFluid(self): + self._addParametersAndInputOptions(self.fluidDensityComboBox) + self._addParametersAndInputOptions(self.fluidHeatCapacityComboBox) + + def _addParametersAndInputOptions(self, comboBox: _qtw.QComboBox) -> None: + self._addOptions(comboBox, self._getParameters(comboBox)) + comboBox.addItem("-----", _models.UNSET) + self._addOptions(comboBox, self._getInputs(comboBox), withUnset=False, clear=False) + + def _reconfigureInputs(self) -> None: + self._addOptions(self.massFlowRateComboBox, self._getInputs(self.massFlowRateComboBox)) + self._addOptions(self.inputTempComboBox, self._getInputs(self.inputTempComboBox)) + + def _reconfigureOutputs(self) -> None: + self._addOptions(self.outputTempComboBox, self._getOutputs(self.outputTempComboBox)) + self._addOptions(self.outputRevTempComboBox, self._getInputs(self.outputRevTempComboBox)) + + @staticmethod + def _addOptions( + comboBox: _qtw.QComboBox, + variables: _cabc.Sequence[_models.Variable], + withUnset: bool = True, + clear: bool = True, + ) -> None: + if clear: + comboBox.clear() + + if withUnset: + comboBox.addItem("", _models.UNSET) + + for variable in variables: + text = variable.getInfo(withRole=True) + comboBox.addItem(text, variable) + + @property + def _selectedVariables(self) -> _cabc.Sequence[_models.Variable]: + selectedVariables = [v for c in self.hydraulicConnections for v in c.allVariables] + return selectedVariables + + def _getParameters(self, comboBox: _qtw.QComboBox) -> _cabc.Sequence[_models.Variable]: + return self._removeSelectedVariablesOfOtherComboBoxes(comboBox, self._variablesByRole.parameters) + + def _getInputs(self, comboBox: _qtw.QComboBox) -> _cabc.Sequence[_models.Variable]: + return self._removeSelectedVariablesOfOtherComboBoxes(comboBox, self._variablesByRole.inputs) + + def _getOutputs(self, comboBox: _qtw.QComboBox) -> _cabc.Sequence[_models.Variable]: + return self._removeSelectedVariablesOfOtherComboBoxes(comboBox, self._variablesByRole.outputs) + + def _removeSelectedVariablesOfOtherComboBoxes( + self, comboBox: _qtw.QComboBox, variables: _cabc.Sequence[_models.Variable] + ) -> _cabc.Sequence[_models.Variable]: + selectedVariableForComboBox = self._getVariableCorrespondingToComboBox(comboBox) + selectedOrOtherUnselectedVariables = [ + v for v in variables if v == selectedVariableForComboBox or v not in self._selectedVariables + ] + return selectedOrOtherUnselectedVariables + + def _getVariableCorrespondingToComboBox(self, comboBox: _qtw.QComboBox) -> _models.Variable | _models.Unset: + selectedConnection = self._getSelectedConnection() + + if comboBox == self.fluidDensityComboBox: + return selectedConnection.fluid.density or _models.UNSET + if comboBox == self.fluidHeatCapacityComboBox: + return selectedConnection.fluid.heatCapacity or _models.UNSET + if comboBox == self.massFlowRateComboBox: + return selectedConnection.inputPort.massFlowRate + if comboBox == self.inputTempComboBox: + return selectedConnection.inputPort.temperature + if comboBox == self.outputTempComboBox: + return selectedConnection.outputPort.temperature + if comboBox == self.outputRevTempComboBox: + return selectedConnection.outputPort.reverseTemperature or _models.UNSET + + raise AssertionError("Unknown combo box.") + + def _getSelectedConnection(self) -> _models.Connection: + selectedItems = self.connectionsListWidget.selectedItems() + selectedItem = _com.getSingle(selectedItems) + selectedConnection = selectedItem.data(_qtc.Qt.UserRole) + assert isinstance(selectedConnection, _models.Connection) + return selectedConnection + + def _configureVariableComboBoxes(self) -> None: + for variableComboBox in self._variableComboBoxes: + onActivatedCallback = _OnActivatedCallBack.createAndConnect(self, variableComboBox) + self._onActivatedCallbacks.append(onActivatedCallback) + + def onVariableComboBoxActivated(self, comboBox: _qtw.QComboBox, newIndex: int) -> None: + data = comboBox.itemData(newIndex) + self._setVariableCorrespondingToComboBox(comboBox, data) + self._reloadConnections() + + def _configureDefaultVisibilityGroupBox(self) -> None: + for defaultVisibility in _dv.DefaultVisibility: + self.defaultVisibilityComboBox.addItem(defaultVisibility.name.capitalize(), defaultVisibility) + + def _reloadConnections(self) -> None: + self._resetOkButton() + self._reloadSelectedComboBoxItems() + self._reloadSummaryText() + + def _reloadSummaryText(self) -> None: + overallSummary = "\n".join(s for c in self.hydraulicConnections if (s := c.getSummary())) + self.summaryTextEdit.setPlainText(overallSummary) + + def _resetOkButton(self) -> None: + okButton = self.okCancelButtonBox.button(_qtw.QDialogButtonBox.Ok) + areAnyRequiredVariablesUnset = self._areAnyRequiredVariablesUnset() + isEnabled = not areAnyRequiredVariablesUnset + okButton.setEnabled(isEnabled) + toolTip = ( + "Please specify the required properties (in bold) for all connections." + if areAnyRequiredVariablesUnset + else "" + ) + okButton.setToolTip(toolTip) + + def _areAnyRequiredVariablesUnset(self) -> bool: + areAnyRequiredVariablesUnset = any(c.areAnyRequiredVariablesUnset for c in self.hydraulicConnections) + return areAnyRequiredVariablesUnset + + def _reloadSelectedComboBoxItems(self) -> None: + self._reconfigureComboBoxOptions() + + for comboBox in self._variableComboBoxes: + data = self._getVariableCorrespondingToComboBox(comboBox) + _setSelected(comboBox, data) + + @property + def _variableComboBoxes(self) -> _cabc.Sequence[_qtw.QComboBox]: + return [ + self.fluidDensityComboBox, + self.fluidHeatCapacityComboBox, + self.massFlowRateComboBox, + self.inputTempComboBox, + self.outputTempComboBox, + self.outputRevTempComboBox, + ] + + def _setVariableCorrespondingToComboBox( + self, comboBox: _qtw.QComboBox, value: _models.Variable | _models.Unset + ) -> None: + selectedConnection = self._getSelectedConnection() + + valueForOptionalVariables = value if isinstance(value, _models.Variable) else None + + if comboBox == self.fluidDensityComboBox: + selectedConnection.fluid.density = valueForOptionalVariables + elif comboBox == self.fluidHeatCapacityComboBox: + selectedConnection.fluid.heatCapacity = valueForOptionalVariables + elif comboBox == self.massFlowRateComboBox: + selectedConnection.inputPort.massFlowRate = value + elif comboBox == self.inputTempComboBox: + selectedConnection.inputPort.temperature = value + elif comboBox == self.outputTempComboBox: + selectedConnection.outputPort.temperature = value + elif comboBox == self.outputRevTempComboBox: + selectedConnection.outputPort.reverseTemperature = valueForOptionalVariables + else: + raise AssertionError("Unknown combo box.") + + @staticmethod + def showDialogAndGetResults( + suggestedHydraulicConnections: _cabc.Sequence[_models.Connection], + variablesByRole: _models.VariablesByRole, + ) -> _cancel.MaybeCancelled[DialogResult]: + dialog = EditHydraulicConnectionsDialog(suggestedHydraulicConnections, variablesByRole) + returnValue = dialog.exec() + + if returnValue == _qtw.QDialog.Rejected: + return _cancel.CANCELLED + + defaultVisibility = ( + dialog.defaultVisibilityComboBox.currentData() + if dialog.advancedOptionsGroupBox.isChecked() + else _dv.DefaultVisibility.LOCAL + ) + + dialogResult = DialogResult(dialog.hydraulicConnections, defaultVisibility) + + return dialogResult + + +def _setSelected(comboBox: _qtw.QComboBox, data: _models.Variable | _models.Unset) -> None: + # Cannot use `QComboBox.findData` as `findData` works by reference, but we want by value + for index in range(comboBox.count()): + rowData = comboBox.itemData(index) + if rowData == data: + comboBox.setCurrentIndex(index) + return + + raise AssertionError(f"{data} not a member of combo box.") diff --git a/trnsysGUI/proforma/models.py b/trnsysGUI/proforma/models.py new file mode 100644 index 00000000..87fdf587 --- /dev/null +++ b/trnsysGUI/proforma/models.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import collections.abc as _cabc +import dataclasses as _dc +import typing as _tp + + +@_dc.dataclass +class Variable: + tmfName: str + order: int + role: str + roleOrder: int + unit: str + bounds: str + defaultValue: float | int + + def getInfo(self, withRole: bool) -> str: + roleOrEmpty = f"{self.role.capitalize()} " if withRole else "" + info = f"{roleOrEmpty}{self.roleOrder}: {self.tmfName} [{self.unit}] ({self.bounds})" + return info + + @property + def info(self) -> str: + return self.getInfo(withRole=False) + + +@_dc.dataclass(frozen=True) +class Unset: + pass + + +UNSET = Unset() + +RequiredVariable = Variable | Unset + + +@_dc.dataclass +class VariableStringConstants: + propertyName: str + variableNamePrefix: str | None + + +class AllVariableStringConstants: + TEMPERATURE = VariableStringConstants("temp", "T") + MASS_FLOW_RATE = VariableStringConstants("mfr", "M") + REVERSE_TEMPERATURE = VariableStringConstants("revtemp", None) + DENSITY = VariableStringConstants("rho", "Rho") + HEAT_CAPACITY = VariableStringConstants("cp", "Cp") + + +def _getSummaryLine( + qualifiedPortName: str, + variableStringConstants: VariableStringConstants, + variable: Variable | None | Unset, + direction: _tp.Literal["input", "output"], +) -> str: + if not isinstance(variable, Variable): + return "" + + computedVariable = f"@{variableStringConstants.propertyName}({qualifiedPortName})" + summaryLine = ( + f'"{variable.tmfName}" = {computedVariable}' + if direction == "input" + else f'{computedVariable} = "{variable.tmfName}"' + ) + + return summaryLine + + +def _joinNonEmptyStringsWithNewLines(*strings: str) -> str: + nonEmptyStrings = [s for s in strings if s] + return "\n".join(nonEmptyStrings) + + +@_dc.dataclass +class Fluid: + density: Variable | None = None + heatCapacity: Variable | None = None + + @property + def allVariables(self) -> _cabc.Sequence[Variable]: + return _removeUnsetAndNone(self.density, self.heatCapacity) + + @property + def areAnyRequiredVariablesUnset(self) -> bool: + return False + + def getSummary(self, qualifiedPortName: str) -> str: + summary = _joinNonEmptyStringsWithNewLines( + _getSummaryLine(qualifiedPortName, AllVariableStringConstants.DENSITY, self.density, "input"), + _getSummaryLine(qualifiedPortName, AllVariableStringConstants.HEAT_CAPACITY, self.heatCapacity, "input"), + ) + + return summary + + +@_dc.dataclass +class Connection: + name: str | None + inputPort: "InputPort" + outputPort: "OutputPort" + fluid: "Fluid" = _dc.field(default_factory=Fluid) + + @property + def allVariables(self) -> _cabc.Sequence[Variable]: + return [*self.inputPort.allVariables, *self.outputPort.allVariables, *self.fluid.allVariables] + + @property + def areAnyRequiredVariablesUnset(self) -> bool: + return ( + self.inputPort.areAnyRequiredVariablesUnset + or self.outputPort.areAnyRequiredVariablesUnset + or self.fluid.areAnyRequiredVariablesUnset + ) + + def getSummary(self) -> str: + qualifiedInputPortName = _getQualifiedPortName(self.name, self.inputPort.name) + fluidSummary = self.fluid.getSummary(qualifiedInputPortName) + inputPortSummary = self.inputPort.getSummary(self.name) + outputPortSummary = self.outputPort.getSummary(self.name) + + subSummaries = _joinNonEmptyStringsWithNewLines(fluidSummary, inputPortSummary, outputPortSummary) + + if not subSummaries: + return "" + + connectionName = self.name if self.name else "Default connection" + summary = f"""\ +** {connectionName} +{subSummaries} +""" + return summary + + +def _getQualifiedPortName(connectionName: str | None, portName: str) -> str: + capitalizedPortName = portName.capitalize() + + qualifiedPortName = f"{connectionName.capitalize()}{capitalizedPortName}" if connectionName else capitalizedPortName + + return qualifiedPortName + + +def _getSetVariable(variable: Variable | Unset) -> Variable: + if not isinstance(variable, Variable): + raise ValueError("Required variable not set.") + + return variable + + +@_dc.dataclass +class InputPort: + name: str + temperature: "RequiredVariable" = UNSET + massFlowRate: "RequiredVariable" = UNSET + + @property + def temperatureSet(self) -> Variable: + return _getSetVariable(self.temperature) + + @property + def massFlowRateSet(self) -> "Variable": + return _getSetVariable(self.massFlowRate) + + @property + def allVariables(self) -> _cabc.Sequence[Variable]: + return _removeUnsetAndNone(self.temperature, self.massFlowRate) + + @property + def areAnyRequiredVariablesUnset(self) -> bool: + return UNSET in (self.temperature, self.massFlowRate) + + def getSummary(self, connectionName: str | None) -> str: + qualifiedPortName = _getQualifiedPortName(connectionName, self.name) + summary = _joinNonEmptyStringsWithNewLines( + _getSummaryLine(qualifiedPortName, AllVariableStringConstants.MASS_FLOW_RATE, self.massFlowRate, "input"), + _getSummaryLine(qualifiedPortName, AllVariableStringConstants.TEMPERATURE, self.temperature, "input"), + ) + + return summary + + +@_dc.dataclass +class OutputPort: + name: str + temperature: "RequiredVariable" = UNSET + reverseTemperature: Variable | None = None + + @property + def temperatureSet(self) -> Variable: + return _getSetVariable(self.temperature) + + @property + def allVariables(self) -> _cabc.Sequence[Variable]: + return _removeUnsetAndNone(self.temperature, self.reverseTemperature) + + @property + def areAnyRequiredVariablesUnset(self): + return self.temperature == UNSET + + def getSummary(self, connectionName: str | None) -> str: + qualifiedPortName = _getQualifiedPortName(connectionName, self.name) + summary = _joinNonEmptyStringsWithNewLines( + _getSummaryLine(qualifiedPortName, AllVariableStringConstants.TEMPERATURE, self.temperature, "output"), + _getSummaryLine( + qualifiedPortName, AllVariableStringConstants.REVERSE_TEMPERATURE, self.reverseTemperature, "input" + ), + ) + + return summary + + +def _removeUnsetAndNone(*variables: Variable | Unset | None) -> _cabc.Sequence[Variable]: + return [v for v in variables if isinstance(v, Variable)] + + +@_dc.dataclass +class VariablesByRole: + parameters: _cabc.Sequence[Variable] + inputs: _cabc.Sequence[Variable] + outputs: _cabc.Sequence[Variable] + + @property + def allVariables(self) -> _cabc.Sequence[Variable]: + return [*self.parameters, *self.inputs, *self.outputs] diff --git a/trnsysGUI/proforma/templates/ddck.jinja b/trnsysGUI/proforma/templates/ddck.jinja new file mode 100644 index 00000000..92857a66 --- /dev/null +++ b/trnsysGUI/proforma/templates/ddck.jinja @@ -0,0 +1,108 @@ +******************************* +** BEGIN {{fileName}} +******************************* + +*************************************************************************** +** Description: +{{description}} +*************************************************************************** + +*************************************************************************** +** Details: +{{details}} +*************************************************************************** + +*********************************** +** inputs from hydraulic solver +*********************************** +{% set inputsWithHydraulicData = (parameters + inputs)|rejectattr("hydraulicConnectionsData", "none")|list %} +{% if inputsWithHydraulicData|length %} +EQUATIONS {{inputsWithHydraulicData|length}} + {% for input in inputsWithHydraulicData %} +{{visibilityModifier}}{{input.hydraulicConnectionsData.variableName}} = {{input.hydraulicConnectionsData.rhs}} + {% endfor %} +{% endif %} + +*********************************** +** outputs to hydraulic solver +*********************************** +{% set outputsWithHydraulicData = outputs|rejectattr("hydraulicConnectionsData", "none")|list %} +{% if outputsWithHydraulicData|length %} +EQUATIONS {{outputsWithHydraulicData|length}} + {% for outputWithHydraulicData in outputsWithHydraulicData %} +{{outputWithHydraulicData.hydraulicConnectionsData.rhs}} = {{visibilityModifier}}{{outputWithHydraulicData.hydraulicConnectionsData.variableName}} + {% endfor %} +{% endif %} + +*********************************** +** outputs to other ddck +*********************************** + + +****************************************************************************************** +** outputs to energy balance in kWh and ABSOLUTE value +****************************************************************************************** + + +*********************************** +** Dependencies with other ddck +*********************************** + + +*********************************** +** Begin CONSTANTS +*********************************** + + +*********************************** +** Begin TYPE +*********************************** +UNIT 1 TYPE {{type}} +PARAMETERS {{parameters|length}} +{% for parameter in parameters %} + {% if parameter.hydraulicConnectionsData %} +{{visibilityModifier}}{{parameter.hydraulicConnectionsData.variableName}} ! {{parameter.info}} + {% else %} +{{parameter.defaultValue}} ! {{parameter.info}} + {% endif %} +{% endfor %} +INPUTS {{inputs|length}} +{% for input in inputs %} + {% if input.hydraulicConnectionsData %} +{{visibilityModifier}}{{input.hydraulicConnectionsData.variableName}} ! {{input.info}} + {% else %} +0,0 ! {{input.roleOrder}}: {{input.tmfName}} [{{input.unit}}] ({{input.bounds}}) + {% endif %} +{% endfor %} +** initial values +{% for input in inputs %} +{{input.defaultValue}} ! {{input.roleOrder}}: {{input.tmfName}} initial value +{% endfor %} + +{% if outputsWithHydraulicData|length %} +EQUATIONS {{outputsWithHydraulicData|length}} ! {{outputs|length}} +{% else %} +! EQUATIONS {{outputs|length}} +{% endif %} +{% for output in outputs %} + {% if output.hydraulicConnectionsData %} +{{visibilityModifier}}{{output.hydraulicConnectionsData.variableName}} = [1, {{output.roleOrder}}] ! {{output.tmfName}} [{{output.unit}}] ({{output.bounds}}) + {% else %} +! {{visibilityModifier}}XXX = [1, {{output.roleOrder}}] ! {{output.tmfName}} [{{output.unit}}] ({{output.bounds}}) + {% endif %} +{% endfor %} + +*********************************** +** Monthly printer +*********************************** + + +*********************************** +** Hourly printer +*********************************** + + +*********************************** +** Online Plotter +*********************************** + diff --git a/trnsysGUI/proforma/xmltmf.xsd b/trnsysGUI/proforma/xmltmf.xsd new file mode 100644 index 00000000..d9f1227b --- /dev/null +++ b/trnsysGUI/proforma/xmltmf.xsd @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/trnsysGUI/pytrnsys-gui.pyproject b/trnsysGUI/pytrnsys-gui.pyproject index e315ed35..f8d22c32 100644 --- a/trnsysGUI/pytrnsys-gui.pyproject +++ b/trnsysGUI/pytrnsys-gui.pyproject @@ -1,3 +1,3 @@ { - "files": ["resources/resources.qrc","hydraulicLoops/_dialogs/edit/_dialog.ui","dialogs/connections/_doublePipe.ui","hydraulicLoops/_dialogs/split/_dialog.ui","hydraulicLoops/_dialogs/merge/_dialog.ui","pumpsAndTaps/_dialog.ui"] + "files": ["hydraulicLoops/_dialogs/split/_dialog.ui","pumpsAndTaps/_dialog.ui","resources/resources.qrc","hydraulicLoops/_dialogs/merge/_dialog.ui","dialogs/connections/_doublePipe.ui","hydraulicLoops/_dialogs/edit/_dialog.ui", "proforma/dialogs/_convert.ui", "proforma/dialogs/_hydraulicConnections.ui"] } diff --git a/trnsysGUI/storageTank/widget.py b/trnsysGUI/storageTank/widget.py index a82b2c61..6a8257af 100644 --- a/trnsysGUI/storageTank/widget.py +++ b/trnsysGUI/storageTank/widget.py @@ -73,6 +73,11 @@ def getDisplayName(self) -> str: def hasDdckPlaceHolders(cls) -> bool: return False + @classmethod + @_tp.override + def hasDdckDirectory(cls) -> bool: + return True + @property def leftDirectPortPairsPortItems(self): return self._getDirectPortPairPortItems(_sd.Side.LEFT) @@ -339,28 +344,10 @@ def _getSingleOrNone(iterable: _tp.Iterable[_T_co]) -> _tp.Optional[_T_co]: return sequence[0] # Misc - def contextMenuEvent(self, event): - menu = _qtw.QMenu() - - rotateRightIcon = _img.ROTATE_TO_RIGHT_PNG.icon() - rotateRightAction = menu.addAction(rotateRightIcon, "Rotate Block clockwise") - rotateRightAction.triggered.connect(self.rotateBlockCW) - - rotateLeftIcon = _img.ROTATE_LEFT_PNG.icon() - rotateLeftIcon = menu.addAction(rotateLeftIcon, "Rotate Block counter-clockwise") - rotateLeftIcon.triggered.connect(self.rotateBlockCCW) - - resetRotationAction = menu.addAction("Reset Rotation") - resetRotationAction.triggered.connect(self.resetRotation) - - deleteBlockAction = menu.addAction("Delete this Block") - deleteBlockAction.triggered.connect(self.deleteBlockCom) - - exportDdckAction = menu.addAction("Export ddck") + def _addChildContextMenuActions(self, contextMenu: _qtw.QMenu) -> None: + exportDdckAction = contextMenu.addAction("Export ddck") exportDdckAction.triggered.connect(self.exportDck) - menu.exec(event.screenPos()) - def mouseDoubleClickEvent(self, event: _qtw.QGraphicsSceneMouseEvent) -> None: renameHelper = _rename.RenameHelper(self.editor.namesManager) dialog = ConfigureStorageDialog(self, self.editor, renameHelper, self.editor.projectFolder) @@ -431,6 +418,15 @@ def exportDck(self) -> None: # pylint: disable=too-many-locals,too-many-stateme projectDirPath = _pl.Path(self.editor.projectFolder) ddckDirPath = _dfh.getComponentDdckDirPath(self.displayName, projectDirPath) + if not ddckDirPath.is_dir(): + _qtw.QMessageBox.information( + None, + "Component ddck directory doesn't exist", + f"The component ddck directory `{ddckDirPath}` does not exist. The ddck file will not be exported. " + f"Please create the directory and try again.", + ) + return + tool.createDDck(str(ddckDirPath), self.displayName, typeFile="ddck") def _getDirectPairPortsForExport(self):