Source code for pytes.submodels.flux_laws

#
# Surface flux laws
#
"""
Surface flux laws: the heat flux across the surface between the fluid and a storage, used by
the couplings (DESIGN D6).

The couplings take the heat from the storage side's discrete energy balance (the ledger), so
energy is conserved exactly whatever law gives the flux. A law is therefore a replaceable
part of a coupling: Newton's law of cooling is the default, and others (a series wall or
contact resistance, a nonlinear or radiative law) need no change to the conservation logic.
Flow-dependent coefficients come from Nusselt number correlations
(:class:`NusseltCorrelation`): :class:`Gnielinski` inside tubes and :class:`WakaoKaguei` in
packed beds. Correlations are registered by flow geometry in :data:`CORRELATIONS`, and
:func:`register_correlation` adds one, from PyTES or from a user, as a value of the models'
"heat transfer coefficient" option.
"""

from collections.abc import Sequence
from dataclasses import dataclass

import numpy as np
import pybamm

HEAT_TRANSFER_COEFFICIENT = "Heat transfer coefficient [W.m-2.K-1]"


[docs] @dataclass(frozen=True) class FluidState: """ The fluid side of a surface, as a surface flux law sees it (DESIGN D6). New fields may be added (e.g. for a refrigerant) without breaking existing laws, which read only the fields they need. Attributes ---------- h : pybamm.Symbol Specific enthalpy of the fluid [J.kg-1]. T : pybamm.Symbol Temperature of the fluid [K], ``fluid.T(h)``. mass_flow_rate : pybamm.Symbol Mass flow rate through the whole device [kg.s-1]. A law built for a family converts it to the flow it needs (per tube, per unit area) from that family's geometry. p : pybamm.Symbol or None Pressure [Pa]; None while the models do not compute it. """ h: pybamm.Symbol T: pybamm.Symbol mass_flow_rate: pybamm.Symbol p: pybamm.Symbol | None = None
[docs] class SurfaceFluxLaw: """ Base class of the surface flux laws. Subclasses implement :meth:`heat_flux`. Geometry a law needs (e.g. a hydraulic diameter, for a correlation) is passed to its constructor by the family; the fluid's state, flow included, comes with each call as a :class:`FluidState`. A law that needs a variable of another submodel (e.g. the storage conductivity, for :class:`Jeffreson`) reads it as a :class:`pybamm.CoupledVariable` listed in :attr:`coupled_variables`, which the coupling declares as its own requirements, so the model's wiring checks see it. Attributes ---------- coupled_variables : dict The :class:`pybamm.CoupledVariable` objects the law reads, by name (none by default). """ coupled_variables = {}
[docs] def heat_flux(self, fluid, fluid_state, T_s): """ Heat flux [W.m-2] from the fluid into the storage surface. Parameters ---------- fluid : object The fluid-property object, for properties at the fluid state. fluid_state : :class:`FluidState` The fluid's state at the surface. T_s : pybamm.Symbol Temperature of the storage surface [K]. Returns ------- pybamm.Symbol The heat flux, positive into the storage. """ raise NotImplementedError
[docs] def variables(self, fluid, fluid_state): """ Variables the law adds to the coupling's (e.g. its heat transfer coefficient), by name; none by default. """ return {}
[docs] class NewtonCooling(SurfaceFluxLaw): """ Newton's law of cooling, ``h (T_f - T_s)``, with a constant heat transfer coefficient ``h``. Parameters ---------- name : str, optional Name of the coefficient parameter (default "Heat transfer coefficient [W.m-2.K-1]"). flow : :class:`NusseltCorrelation`, optional The flow geometry (e.g. a :class:`TubeCorrelation`), for reporting the surface Reynolds and Prandtl numbers only (issue #14); it does not change the coefficient. Attributes ---------- coefficient : pybamm.Parameter The heat transfer coefficient [W.m-2.K-1]. """ #: The flow geometry for reporting, or None flow = None def __init__(self, name="Heat transfer coefficient [W.m-2.K-1]", flow=None): self.coefficient = pybamm.Parameter(name) self.flow = flow
[docs] def heat_transfer_coefficient(self, fluid, fluid_state): """ The heat transfer coefficient [W.m-2.K-1] at the fluid state. Parameters ---------- fluid : object The fluid-property object. fluid_state : :class:`FluidState` The fluid's state at the surface. """ return self.coefficient
[docs] def heat_flux(self, fluid, fluid_state, T_s): h = self.heat_transfer_coefficient(fluid, fluid_state) return h * (fluid_state.T - T_s)
[docs] def variables(self, fluid, fluid_state): """ The heat transfer coefficient, as "Heat transfer coefficient [W.m-2.K-1]", and, given a flow geometry, the "Surface Reynolds number" and "Surface Prandtl number" """ variables = { HEAT_TRANSFER_COEFFICIENT: self.heat_transfer_coefficient( fluid, fluid_state ) } if self.flow is not None: variables["Surface Reynolds number"] = self.flow.reynolds( fluid, fluid_state ) variables["Surface Prandtl number"] = self.flow.prandtl(fluid, fluid_state) return variables
[docs] class NusseltCorrelation(NewtonCooling): """ Newton's law of cooling with a heat transfer coefficient from a Nusselt number correlation, ``h = Nu k / L``, with ``Nu`` a function of the Reynolds and Prandtl numbers (issue #6): ``Re = mdot_path L / (A mu)``, ``Pr = mu c_p / k``, where ``mdot_path`` is the mass flow rate per flow path, ``L`` the characteristic length of the correlation, ``A`` the area its velocity is based on, and the properties ``mu``, ``c_p`` and ``k`` come from the fluid-property object at the fluid state. The flow may run either way: the Reynolds number uses the flow's magnitude. Subclasses implement :meth:`nusselt`. The correlation's constants are part of it, so a correlation has no parameters of its own: the geometry comes from the family, through the constructor. Parameters ---------- length : pybamm.Symbol The characteristic length ``L`` [m] (a hydraulic or particle diameter). area : pybamm.Symbol The area ``A`` [m2] the velocity is based on, per flow path. n_paths : pybamm.Symbol or float, optional Number of flow paths sharing the device's mass flow rate (default 1). """ def __init__(self, length, area, n_paths=1): self.length = length self.area = area self.n_paths = n_paths
[docs] def reynolds(self, fluid, fluid_state): """The Reynolds number at the fluid state""" mdot_path = pybamm.AbsoluteValue(fluid_state.mass_flow_rate) / self.n_paths return mdot_path * self.length / (self.area * fluid.mu(fluid_state.h))
[docs] def prandtl(self, fluid, fluid_state): """The Prandtl number at the fluid state""" h = fluid_state.h return fluid.mu(h) * fluid.c_p(h) / fluid.k(h)
[docs] def nusselt(self, Re, Pr): """The Nusselt number from the Reynolds and Prandtl numbers""" raise NotImplementedError
[docs] def heat_transfer_coefficient(self, fluid, fluid_state): Re = self.reynolds(fluid, fluid_state) Pr = self.prandtl(fluid, fluid_state) return self.nusselt(Re, Pr) * fluid.k(fluid_state.h) / self.length
[docs] def variables(self, fluid, fluid_state): """ The heat transfer coefficient, and the Reynolds, Prandtl and Nusselt numbers of the surface """ Re = self.reynolds(fluid, fluid_state) Pr = self.prandtl(fluid, fluid_state) return { **super().variables(fluid, fluid_state), "Surface Reynolds number": Re, "Surface Prandtl number": Pr, "Surface Nusselt number": self.nusselt(Re, Pr), }
[docs] class TubeCorrelation(NusseltCorrelation): """ A Nusselt correlation for the flow inside tubes (flow ``"tube"``, the finned-tube models): the characteristic length is the tube diameter and the velocity the mean velocity in a tube. Subclasses implement :meth:`nusselt`. Parameters ---------- radius : pybamm.Symbol The tube radius [m]. n_tubes : pybamm.Symbol or float, optional Number of tubes sharing the device's mass flow rate (default 1). """ def __init__(self, radius, n_tubes=1): super().__init__(2 * radius, np.pi * radius**2, n_tubes)
[docs] class PackedBedCorrelation(NusseltCorrelation): """ A Nusselt correlation for the flow through a packed bed (flow ``"packed bed"``, the packed-bed model): the characteristic length is the particle diameter and the velocity the superficial velocity, the mass flow rate over the whole bed cross-section. Subclasses implement :meth:`nusselt`. Parameters ---------- radius : pybamm.Symbol The particle radius [m]. bed_area : pybamm.Symbol The bed cross-sectional area [m2], porosity not removed. """ def __init__(self, radius, bed_area): super().__init__(2 * radius, bed_area)
[docs] class Gnielinski(TubeCorrelation): """ Fully developed flow in a smooth tube: the Gnielinski correlation in turbulent flow, the laminar Nusselt number at a uniform wall temperature, 3.66, below ``Re = 2300``, and a linear interpolation between them in the transition, as in the VDI Heat Atlas (`Gnielinski, "On heat transfer in tubes", International Journal of Heat and Mass Transfer 63, 2013 <https://doi.org/10.1016/j.ijheatmasstransfer.2013.04.015>`__): - ``Nu = 3.66`` for ``Re <= 2300``; - ``Nu = (1 - g) 3.66 + g Nu_t(10^4)``, ``g = (Re - 2300) / (10^4 - 2300)``, for ``2300 < Re < 10^4``; - ``Nu = Nu_t(Re)`` for ``Re >= 10^4``, with ``Nu_t = (f / 8) (Re - 1000) Pr / (1 + 12.7 sqrt(f / 8) (Pr^(2/3) - 1))`` and the friction factor ``f = (0.79 ln(Re) - 1.64)^-2``. The entrance region and the variation of the properties between the bulk and the wall are neglected. The Nusselt number is continuous in ``Re``; its slope is not, at ``Re = 2300`` and ``10^4``. Registered as "Gnielinski" for the flow ``"tube"``. """ #: Laminar Nusselt number, fully developed flow at a uniform wall temperature nusselt_laminar = 3.66 #: Reynolds numbers at which the transition starts and ends reynolds_transition = (2300, 1e4)
[docs] def nusselt_turbulent(self, Re, Pr): """The Gnielinski correlation, for ``Re >= 10^4``""" f = (0.79 * pybamm.log(Re) - 1.64) ** -2 return ( (f / 8) * (Re - 1000) * Pr / (1 + 12.7 * pybamm.sqrt(f / 8) * (Pr ** (2 / 3) - 1)) )
[docs] def nusselt(self, Re, Pr): Re_lam, Re_turb = self.reynolds_transition g = pybamm.minimum(pybamm.maximum((Re - Re_lam) / (Re_turb - Re_lam), 0), 1) Nu_turb = self.nusselt_turbulent(pybamm.maximum(Re, Re_turb), Pr) return (1 - g) * self.nusselt_laminar + g * Nu_turb
[docs] class WakaoKaguei(PackedBedCorrelation): """ Fluid to particle heat transfer in a packed bed: the correlation of `Wakao, Kaguei and Funazkri (Chemical Engineering Science 34, 1979, 325-336) <https://doi.org/10.1016/0009-2509(79)85064-2>`__, ``Nu = 2 + 1.1 Re^0.6 Pr^(1/3)``, with the particle diameter as the characteristic length and the superficial velocity in the Reynolds number. Fitted for ``15 < Re < 8500``. Registered as "Wakao-Kaguei" for the flow ``"packed bed"``. """
[docs] def nusselt(self, Re, Pr): return 2 + 1.1 * Re**0.6 * Pr ** (1 / 3)
[docs] class Jeffreson(SurfaceFluxLaw): """ Newton's law of cooling for a lumped sphere, with Jeffreson's correction for the conduction resistance inside it (issue #5): the heat flux into the sphere is ``h_eff (T_f - T_s)``, with ``T_s`` the sphere's (uniform) temperature and ``1 / h_eff = 1 / h + R / (5 k)``, that is ``h_eff = h / (1 + Bi / 5)``, ``Bi = h R / k``, where ``h`` is the heat transfer coefficient of the law it corrects, ``R`` the sphere's radius and ``k`` its conductivity (`Jeffreson, "Prediction of breakthrough curves in packed beds: 1. Applicability of single parameter models", AIChE Journal 18, 1972 <https://doi.org/10.1002/aic.690180225>`__). The correction is exact for the slow variation of a sensible sphere; while a PCM melts or freezes inside the sphere it is an approximation, with ``k`` the conductivity at the sphere's mean enthalpy. Parameters ---------- law : :class:`NewtonCooling` The law it corrects, which gives ``h`` (a constant coefficient or a Nusselt correlation). radius : pybamm.Symbol The sphere's radius ``R`` [m]. conductivity : str Name of the variable holding the sphere's conductivity [W.m-1.K-1], e.g. "Phase-change material thermal conductivity [W.m-1.K-1]" from a :class:`pytes.submodels.LumpedStorage`. domain : str or list of str, optional Primary domain of that variable (default: none). Raises ------ TypeError If ``law`` is not a :class:`NewtonCooling`. """ def __init__(self, law, radius, conductivity, domain=None): if not isinstance(law, NewtonCooling): raise TypeError( f"Jeffreson's correction applies to a heat transfer coefficient (a " f"NewtonCooling law), got {type(law).__name__}" ) self.law = law self.radius = radius self.conductivity = pybamm.CoupledVariable(conductivity, domain) self.coupled_variables = {conductivity: self.conductivity}
[docs] def biot(self, fluid, fluid_state): """The Biot number of the sphere, ``h R / k``""" h = self.law.heat_transfer_coefficient(fluid, fluid_state) return h * self.radius / self.conductivity
[docs] def heat_transfer_coefficient(self, fluid, fluid_state): """The corrected coefficient ``h_eff`` [W.m-2.K-1]""" h = self.law.heat_transfer_coefficient(fluid, fluid_state) return h / (1 + self.biot(fluid, fluid_state) / 5)
[docs] def heat_flux(self, fluid, fluid_state, T_s): h_eff = self.heat_transfer_coefficient(fluid, fluid_state) return h_eff * (fluid_state.T - T_s)
[docs] def variables(self, fluid, fluid_state): """ Those of the law it corrects, plus "Effective heat transfer coefficient [W.m-2.K-1]" and "Surface Biot number" """ return { **self.law.variables(fluid, fluid_state), "Effective heat transfer coefficient [W.m-2.K-1]": ( self.heat_transfer_coefficient(fluid, fluid_state) ), "Surface Biot number": self.biot(fluid, fluid_state), }
#: The base class of the correlations of each flow geometry FLOWS = {"tube": TubeCorrelation, "packed bed": PackedBedCorrelation} #: The correlations shipped with PyTES, by flow geometry and name BUILT_IN_CORRELATIONS = { "tube": {"Gnielinski": Gnielinski}, "packed bed": {"Wakao-Kaguei": WakaoKaguei}, } #: The registered correlations, by flow geometry and name: those shipped with PyTES and #: those added by :func:`register_correlation` CORRELATIONS = { flow: dict(built_in) for flow, built_in in BUILT_IN_CORRELATIONS.items() } #: The value of the "heat transfer coefficient" option for a constant coefficient CONSTANT = "constant" def _flow_base(flow): try: return FLOWS[flow] except KeyError: raise ValueError( f"Unknown flow '{flow}'; the flows are {', '.join(map(repr, FLOWS))}" ) from None
[docs] def register_correlation(name, nusselt, flow, replace=False): """ Register a Nusselt correlation, so that the models of its flow geometry accept its name as a value of the "heat transfer coefficient" option. The family supplies the geometry (see :class:`TubeCorrelation` and :class:`PackedBedCorrelation`) and the fluid-property object the properties, so a correlation is only its Nusselt number. Energy is conserved whatever the correlation (DESIGN D6); :func:`pytes.conformance.check_model` checks a model that uses it. Parameters ---------- name : str The name of the correlation, the option value that selects it. nusselt : callable or type A function ``nusselt(Re, Pr)`` of the Reynolds and Prandtl numbers, as PyBaMM symbols (so use ``pybamm.log``, ``pybamm.sqrt``, etc.), returning the Nusselt number; or a subclass of the flow's correlation class implementing :meth:`NusseltCorrelation.nusselt`. flow : str The flow geometry: ``"tube"`` (the finned-tube models) or ``"packed bed"`` (the packed-bed model). replace : bool, optional Whether to replace a correlation registered earlier under the same name, e.g. when a notebook cell runs again (default False). A correlation shipped with PyTES cannot be replaced. Returns ------- type The correlation class, a subclass of the flow's correlation class. Raises ------ ValueError If the flow is unknown, or the name is not a non-empty string, is "constant", is a built-in correlation, or is already registered and ``replace`` is False. TypeError If ``nusselt`` is neither callable nor a subclass of the flow's correlation class. Examples -------- >>> import pytes >>> pytes.submodels.register_correlation( ... "Dittus-Boelter", lambda Re, Pr: 0.023 * Re**0.8 * Pr**0.4, flow="tube" ... ) # doctest: +SKIP >>> model = pytes.SharpFrontModel( ... options={"heat transfer coefficient": "Dittus-Boelter"} ... ) # doctest: +SKIP """ base = _flow_base(flow) if not isinstance(name, str) or not name: raise ValueError( f"A correlation's name must be a non-empty string, got {name!r}" ) if name == CONSTANT: raise ValueError(f"'{CONSTANT}' is the constant coefficient, not a correlation") if name in BUILT_IN_CORRELATIONS[flow]: raise ValueError(f"'{name}' is a correlation of PyTES and cannot be replaced") if name in CORRELATIONS[flow] and not replace: raise ValueError( f"A correlation '{name}' is already registered for the flow '{flow}'; pass " "replace=True to replace it" ) if isinstance(nusselt, type): if not issubclass(nusselt, base): raise TypeError( f"A correlation class for the flow '{flow}' must subclass " f"{base.__name__}, got {nusselt.__name__}" ) correlation = nusselt elif callable(nusselt): correlation = type( base.__name__, (base,), { "nusselt": lambda self, Re, Pr: nusselt(Re, Pr), "__doc__": f"The correlation '{name}', registered for the flow '{flow}'", }, ) else: raise TypeError( "A correlation is a function nusselt(Re, Pr) or a subclass of " f"{base.__name__}, got {nusselt!r}" ) CORRELATIONS[flow][name] = correlation return correlation
[docs] class HeatTransferCoefficientOptions(Sequence): """ The values of a model's "heat transfer coefficient" option: "constant", then the names of the correlations registered for its flow geometry. They are read from :data:`CORRELATIONS` when used, so a correlation registered after the model class was defined is allowed. Parameters ---------- flow : str The flow geometry, ``"tube"`` or ``"packed bed"``. """ def __init__(self, flow): _flow_base(flow) self.flow = flow def _values(self): return (CONSTANT, *CORRELATIONS[self.flow]) def __getitem__(self, index): return self._values()[index] def __len__(self): return len(self._values()) def __repr__(self): return f"HeatTransferCoefficientOptions({self.flow!r}): {list(self)}"
[docs] def flux_law(self, value, *geometry): """ The surface flux law an option value selects. Parameters ---------- value : str The option value, "constant" or a registered correlation. *geometry : pybamm.Symbol The family's geometry, passed to the correlation's constructor. Returns ------- :class:`SurfaceFluxLaw` """ if value == CONSTANT: # The flow geometry reports the surface Reynolds and Prandtl numbers return NewtonCooling(flow=FLOWS[self.flow](*geometry)) return CORRELATIONS[self.flow][value](*geometry)