Coverage for pyrc \ core \ inputs.py: 65%
361 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-13 16:59 +0200
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-13 16:59 +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# ------------------------------------------------------------------------------
8from __future__ import annotations
10import sys
11from datetime import datetime
13import numpy as np
14from sympy import symbols
15from typing import TYPE_CHECKING, Callable
16from scipy.constants import Stefan_Boltzmann
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
33if TYPE_CHECKING:
34 from pyrc.core.nodes import Node, MassFlowNode
35 from pyrc.core.components.resistor import Resistor
37_bc_missing_counterparts: list[str] = []
39# TODO: create a new class implementing the settings (and same for rcobjects) as class variable which can be set
40# initially for all subclasses to eliminate the need of passing the instances to each new class instances.
43class BoundaryCondition(TemperatureNode, Input):
44 def __init__(
45 self,
46 temperature,
47 rc_objects: RCObjects = initial_rc_objects,
48 rc_solution: RCSolution = solution_object,
49 is_mass_flow_start: bool = False,
50 heat_transfer_coefficient: float = np.nan,
51 is_fluid: bool = False,
52 settings: Settings = initial_settings,
53 **kwargs,
54 ):
55 """
56 Boundary condition of the RC network. Only sets a temperature without having a capacity.
58 Parameters
59 ----------
60 temperature : float | int | np.number
61 The temperature of the node.
62 It is recommended to use the SI unit Kelvin instead of degrees Celsius.
63 position : np.ndarray
64 The position of the node in 2D/3D space.
65 If 2D, a zero is added for the z coordinate.
66 is_mass_flow_start : bool
67 If True, the `BoundaryCondition` is a start of a mass flow.
68 If so, it must be connected to a `MassTransport` resistor.
69 heat_transfer_coefficient : float
70 The heat transfer coefficient used to calculate free convection to the surrounding solid nodes.
71 is_fluid : bool, optional
72 If True, the BC is considered as fluid, otherwise as solid Material.
73 settings : Settings, optional
74 A `Settings` object with general settings, including `SolveSettings`
75 kwargs : dict
76 Optional arguments passed to `TemperatureNode`\.
77 """
78 TemperatureNode.__init__(
79 self, temperature=temperature, rc_objects=rc_objects, rc_solution=rc_solution, **kwargs
80 )
81 Input.__init__(self, settings=settings)
82 self._is_fluid = is_fluid
83 # TODO: The following attributes should be moved to the correct subclasses
84 self.is_mass_flow_start: bool = is_mass_flow_start
85 self.htc: float | np.float64 = heat_transfer_coefficient # in W/m/m/K
87 def __init_subclass__(cls, **kwargs):
88 """
89 Warns if no subclass of this class was found in this module (py-file) that inherits from `Cell`\.
91 Every BoundaryCondition class should have a counterpart that also is a `Cell` and one the is a `Geometric`\.
92 This way, the boundary condition can be used in algorithms like Capacitors that also are cells (in meshes).
93 The classes should be named like the non-Cell/Geometric-classes extended with "Cell"/"Geometric".
95 This class is only useful during development, when new `BoundaryConditions` are added.
97 Parameters
98 ----------
99 kwargs
101 Returns
102 -------
104 """
105 super().__init_subclass__(**kwargs)
106 if cls.__name__.endswith("Geometric") or cls.__name__.endswith("Cell"):
107 # The method is run in the Cell/Geometric subclass, so no check is needed :D
108 return
109 for geometric_version in ["Cell", "Geometric"]:
110 _bc_missing_counterparts.append(cls.__name__ + geometric_version)
112 @property
113 def is_solid(self):
114 return not self._is_fluid
116 @property
117 def is_fluid(self):
118 return self._is_fluid
120 def __str__(self):
121 return self.__repr__()
123 def __repr__(self):
124 return f"{self.__class__.__name__}: ϑ={self.temperature}"
126 @property
127 def heat_transfer_coefficient(self):
128 return self.htc
130 @property
131 def index(self) -> int:
132 """
133 The index of `self` within the input vector (row in input matrix).
135 The value is cached to improve performance.
137 Returns
138 -------
139 int
140 """
141 if not self._index:
142 self._index = self.rc_objects.inputs.index(self)
143 return self._index
145 @property
146 def initial_value(self):
147 return self.initial_temperature
149 @initial_value.setter
150 def initial_value(self, value):
151 self.initial_temperature = value
153 @property
154 def temperature(self) -> float | int | np.number:
155 """
156 The temperature of `self`\.
158 If no solution is saved yet, the initial temperature is returned.
160 Returns
161 -------
162 float | int | np.number
163 """
164 if not self.solutions.input_exists:
165 return self.initial_temperature
166 return self.solutions.last_value_input(self.index)
168 @property
169 def temperature_vector(self):
170 """
171 The vector with all temperature values of `self` of all (currently existing) time steps.
173 If no solution is saved (yet), the initial temperature is returned as vector with `time_step` length.
175 Returns
176 -------
177 np.ndarray | np.number
178 """
179 if not self.solutions.exist:
180 result = np.array([self.temperature] * self.solutions.time_steps_count)
181 return result
182 return self.solutions.input_vectors[:, self.index]
184 def calculate_static(self, tau, temp_vector, _input_vector, *args, **kwargs):
185 """
187 Parameters
188 ----------
189 tau
190 temp_vector
191 _input_vector
192 args
193 kwargs
195 Returns
196 -------
198 """
199 return self.temperature
202class BoundaryConditionGeometric(BoundaryCondition, Geometric):
203 def __init__(self, *args, position, **kwargs):
204 Geometric.__init__(self, position=position)
205 BoundaryCondition.__init__(self, *args, **kwargs)
208class BoundaryConditionCell(BoundaryCondition, Cell):
209 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
210 Cell.__init__(self, position=position, delta=delta)
211 BoundaryCondition.__init__(self, *args, **kwargs)
214class FluidBoundaryCondition(BoundaryCondition):
215 def __init__(
216 self,
217 *args,
218 rc_objects: RCObjects = initial_rc_objects,
219 rc_solution: RCSolution = solution_object,
220 **kwargs: Settings | float | int | np.ndarray,
221 ):
222 super().__init__(*args, rc_objects=rc_objects, rc_solution=rc_solution, is_fluid=True, **kwargs)
225class FluidBoundaryConditionGeometric(FluidBoundaryCondition, Geometric):
226 def __init__(self, *args, position, **kwargs):
227 Geometric.__init__(self, position=position)
228 FluidBoundaryCondition.__init__(self, *args, **kwargs)
231class FluidBoundaryConditionCell(FluidBoundaryCondition, Cell):
232 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
233 Cell.__init__(self, position=position, delta=delta)
234 FluidBoundaryCondition.__init__(self, *args, **kwargs)
237class InteriorBoundaryCondition(FluidBoundaryCondition):
238 pass
241class InteriorBoundaryConditionGeometric(InteriorBoundaryCondition, Geometric):
242 def __init__(self, *args, position, **kwargs):
243 Geometric.__init__(self, position=position)
244 InteriorBoundaryCondition.__init__(self, *args, **kwargs)
247class InteriorBoundaryConditionCell(InteriorBoundaryCondition, Cell):
248 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
249 Cell.__init__(self, position=position, delta=delta)
250 InteriorBoundaryCondition.__init__(self, *args, **kwargs)
253class ExteriorTemperatureInputMixin:
254 def __init__(
255 self,
256 temperature_delta=None,
257 middle_temperature=None,
258 settings: Settings = initial_settings,
259 ):
260 """
261 Class to calculate the exterior temperature.
263 The temperature can be returned:
264 1. static
265 2. dynamic: sinus curve using ``temperature_delta`` and ``middle_temperature``
266 3. dynamic: weather data. The information is in the ``settings`` object.
268 Parameters
269 ----------
270 temperature_delta : float | int, optional
271 The amplitude of the sinus curve.
272 Not used if ``self.settings.use_weather_data is True``
273 middle_temperature : float | int, optional
274 The middle temperature of the sinus curve.
275 Not used if ``self.settings.use_weather_data is True``
276 settings : Settings, optional
277 A settings object with all important settings.
278 If not given the initial Settings object is used.
279 """
280 self.settings = settings
281 if not self.settings.use_weather_data:
282 assert temperature_delta is not None and middle_temperature is not None
283 self.temperature_delta = temperature_delta
284 self.middle_temperature = middle_temperature
286 def get_kwargs_functions(self) -> dict:
287 """
288 See Input.get_kwargs()
290 Returns
291 -------
292 dict :
293 The names of the values that must be passed to calculate_static/dynamic and the function to calculate
294 this values.
295 If for example the exterior temperature is needed for calculate_dynamic, the dict looks like this:
296 {"exterior_temperature": lambda tau: my_fancy_algorithm_to_calculate/get_the_exterior_temperature(tau)}
297 To use it, this dict must be passed to calculate_dynamic like this:
298 my_obj.calculate_dynamic(tau, temp_vector, _input_vector, **the_get_kwargs_dict)
299 """
300 if self.settings.calculate_static:
301 return {"exterior_temperature": self.calculate_static_fun()}
302 return {"exterior_temperature": self.calculate_dynamic_fun()}
304 def calculate_static_fun(self) -> Callable:
305 dynamic_fun = self.calculate_dynamic_fun()
306 value = dynamic_fun(0)
307 return lambda *args, **_kwargs: value
309 def calculate_dynamic_fun(self) -> Callable:
310 if not self.settings.use_weather_data:
311 return lambda tau, *args, **kwargs: (
312 (self.temperature_delta * -np.cos(np.pi / 43200 * (tau + self.settings.start_shift - 14400))) / 2
313 + self.middle_temperature
314 )
315 # use weather data
316 return self.settings.weather_data.get_interpolator("exterior_temperature")
318 def calculate_dynamic(self, *args, exterior_temperature=None, **kwargs):
319 return exterior_temperature
322class ExteriorBoundaryCondition(FluidBoundaryCondition, ExteriorTemperatureInputMixin):
323 def __init__(
324 self,
325 *args,
326 temperature_delta=None,
327 middle_temperature=None,
328 rc_objects: RCObjects = initial_rc_objects,
329 rc_solution: RCSolution = solution_object,
330 settings: Settings = initial_settings,
331 **kwargs: bool | float | int,
332 ):
333 FluidBoundaryCondition.__init__(
334 self, *args, rc_objects=rc_objects, rc_solution=rc_solution, settings=settings, **kwargs
335 )
336 ExteriorTemperatureInputMixin.__init__(self, temperature_delta, middle_temperature, settings=settings)
338 def get_kwargs_functions(self) -> dict:
339 return ExteriorTemperatureInputMixin.get_kwargs_functions(self)
341 def calculate_static(self, *args, **kwargs):
342 return self.calculate_dynamic(*args, **kwargs)
344 def calculate_dynamic(self, tau, temp_vector, _input_vector, *args, **kwargs):
345 # Logic is in ExteriorTemperatureInputMixin.calculate_dynamic_fun.
346 # In **kwargs there must be a keyword "exterior_temperature" to work correctly
347 return ExteriorTemperatureInputMixin.calculate_dynamic(self, **kwargs)
350class ExteriorBoundaryConditionGeometric(ExteriorBoundaryCondition, Geometric):
351 def __init__(self, *args, position, **kwargs):
352 Geometric.__init__(self, position=position)
353 ExteriorBoundaryCondition.__init__(self, *args, **kwargs)
356class ExteriorBoundaryConditionCell(ExteriorBoundaryCondition, Cell):
357 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
358 Cell.__init__(self, position=position, delta=delta)
359 ExteriorBoundaryCondition.__init__(self, *args, **kwargs)
362class SolidBoundaryCondition(BoundaryCondition):
363 def __init__(
364 self, *args, rc_objects: RCObjects = initial_rc_objects, rc_solution: RCSolution = solution_object, **kwargs
365 ):
366 super().__init__(*args, rc_objects=rc_objects, rc_solution=rc_solution, is_fluid=False, **kwargs)
369class SolidBoundaryConditionGeometric(SolidBoundaryCondition, Geometric):
370 def __init__(self, *args, position, **kwargs):
371 Geometric.__init__(self, position=position)
372 SolidBoundaryCondition.__init__(self, *args, **kwargs)
375class SolidBoundaryConditionCell(SolidBoundaryCondition, Cell):
376 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
377 Cell.__init__(self, position=position, delta=delta)
378 SolidBoundaryCondition.__init__(self, *args, **kwargs)
381class FlowBoundaryCondition(FluidBoundaryCondition, ConnectedFlowObject):
382 def __init__(
383 self,
384 *args,
385 volume_flow=None,
386 rc_objects: RCObjects = initial_rc_objects,
387 rc_solution: RCSolution = solution_object,
388 **kwargs,
389 ):
390 FluidBoundaryCondition.__init__(self, *args, rc_objects=rc_objects, rc_solution=rc_solution, **kwargs)
391 ConnectedFlowObject.__init__(self)
393 self.volume_flow = volume_flow
395 if self.is_mass_flow_start:
396 self.volume_flow_is_balanced = True
398 @property
399 def balance(self):
400 from pyrc.core.resistors import MassTransport
402 return calculate_balance_for_resistors(self, [res for res in self.neighbours if isinstance(res, MassTransport)])
404 @property
405 def volume_flow(self):
406 return super().volume_flow
408 @volume_flow.setter
409 def volume_flow(self, value):
410 self._volume_flow = value
412 @property
413 def sinks(self) -> list[MassFlowNode]:
414 """
415 A list with all `MassFlowNode`\s that are sinks of mass flow for `self`\.
417 Returns
418 -------
419 list[MassFlowNode]
420 """
421 from pyrc.core.resistors import MassTransport
423 result = []
424 if self.is_mass_flow_start:
425 for neighbour in self.neighbours:
426 if isinstance(neighbour, MassTransport):
427 result.append(neighbour.get_connected_node(self))
428 return result
430 @property
431 def sources(self) -> list[MassFlowNode]:
432 """
433 A list with all `MassFlowNode`\s that are sources of mass flow for `self`\.
435 Returns
436 -------
437 list[MassFlowNode]
438 """
439 from pyrc.core.resistors import MassTransport
441 result = []
442 if not self.is_mass_flow_start:
443 for neighbour in self.neighbours:
444 if isinstance(neighbour, MassTransport):
445 result.append(neighbour.get_connected_node(self))
446 return result
448 def check_balance(self) -> bool:
449 """
450 If the sum of all sinks and sources of `self` is 0 this method returns True, False otherwise.
452 Returns
453 -------
454 bool
455 """
456 if self.volume_flow_is_balanced:
457 return True
458 # check if start or end
459 if self.is_mass_flow_start:
460 self.volume_flow_is_balanced = True
461 return True
462 # self is end. So check all connected ConnectedFlowObjects for check_balance
463 flow_objects = []
464 resistor: Resistor
465 for node in [resistor.get_connected_node(self) for resistor in self.neighbours]:
466 if isinstance(node, ConnectedFlowObject):
467 if node in flow_objects:
468 continue
469 flow_objects.append(node)
470 for flow_object in flow_objects:
471 if flow_object.check_balance():
472 continue
473 else:
474 return False
475 return True
477 def connect(self, *args, **kwargs):
478 from pyrc.core.resistors import MassTransport
480 super().connect(*args, **kwargs)
481 # Prevent the connection to every other Resistor than MassTransport.
482 assert isinstance(self.neighbours[-1], MassTransport)
485class FlowBoundaryConditionGeometric(FlowBoundaryCondition, Geometric):
486 def __init__(self, *args, position, **kwargs):
487 Geometric.__init__(self, position=position)
488 FlowBoundaryCondition.__init__(self, *args, **kwargs)
491class FlowBoundaryConditionCell(FlowBoundaryCondition, Cell):
492 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
493 Cell.__init__(self, position=position, delta=delta)
494 FlowBoundaryCondition.__init__(self, *args, **kwargs)
497class ExteriorInletFlow(FlowBoundaryCondition, ExteriorTemperatureInputMixin):
498 """
499 A boundary condition with constant mass flow and the temperature of `ExteriorTemperatureInput`\.
501 This is the start of a mass flow.
502 """
504 def __init__(
505 self,
506 *args,
507 volume_flow=None,
508 temperature_delta=None,
509 middle_temperature=None,
510 rc_objects: RCObjects = initial_rc_objects,
511 rc_solution: RCSolution = solution_object,
512 settings: Settings = initial_settings,
513 **kwargs: np.ndarray | bool | float | int,
514 ):
515 FlowBoundaryCondition.__init__(
516 self,
517 *args,
518 volume_flow=volume_flow,
519 rc_objects=rc_objects,
520 rc_solution=rc_solution,
521 settings=settings,
522 **kwargs,
523 )
524 ExteriorTemperatureInputMixin.__init__(self, temperature_delta, middle_temperature, settings=settings)
525 self.is_mass_flow_start = True
527 def get_kwargs_functions(self) -> dict:
528 return ExteriorTemperatureInputMixin.get_kwargs_functions(self)
530 def calculate_static(self, *args, **kwargs):
531 return self.calculate_dynamic(*args, **kwargs)
533 def calculate_dynamic(self, tau, temp_vector, _input_vector, *args, **kwargs):
534 # Logic is in ExteriorTemperatureInputMixin.calculate_dynamic_fun.
535 # In **kwargs there must be a keyword "exterior_temperature" to work correctly
536 return ExteriorTemperatureInputMixin.calculate_dynamic(self, **kwargs)
539class ExteriorInletFlowGeometric(ExteriorInletFlow, Geometric):
540 def __init__(self, *args, position, **kwargs):
541 Geometric.__init__(self, position=position)
542 ExteriorInletFlow.__init__(self, *args, **kwargs)
545class ExteriorInletFlowCell(ExteriorInletFlow, Cell):
546 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
547 Cell.__init__(self, position=position, delta=delta)
548 ExteriorInletFlow.__init__(self, *args, **kwargs)
551class ExteriorOutletFlow(FlowBoundaryCondition, ExteriorTemperatureInputMixin):
552 """
553 A boundary condition serving as the end of a mass flow. The `volume_flow` value has no effect.
555 The temperature is set as `ExteriorTemperatureInput` to easily calculate the heat flux between the connected
556 `MassFlowNode` and the exterior Temperature. You could redefine this if you want to calculate another
557 difference. It has no effect for solving the RC network because only `MassTransport` Resistors are allowed as
558 neighbours and their are always just passing energy INTO this ExteriorOutletFlow boundary condition and never out
559 of it.
561 This is the end of a mass flow.
562 """
564 def __init__(
565 self,
566 *args,
567 temperature_delta=None,
568 middle_temperature=None,
569 rc_objects: RCObjects = initial_rc_objects,
570 rc_solution: RCSolution = solution_object,
571 settings: Settings = initial_settings,
572 **kwargs: np.ndarray | bool | float | int,
573 ):
574 FlowBoundaryCondition.__init__(
575 self, *args, rc_objects=rc_objects, rc_solution=rc_solution, settings=settings, **kwargs
576 )
577 ExteriorTemperatureInputMixin.__init__(self, temperature_delta, middle_temperature, settings=settings)
578 self.is_mass_flow_start = False
580 def get_kwargs_functions(self) -> dict:
581 return ExteriorTemperatureInputMixin.get_kwargs_functions(self)
583 def calculate_static(self, *args, **kwargs):
584 return self.calculate_dynamic(*args, **kwargs)
586 def calculate_dynamic(self, tau, temp_vector, _input_vector, *args, **kwargs):
587 # Logic is in ExteriorTemperatureInputMixin.calculate_dynamic_fun.
588 # In **kwargs there must be a keyword "exterior_temperature" to work correctly
589 return ExteriorTemperatureInputMixin.calculate_dynamic(self, **kwargs)
592class ExteriorOutletFlowGeometric(ExteriorOutletFlow, Geometric):
593 def __init__(self, *args, position, **kwargs):
594 Geometric.__init__(self, position=position)
595 ExteriorOutletFlow.__init__(self, *args, **kwargs)
598class ExteriorOutletFlowCell(ExteriorOutletFlow, Cell):
599 def __init__(self, *args, position, delta: np.ndarray | tuple = None, **kwargs):
600 Cell.__init__(self, position=position, delta=delta)
601 ExteriorOutletFlow.__init__(self, *args, **kwargs)
604class InternalHeatSource(EquationItem, Input):
605 def __init__(
606 self,
607 node: Node,
608 specific_power_in_w_per_cubic_meter=None,
609 specific_power_in_w_per_meter_squared=None,
610 area_direction: np.ndarray = None,
611 settings: Settings = initial_settings,
612 ):
613 EquationItem.__init__(self)
614 Input.__init__(self, settings=settings)
615 self.node: Node = node
616 self.volume_specific_power = specific_power_in_w_per_cubic_meter # in W/(m**3)
617 self.__area_specific_power = specific_power_in_w_per_meter_squared # in W/(m**3)
618 self.symbol = symbols(f"Q_dot_{self.node.id}")
619 self.area_direction: np.ndarray = area_direction # should be like (1,0,0) in any order and sign
621 # must be placed after initialization!
622 if specific_power_in_w_per_cubic_meter is not None and specific_power_in_w_per_meter_squared is not None:
623 raise ValueError("Do not set both power variables.")
624 elif specific_power_in_w_per_cubic_meter is None and specific_power_in_w_per_meter_squared is not None:
625 if area_direction is None:
626 raise ValueError("If specific_power_in_w_per_meter_squared is set, the direction must be set, too.")
628 # The calculation of the volume_specific_power is done later because during initialization the volume isn't
629 # known (because it depends on the connected Nodes that are still created during initialization).
631 # check, if self.volume_specific_power is None and replace it with 0 if so
632 if self.volume_specific_power is None and self.__area_specific_power is None:
633 self.volume_specific_power = np.float64(0)
634 self._area = None
636 @property
637 def initial_value(self):
638 return self.power
640 @initial_value.setter
641 def initial_value(self, value):
642 self.volume_specific_power = value / self.node.volume
644 @property
645 def area(self):
646 if self._area is None:
647 self._area = self.node.area(self.area_direction)
648 return self._area
650 @property
651 def index(self) -> int:
652 if not self._index:
653 self._index = self.node.rc_objects.inputs.index(self)
654 return self._index
656 @property
657 def power(self):
658 if self.volume_specific_power is None:
659 self.set_area_specific_power(self.__area_specific_power)
660 return self.volume_specific_power * self.node.volume
662 @property
663 def area_specific_power(self):
664 if self.area_direction is None:
665 return None
666 return self.power / self.area
668 def set_area_specific_power(self, area_specific_power_in_w_per_square_meter, direction: np.ndarray = None):
669 """
670 Sets the volume specific power by calculating it with the area specific power and a direction/normal of the
671 effective surface.
673 Parameters
674 ----------
675 area_specific_power_in_w_per_square_meter : float | int
676 The area specific power in W/m**2.
677 direction : np.ndarray, optional
678 The normal of the surface where the power is applied to. Should be (1,0,0) with any order and sign.
679 Is used to get the area of the node using ``self.node.area(direction)``\.
681 """
682 if direction is None:
683 assert self.area_direction is not None
684 if self.area_direction is None:
685 assert isinstance(direction, np.ndarray) and len(direction) == 3, "Direction to face must be passed!"
686 self.area_direction = direction
687 else:
688 if direction is not None:
689 assert isinstance(direction, np.ndarray) and len(direction) == 3
690 print("Warning: direction is overwritten.")
691 self.area_direction = direction
692 self.volume_specific_power = area_specific_power_in_w_per_square_meter * self.area / self.node.volume
694 def calculate_static(self, tau, temp_vector, _input_vector, **kwargs):
695 return self.power
697 @property
698 def symbols(self) -> list:
699 return [self.symbol]
701 @property
702 def values(self) -> list:
703 return [self.power]
706class Radiation(InternalHeatSource):
707 def __init__(self, *args, epsilon_short=0.7, epsilon_long=0.93, **kwargs):
708 super().__init__(*args, **kwargs)
709 self.epsilon_short = epsilon_short
710 self.epsilon_long = epsilon_long
711 self.epsilon_long_boltzmann = self.epsilon_long * Stefan_Boltzmann # pre-calculation of calculate_dynamic
712 self._bc_temp_input_index = None
714 @property
715 def bc_temp_input_index(self):
716 if self._bc_temp_input_index is None:
717 for fbc in self.node.get_connected_nodes(variant=FluidBoundaryCondition):
718 # TODO: If multiple BoundaryConditions are connected, only the last one is used for temp. calculation
719 # If one capacitor has multiple BCs connected the calculation won't work as it is implemented currently
720 # with calculate_dynamic_functions if no weather data is used.
721 fbc: FluidBoundaryCondition
722 self._bc_temp_input_index = fbc.index
723 assert self._bc_temp_input_index is not None
724 return self._bc_temp_input_index
726 def get_kwargs_functions(self) -> dict:
727 if self.settings.calculate_static:
728 return self.calculate_static_functions()
729 return self.calculate_dynamic_functions()
731 def calculate_static_functions(self) -> dict:
732 dyn_functions: dict = self.calculate_dynamic_functions()
733 result = {}
734 for name, fun in dyn_functions.items():
735 result[name] = (
736 lambda f: lambda tau, temp_vec, input_vec, *args, **kwargs: f(0, temp_vec, input_vec, *args, **kwargs)
737 )(fun)
738 return result
740 def calculate_dynamic_functions(self) -> dict:
741 """
742 This function is used to pre-calculate some values during solving that are node independent.
744 This way calculation time can be decreased.
746 Returns
747 -------
748 dict :
749 A dictionary where the key stands for the parameter that is passed to the function calculating node
750 dependent stuff and the value is its value (crazy, I know).
751 """
752 result = {}
754 if self.settings.use_weather_data:
755 result["incoming_area_specific_power_long"] = self.settings.area_specific_radiation_interpolator_long
756 result["incoming_area_specific_power_short"] = self.settings.area_specific_radiation_interpolator_short
757 else:
758 dt = datetime.fromisoformat(self.settings.start_date)
759 seconds_today = dt.hour * 3600 + dt.minute * 60 + dt.second + dt.microsecond / 1e6
760 result["incoming_area_specific_power_short"] = lambda tau, *args, **kwargs: (
761 sin_function(tau, seconds_today) * self.settings.maximum_area_specific_power
762 )
763 result["incoming_area_specific_power_long"] = lambda tau, *args, **kwargs: 0
765 # the following only works if only one boundary condition is used for all Radiation objects (
766 # e.g. the ExteriorTemperature and all Radiation objects are influenced by the same. If multiple
767 # influences between Radiation objects and BCs would exists, the index is changing, depending on the
768 # Radiation object. For that, calculate the value directly in each object (increases calculation overhead)
769 # or create a unique kwarg for each of them (recommended with new classes))
770 index = self.bc_temp_input_index
771 result["sky_temp_4_diff"] = lambda tau, temp_vector, _input_vector, *args, _index=index, **kwargs: (
772 sky_temp_according_to_swinbank(_input_vector.ravel()[_index]) ** 4
773 - sky_temp_according_to_swinbank(_input_vector.ravel()[_index]) ** 4
774 )
775 return result
777 def calculate_static(self, tau, temp_vector, _input_vector, **kwargs):
778 return self.calculate_dynamic(0, temp_vector, _input_vector, **kwargs)
780 def calculate_dynamic(
781 self,
782 tau,
783 temp_vector,
784 _input_vector,
785 incoming_area_specific_power_short=np.nan,
786 incoming_area_specific_power_long=np.nan,
787 sky_temp_4_diff=np.nan,
788 **kwargs,
789 ):
790 temp_vector_flat = temp_vector.ravel()
792 node_temp_4 = temp_vector_flat[self.node.index] ** 4
794 if self.settings.use_weather_data:
795 outgoing_spec_power = self.epsilon_long_boltzmann * node_temp_4 # in W/m^2
796 else:
797 # here incoming_area_specific_power only contains the direct radiation. So the radiation towards sky and
798 # ambient has to be calculated, too.
799 # TODO: Use the sight factor of the wall for both sky and bc/ambient (but this information is not accessible
800 # here)
801 outgoing_spec_power = 0.5 * self.epsilon_long_boltzmann * (2 * node_temp_4 - sky_temp_4_diff)
802 power = (
803 incoming_area_specific_power_long * self.epsilon_long
804 + incoming_area_specific_power_short * self.epsilon_short
805 - outgoing_spec_power
806 ) * self.area
807 self.volume_specific_power = power / self.node.volume
808 return power
811def sky_temp_according_to_swinbank(air_temperature: float):
812 return 0.0552 * air_temperature ** 1.5
815def sin_function(tau, shift_start_time):
816 return np.maximum(0.0, -np.cos(np.pi / 43200 * (tau + shift_start_time)))
819# must be at the end of this module
820# This checks if for every BoundaryCondition also a similar class exists that inherits from Geometry / Cell (both separately)
821_module = sys.modules[__name__]
822for _counterpart in _bc_missing_counterparts:
823 if not hasattr(_module, _counterpart):
824 import warnings
826 warnings.warn(f"Missing counterpart {_counterpart}", stacklevel=2)