heatpumplab 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. heatpumplab-0.1.0/.gitignore +27 -0
  2. heatpumplab-0.1.0/HeatPumpLab/__init__.py +17 -0
  3. heatpumplab-0.1.0/HeatPumpLab/components/__init__.py +29 -0
  4. heatpumplab-0.1.0/HeatPumpLab/components/common/__init__.py +5 -0
  5. heatpumplab-0.1.0/HeatPumpLab/components/common/base_heat_exchanger.py +496 -0
  6. heatpumplab-0.1.0/HeatPumpLab/components/compressor.py +166 -0
  7. heatpumplab-0.1.0/HeatPumpLab/components/custom_relationship.py +355 -0
  8. heatpumplab-0.1.0/HeatPumpLab/components/economiser.py +180 -0
  9. heatpumplab-0.1.0/HeatPumpLab/components/evaporator.py +30 -0
  10. heatpumplab-0.1.0/HeatPumpLab/components/expansion_valve.py +64 -0
  11. heatpumplab-0.1.0/HeatPumpLab/components/gas_cooler.py +31 -0
  12. heatpumplab-0.1.0/HeatPumpLab/components/heat_exchanger.py +33 -0
  13. heatpumplab-0.1.0/HeatPumpLab/components/mixer.py +73 -0
  14. heatpumplab-0.1.0/HeatPumpLab/components/plate_heat_exchanger.py +38 -0
  15. heatpumplab-0.1.0/HeatPumpLab/components/setRelationship.py +79 -0
  16. heatpumplab-0.1.0/HeatPumpLab/components/splitter.py +64 -0
  17. heatpumplab-0.1.0/HeatPumpLab/components/stream.py +229 -0
  18. heatpumplab-0.1.0/HeatPumpLab/components/turbine.py +44 -0
  19. heatpumplab-0.1.0/HeatPumpLab/cycles/BraytonHeatPump/__init__.py +12 -0
  20. heatpumplab-0.1.0/HeatPumpLab/cycles/BraytonHeatPump/brayton_hp.py +558 -0
  21. heatpumplab-0.1.0/HeatPumpLab/cycles/CascadeHeatPump/__init__.py +6 -0
  22. heatpumplab-0.1.0/HeatPumpLab/cycles/CascadeHeatPump/cascade_hp.py +502 -0
  23. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/__init__.py +66 -0
  24. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/compressor_curves.py +540 -0
  25. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/compressor_data.py +117 -0
  26. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/compressor_window.py +205 -0
  27. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/conventional_hp.py +771 -0
  28. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/density_basis.py +291 -0
  29. heatpumplab-0.1.0/HeatPumpLab/cycles/ConventionalHeatPump/water_steam.py +123 -0
  30. heatpumplab-0.1.0/HeatPumpLab/cycles/MVRHeatPump/__init__.py +13 -0
  31. heatpumplab-0.1.0/HeatPumpLab/cycles/MVRHeatPump/mvr_hp.py +540 -0
  32. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/ABOUT.md +24 -0
  33. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/BAYESIAN_OPTIMISATION.md +107 -0
  34. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/BOTORCH_COMPARISON.md +95 -0
  35. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/BOTORCH_FAILURE_INVESTIGATION.md +100 -0
  36. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/BOTORCH_PROTOCOL.md +100 -0
  37. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/NEAR_ZERO_INVESTIGATION.md +184 -0
  38. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/OPTIMISER_COMPARISON.md +168 -0
  39. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/OPTIMISER_RETEST_V13_400.md +32 -0
  40. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/ORIGIN_OPTIMISER_COMPARISON.md +112 -0
  41. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/PH_INTERPOLATION_INVESTIGATION.md +57 -0
  42. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/REMAINING_ISSUES_INVESTIGATION.md +91 -0
  43. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/ROBUSTNESS_INVESTIGATION.md +621 -0
  44. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/SMOOTHNESS_FIXES.md +98 -0
  45. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/TRUST_REGION_COMPARISON.md +121 -0
  46. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/TRUST_REGION_PROTOCOL.md +101 -0
  47. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/V14_SOLVER_FIXES.md +61 -0
  48. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/V15_SOLVER_FIXES.md +71 -0
  49. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/V17_INTERNAL_PH_FALLBACK.md +50 -0
  50. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/__init__.py +25 -0
  51. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/_refrigerant.py +141 -0
  52. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/__init__.py +56 -0
  53. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_batch.py +248 -0
  54. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_bayesian.py +424 -0
  55. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_cases.py +262 -0
  56. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_constraint_model.py +102 -0
  57. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_incumbents.py +280 -0
  58. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_isolated_evaluation.py +207 -0
  59. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_objective.py +551 -0
  60. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_results_io.py +398 -0
  61. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_torch_env.py +25 -0
  62. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/_types.py +327 -0
  63. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/origin_search.py +115 -0
  64. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/outer_design.py +119 -0
  65. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/optimisation/surrogate_search.py +173 -0
  66. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/rhp_base.py +259 -0
  67. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/rhp_helpers.py +781 -0
  68. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/__init__.py +5 -0
  69. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/_cycle_analysis.py +138 -0
  70. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/_discharge_pressure.py +403 -0
  71. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/_evaporator_alignment.py +100 -0
  72. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/_fluid_policy.py +20 -0
  73. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/_gas_cooler_pinch.py +180 -0
  74. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/_metrics.py +91 -0
  75. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/solver/cycle_solver.py +181 -0
  76. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/__init__.py +17 -0
  77. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/analysis.py +516 -0
  78. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/cases.py +243 -0
  79. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/composition_pareto.py +170 -0
  80. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/fluid_properties_table.py +73 -0
  81. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/optimiser_benchmark_report.py +164 -0
  82. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/origin_benchmark_report.py +260 -0
  83. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/plot_comp2_power_frac_analysis.py +401 -0
  84. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/plot_heatmap_metrics.py +495 -0
  85. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/single_case_analysis.py +159 -0
  86. heatpumplab-0.1.0/HeatPumpLab/cycles/RecuperativeHeatPump/utils/summarize_best_results.py +313 -0
  87. heatpumplab-0.1.0/HeatPumpLab/cycles/SplitHeatPump/__init__.py +5 -0
  88. heatpumplab-0.1.0/HeatPumpLab/cycles/SplitHeatPump/split_hp.py +452 -0
  89. heatpumplab-0.1.0/HeatPumpLab/cycles/TTCascadeHeatPump/__init__.py +8 -0
  90. heatpumplab-0.1.0/HeatPumpLab/cycles/TTCascadeHeatPump/tt_cascade.py +498 -0
  91. heatpumplab-0.1.0/HeatPumpLab/cycles/__init__.py +1 -0
  92. heatpumplab-0.1.0/HeatPumpLab/cycles/common/__init__.py +27 -0
  93. heatpumplab-0.1.0/HeatPumpLab/cycles/common/dryer.py +611 -0
  94. heatpumplab-0.1.0/HeatPumpLab/cycles/common/heatPump.py +94 -0
  95. heatpumplab-0.1.0/HeatPumpLab/cycles/common/scalar_search.py +191 -0
  96. heatpumplab-0.1.0/HeatPumpLab/cycles/common/solver.py +120 -0
  97. heatpumplab-0.1.0/HeatPumpLab/utils/__init__.py +33 -0
  98. heatpumplab-0.1.0/HeatPumpLab/utils/analysis.py +219 -0
  99. heatpumplab-0.1.0/HeatPumpLab/utils/coolprop_properties.py +858 -0
  100. heatpumplab-0.1.0/HeatPumpLab/utils/decorator_functions.py +16 -0
  101. heatpumplab-0.1.0/HeatPumpLab/utils/graph.py +447 -0
  102. heatpumplab-0.1.0/HeatPumpLab/utils/parallel_solving.py +55 -0
  103. heatpumplab-0.1.0/HeatPumpLab/utils/properties_package.py +789 -0
  104. heatpumplab-0.1.0/HeatPumpLab/utils/property_cache.py +59 -0
  105. heatpumplab-0.1.0/HeatPumpLab/utils/readWriteOutput.py +126 -0
  106. heatpumplab-0.1.0/HeatPumpLab/utils/solve_profile.py +92 -0
  107. heatpumplab-0.1.0/LICENSE +177 -0
  108. heatpumplab-0.1.0/PKG-INFO +263 -0
  109. heatpumplab-0.1.0/README.md +234 -0
  110. heatpumplab-0.1.0/apps/dryer/README.md +38 -0
  111. heatpumplab-0.1.0/apps/steam/README.md +40 -0
  112. heatpumplab-0.1.0/pyproject.toml +72 -0
@@ -0,0 +1,27 @@
1
+ # Python environments and generated caches
2
+ *.py[cod]
3
+ __pycache__/
4
+ /.venv/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ .hypothesis/
9
+ .ipynb_checkpoints/
10
+
11
+ # Python packaging and coverage reports
12
+ /build/
13
+ /dist/
14
+ *.egg-info/
15
+ htmlcov/
16
+
17
+ # macOS metadata
18
+ .DS_Store
19
+
20
+ # Local research outputs
21
+ /results/
22
+ /paper/
23
+ /experiments/
24
+ /outputs/
25
+
26
+ # Local runner scripts and their tests (not tracked)
27
+ /local_scripts/
@@ -0,0 +1,17 @@
1
+ """Recuperative Heat Pump public API helpers."""
2
+
3
+ import warnings
4
+
5
+ # linear_operator 0.6.1 (via GPyTorch/BoTorch) decorates two CG helpers with @torch.jit.script at
6
+ # import time, and recent torch releases warn that torch.jit.script is deprecated. Harmless; silence only that
7
+ # message, before anything imports BoTorch. Child processes inherit nothing, but they import this
8
+ # package first as well.
9
+ warnings.filterwarnings(
10
+ "ignore", message=r"`torch\.jit\.script` is deprecated", category=FutureWarning
11
+ )
12
+
13
+ from .utils import get_parameter_sweep # noqa: E402
14
+
15
+ __all__ = [
16
+ "get_parameter_sweep",
17
+ ]
@@ -0,0 +1,29 @@
1
+ """Convenience imports for component classes."""
2
+
3
+ from .stream import Stream
4
+ from .compressor import Compressor
5
+ from .evaporator import Evaporator
6
+ from .expansion_valve import ExpansionValve
7
+ from .gas_cooler import GasCooler
8
+ from .plate_heat_exchanger import PlateHeatExchanger
9
+ from .turbine import Turbine
10
+ from .custom_relationship import CustomRelationship
11
+ from .setRelationship import SetRelationship
12
+ from .economiser import Economiser
13
+ from .mixer import Mixer
14
+ from .splitter import Splitter
15
+
16
+ __all__ = [
17
+ "Stream",
18
+ "Compressor",
19
+ "Evaporator",
20
+ "ExpansionValve",
21
+ "GasCooler",
22
+ "PlateHeatExchanger",
23
+ "Turbine",
24
+ "CustomRelationship",
25
+ "SetRelationship",
26
+ "Economiser",
27
+ "Mixer",
28
+ "Splitter",
29
+ ]
@@ -0,0 +1,5 @@
1
+ """Expose common component base classes and physical-domain errors."""
2
+
3
+ from .base_heat_exchanger import BaseHeatExchanger, TemperatureCrossError
4
+
5
+ __all__ = ["BaseHeatExchanger", "TemperatureCrossError"]
@@ -0,0 +1,496 @@
1
+ """Abstract base support for heat exchanger component models."""
2
+
3
+ from math import log
4
+ from ..stream import Stream
5
+
6
+
7
+ class TemperatureCrossError(ValueError):
8
+ """Report a non-positive heat-exchanger temperature approach."""
9
+
10
+ def __init__(self, approach_violation: float) -> None:
11
+ self.approach_violation = max(0.0, float(approach_violation))
12
+ super().__init__("LMTD is undefined because a temperature cross occurs.")
13
+
14
+
15
+ class BaseHeatExchanger:
16
+ """Shared heat-exchanger behaviour extended by concrete component models."""
17
+
18
+ # Set the stream connection points
19
+ name: str = ""
20
+ model: str
21
+ source_fluid: str
22
+ source_in: Stream
23
+ source_pinch: Stream
24
+ source_out: Stream
25
+ source_out_max: Stream
26
+ sink_fluid: str
27
+ sink_in: Stream
28
+ sink_pinch: Stream
29
+ sink_out: Stream
30
+ sink_out_max: Stream
31
+ pinch_length = None
32
+ bypassed: bool = False
33
+ converged: bool = False
34
+ HX_Eff: float = 0.0
35
+ NTU: float = 0.0
36
+ c: float = 0.0
37
+ U: float = 0.0
38
+ Area: float = 0.0
39
+ duty: float = 0.0
40
+ delta_entropy_src: float = 0.0
41
+ delta_entropy_snk: float = 0.0
42
+ dP_h: float = 0.0
43
+ dP_c: float = 0.0
44
+ variable_dP_model: bool = False
45
+ tol: float = 1e-6
46
+
47
+ def __init__(self) -> None:
48
+ """Initialise placeholder attributes for subclasses."""
49
+ pass
50
+
51
+ def set_port_mass_streams(self, source_fluid: str, sink_fluid: str) -> None:
52
+ """Create inlet, pinch, and outlet streams for both sides."""
53
+ self.source_in = Stream(name="source in", fluid=source_fluid)
54
+ self.source_pinch = Stream(name="source pinch", fluid=source_fluid)
55
+ self.source_out = Stream(name="source out", fluid=source_fluid)
56
+ self.source_out_max = Stream(name="source out max", fluid=source_fluid)
57
+
58
+ self.sink_in = Stream(name="sink in", fluid=sink_fluid)
59
+ self.sink_pinch = Stream(name="sink pinch", fluid=sink_fluid)
60
+ self.sink_out = Stream(name="sink out", fluid=sink_fluid)
61
+ self.sink_out_max = Stream(name="sink out max", fluid=sink_fluid)
62
+
63
+ def solve(self):
64
+ """Evaluate the exchanger according to the selected performance model."""
65
+ if self.bypassed:
66
+ self.source_out = self.source_in
67
+ self.sink_out = self.sink_in
68
+ self.converged = True
69
+ else:
70
+ if (
71
+ self.source_in.mass_flow <= self.tol
72
+ or self.sink_in.mass_flow <= self.tol
73
+ ):
74
+ self.source_out.copy_fld_props(fld=self.source_in)
75
+ self.source_out.set_flows(self.source_in.mass_flow)
76
+ self.sink_out.copy_fld_props(fld=self.sink_in)
77
+ self.sink_out.set_flows(self.sink_in.mass_flow)
78
+ self.converged = True
79
+
80
+ else:
81
+ self.get_performance()
82
+
83
+ self.duty = self.sink_out.heat_flow - self.sink_in.heat_flow
84
+ if self._has_active_heat_transfer():
85
+ self.converged = self.converged and self._has_valid_heat_transfer_direction()
86
+ self.delta_entropy_src = (
87
+ abs(self.source_in.entropy - self.source_out.entropy)
88
+ * self.source_out.mass_flow
89
+ )
90
+ self.delta_entropy_snk = (
91
+ abs(self.sink_in.entropy - self.sink_out.entropy) * self.sink_out.mass_flow
92
+ )
93
+
94
+ def _has_active_heat_transfer(self) -> bool:
95
+ """Return whether the exchanger should enforce heat-flow direction."""
96
+ return (
97
+ not self.bypassed
98
+ and self.source_in.mass_flow > self.tol
99
+ and self.sink_in.mass_flow > self.tol
100
+ )
101
+
102
+ def _has_valid_heat_transfer_direction(self) -> bool:
103
+ """Return whether heat flows from the source side to the sink side."""
104
+ return (
105
+ self.duty >= -self.tol
106
+ and self.source_out.temperature <= self.source_in.temperature + self.tol
107
+ and self.sink_out.temperature >= self.sink_in.temperature - self.tol
108
+ )
109
+
110
+ def get_performance(self):
111
+ """Dispatch to the underlying heat exchanger formulation."""
112
+ if self.model == "dTmin_terminal":
113
+ self.get_dTmin_terminal_model()
114
+
115
+ elif self.model == "effectiveness":
116
+ self.get_effectiveness_model()
117
+
118
+ elif self.model == "fixed cold duty":
119
+ self.get_fixed_cold_duty_model()
120
+
121
+ elif self.model == "fixed hot duty":
122
+ self.get_fixed_hot_duty_model()
123
+
124
+ elif self.model == "fixed cold temperature outlet":
125
+ self.get_fixed_cold_T_outlet_model()
126
+
127
+ elif self.model == "fixed hot temperature outlet":
128
+ self.get_fixed_hot_T_outlet_model()
129
+
130
+ elif self.model == "fixed hot quality outlet":
131
+ self.get_fixed_hot_Q_outlet_model()
132
+
133
+ elif self.model == "fixed cold delta temperature":
134
+ self.get_fixed_cold_delta_T_model()
135
+
136
+ elif self.model == "fixed hot delta temperature":
137
+ self.get_fixed_hot_delta_T_model()
138
+
139
+ elif self.model == "saturated gas outlet evaporator":
140
+ self.get_saturated_gas_sink_out_model()
141
+
142
+ elif self.model == "specialised":
143
+ self.get_detailed_performance()
144
+
145
+ else:
146
+ raise ValueError("No heat exchanger model selected.")
147
+
148
+ def _preserve_negligible_duty_states(self):
149
+ """Keep unchanged states when heat is below the absolute duty tolerance."""
150
+ if not (0 <= self.duty <= self.tol and self.dP_h == 0 and self.dP_c == 0):
151
+ return False
152
+ self.source_out.copy_fld_props(self.source_in)
153
+ self.source_out.set_flows(self.source_in.mass_flow)
154
+ self.sink_out.copy_fld_props(self.sink_in)
155
+ self.sink_out.set_flows(self.sink_in.mass_flow)
156
+ self.duty = 0.0
157
+ self.converged = self.source_in.temperature >= self.sink_in.temperature
158
+ return True
159
+
160
+ def get_dTmin_terminal_model(self):
161
+ """Solve for the duty limited by the specified terminal delta T."""
162
+ try:
163
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
164
+ self.source_out_max.props_in_ab(
165
+ "PT", self.source_out.pressure, self.sink_in.temperature + self.dTmin
166
+ )
167
+ self.dH_h_max = self.source_in.mass_flow * (
168
+ self.source_in.enthalpy - self.source_out_max.enthalpy
169
+ )
170
+
171
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
172
+ self.sink_out_max.props_in_ab(
173
+ "PT", self.sink_out.pressure, self.source_in.temperature - self.dTmin
174
+ )
175
+ self.dH_c_max = self.sink_in.mass_flow * (
176
+ self.sink_out_max.enthalpy - self.sink_in.enthalpy
177
+ )
178
+
179
+ self.duty = min(self.dH_h_max, self.dH_c_max)
180
+
181
+ if self._preserve_negligible_duty_states():
182
+ # Below the component's absolute duty tolerance, preserve both
183
+ # inlet states. Reflashing can introduce a larger enthalpy error
184
+ # than the heat being transferred. Zero duty conserves energy
185
+ # exactly on both sides; never apply this to pressure drops or
186
+ # negative requested duty.
187
+ return
188
+
189
+ self.source_out.enthalpy = (
190
+ self.source_in.enthalpy - self.duty / self.source_in.mass_flow
191
+ )
192
+ self.source_out.props_in_mPH()
193
+ self.sink_out.enthalpy = (
194
+ self.sink_in.enthalpy + self.duty / self.sink_in.mass_flow
195
+ )
196
+ self.sink_out.props_in_mPH()
197
+ except Exception as exc:
198
+ raise ValueError(
199
+ "Warning: Phase change occuring in heat exchanger " + self.name
200
+ ) from exc
201
+
202
+ self.converged = (
203
+ True
204
+ if self.source_out.temperature > self.sink_in.temperature
205
+ and self.source_in.temperature > self.sink_out.temperature
206
+ else False
207
+ )
208
+
209
+ def get_effectiveness_model(self):
210
+ """Apply an effectiveness definition to compute energy transfer."""
211
+ try:
212
+ self.dT_max = abs(self.source_in.temperature - self.sink_in.temperature)
213
+
214
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
215
+ self.source_out_max.pressure = self.source_out.pressure
216
+ self.source_out_max.temperature = (
217
+ self.sink_in.temperature + self.dT_max * self.HX_Eff
218
+ )
219
+ self.source_out_max.props_in_mPT()
220
+ self.dH_h_max = self.source_in.mass_flow * (
221
+ self.source_in.enthalpy - self.source_out_max.enthalpy
222
+ )
223
+
224
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
225
+ self.sink_out_max.pressure = self.sink_out.pressure
226
+ self.sink_out_max.temperature = (
227
+ self.source_in.temperature - self.dT_max * self.HX_Eff
228
+ )
229
+ self.sink_out_max.props_in_mPT()
230
+ self.dH_c_max = self.sink_in.mass_flow * (
231
+ self.sink_out_max.enthalpy - self.sink_in.enthalpy
232
+ )
233
+
234
+ self.duty = min(self.dH_h_max, self.dH_c_max)
235
+
236
+ self.source_out.enthalpy = (
237
+ self.source_in.enthalpy - self.duty / self.source_in.mass_flow
238
+ )
239
+ self.source_out.props_in_mPH()
240
+ self.sink_out.enthalpy = (
241
+ self.sink_in.enthalpy + self.duty / self.sink_in.mass_flow
242
+ )
243
+ self.sink_out.props_in_mPH()
244
+
245
+ except Exception as exc:
246
+ raise ValueError(
247
+ "Warning: Phase change occuring in heat exchanger " + self.name
248
+ ) from exc
249
+
250
+ self.converged = (
251
+ True
252
+ if self.source_out.temperature > self.sink_in.temperature
253
+ and self.source_in.temperature > self.sink_out.temperature
254
+ else False
255
+ )
256
+
257
+ def get_fixed_cold_duty_model(self):
258
+ """Resolve state changes when cold-side duty is fixed."""
259
+ self.duty = self.sink_out.heat_flow - self.sink_in.heat_flow
260
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
261
+ self.sink_out.props_in_mPH()
262
+
263
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
264
+ self.source_out.enthalpy = (
265
+ self.source_in.enthalpy - self.duty / self.source_out.mass_flow
266
+ )
267
+ self.source_out.props_in_mPH()
268
+
269
+ self.converged = (
270
+ True
271
+ if self.source_out.temperature > self.sink_in.temperature
272
+ and self.source_in.temperature > self.sink_out.temperature
273
+ else False
274
+ )
275
+
276
+ def get_fixed_hot_duty_model(self):
277
+ """Resolve state changes when hot-side duty is fixed."""
278
+ self.duty = self.source_in.heat_flow - self.source_out.heat_flow
279
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
280
+ self.source_out.props_in_mPH()
281
+
282
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
283
+ self.sink_out.enthalpy = (
284
+ self.sink_in.enthalpy + self.duty / self.sink_in.mass_flow
285
+ )
286
+ self.sink_out.props_in_mPH()
287
+
288
+ self.converged = (
289
+ True
290
+ if self.source_out.temperature > self.sink_in.temperature
291
+ and self.source_in.temperature > self.sink_out.temperature
292
+ else False
293
+ )
294
+
295
+ def get_fixed_cold_T_outlet_model(self):
296
+ """Solve with a fixed cold outlet temperature constraint."""
297
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
298
+ self.sink_out.props_in_mPT()
299
+ self.duty = self.sink_out.heat_flow - self.sink_in.heat_flow
300
+
301
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
302
+ self.source_out.enthalpy = (
303
+ self.source_in.enthalpy - self.duty / self.source_out.mass_flow
304
+ )
305
+ self.source_out.props_in_mPH()
306
+
307
+ self.converged = (
308
+ True
309
+ if self.source_out.temperature > self.sink_in.temperature
310
+ and self.source_in.temperature > self.sink_out.temperature
311
+ else False
312
+ )
313
+
314
+ def get_fixed_hot_T_outlet_model(self):
315
+ """Solve with a fixed hot outlet temperature constraint."""
316
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
317
+ self.source_out.props_in_mPT()
318
+ self.duty = self.source_in.heat_flow - self.source_out.heat_flow
319
+
320
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
321
+ self.sink_out.enthalpy = (
322
+ self.sink_in.enthalpy + self.duty / self.sink_in.mass_flow
323
+ )
324
+ self.sink_out.props_in_mPH()
325
+
326
+ self.converged = (
327
+ True
328
+ if self.source_out.temperature > self.sink_in.temperature
329
+ and self.source_in.temperature > self.sink_out.temperature
330
+ else False
331
+ )
332
+
333
+ def get_fixed_hot_Q_outlet_model(self):
334
+ """Solve with a fixed hot outlet vapour quality constraint."""
335
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
336
+ self.source_out.props_in_mPQ()
337
+ self.duty = self.source_in.heat_flow - self.source_out.heat_flow
338
+
339
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
340
+ self.sink_out.enthalpy = (
341
+ self.sink_in.enthalpy + self.duty / self.sink_in.mass_flow
342
+ )
343
+ self.sink_out.props_in_mPH()
344
+
345
+ self.converged = (
346
+ True
347
+ if self.source_out.temperature > self.sink_in.temperature
348
+ and self.source_in.temperature > self.sink_out.temperature
349
+ else False
350
+ )
351
+
352
+ def get_fixed_cold_delta_T_model(self):
353
+ """Resolve states when enforcing a cold-side temperature rise."""
354
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
355
+ self.sink_out.temperature = self.sink_in.temperature + self.dT_c
356
+ self.sink_out.props_in_mPT()
357
+ self.duty = self.sink_out.heat_flow - self.sink_in.heat_flow
358
+
359
+ if 0 <= self.dT_c <= self.tol and self._preserve_negligible_duty_states():
360
+ return
361
+
362
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
363
+ self.source_out.enthalpy = (
364
+ self.source_in.enthalpy - self.duty / self.source_out.mass_flow
365
+ )
366
+ self.source_out.props_in_mPH()
367
+
368
+ self.converged = (
369
+ True
370
+ if self.source_out.temperature > self.sink_in.temperature
371
+ and self.source_in.temperature > self.sink_out.temperature
372
+ else False
373
+ )
374
+
375
+ def get_fixed_hot_delta_T_model(self):
376
+ """Resolve states when enforcing a hot-side temperature drop."""
377
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
378
+ self.source_out.temperature = self.source_in.temperature - self.dT_h
379
+ self.source_out.props_in_mPT()
380
+ self.duty = self.source_in.heat_flow - self.source_out.heat_flow
381
+
382
+ if 0 <= self.dT_h <= self.tol and self._preserve_negligible_duty_states():
383
+ return
384
+
385
+ self.sink_out.pressure = self.sink_in.pressure - self.dP_c
386
+ self.sink_out.enthalpy = (
387
+ self.sink_in.enthalpy + self.duty / self.sink_in.mass_flow
388
+ )
389
+ self.sink_out.props_in_mPH()
390
+
391
+ self.converged = (
392
+ True
393
+ if self.source_out.temperature > self.sink_in.temperature
394
+ and self.source_in.temperature > self.sink_out.temperature
395
+ else False
396
+ )
397
+
398
+ def get_saturated_gas_sink_out_model(self):
399
+ """Evaluate an evaporator with saturated-vapour outlet constraint."""
400
+ self.sink_out.props_in_mTQ()
401
+ self.sink_in.pressure = self.sink_out.pressure + self.dP_c
402
+ self.sink_in.props_in_mPH()
403
+ self.duty = self.sink_out.heat_flow - self.sink_in.heat_flow
404
+
405
+ self.source_out.pressure = self.source_in.pressure - self.dP_h
406
+ self.source_out.enthalpy = (
407
+ self.source_in.enthalpy - self.duty / self.source_out.mass_flow
408
+ )
409
+ self.source_out.props_in_mPH()
410
+
411
+ # if self.source_out.temperature < self.sink_in.temperature or self.source_in.temperature < self.sink_out.temperature:
412
+ # print('temperature cross occuring in the heat exchanger: ' + self.name)
413
+
414
+ self.converged = (
415
+ True
416
+ if self.source_out.temperature > self.sink_in.temperature
417
+ and self.source_in.temperature > self.sink_out.temperature
418
+ else False
419
+ )
420
+
421
+ def get_graph_data(self, divisions=50):
422
+ """Sample temperature profiles along the exchanger length."""
423
+ data = {"source": [], "sink": []}
424
+
425
+ def get_inter_data(s_in: Stream, s_out: Stream):
426
+ """Interpolate intermediate stream states for plotting."""
427
+ ls = []
428
+ dP_tot = s_in.pressure - s_out.pressure
429
+ dh_tot = s_in.enthalpy - s_out.enthalpy
430
+
431
+ for i in range(divisions + 1):
432
+ if i == 0:
433
+ m_str = Stream(
434
+ name="source i",
435
+ fluid=s_in.fluid,
436
+ pp_instance=s_in.pp_instance,
437
+ mass_flow=s_in.mass_flow,
438
+ )
439
+ m_str.copy_fld_props(s_in)
440
+ m_str.set_flows(s_in.mass_flow)
441
+ ls.append(m_str)
442
+ continue
443
+ if i == divisions:
444
+ m_str = Stream(
445
+ name="source i",
446
+ fluid=s_out.fluid,
447
+ pp_instance=s_out.pp_instance,
448
+ mass_flow=s_out.mass_flow,
449
+ )
450
+ m_str.copy_fld_props(s_out)
451
+ m_str.set_flows(s_out.mass_flow)
452
+ ls.append(m_str)
453
+ continue
454
+ m_str = Stream(
455
+ name="source i",
456
+ fluid=s_in.fluid,
457
+ pp_instance=s_in.pp_instance,
458
+ pressure=s_in.pressure - dP_tot * i / divisions,
459
+ enthalpy=s_in.enthalpy - dh_tot * i / divisions,
460
+ mass_flow=s_in.mass_flow,
461
+ )
462
+ m_str.props_in_mPH()
463
+ ls.append(m_str)
464
+ return ls
465
+
466
+ data["sink"] = get_inter_data(self.sink_in, self.sink_out)
467
+ data["source"] = get_inter_data(self.source_in, self.source_out)
468
+ return data
469
+
470
+ def get_LMTD_ave(self):
471
+ """Average the segment-wise log-mean temperature difference."""
472
+ data = self.get_graph_data()
473
+ div = len(data["source"]) - 1
474
+ LMTD_seg = []
475
+ for i in range(div):
476
+ LMTD_seg.append(
477
+ _get_LMTD(
478
+ # counterflow: sink segment i (sink[i] -> sink[i+1]) faces source[div-1-i] -> source[div-i]
479
+ Thin=data["source"][div - 1 - i].temperature,
480
+ Thout=data["source"][div - i].temperature,
481
+ Tcin=data["sink"][i].temperature,
482
+ Tcout=data["sink"][i + 1].temperature,
483
+ )
484
+ )
485
+ return sum(LMTD_seg) / len(LMTD_seg)
486
+
487
+
488
+ def _get_LMTD(Thin, Thout, Tcin, Tcout):
489
+ """Compute log mean temperature difference for a segment."""
490
+ dt_hot = Thin - Tcout
491
+ dt_cold = Thout - Tcin
492
+ if dt_hot <= 0.0 or dt_cold <= 0.0:
493
+ raise TemperatureCrossError(-min(dt_hot, dt_cold))
494
+ if dt_hot == dt_cold:
495
+ return dt_hot
496
+ return (dt_hot - dt_cold) / log(dt_hot / dt_cold)