Coverage for pyrc \ core \ inputs.py: 65%

379 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-29 14:14 +0200

1# ------------------------------------------------------------------------------- 

2# Copyright (C) 2026 Joel Kimmich, Tim Jourdan 

3# ------------------------------------------------------------------------------ 

4# License 

5# This file is part of PyRC, distributed under GPL-3.0-or-later. 

6# ------------------------------------------------------------------------------ 

7 

8from __future__ import annotations 

9 

10import sys 

11from datetime import datetime 

12 

13import numpy as np 

14from sympy import symbols 

15from typing import TYPE_CHECKING, Callable 

16from scipy.constants import Stefan_Boltzmann 

17 

18from pyrc.core.components.node import TemperatureNode 

19from pyrc.core.settings import initial_settings, Settings 

20from pyrc.core.components.templates import ( 

21 Geometric, 

22 ConnectedFlowObject, 

23 calculate_balance_for_resistors, 

24 EquationItem, 

25 RCObjects, 

26 initial_rc_objects, 

27 RCSolution, 

28 solution_object, 

29 Cell, 

30) 

31from pyrc.core.components.input import Input 

32 

33if TYPE_CHECKING: 

34 from pyrc.core.nodes import MassFlowNode 

35 from pyrc.core.components.resistor import Resistor 

36 from sympy import Expr 

37 from pyrc import Capacitor 

38 

39_bc_missing_counterparts: list[str] = [] 

40 

41 

42# TODO: create a new class implementing the settings (and same for rcobjects) as class variable which can be set 

43# initially for all subclasses to eliminate the need of passing the instances to each new class instances. 

44 

45 

46class BoundaryCondition(TemperatureNode, Input): 

47 def __init__( 

48 self, 

49 temperature, 

50 rc_objects: RCObjects = initial_rc_objects, 

51 rc_solution: RCSolution = solution_object, 

52 is_mass_flow_start: bool = False, 

53 heat_transfer_coefficient: float = np.nan, 

54 is_fluid: bool = False, 

55 settings: Settings = initial_settings, 

56 **kwargs, 

57 ): 

58 """ 

59 Boundary condition of the RC network. Only sets a temperature without having a capacity. 

60 

61 Parameters 

62 ---------- 

63 temperature : float | int | np.number 

64 The temperature of the node. 

65 It is recommended to use the SI unit Kelvin instead of degrees Celsius. 

66 position : np.ndarray 

67 The position of the node in 2D/3D space. 

68 If 2D, a zero is added for the z coordinate. 

69 is_mass_flow_start : bool 

70 If True, the `BoundaryCondition` is a start of a mass flow. 

71 If so, it must be connected to a `MassTransport` resistor. 

72 heat_transfer_coefficient : float 

73 The heat transfer coefficient used to calculate free convection to the surrounding solid nodes. 

74 is_fluid : bool, optional 

75 If True, the BC is considered as fluid, otherwise as solid Material. 

76 settings : Settings, optional 

77 A `Settings` object with general settings, including `SolveSettings` 

78 kwargs : dict 

79 Optional arguments passed to `TemperatureNode`\\. 

80 """ 

81 TemperatureNode.__init__( 

82 self, temperature=temperature, rc_objects=rc_objects, rc_solution=rc_solution, **kwargs 

83 ) 

84 Input.__init__(self, settings=settings) 

85 self._is_fluid = is_fluid 

86 # TODO: The following attributes should be moved to the correct subclasses 

87 self.is_mass_flow_start: bool = is_mass_flow_start 

88 self.htc: float | np.float64 = heat_transfer_coefficient # in W/m/m/K 

89 

90 def __init_subclass__(cls, **kwargs): 

91 """ 

92 Warns if no subclass of this class was found in this module (py-file) that inherits from `Cell`\\. 

93 

94 Every BoundaryCondition class should have a counterpart that also is a `Cell` and one that's a `Geometric`\\. 

95 This way, the boundary condition can be used in algorithms like Capacitors that also are cells (in meshes). 

96 The classes should be named like the non-Cell/Geometric-classes extended with "Cell"/"Geometric". 

97 

98 This class is only useful during development, when new `BoundaryConditions` are added. 

99 

100 Parameters 

101 ---------- 

102 kwargs 

103 

104 Returns 

105 ------- 

106 

107 """ 

108 super().__init_subclass__(**kwargs) 

109 if cls.__name__.endswith("Geometric") or cls.__name__.endswith("Cell"): 

110 # The method is run in the Cell/Geometric subclass, so no check is needed :D 

111 return 

112 for geometric_version in ["Cell", "Geometric"]: 

113 _bc_missing_counterparts.append(cls.__name__ + geometric_version) 

114 

115 @property 

116 def is_solid(self): 

117 return not self._is_fluid 

118 

119 @property 

120 def is_fluid(self): 

121 return self._is_fluid 

122 

123 def __str__(self): 

124 return self.__repr__() 

125 

126 def __repr__(self): 

127 return f"{self.__class__.__name__}: ϑ={self.temperature}" 

128 

129 @property 

130 def heat_transfer_coefficient(self): 

131 return self.htc 

132 

133 @property 

134 def index(self) -> int: 

135 """ 

136 The index of `self` within the input vector (row in input matrix). 

137 

138 The value is cached to improve performance. 

139 

140 Returns 

141 ------- 

142 int 

143 """ 

144 if not self._index: 

145 self._index = self.rc_objects.inputs.index(self) 

146 return self._index 

147 

148 @property 

149 def initial_value(self): 

150 return self.initial_temperature 

151 

152 @initial_value.setter 

153 def initial_value(self, value): 

154 self.initial_temperature = value 

155 

156 @property 

157 def temperature(self) -> float | int | np.number: 

158 """ 

159 The temperature of `self`\\. 

160 

161 If no solution is saved yet, the initial temperature is returned. 

162 

163 Returns 

164 ------- 

165 float | int | np.number 

166 """ 

167 if not self.solutions.input_exists: 

168 return self.initial_temperature 

169 return self.solutions.last_value_input(self.index) 

170 

171 @property 

172 def temperature_vector(self): 

173 """ 

174 The vector with all temperature values of `self` of all (currently existing) time steps. 

175 

176 If no solution is saved (yet), the initial temperature is returned as vector with `time_step` length. 

177 

178 Returns 

179 ------- 

180 np.ndarray | np.number 

181 """ 

182 if not self.solutions.exist: 

183 result = np.array([self.temperature] * self.solutions.time_steps_count) 

184 return result 

185 return self.solutions.input_vectors[:, self.index] 

186 

187 def calculate_static(self, tau, temp_vector, _input_vector, *args, **kwargs): 

188 """ 

189 

190 Parameters 

191 ---------- 

192 tau 

193 temp_vector 

194 _input_vector 

195 args 

196 kwargs 

197 

198 Returns 

199 ------- 

200 

201 """ 

202 return self.temperature 

203 

204 

205class BoundaryConditionGeometric(BoundaryCondition, Geometric): 

206 def __init__(self, *args, position, **kwargs): 

207 Geometric.__init__(self, position=position) 

208 BoundaryCondition.__init__(self, *args, **kwargs) 

209 

210 

211class BoundaryConditionCell(BoundaryCondition, Cell): 

212 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

213 Cell.__init__(self, position=position, delta=delta) 

214 BoundaryCondition.__init__(self, *args, **kwargs) 

215 

216 

217class FluidBoundaryCondition(BoundaryCondition): 

218 def __init__( 

219 self, 

220 *args, 

221 rc_objects: RCObjects = initial_rc_objects, 

222 rc_solution: RCSolution = solution_object, 

223 **kwargs: Settings | float | int | np.ndarray, 

224 ): 

225 super().__init__(*args, rc_objects=rc_objects, rc_solution=rc_solution, is_fluid=True, **kwargs) 

226 

227 

228class FluidBoundaryConditionGeometric(FluidBoundaryCondition, Geometric): 

229 def __init__(self, *args, position, **kwargs): 

230 Geometric.__init__(self, position=position) 

231 FluidBoundaryCondition.__init__(self, *args, **kwargs) 

232 

233 

234class FluidBoundaryConditionCell(FluidBoundaryCondition, Cell): 

235 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

236 Cell.__init__(self, position=position, delta=delta) 

237 FluidBoundaryCondition.__init__(self, *args, **kwargs) 

238 

239 

240class InteriorBoundaryCondition(FluidBoundaryCondition): 

241 pass 

242 

243 

244class InteriorBoundaryConditionGeometric(InteriorBoundaryCondition, Geometric): 

245 def __init__(self, *args, position, **kwargs): 

246 Geometric.__init__(self, position=position) 

247 InteriorBoundaryCondition.__init__(self, *args, **kwargs) 

248 

249 

250class InteriorBoundaryConditionCell(InteriorBoundaryCondition, Cell): 

251 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

252 Cell.__init__(self, position=position, delta=delta) 

253 InteriorBoundaryCondition.__init__(self, *args, **kwargs) 

254 

255 

256class ExteriorTemperatureInputMixin: 

257 def __init__( 

258 self, 

259 temperature_delta=None, 

260 middle_temperature=None, 

261 settings: Settings = initial_settings, 

262 ): 

263 """ 

264 Class to calculate the exterior temperature. 

265 

266 The temperature can be returned: 

267 1. static 

268 2. dynamic: sinus curve using ``temperature_delta`` and ``middle_temperature`` 

269 3. dynamic: weather data. The information is in the ``settings`` object. 

270 

271 Parameters 

272 ---------- 

273 temperature_delta : float | int, optional 

274 The amplitude of the sinus curve. 

275 Not used if ``self.settings.use_weather_data is True`` 

276 middle_temperature : float | int, optional 

277 The middle temperature of the sinus curve. 

278 Not used if ``self.settings.use_weather_data is True`` 

279 settings : Settings, optional 

280 A settings object with all important settings. 

281 If not given the initial Settings object is used. 

282 """ 

283 self.settings = settings 

284 if not self.settings.use_weather_data: 

285 assert temperature_delta is not None and middle_temperature is not None 

286 self.temperature_delta = temperature_delta 

287 self.middle_temperature = middle_temperature 

288 

289 def get_kwargs_functions(self) -> dict: 

290 """ 

291 See Input.get_kwargs() 

292 

293 Returns 

294 ------- 

295 dict : 

296 The names of the values that must be passed to calculate_static/dynamic and the function to calculate 

297 this values. 

298 If for example the exterior temperature is needed for calculate_dynamic, the dict looks like this: 

299 {"exterior_temperature": lambda tau: my_fancy_algorithm_to_calculate/get_the_exterior_temperature(tau)} 

300 To use it, this dict must be passed to calculate_dynamic like this: 

301 my_obj.calculate_dynamic(tau, temp_vector, _input_vector, **the_get_kwargs_dict) 

302 """ 

303 if self.settings.calculate_static: 

304 return {"exterior_temperature": self.calculate_static_fun()} 

305 return {"exterior_temperature": self.calculate_dynamic_fun()} 

306 

307 def calculate_static_fun(self) -> Callable: 

308 dynamic_fun = self.calculate_dynamic_fun() 

309 value = dynamic_fun(0) 

310 return lambda *args, **_kwargs: value 

311 

312 def calculate_dynamic_fun(self) -> Callable: 

313 if not self.settings.use_weather_data: 

314 return lambda tau, *args, **kwargs: ( 

315 (self.temperature_delta * -np.cos(np.pi / 43200 * (tau + self.settings.start_shift - 14400))) / 2 

316 + self.middle_temperature 

317 ) 

318 # use weather data 

319 return self.settings.weather_data.get_interpolator("exterior_temperature") 

320 

321 def calculate_dynamic(self, *args, exterior_temperature=None, **kwargs): 

322 return exterior_temperature 

323 

324 

325class ExteriorBoundaryCondition(FluidBoundaryCondition, ExteriorTemperatureInputMixin): 

326 def __init__( 

327 self, 

328 *args, 

329 temperature_delta=None, 

330 middle_temperature=None, 

331 rc_objects: RCObjects = initial_rc_objects, 

332 rc_solution: RCSolution = solution_object, 

333 settings: Settings = initial_settings, 

334 **kwargs: bool | float | int, 

335 ): 

336 FluidBoundaryCondition.__init__( 

337 self, *args, rc_objects=rc_objects, rc_solution=rc_solution, settings=settings, **kwargs 

338 ) 

339 ExteriorTemperatureInputMixin.__init__(self, temperature_delta, middle_temperature, settings=settings) 

340 

341 def get_kwargs_functions(self) -> dict: 

342 return ExteriorTemperatureInputMixin.get_kwargs_functions(self) 

343 

344 def calculate_static(self, *args, **kwargs): 

345 return self.calculate_dynamic(*args, **kwargs) 

346 

347 def calculate_dynamic(self, tau, temp_vector, _input_vector, *args, **kwargs): 

348 # Logic is in ExteriorTemperatureInputMixin.calculate_dynamic_fun. 

349 # In **kwargs there must be a keyword "exterior_temperature" to work correctly 

350 return ExteriorTemperatureInputMixin.calculate_dynamic(self, **kwargs) 

351 

352 

353class ExteriorBoundaryConditionGeometric(ExteriorBoundaryCondition, Geometric): 

354 def __init__(self, *args, position, **kwargs): 

355 Geometric.__init__(self, position=position) 

356 ExteriorBoundaryCondition.__init__(self, *args, **kwargs) 

357 

358 

359class ExteriorBoundaryConditionCell(ExteriorBoundaryCondition, Cell): 

360 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

361 Cell.__init__(self, position=position, delta=delta) 

362 ExteriorBoundaryCondition.__init__(self, *args, **kwargs) 

363 

364 

365class SolidBoundaryCondition(BoundaryCondition): 

366 def __init__( 

367 self, *args, rc_objects: RCObjects = initial_rc_objects, rc_solution: RCSolution = solution_object, **kwargs 

368 ): 

369 super().__init__(*args, rc_objects=rc_objects, rc_solution=rc_solution, is_fluid=False, **kwargs) 

370 

371 

372class SolidBoundaryConditionGeometric(SolidBoundaryCondition, Geometric): 

373 def __init__(self, *args, position, **kwargs): 

374 Geometric.__init__(self, position=position) 

375 SolidBoundaryCondition.__init__(self, *args, **kwargs) 

376 

377 

378class SolidBoundaryConditionCell(SolidBoundaryCondition, Cell): 

379 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

380 Cell.__init__(self, position=position, delta=delta) 

381 SolidBoundaryCondition.__init__(self, *args, **kwargs) 

382 

383 

384class FlowBoundaryCondition(FluidBoundaryCondition, ConnectedFlowObject): 

385 def __init__( 

386 self, 

387 *args, 

388 volume_flow=None, 

389 rc_objects: RCObjects = initial_rc_objects, 

390 rc_solution: RCSolution = solution_object, 

391 **kwargs, 

392 ): 

393 FluidBoundaryCondition.__init__(self, *args, rc_objects=rc_objects, rc_solution=rc_solution, **kwargs) 

394 ConnectedFlowObject.__init__(self) 

395 

396 self.volume_flow = volume_flow 

397 

398 if self.is_mass_flow_start: 

399 self.volume_flow_is_balanced = True 

400 

401 @property 

402 def balance(self): 

403 from pyrc.core.resistors import MassTransport 

404 

405 return calculate_balance_for_resistors(self, [res for res in self.neighbours if isinstance(res, MassTransport)]) 

406 

407 @property 

408 def volume_flow(self): 

409 return super().volume_flow 

410 

411 @volume_flow.setter 

412 def volume_flow(self, value): 

413 self._volume_flow = value 

414 

415 @property 

416 def sinks(self) -> list[MassFlowNode]: 

417 """ 

418 A list with all `MassFlowNode`\\s that are sinks of mass flow for `self`\\. 

419 

420 Returns 

421 ------- 

422 list[MassFlowNode] 

423 """ 

424 from pyrc.core.resistors import MassTransport 

425 

426 result = [] 

427 if self.is_mass_flow_start: 

428 for neighbour in self.neighbours: 

429 if isinstance(neighbour, MassTransport): 

430 result.append(neighbour.get_connected_node(self)) 

431 return result 

432 

433 @property 

434 def sources(self) -> list[MassFlowNode]: 

435 """ 

436 A list with all `MassFlowNode`\\s that are sources of mass flow for `self`\\. 

437 

438 Returns 

439 ------- 

440 list[MassFlowNode] 

441 """ 

442 from pyrc.core.resistors import MassTransport 

443 

444 result = [] 

445 if not self.is_mass_flow_start: 

446 for neighbour in self.neighbours: 

447 if isinstance(neighbour, MassTransport): 

448 result.append(neighbour.get_connected_node(self)) 

449 return result 

450 

451 def check_balance(self) -> bool: 

452 """ 

453 If the sum of all sinks and sources of `self` is 0 this method returns True, False otherwise. 

454 

455 Returns 

456 ------- 

457 bool 

458 """ 

459 if self.volume_flow_is_balanced: 

460 return True 

461 # check if start or end 

462 if self.is_mass_flow_start: 

463 self.volume_flow_is_balanced = True 

464 return True 

465 # self is end. So check all connected ConnectedFlowObjects for check_balance 

466 flow_objects = [] 

467 resistor: Resistor 

468 for node in [resistor.get_connected_node(self) for resistor in self.neighbours]: 

469 if isinstance(node, ConnectedFlowObject): 

470 if node in flow_objects: 

471 continue 

472 flow_objects.append(node) 

473 for flow_object in flow_objects: 

474 if flow_object.check_balance(): 

475 continue 

476 else: 

477 return False 

478 return True 

479 

480 def connect(self, *args, **kwargs): 

481 from pyrc.core.resistors import MassTransport 

482 

483 super().connect(*args, **kwargs) 

484 # Prevent the connection to every other Resistor than MassTransport. 

485 assert isinstance(self.neighbours[-1], MassTransport) 

486 

487 

488class FlowBoundaryConditionGeometric(FlowBoundaryCondition, Geometric): 

489 def __init__(self, *args, position, **kwargs): 

490 Geometric.__init__(self, position=position) 

491 FlowBoundaryCondition.__init__(self, *args, **kwargs) 

492 

493 

494class FlowBoundaryConditionCell(FlowBoundaryCondition, Cell): 

495 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

496 Cell.__init__(self, position=position, delta=delta) 

497 FlowBoundaryCondition.__init__(self, *args, **kwargs) 

498 

499 

500class ExteriorInletFlow(FlowBoundaryCondition, ExteriorTemperatureInputMixin): 

501 """ 

502 A boundary condition with constant mass flow and the temperature of `ExteriorTemperatureInput`\\. 

503 

504 This is the start of a mass flow. 

505 """ 

506 

507 def __init__( 

508 self, 

509 *args, 

510 volume_flow=None, 

511 temperature_delta=None, 

512 middle_temperature=None, 

513 rc_objects: RCObjects = initial_rc_objects, 

514 rc_solution: RCSolution = solution_object, 

515 settings: Settings = initial_settings, 

516 **kwargs: np.ndarray | bool | float | int, 

517 ): 

518 FlowBoundaryCondition.__init__( 

519 self, 

520 *args, 

521 volume_flow=volume_flow, 

522 rc_objects=rc_objects, 

523 rc_solution=rc_solution, 

524 settings=settings, 

525 **kwargs, 

526 ) 

527 ExteriorTemperatureInputMixin.__init__(self, temperature_delta, middle_temperature, settings=settings) 

528 self.is_mass_flow_start = True 

529 

530 def get_kwargs_functions(self) -> dict: 

531 return ExteriorTemperatureInputMixin.get_kwargs_functions(self) 

532 

533 def calculate_static(self, *args, **kwargs): 

534 return self.calculate_dynamic(*args, **kwargs) 

535 

536 def calculate_dynamic(self, tau, temp_vector, _input_vector, *args, **kwargs): 

537 # Logic is in ExteriorTemperatureInputMixin.calculate_dynamic_fun. 

538 # In **kwargs there must be a keyword "exterior_temperature" to work correctly 

539 return ExteriorTemperatureInputMixin.calculate_dynamic(self, **kwargs) 

540 

541 

542class ExteriorInletFlowGeometric(ExteriorInletFlow, Geometric): 

543 def __init__(self, *args, position, **kwargs): 

544 Geometric.__init__(self, position=position) 

545 ExteriorInletFlow.__init__(self, *args, **kwargs) 

546 

547 

548class ExteriorInletFlowCell(ExteriorInletFlow, Cell): 

549 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

550 Cell.__init__(self, position=position, delta=delta) 

551 ExteriorInletFlow.__init__(self, *args, **kwargs) 

552 

553 

554class ExteriorOutletFlow(FlowBoundaryCondition, ExteriorTemperatureInputMixin): 

555 """ 

556 A boundary condition serving as the end of a mass flow. The `volume_flow` value has no effect. 

557 

558 The temperature is set as `ExteriorTemperatureInput` to easily calculate the heat flux between the connected 

559 `MassFlowNode` and the exterior Temperature. You could redefine this if you want to calculate another 

560 difference. It has no effect for solving the RC network because only `MassTransport` Resistors are allowed as 

561 neighbors and they are always just passing energy INTO this ExteriorOutletFlow boundary condition and never out 

562 of it. 

563 

564 This is the end of a mass flow. 

565 """ 

566 

567 def __init__( 

568 self, 

569 *args, 

570 temperature_delta=None, 

571 middle_temperature=None, 

572 rc_objects: RCObjects = initial_rc_objects, 

573 rc_solution: RCSolution = solution_object, 

574 settings: Settings = initial_settings, 

575 **kwargs: np.ndarray | bool | float | int, 

576 ): 

577 FlowBoundaryCondition.__init__( 

578 self, *args, rc_objects=rc_objects, rc_solution=rc_solution, settings=settings, **kwargs 

579 ) 

580 ExteriorTemperatureInputMixin.__init__(self, temperature_delta, middle_temperature, settings=settings) 

581 self.is_mass_flow_start = False 

582 

583 def get_kwargs_functions(self) -> dict: 

584 return ExteriorTemperatureInputMixin.get_kwargs_functions(self) 

585 

586 def calculate_static(self, *args, **kwargs): 

587 return self.calculate_dynamic(*args, **kwargs) 

588 

589 def calculate_dynamic(self, tau, temp_vector, _input_vector, *args, **kwargs): 

590 # Logic is in ExteriorTemperatureInputMixin.calculate_dynamic_fun. 

591 # In **kwargs there must be a keyword "exterior_temperature" to work correctly 

592 return ExteriorTemperatureInputMixin.calculate_dynamic(self, **kwargs) 

593 

594 

595class ExteriorOutletFlowGeometric(ExteriorOutletFlow, Geometric): 

596 def __init__(self, *args, position, **kwargs): 

597 Geometric.__init__(self, position=position) 

598 ExteriorOutletFlow.__init__(self, *args, **kwargs) 

599 

600 

601class ExteriorOutletFlowCell(ExteriorOutletFlow, Cell): 

602 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs): 

603 Cell.__init__(self, position=position, delta=delta) 

604 ExteriorOutletFlow.__init__(self, *args, **kwargs) 

605 

606 

607class InternalHeatSource(EquationItem, Input): 

608 def __init__( 

609 self, 

610 node: Capacitor, 

611 power: float | int | Expr = None, 

612 specific_power_in_w_per_cubic_meter: float | int | Expr = None, 

613 specific_power_in_w_per_meter_squared: float | int | Expr = None, 

614 area_direction: np.ndarray = None, 

615 settings: Settings = initial_settings, 

616 ): 

617 """ 

618 Internal heat source (energy source or sink). 

619 

620 Parameters 

621 ---------- 

622 node : Capacitor 

623 The Capacitor it belongs to. 

624 power : float | int | Expr, optional 

625 The power of the heat source. Negative values act as sink. 

626 specific_power_in_w_per_cubic_meter : float | int | Expr, optional 

627 The volume specific power of the heat source. Negative values act as sink. 

628 specific_power_in_w_per_meter_squared : float | int | Expr, optional 

629 The area specific power of the heat source. Negative values act as sink. 

630 The area used to calculate the actual value is determined by area_direction vector. It points to the 

631 surface that should be used. 

632 Works only for Nodes, not for Capacitors that are no Cells. 

633 area_direction : np.ndarray, optional 

634 The direction to the area that should be used to calculate the area specific power. 

635 settings : Settings, optional 

636 The settings object to pass to Input class. 

637 """ 

638 EquationItem.__init__(self) 

639 Input.__init__(self, settings=settings) 

640 self.node: Capacitor = node 

641 self.__power: float | int | Expr = power 

642 self.volume_specific_power: float | int | Expr = specific_power_in_w_per_cubic_meter # in W/(m**3) 

643 self.__area_specific_power: float | int | Expr = specific_power_in_w_per_meter_squared # in W/(m**3) 

644 self.symbol = symbols(f"Q_dot_{self.node.id}") 

645 self.area_direction: np.ndarray = area_direction # should be like (1,0,0) in any order and sign 

646 

647 # must be placed after initialization! 

648 power_params = [specific_power_in_w_per_cubic_meter, specific_power_in_w_per_meter_squared, power] 

649 if sum(p is not None for p in power_params) > 1: 

650 raise ValueError( 

651 "Set only one of: specific_power_in_w_per_cubic_meter, specific_power_in_w_per_meter_squared, power." 

652 ) 

653 if specific_power_in_w_per_meter_squared is not None and area_direction is None: 

654 raise ValueError("If specific_power_in_w_per_meter_squared is set, area_direction must be set too.") 

655 if ( 

656 area_direction is not None 

657 and specific_power_in_w_per_meter_squared is None 

658 and any(p is not None for p in power_params) 

659 ): 

660 warnings.warn("area_direction is set but specific_power_in_w_per_meter_squared is not used.") 

661 if specific_power_in_w_per_meter_squared is not None: 

662 assert isinstance(self.node, Cell), "When using area specific power self.node must be a Cell object." 

663 

664 # The calculation of the volume_specific_power is done later because during initialization the volume isn't 

665 # known (because it depends on the connected Nodes that are still created during initialization). 

666 

667 # check, if self.volume_specific_power is None and replace it with 0 if so 

668 if self.volume_specific_power is None and self.__area_specific_power is None: 

669 self.volume_specific_power = np.float64(0) 

670 self._area = None 

671 

672 @property 

673 def initial_value(self) -> float | int | Expr: 

674 return self.power 

675 

676 @initial_value.setter 

677 def initial_value(self, value): 

678 """ 

679 Set the intial power value. 

680 

681 Parameters 

682 ---------- 

683 value : float | int | Expr 

684 The initial power value in Watts. 

685 """ 

686 self.__power = value 

687 

688 def no_node(self): 

689 """ 

690 Checks, if self.node is not of class Node and warns if so. 

691 

692 Returns 

693 ------- 

694 bool : 

695 False if self.node is of class Node. 

696 """ 

697 if isinstance(self.node, Cell): 

698 return False 

699 print(f"This only works for Cell nodes, not Capacitors.") 

700 return True 

701 

702 @property 

703 def area(self): 

704 if self._area is None: 

705 if self.no_node(): 

706 return None 

707 if self._area is None: 

708 self._area = self.node.area(self.area_direction) 

709 return self._area 

710 

711 @property 

712 def index(self) -> int: 

713 if not self._index: 

714 self._index = self.node.rc_objects.inputs.index(self) 

715 return self._index 

716 

717 @property 

718 def power(self) -> float | int | Expr: 

719 if self.__power is None: 

720 if self.no_node(): 

721 ValueError( 

722 "If InternalHeatSource.node is a Capacitor the power must be set direct, " 

723 "not with area/volume specific parameters." 

724 ) 

725 if self.volume_specific_power is None: 

726 self.set_area_specific_power(self.__area_specific_power) 

727 self.__power = self.volume_specific_power * self.node.volume 

728 return self.__power 

729 

730 @property 

731 def area_specific_power(self): 

732 if self.area_direction is None: 

733 return None 

734 return self.power / self.area 

735 

736 def set_area_specific_power( 

737 self, area_specific_power_in_w_per_square_meter: float | int | Expr, direction: np.ndarray = None 

738 ): 

739 """ 

740 Sets the volume specific power by calculating it with the area specific power and a direction/normal of the 

741 effective surface. 

742 

743 Parameters 

744 ---------- 

745 area_specific_power_in_w_per_square_meter : float | int | Expr 

746 The area specific power in W/m**2. 

747 direction : np.ndarray, optional 

748 The normal of the surface where the power is applied to. Should be (1,0,0) with any order and sign. 

749 Is used to get the area of the node using ``self.node.area(direction)``\\. 

750 

751 """ 

752 assert not self.no_node(), "All area/volume specific values can only be used with Nodes, not with Capacitors." 

753 if direction is None: 

754 assert self.area_direction is not None 

755 if self.area_direction is None: 

756 assert isinstance(direction, np.ndarray) and len(direction) == 3, "Direction to face must be passed!" 

757 self.area_direction = direction 

758 else: 

759 if direction is not None: 

760 assert isinstance(direction, np.ndarray) and len(direction) == 3 

761 print("Warning: direction is overwritten.") 

762 self.area_direction = direction 

763 self.volume_specific_power = area_specific_power_in_w_per_square_meter * self.area / self.node.volume 

764 

765 def calculate_static(self, tau, temp_vector, _input_vector, **kwargs): 

766 return self.power 

767 

768 @property 

769 def symbols(self) -> list: 

770 return [self.symbol] 

771 

772 @property 

773 def values(self) -> list: 

774 return [self.power] 

775 

776 

777class Radiation(InternalHeatSource): 

778 def __init__(self, *args, epsilon_short=0.7, epsilon_long=0.93, **kwargs): 

779 super().__init__(*args, **kwargs) 

780 self.epsilon_short = epsilon_short 

781 self.epsilon_long = epsilon_long 

782 self.epsilon_long_boltzmann = self.epsilon_long * Stefan_Boltzmann # pre-calculation of calculate_dynamic 

783 self._bc_temp_input_index = None 

784 

785 @property 

786 def bc_temp_input_index(self): 

787 if self._bc_temp_input_index is None: 

788 for fbc in self.node.get_connected_nodes(variant=FluidBoundaryCondition): 

789 # TODO: If multiple BoundaryConditions are connected, only the last one is used for temp. calculation 

790 # If one capacitor has multiple BCs connected the calculation won't work as it is implemented currently 

791 # with calculate_dynamic_functions if no weather data is used. 

792 fbc: FluidBoundaryCondition 

793 self._bc_temp_input_index = fbc.index 

794 assert self._bc_temp_input_index is not None 

795 return self._bc_temp_input_index 

796 

797 def get_kwargs_functions(self) -> dict: 

798 if self.settings.calculate_static: 

799 return self.calculate_static_functions() 

800 return self.calculate_dynamic_functions() 

801 

802 def calculate_static_functions(self) -> dict: 

803 dyn_functions: dict = self.calculate_dynamic_functions() 

804 result = {} 

805 for name, fun in dyn_functions.items(): 

806 result[name] = ( 

807 lambda f: lambda tau, temp_vec, input_vec, *args, **kwargs: f(0, temp_vec, input_vec, *args, **kwargs) 

808 )(fun) 

809 return result 

810 

811 def calculate_dynamic_functions(self) -> dict: 

812 """ 

813 This function is used to pre-calculate some values during solving that are node independent. 

814 

815 This way calculation time can be decreased. 

816 

817 Returns 

818 ------- 

819 dict : 

820 A dictionary where the key stands for the parameter that is passed to the function calculating node 

821 dependent stuff and the value is its value (crazy, I know). 

822 """ 

823 result = {} 

824 

825 if self.settings.use_weather_data: 

826 result["incoming_area_specific_power_long"] = self.settings.area_specific_radiation_interpolator_long 

827 result["incoming_area_specific_power_short"] = self.settings.area_specific_radiation_interpolator_short 

828 else: 

829 dt = datetime.fromisoformat(self.settings.start_date) 

830 seconds_today = dt.hour * 3600 + dt.minute * 60 + dt.second + dt.microsecond / 1e6 

831 result["incoming_area_specific_power_short"] = lambda tau, *args, **kwargs: ( 

832 sin_function(tau, seconds_today) * self.settings.maximum_area_specific_power 

833 ) 

834 result["incoming_area_specific_power_long"] = lambda tau, *args, **kwargs: 0 

835 

836 # the following only works if only one boundary condition is used for all Radiation objects ( 

837 # e.g. the ExteriorTemperature and all Radiation objects are influenced by the same. If multiple 

838 # influences between Radiation objects and BCs would exists, the index is changing, depending on the 

839 # Radiation object. For that, calculate the value directly in each object (increases calculation overhead) 

840 # or create a unique kwarg for each of them (recommended with new classes)) 

841 index = self.bc_temp_input_index 

842 result["sky_temp_4_diff"] = lambda tau, temp_vector, _input_vector, *args, _index=index, **kwargs: ( 

843 sky_temp_according_to_swinbank(_input_vector.ravel()[_index]) ** 4 

844 - sky_temp_according_to_swinbank(_input_vector.ravel()[_index]) ** 4 

845 ) 

846 return result 

847 

848 def calculate_static(self, tau, temp_vector, _input_vector, **kwargs): 

849 return self.calculate_dynamic(0, temp_vector, _input_vector, **kwargs) 

850 

851 def calculate_dynamic( 

852 self, 

853 tau, 

854 temp_vector, 

855 _input_vector, 

856 incoming_area_specific_power_short=np.nan, 

857 incoming_area_specific_power_long=np.nan, 

858 sky_temp_4_diff=np.nan, 

859 **kwargs, 

860 ): 

861 temp_vector_flat = temp_vector.ravel() 

862 

863 node_temp_4 = temp_vector_flat[self.node.index] ** 4 

864 

865 if self.settings.use_weather_data: 

866 outgoing_spec_power = self.epsilon_long_boltzmann * node_temp_4 # in W/m^2 

867 else: 

868 # here incoming_area_specific_power only contains the direct radiation. So the radiation towards sky and 

869 # ambient has to be calculated, too. 

870 # TODO: Use the sight factor of the wall for both sky and bc/ambient (but this information is not accessible 

871 # here) 

872 outgoing_spec_power = 0.5 * self.epsilon_long_boltzmann * (2 * node_temp_4 - sky_temp_4_diff) 

873 power = ( 

874 incoming_area_specific_power_long * self.epsilon_long 

875 + incoming_area_specific_power_short * self.epsilon_short 

876 - outgoing_spec_power 

877 ) * self.area 

878 self.volume_specific_power = power / self.node.volume 

879 return power 

880 

881 

882def sky_temp_according_to_swinbank(air_temperature: float): 

883 return 0.0552 * air_temperature**1.5 

884 

885 

886def sin_function(tau, shift_start_time): 

887 return np.maximum(0.0, -np.cos(np.pi / 43200 * (tau + shift_start_time))) 

888 

889 

890# must be at the end of this module 

891# This checks if for every BoundaryCondition also a similar class exists that inherits from Geometry / Cell (both separately) 

892_module = sys.modules[__name__] 

893for _counterpart in _bc_missing_counterparts: 

894 if not hasattr(_module, _counterpart): 

895 import warnings 

896 

897 warnings.warn(f"Missing counterpart {_counterpart}", stacklevel=2)