Source code for archetypal.template.constructions.window_construction

"""Window module handles window settings.

Notes:
    Thank you to `honeybee-energy <https://github.com/ladybug-tools/honeybee-energy/
    blob/master/honeybee_energy/construction/window.py>`_ for implementing center
    of glass resistance formulas from ISO. Those where adapted to the structure of the
    archetypal.template module.
"""

import collections
from enum import Enum

from validator_collection import validators

from archetypal.simple_glazing import calc_simple_glazing
from archetypal.template.constructions.base_construction import LayeredConstruction
from archetypal.template.materials.gas_layer import GasLayer
from archetypal.template.materials.gas_material import GasMaterial
from archetypal.template.materials.glazing_material import GlazingMaterial
from archetypal.template.materials.material_layer import MaterialLayer


[docs]class WindowType(Enum): """Refers to the window type. Two choices are available: interior or exterior.""" External = 0 Internal = 1 def __lt__(self, other): """Return true if self lower than other.""" return self._value_ < other._value_ def __gt__(self, other): """Return true if self higher than other.""" return self._value_ > other._value_
[docs]class ShadingType(Enum): """Refers to window shading types. Hint: EnergyPlus specifies 8 different shading types, but only 2 are supported here: InteriorShade and ExteriorShade. See shading_ for more info. .. _shading: https://bigladdersoftware.com/epx/docs/8-4/input-output-reference/group-thermal-zone-description-geometry.html#field-shading-type """ ExteriorShade = 0 InteriorShade = 1 def __lt__(self, other): """Return true if self lower than other.""" return self._value_ < other._value_ def __gt__(self, other): """Return true if self higher than other.""" return self._value_ > other._value_
[docs]class WindowConstruction(LayeredConstruction): """Window Construction. .. image:: ../images/template/constructions-window.png """ _CATEGORIES = ("single", "double", "triple", "quadruple") __slots__ = ("_category",) def __init__(self, Name, Layers, Category="Double", **kwargs): """Initialize a WindowConstruction. Args: Name (str): Name of the WindowConstruction. Layers (list of (MaterialLayer or GasLayer)): List of MaterialLayer and GasLayer. Category (str): "Single", "Double" or "Triple". **kwargs: Other keywords passed to the constructor. """ super(WindowConstruction, self).__init__( Name, Layers, Category=Category, **kwargs, ) self.Category = Category # set here for validators @property def Category(self): """Get or set the Category. Choices are ("single", "double", "triple").""" return self._category @Category.setter def Category(self, value): assert value.lower() in self._CATEGORIES, ( f"Input error for value '{value}'. The " f"Category must be one of ({self._CATEGORIES})" ) self._category = value @property def gap_count(self): """Get the number of gas gaps contained within the window construction.""" count = 0 for layer in self.Layers: if isinstance(layer, GasLayer): count += 1 return count @property def glazing_count(self): """Get the nb of glazing materials contained within the window construction.""" count = 0 for layer in self.Layers: if isinstance(layer, MaterialLayer): count += 1 return count @property def r_factor(self): """Get the construction R-factor [m2-K/W]. Note: including standard resistances for air films. Formulas for film coefficients come from EN673 / ISO10292. """ gap_count = self.gap_count if gap_count == 0: # single pane return ( self.Layers[0].r_value + (1 / self.out_h_simple()) + (1 / self.in_h_simple()) ) elif gap_count == 1: heat_transfers, temperature_profile = self.heat_balance("summer", 0) *_, Q_dot_i4 = heat_transfers return (temperature_profile[-1] - temperature_profile[0]) / Q_dot_i4 r_vals, emissivities = self._layered_r_value_initial(gap_count) r_vals = self._solve_r_values(r_vals, emissivities) return sum(r_vals) @property def r_value(self): """Get or set the thermal resistance [K⋅m2/W] (excluding air films).""" gap_count = self.gap_count if gap_count == 0: # single pane return self.Layers[0].r_value r_vals, emissivities = self._layered_r_value_initial(gap_count) r_vals = self._solve_r_values(r_vals, emissivities) return sum(r_vals[1:-1]) @property def outside_emissivity(self): """Get the hemispherical emissivity of the outside face of the construction.""" return self.Layers[0].Material.IREmissivityFront @property def inside_emissivity(self): """Get the hemispherical emissivity of the inside face of the construction.""" return self.Layers[-1].Material.IREmissivityBack @property def solar_transmittance(self): """Get the solar transmittance of the window at normal incidence.""" if self.glazing_count == 2: tau_1 = self.Layers[0].Material.SolarTransmittance tau_2 = self.Layers[-1].Material.SolarTransmittance rho_1 = self.Layers[0].Material.SolarReflectanceFront rho_2 = self.Layers[-1].Material.SolarReflectanceFront return (tau_1 * tau_2) / (1 - rho_1 * rho_2) trans = 1 for layer in self.Layers: if isinstance(layer.Material, GlazingMaterial): trans *= layer.Material.SolarTransmittance return trans @property def visible_transmittance(self): """Get the visible transmittance of the window at normal incidence.""" trans = 1 for layer in self.Layers: if isinstance(layer.Material, GlazingMaterial): trans *= layer.Material.VisibleTransmittance return trans @property def thickness(self): """Thickness of the construction [m].""" thickness = 0 for layer in self.Layers: thickness += layer.Thickness return thickness
[docs] @classmethod def from_dict(cls, data, materials, **kwargs): """Create an WindowConstruction from a dictionary. Args: data (dict): The python dictionary. materials (dict): A dictionary of materials with their id as keys. **kwargs: keywords passed to the constructor. .. code-block:: python data = { "$id": "57", "Layers": [ {"Material": {"$ref": "7"}, "Thickness": 0.003}, {"Material": {"$ref": "1"}, "Thickness": 0.006}, {"Material": {"$ref": "7"}, "Thickness": 0.003}, ], "AssemblyCarbon": 0.0, "AssemblyCost": 0.0, "AssemblyEnergy": 0.0, "DisassemblyCarbon": 0.0, "DisassemblyEnergy": 0.0, "Category": "Double", "Comments": "default", "DataSource": "default", "Name": "B_Dbl_Air_Cl", } """ _id = data.pop("$id") layers = [ MaterialLayer(materials[layer["Material"]["$ref"]], layer["Thickness"]) if isinstance( materials[layer["Material"]["$ref"]], (MaterialLayer, GlazingMaterial) ) else GasLayer(materials[layer["Material"]["$ref"]], layer["Thickness"]) for layer in data.pop("Layers") ] return cls(Layers=layers, id=_id, **data, **kwargs)
[docs] @classmethod def from_epbunch(cls, Construction, **kwargs): """Create :class:`WindowConstruction` object from idf Construction object. Example: >>> from archetypal import IDF >>> from archetypal.template.window_setting import WindowSetting >>> idf = IDF("myidf.idf") >>> construction_name = "Some construction name" >>> WindowConstruction.from_epbunch(Name=construction_name, idf=idf) Args: Construction (EpBunch): The Construction epbunch object. **kwargs: Other keywords passed to the constructor. """ layers = WindowConstruction._layers_from_construction(Construction, **kwargs) catdict = {0: "Single", 1: "Single", 2: "Double", 3: "Triple", 4: "Quadruple"} category = catdict[ len([lyr for lyr in layers if isinstance(lyr.Material, GlazingMaterial)]) ] return cls(Name=Construction.Name, Layers=layers, Category=category, **kwargs)
[docs] @classmethod def from_shgc( cls, Name, solar_heat_gain_coefficient, u_factor, visible_transmittance=None, **kwargs, ): """Create a WindowConstruction from shgc, u_factor and visible_transmittance. Args: Name (str): The name of the window construction. shgc (double): The window's Solar Heat Gain Coefficient. u_factor (double): The window's U-value. visible_transmittance (double, optional): The window's visible transmittance. If none, the visible transmittance defaults to the solar transmittance t_sol. kwargs: keywrods passed to the parent constructor. Returns: """ glass_properties = calc_simple_glazing( solar_heat_gain_coefficient, u_factor, visible_transmittance, ) material_obj = GlazingMaterial(Name="Simple Glazing", **glass_properties) material_layer = MaterialLayer(material_obj, glass_properties["Thickness"]) return cls(Name, Layers=[material_layer], **kwargs)
[docs] def to_dict(self): """Return WindowConstruction dictionary representation.""" self.validate() # Validate object before trying to get json format data_dict = collections.OrderedDict() data_dict["$id"] = str(self.id) data_dict["Layers"] = [layer.to_dict() for layer in self.Layers] data_dict["AssemblyCarbon"] = self.AssemblyCarbon data_dict["AssemblyCost"] = self.AssemblyCost data_dict["AssemblyEnergy"] = self.AssemblyEnergy data_dict["DisassemblyCarbon"] = self.DisassemblyCarbon data_dict["DisassemblyEnergy"] = self.DisassemblyEnergy data_dict["Category"] = self.Category data_dict["Comments"] = validators.string(self.Comments, allow_empty=True) data_dict["DataSource"] = self.DataSource data_dict["Name"] = self.Name return data_dict
[docs] def to_epbunch(self, idf): """Convert self to a `Construction` epbunch given an idf model. Args: idf (IDF): The idf model in which the EpBunch is created. .. code-block:: python Construction, B_Dbl_Air_Cl, !- Name B_Glass_Clear_3_0.003_B_Dbl_Air_Cl, !- Outside Layer AIR_0.006_B_Dbl_Air_Cl, !- Layer 2 B_Glass_Clear_3_0.003_B_Dbl_Air_Cl; !- Layer 3 Returns: EpBunch: The EpBunch object added to the idf model. """ data = {"Name": self.Name} for i, layer in enumerate(self.Layers): mat = layer.to_epbunch(idf) if i < 1: data["Outside_Layer"] = mat.Name else: data[f"Layer_{i+1}"] = mat.Name return idf.newidfobject("CONSTRUCTION", **data)
[docs] def mapping(self, validate=True): """Get a dict based on the object properties, useful for dict repr. Args: validate (bool): If True, try to validate object before returning the mapping. """ if validate: self.validate() return dict( Layers=self.Layers, AssemblyCarbon=self.AssemblyCarbon, AssemblyCost=self.AssemblyCost, AssemblyEnergy=self.AssemblyEnergy, DisassemblyCarbon=self.DisassemblyCarbon, DisassemblyEnergy=self.DisassemblyEnergy, Category=self.Category, Comments=self.Comments, DataSource=self.DataSource, Name=self.Name, )
[docs] def combine(self, other, weights=None): """Append other to self. Return self + other as a new object. For now, simply returns self. todo: - Implement equivalent window layers for constant u-factor. """ # Check if other is None. Simply return self if not other: return self if not self: return other return self
[docs] def validate(self): """Validate object and fill in missing values. todo: - Implement validation """ return self
[docs] def duplicate(self): """Get copy of self.""" return self.__copy__()
[docs] def temperature_profile( self, outside_temperature=-18, inside_temperature=21, wind_speed=6.7, height=1.0, angle=90.0, pressure=101325, ): """Get a list of temperatures at each material boundary across the construction. Args: outside_temperature: The temperature on the outside of the construction [C]. Default is -18, which is consistent with NFRC 100-2010. inside_temperature: The temperature on the inside of the construction [C]. Default is 21, which is consistent with NFRC 100-2010. wind_speed: The average outdoor wind speed [m/s]. This affects outdoor convective heat transfer coefficient. Default is 6.7 m/s. height: An optional height for the surface in meters. Default is 1.0 m. angle: An angle in degrees between 0 and 180. 0 = A horizontal surface with the outside boundary on the bottom. 90 = A vertical surface 180 = A horizontal surface with the outside boundary on the top. pressure: The average pressure of in Pa. Default is 101325 Pa for standard pressure at sea level. Returns: A tuple with two elements - temperatures: A list of temperature values [C]. The first value will always be the outside temperature and the second will be the exterior surface temperature. The last value will always be the inside temperature and the second to last will be the interior surface temperature. - r_values: A list of R-values for each of the material layers [m2-K/W]. The first value will always be the resistance of the exterior air and the last value is the resistance of the interior air. The sum of this list is the R-factor for this construction given the input parameters. """ # reverse the angle if the outside temperature is greater than the inside one if angle != 90 and outside_temperature > inside_temperature: angle = abs(180 - angle) gap_count = self.gap_count # single pane or simple glazing system if gap_count == 0: in_r_init = 1 / self.in_h_simple() r_values = [ 1 / self.out_h(wind_speed, outside_temperature + 273.15), self.Layers[0].r_value, in_r_init, ] in_delta_t = (in_r_init / sum(r_values)) * ( outside_temperature - inside_temperature ) r_values[-1] = 1 / self.in_h( inside_temperature - (in_delta_t / 2) + 273.15, in_delta_t, height, angle, pressure, ) temperatures = self._temperature_profile_from_r_values( r_values, outside_temperature, inside_temperature ) return temperatures, r_values # multi-layered window construction guess = abs(inside_temperature - outside_temperature) / 2 guess = 1 if guess < 1 else guess # prevents zero division with gas conductance avg_guess = ((inside_temperature + outside_temperature) / 2) + 273.15 r_values, emissivities = self._layered_r_value_initial( gap_count, guess, avg_guess, wind_speed ) r_last = 0 r_next = sum(r_values) while abs(r_next - r_last) > 0.001: # 0.001 is the r-value tolerance r_last = sum(r_values) temperatures = self._temperature_profile_from_r_values( r_values, outside_temperature, inside_temperature ) r_values = self._layered_r_value( temperatures, r_values, emissivities, height, angle, pressure ) r_next = sum(r_values) temperatures = self._temperature_profile_from_r_values( r_values, outside_temperature, inside_temperature ) return temperatures, r_values
@staticmethod def _layers_from_construction(construction, **kwargs): """Retrieve layers for the Construction epbunch.""" layers = [] for field in construction.fieldnames[2:]: # Loop through the layers from the outside layer towards the # indoor layers and get the material they are made of. material = construction.get_referenced_object(field) or kwargs.get( "material", None ) if material: # Create the WindowMaterial:Glazing or the WindowMaterial:Gas # and append to the list of layers if material.key.upper() == "WindowMaterial:Glazing".upper(): material_obj = GlazingMaterial( Name=material.Name, Conductivity=material.Conductivity, SolarTransmittance=material.Solar_Transmittance_at_Normal_Incidence, SolarReflectanceFront=material.Front_Side_Solar_Reflectance_at_Normal_Incidence, SolarReflectanceBack=material.Back_Side_Solar_Reflectance_at_Normal_Incidence, VisibleTransmittance=material.Visible_Transmittance_at_Normal_Incidence, VisibleReflectanceFront=material.Front_Side_Visible_Reflectance_at_Normal_Incidence, VisibleReflectanceBack=material.Back_Side_Visible_Reflectance_at_Normal_Incidence, IRTransmittance=material.Infrared_Transmittance_at_Normal_Incidence, IREmissivityFront=material.Front_Side_Infrared_Hemispherical_Emissivity, IREmissivityBack=material.Back_Side_Infrared_Hemispherical_Emissivity, DirtFactor=material.Dirt_Correction_Factor_for_Solar_and_Visible_Transmittance, Optical=material.Optical_Data_Type, OpticalData=material.Window_Glass_Spectral_Data_Set_Name, ) material_layer = MaterialLayer(material_obj, material.Thickness) elif material.key.upper() == "WindowMaterial:Gas".upper(): # Todo: Make gas name generic, like in UmiTemplateLibrary Editor material_obj = GasMaterial( Name=material.Gas_Type.upper(), Conductivity=0.02 ) material_layer = GasLayer(material_obj, material.Thickness) elif material.key.upper() == "WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM": glass_properties = calc_simple_glazing( material.Solar_Heat_Gain_Coefficient, material.UFactor, material.Visible_Transmittance, ) material_obj = GlazingMaterial( Name=material.Name, **glass_properties ) material_layer = MaterialLayer( material_obj, glass_properties["Thickness"] ) layers.append(material_layer) break else: continue layers.append(material_layer) return layers def _layered_r_value_initial( self, gap_count, delta_t_guess=15, avg_t_guess=273.15, wind_speed=6.7 ): """Compute initial r-values of each layer within a layered construction.""" r_vals = [1 / self.out_h(wind_speed, avg_t_guess - delta_t_guess)] emiss = [] delta_t = delta_t_guess / gap_count for i, lyr in enumerate(self.Layers): if isinstance(lyr, MaterialLayer): r_vals.append(lyr.r_value) emiss.append(None) else: # gas layer e_front = self.Layers[i + 1].Material.IREmissivityFront e_back = self.Layers[i + 1].Material.IREmissivityBack r_vals.append( 1 / lyr.u_value(delta_t, e_back, e_front, t_kelvin=avg_t_guess) ) emiss.append((e_back, e_front)) r_vals.append(1 / self.in_h_simple()) return r_vals, emiss def __hash__(self): """Return the hash value of self.""" return hash((self.__class__.__name__, getattr(self, "Name", None))) def __eq__(self, other): """Assert self is equivalent to other.""" if not isinstance(other, WindowConstruction): return NotImplemented else: return all( [ self.Category == other.Category, self.AssemblyCarbon == other.AssemblyCarbon, self.AssemblyCost == other.AssemblyCost, self.AssemblyEnergy == other.AssemblyEnergy, self.DisassemblyCarbon == other.DisassemblyCarbon, self.DisassemblyEnergy == other.DisassemblyEnergy, self.Layers == other.Layers, ] ) def __copy__(self): """Create a copy of self.""" return self.__class__(**self.mapping()) def __add__(self, other): """Combine self and other.""" return self.combine(other) def _solve_r_values(self, r_values, emissivities): """Solve iteratively for R-values.""" r_last = 0 r_next = sum(r_values) while abs(r_next - r_last) > 0.001: # 0.001 is the r-value tolerance r_last = sum(r_values) temperatures = self._temperature_profile_from_r_values(r_values) r_values = self._layered_r_value(temperatures, r_values, emissivities) r_next = sum(r_values) return r_values def _temperature_profile_from_r_values( self, r_values, outside_temperature=-18, inside_temperature=21 ): """Get a list of temperatures at each material boundary between R-values.""" r_factor = sum(r_values) delta_t = inside_temperature - outside_temperature temperatures = [outside_temperature] for i, r_val in enumerate(r_values): temperatures.append(temperatures[i] + (delta_t * (r_val / r_factor))) return temperatures def _layered_r_value( self, temperatures, r_values_init, emiss, height=1.0, angle=90.0, pressure=101325, ): """Compute delta_t adjusted r-values of each layer within a construction.""" r_vals = [r_values_init[0]] for i, layer in enumerate(self.Layers): if isinstance(layer, MaterialLayer): r_vals.append(r_values_init[i + 1]) elif isinstance(layer, GasLayer): # gas layer delta_t = abs(temperatures[i + 1] - temperatures[i + 2]) avg_temp = ((temperatures[i + 1] + temperatures[i + 2]) / 2) + 273.15 r_vals.append( 1 / layer.u_value_at_angle( delta_t, emiss[i][0], emiss[i][1], height, angle, avg_temp, pressure, ) ) delta_t = abs(temperatures[-1] - temperatures[-2]) avg_temp = ((temperatures[-1] + temperatures[-2]) / 2) + 273.15 r_vals.append(1 / self.in_h(avg_temp, delta_t, height, angle, pressure)) return r_vals
[docs] def shgc(self, environmental_conditions="summer", global_radiation=783): """Calculate the shgc given environmental conditions. Notes: This method implements a heat balance at each interface of the glazing unit including outside and inside air film resistances, solar radiation absorption in the glass. See :meth:`~archetypal.template.constructions.window_construction .WindowConstruction.heat_balance` for more details. Args: environmental_conditions (str): "summer" or "winter". A window shgc is usually calculated with summer conditions. Default is "summer". global_radiation (float): Incident solar radiation [W / m ^ 2]. Overwrite the solar radiation used in the calculation of the shgc. Returns: float: The shgc of the window construction for the given environmental conditions. """ # Q_dot_noSun heat_transfers, temperature_profile = self.heat_balance( environmental_conditions, 0 ) *_, Q_dot_noSun = heat_transfers # Q_dot_Sun heat_transfers, temperature_profile = self.heat_balance( environmental_conditions, 783 ) *_, Q_dot_i4 = heat_transfers Q_dot_sun = -Q_dot_i4 + self.solar_transmittance * global_radiation shgc = (Q_dot_sun - -Q_dot_noSun) / global_radiation return shgc
[docs] def heat_balance(self, environmental_conditions="summer", G_t=783): """Return heat flux and temperatures at each surface of the window. Note: Only implemented for glazing with two layers. Args: environmental_conditions (str): The environmental conditions from the NRFC standard used to calculate the heat balance. Default is "summer". G_t (float): The incident radiation. Returns: tuple: heat_flux, temperature_profile """ assert ( self.glazing_count == 2 ), f"Expected a window with 2 glazing layers, not {self.glazing_count}." ENV_CONDITIONS = {"summer": [32, 24, 2.75], "winter": [-18, 21, 5.5]} ( outside_temperature, inside_temperature, environmental_conditions, ) = ENV_CONDITIONS[environmental_conditions] temperatures_next, r_values = self.temperature_profile( outside_temperature, inside_temperature, environmental_conditions ) temperatures_last = [0] * 6 pressure = 101325 # [Pa] height = 1 # [m] angle = 90 # degree k_g = 1 # [W / m - K] sigma = 5.670e-8 # [W / m2 - K4] # "21011 (SGG Planiclear 4 mm), Air 12 mm, 21414 (SGG Planitherm One 4 mm)" epsilon_1 = self.Layers[0].Material.IREmissivityFront # [-] epsilon_2 = self.Layers[0].Material.IREmissivityBack # [-] epsilon_3 = self.Layers[-1].Material.IREmissivityFront # [-] epsilon_4 = self.Layers[-1].Material.IREmissivityBack # [-] abs_1 = 0.0260 # [-] abs_2 = 0.0737 # [-] L_12 = self.Layers[0].Thickness # [m] L_23 = self.Layers[1].Thickness # [m] # included in Layers[1] used below L_34 = self.Layers[2].Thickness # [m] # "Solar absorption distribution" Q_dot_abs_1 = abs_1 / self.glazing_count * G_t Q_dot_abs_2 = abs_1 / self.glazing_count * G_t Q_dot_abs_3 = abs_2 / self.glazing_count * G_t Q_dot_abs_4 = abs_2 / self.glazing_count * G_t T_o, T_1, T_2, T_3, T_4, T_i = temperatures_next while not self.assert_almost_equal(temperatures_last, temperatures_next): temperatures_last = [T_o, T_1, T_2, T_3, T_4, T_i] # "Heat balance at surface 1" h_c_o = 4 + 4 * environmental_conditions Q_dot_c_1o = h_c_o * (T_1 - T_o) Q_dot_r_1o = epsilon_1 * sigma * ((T_1 + 273.15) ** 4 - (T_o + 273.15) ** 4) Q_dot_1o = Q_dot_c_1o + Q_dot_r_1o Q_dot_21 = k_g / L_12 * (T_2 - T_1) # + Q_dot_abs_1 # "Heat balance at surface 2" h_c_32 = self.Layers[1].convective_conductance_at_angle( abs(T_3 - T_2), height, angle, (T_3 + T_2) / 2 + 273.15, pressure ) Q_dot_c_32 = h_c_32 * (T_3 - T_2) Q_dot_r_32 = ( sigma * ((T_3 + 273.15) ** 4 - (T_2 + 273.15) ** 4) / (1 / epsilon_2 + 1 / epsilon_3 - 1) ) Q_dot_32 = Q_dot_c_32 + Q_dot_r_32 # "Heat balance at surface 3" Q_dot_43 = k_g / L_34 * (T_4 - T_3) # + Q_dot_abs_3 # Q_dot_32 = Q_dot_abs_3 + Q_dot_43 # "Heat balance at surface 4" h_c_i = self.in_h_c( (T_4 + T_i) / 2 + 273.15, abs(T_i - T_4), height, angle, pressure ) Q_dot_c_i4 = h_c_i * (T_i - T_4) Q_dot_r_i4 = epsilon_4 * sigma * ((T_i + 273.15) ** 4 - (T_4 + 273.15) ** 4) Q_dot_i4 = Q_dot_c_i4 + Q_dot_r_i4 # calc new temps T_1 = T_2 - (Q_dot_1o - Q_dot_abs_1) / k_g * L_12 T_2 = (Q_dot_abs_2 + Q_dot_32) / k_g * L_12 + T_1 T_3 = T_4 - (Q_dot_32 - Q_dot_abs_3) / k_g * L_34 T_4 = (Q_dot_abs_4 + Q_dot_i4) / k_g * L_34 + T_3 temperatures_next = [T_o, T_1, T_2, T_3, T_4, T_i] heat_transfers = [Q_dot_1o, Q_dot_21, Q_dot_32, Q_dot_43, Q_dot_i4] return heat_transfers, temperatures_next
def assert_almost_equal(self, temperatures_last, temperatures_next): return all( abs(desired - actual) < 1.5 * 10 ** (-3) for desired, actual in zip(temperatures_last, temperatures_next) )