Coverage for backend/pinch_service/OpenPinch/src/classes/stream.py: 75%
178 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1from typing import Optional
2from ..lib.enums import *
3from .value import Value
6class Stream():
7 """Class representing a generic stream (process or utility)."""
9 def __init__(
10 self,
11 name: str = "Stream",
12 t_supply: Optional[float] = None,
13 t_target: Optional[float] = None,
14 dt_cont: float = 0.0,
15 heat_flow: float = 0.0,
16 htc: float = 1.0,
17 price: float = 0.0,
18 is_process_stream: bool = True,
19 ):
20 self._name: str = name
21 self._type: str = None
22 self._t_supply: Optional[float] = t_supply
23 self._t_target: Optional[float] = t_target
24 self._dt_cont: float = dt_cont
25 self._heat_flow: float = heat_flow
26 self._htc: float = htc if htc != 0.0 else 1.0
27 self._htr: float = 1 / self._htc
28 self._price: float = price
29 self._is_process_stream: bool = is_process_stream
30 self._active = True
31 self._update_attributes()
33 # === Core Properties ===
35 @property
36 def name(self) -> str:
37 """Stream name."""
38 return self._name
39 @name.setter
40 def name(self, value: str):
41 self._name = value
43 @property
44 def is_process_stream(self) -> bool:
45 """Process or utility stream."""
46 return self._is_process_stream
47 @is_process_stream.setter
48 def is_process_stream(self, value: bool):
49 self._is_process_stream = value
51 @property
52 def type(self) -> Optional[str]:
53 """Stream type (Hot, Cold, Both)."""
54 return self._type
55 @type.setter
56 def type(self, value: str):
57 self._type = value
59 @property
60 def t_supply(self) -> Optional[float]:
61 """Supply temperature."""
62 return self._t_supply
63 @t_supply.setter
64 def t_supply(self, value: float):
65 self._t_supply = value
66 self._update_attributes()
68 @property
69 def t_target(self) -> Optional[float]:
70 """Target temperature."""
71 return self._t_target
72 @t_target.setter
73 def t_target(self, value: float):
74 self._t_target = value
75 self._update_attributes()
77 @property
78 def dt_cont(self) -> float:
79 """Delta T minimum (approach temperature)."""
80 return self._dt_cont
81 @dt_cont.setter
82 def dt_cont(self, value: float):
83 self._dt_cont = value
84 self._update_attributes()
86 @property
87 def heat_flow(self) -> float:
88 """Stream heat flow (kW)."""
89 return self._heat_flow
90 @heat_flow.setter
91 def heat_flow(self, value: float):
92 self._heat_flow = value
93 self._update_attributes()
95 @property
96 def htc(self) -> float:
97 """Heat transfer coefficient."""
98 return self._htc
99 @htc.setter
100 def htc(self, value: float):
101 self._htc = value
102 self._update_attributes()
104 @property
105 def htr(self) -> float:
106 """Heat transfer coefficient."""
107 return self._htr
108 @htr.setter
109 def htr(self, value: float):
110 self._htr = value
112 @property
113 def price(self) -> float:
114 """Unit energy price ($/MWh or similar)."""
115 return self._price
116 @price.setter
117 def price(self, value: float):
118 self._price = value
120 @property
121 def ut_cost(self) -> float:
122 """Utility cost contribution (if relevant)."""
123 return self._ut_cost
124 @ut_cost.setter
125 def ut_cost(self, value: float):
126 self._ut_cost = value
128 @property
129 def CP(self) -> float:
130 """Heat capacity flowrate (kW/K)."""
131 return self._CP
132 @CP.setter
133 def CP(self, value: float):
134 self._CP = value
136 @property
137 def rCP(self) -> Optional[float]:
138 """Resistance-capacity product (1/heat transfer rate)."""
139 return self._RCP_prod
140 @rCP.setter
141 def rCP(self, value: float):
142 self._RCP_prod = value
144 @property
145 def active(self) -> bool:
146 """Whether the stream is active in analysis."""
147 return self._active
148 @active.setter
149 def active(self, value: bool):
150 self._active = value
152 # === Computed Temperature Bounds ===
154 @property
155 def t_min(self) -> Optional[float]:
156 """Minimum temperature (supply or target depending on hot/cold)."""
157 return self._t_min
158 @t_min.setter
159 def t_min(self, value: float):
160 self._t_min = value
162 @property
163 def t_max(self) -> Optional[float]:
164 """Maximum temperature (supply or target depending on hot/cold)."""
165 return self._t_max
166 @t_max.setter
167 def t_max(self, value: float):
168 self._t_max = value
170 @property
171 def t_min_star(self) -> Optional[float]:
172 """Adjusted minimum temperature (accounting for DTmin)."""
173 return self._t_min_star
174 @t_min_star.setter
175 def t_min_star(self, value: float):
176 self._t_min_star = value
178 @property
179 def t_max_star(self) -> Optional[float]:
180 """Adjusted maximum temperature (accounting for DTmin)."""
181 return self._t_max_star
182 @t_max_star.setter
183 def t_max_star(self, value: float):
184 self._t_max_star = value
187 # === Methods ===
189 def _update_attributes(self) -> None:
190 """Calculates key stream attributes based on temperatures."""
191 if self._t_supply is None or self._t_target is None or self._htc is None: 191 ↛ 192line 191 didn't jump to line 192 because the condition on line 191 was never true
192 return
194 if self._t_supply > self._t_target:
195 # Hot stream
196 self._set_hot_stream_min_max_temperatures()
197 elif self._t_supply < self._t_target: 197 ↛ 201line 197 didn't jump to line 201 because the condition on line 197 was always true
198 # Cold stream
199 self._set_cold_stream_min_max_temperatures()
200 else:
201 if isinstance(self._heat_flow, float | int):
202 if self._heat_flow > 0.0:
203 # Cold stream
204 self._t_target = self._t_supply + 0.01
205 self._set_cold_stream_min_max_temperatures()
206 elif self._heat_flow < 0.0:
207 # Hot stream
208 self._t_target = self._t_supply - 0.01
209 self._set_hot_stream_min_max_temperatures()
211 if isinstance(self._heat_flow, float | int): 211 ↛ 213line 211 didn't jump to line 213 because the condition on line 211 was always true
212 self._CP = self._heat_flow / (self._t_max - self._t_min)
213 elif isinstance(self._CP, float | int):
214 self._heat_flow = self._CP * (self._t_max - self._t_min)
216 self._calc_utility_cost()
217 self._calc_htr_and_cp_product()
220 def set_heat_flow(self, value: float, units: str = "kW") -> None:
221 """Sets the heat flow and updates CP and utility cost."""
222 self._heat_flow = value
223 self._calc_utility_cost()
224 if self._t_supply is not None and self._t_target is not None and abs(self._t_supply - self._t_target) > 0: 224 ↛ exitline 224 didn't return from function 'set_heat_flow' because the condition on line 224 was always true
225 self._CP = value / abs(self._t_supply - self._t_target)
226 self._RCP_prod = self._htr * self._CP
229 def _calc_utility_cost(self):
230 if isinstance(self._heat_flow, float | int) and isinstance(self._price, float | int): 230 ↛ exitline 230 didn't return from function '_calc_utility_cost' because the condition on line 230 was always true
231 self._ut_cost = (self._heat_flow / 1000) * self._price
234 def _calc_htr_and_cp_product(self):
235 if isinstance(self._heat_flow, float | int) and isinstance(self._price, float | int): 235 ↛ exitline 235 didn't return from function '_calc_htr_and_cp_product' because the condition on line 235 was always true
236 if self._htc != 0.0: 236 ↛ exitline 236 didn't return from function '_calc_htr_and_cp_product' because the condition on line 236 was always true
237 self._htr = 1 / self._htc
238 self._RCP_prod = self._CP * self._htr if self._htc > 0.0 else 0.0
241 def _set_hot_stream_min_max_temperatures(self):
242 self._t_min = self._t_target
243 self._t_max = self._t_supply
244 self._t_min_star = self._t_min - self._dt_cont
245 self._t_max_star = self._t_max - self._dt_cont
246 if self._type is None:
247 self._type = StreamType.Hot.value
250 def _set_cold_stream_min_max_temperatures(self):
251 self._t_min = self._t_supply
252 self._t_max = self._t_target
253 self._t_min_star = self._t_min + self._dt_cont
254 self._t_max_star = self._t_max + self._dt_cont
255 if self._type is None:
256 self._type = StreamType.Cold.value