Source code for pytanksim.classes.storagetankclasses

# -*- coding: utf-8 -*-
"""Contains classes which store the properties of the storage tanks.

The StorageTank and SorbentTank classes are part of this module.
"""
# This file is a part of the python package pytanksim.
#
# Copyright (c) 2024 Muhammad Irfan Maulana Kusdhany, Kyushu University
#
# pytanksim is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

__all__ = ["StorageTank", "SorbentTank"]

from pytanksim.classes.fluidsorbentclasses import StoredFluid, SorbentMaterial
import CoolProp as CP
import numpy as np
import scipy as sp
import pandas as pd
from scipy.optimize import OptimizeResult
from typing import Callable, Union


def Cs_gen(mads: float, mcarbon: float, malum: float,
           msteel: float, Tads: float = 1500,
           MWads: float = 12.01E-3, func: Callable[[float], float] = None
           ) -> Callable[[float], float]:
    """Generate a function to find the heat capacity at a given temperature.

    Based on Debye's model. Combines contributions from the various materials
    making up the storage tank.

    Parameters
    ----------
    mads : float
        Mass of sorbent (kg).

    mcarbon : float
        Carbon fiber mass (kg).

    malum : float
        Aluminum mass (kg).

    msteel : float
        Steel mass (kg).

    Tads : float, optional
        Debye temperature of the sorbent material (K). The default is 1500,
        which is the value for carbon.

    MWads : float, optional
        The molecular weight of the sorbent material (mol/kg). The default is
        12.01E-3, which is the value for carbon.

    func : Callable[[float],float], optional
        Custom function that returns the specific heat capacity (J/(kg K)) of
        the sorbent material given its temperature.

    Returns
    -------
    Callable[[float], float]
        A function which takes the tank's temperature as an input and returns
        the heat capacity of the tank (J/K)

    """
    R = sp.constants.R

    def Cdebye(T, theta):
        N = 50
        grid = np.linspace(0, theta/T, N)
        y = np.zeros_like(grid)

        def integrand(x):
            return (x**4) * np.exp(x) / ((np.exp(x)-1)**2)

        for i in range(1, N):
            y[i] = integrand(grid[i])
        return 9 * R * ((T/theta)**3) * sp.integrate.simps(y, grid)
    carbon_molar_mass = 12.01E-3
    alum_molar_mass = 26.98E-3
    iron_molar_mass = 55.845E-3

    if func is not None:
        def Cads(T):
            return func(T) * mads
    else:
        def Cads(T):
            return (mads/MWads)*Cdebye(T, Tads)

    def Cs(T):
        return Cads(T) + (mcarbon / carbon_molar_mass) *\
            Cdebye(T, 1500) + (malum/alum_molar_mass) * Cdebye(T, 389.4) +\
            (msteel/iron_molar_mass) * Cdebye(T, 500)
    return Cs


[docs]class StorageTank: """Stores the properties of the storage tank. It also has methods to calculate useful quantities such as tank dormancy given a constant heat leakage rate, the internal energy of the fluid being stored at various conditions, etc. Attributes ---------- volume : float Internal volume of the storage tank (m^3). stored_fluid : StoredFluid Object to calculate the thermophysical properties of the fluid being stored. aluminum_mass : float, optional The mass of aluminum making up the tank walls (kg). The default is 0. carbon_fiber_mass : float, optional The mass of carbon fiber making up the tank walls (kg). The default is 0. steel_mass : float, optional The mass of steel making up the tank walls (kg). The default is 0. vent_pressure : float, optional The pressure (Pa) at which the fluid being stored must be vented. The default is None. If None, the value will be taken as the maximum value where the CoolProp backend can calculate the properties of the fluid being stored. min_supply_pressure : float, optional The minimum supply pressure (Pa) for discharging simulations.The default is 1E5. thermal_resistance : Callable, optional The thermal resistance of the tank walls (K/W). The default is 0. If 0, the value will not be considered in simulations. surface_area : float, optional The surface area of the tank that is in contact with the environment (m^2). The default is 0. heat_transfer_coefficient : Callable, optional The heat transfer coefficient of the tank surface (W/(m^2 K)). The default is 0. """ def __init__(self, stored_fluid: StoredFluid, aluminum_mass: float = 0, carbon_fiber_mass: float = 0, steel_mass: float = 0, vent_pressure: float = None, min_supply_pressure: float = 1E5, thermal_resistance: Union[Callable[[float, float, float, float], float]] = 0, surface_area: float = 0, heat_transfer_coefficient: Union[Callable[[float, float, float, float], float]] = 0, volume: float = None, set_capacity: float = None, full_pressure: float = None, empty_pressure: float = None, full_temperature: float = None, empty_temperature: float = None, full_quality: float = 1, empty_quality: float = 1 ): """Initialize a StorageTank object. Parameters ---------- stored_fluid : StoredFluid Object to calculate the thermophysical properties of the fluid being stored. aluminum_mass : float, optional The mass of aluminum making up the tank walls (kg). The default is 0. carbon_fiber_mass : float, optional The mass of carbon fiber making up the tank walls (kg). The default is 0. steel_mass : float, optional The mass of steel making up the tank walls (kg). The default is 0. vent_pressure : float, optional The pressure (Pa) at which the fluid being stored must be vented. The default is None. If None, the value will be taken as the maximum value where the CoolProp backend can calculate the properties of the fluid being stored. min_supply_pressure : float, optional The minimum supply pressure (Pa) for discharging simulations.The default is 1E5. thermal_resistance : Callable or float, optional A function which returns the thermal resistance of the tank walls (K/W) as a function of tank pressure (Pa), tank temperature (K), time (s), and temperature of surroundings (K). The default is 0. If a float is provided, it will be converted to a function which returns that value everywhere. If both this and the arguments 'surface_area' and 'heat_transfer_coefficient' are passed, two values of thermal resistance will be calculated and the highest value between the two will be taken at each time step. Thus, to avoid confusion, one should either: (a) use the other two arguments together, or (b) use this one, but not both at the same time. If a callable is passed, it must have the signature:: def tr_function(p, T, time, env_temp): # 'p' is tank pressure (Pa) # 'T' is tank temperature (K) # 'time' is the time elapsed within the simulation (s) # 'env_temp' is the temperature of surroundings (K) .... # Returned is the thermal resistance (K/W) return tr_value surface_area : float, optional The surface area of the tank that is in contact with the environment (m^2). The default is 0. heat_transfer_coefficient : Callable or float, optional A function which returns the heat transfer coefficient of the tank walls (W/(m^2 K)) as a function of tank pressure (Pa), tank temperature (K), time (s), and temperature of surroundings (K). The default is 0. If a float is provided, it will be converted to a function which returns that value everywhere. If a callable is passed, it must have the signature:: def htc_function(p, T, time, env_temp): # 'p' is tank pressure (Pa) # 'T' is tank temperature (K) # 'time' is the time elapsed within the simulation (s) # 'env_temp' is the temperature of surroundings (K) .... # Returned is the heat transfer coefficient (W/(m^2 K)) return heat_transfer_coef volume : float, optional Internal volume of the storage tank (m^3). The default is None. This value is required unless the set capacity and operating conditions are defined, in which case the volume is calculated from the capacity and operating conditions. set_capacity : float, optional Set internal capacity of the storage tank (mol). The default is None. If specified, this will override the user-specified tank volume. full_pressure : float, optional Pressure (Pa) of the tank when it is considered full. The default is None. empty_pressure : float, optional Pressure (Pa) of the tank when it is considered empty. The default is None. full_temperature : float, optional Temperature (K) of the tank when it is considered full. The default is None. empty_temperature : float, optional Temperature (K) of the tank when it is considered empty. The default is None. full_quality : float, optional Vapor quality of the tank when it is considered full. The default is 1 (Gas). empty_quality : float, optional Vapor quality of the tank when it is considered empty. The default is 1 (Gas). Raises ------ ValueError If any of the mass values provided are less than 0. ValueError If the vent pressure set is higher than what can be calculated by 'CoolProp'. ValueError If neither the volume nor the complete capacity and the pressure and temperature swing conditions were provided. Returns ------- StorageTank A storage tank object which can be passed as arguments to dynamic simulations and can calculate certain properties on its own. """ def float_function_generator(floatingvalue): def float_function(p, T, time, env_temp): return float(floatingvalue) return float_function if (aluminum_mass or carbon_fiber_mass or steel_mass) < 0: raise ValueError("Please input valid values for the mass") if volume is None and (set_capacity or full_pressure or full_temperature or empty_pressure or empty_temperature) is None: raise ValueError("Please input the complete capacity + pressure " "and temperature swing information, or input " "the tank volume") self.volume = volume self.aluminum_mass = aluminum_mass self.carbon_fiber_mass = carbon_fiber_mass self.steel_mass = steel_mass self.heat_capacity = Cs_gen(mads=0, mcarbon=self.carbon_fiber_mass, malum=self.aluminum_mass, msteel=self.steel_mass) self.stored_fluid = stored_fluid self.min_supply_pressure = min_supply_pressure backend = self.stored_fluid.backend self.max_pressure = backend.pmax()/10 if vent_pressure is None: self.vent_pressure = self.max_pressure else: self.vent_pressure = vent_pressure if self.max_pressure < self.vent_pressure and\ stored_fluid.EOS == "HEOS": raise ValueError( "You set the venting pressure to be larger than the valid \n" + "pressure range input for CoolProp.") self.surface_area = surface_area self.heat_transfer_coefficient = heat_transfer_coefficient self.htc_original = heat_transfer_coefficient if isinstance(self.heat_transfer_coefficient, (float, int)): self.heat_transfer_coefficient = float_function_generator( heat_transfer_coefficient) self.tr_original = thermal_resistance if isinstance(thermal_resistance, (float, int)): self.thermal_resistance = float_function_generator( thermal_resistance) else: self.thermal_resistance = thermal_resistance if set_capacity is not None: def min_func(vol): self.volume = vol cap_full = self.capacity(full_pressure, full_temperature, full_quality) cap_empty = self.capacity(empty_pressure, empty_temperature, empty_quality) return ((cap_full-cap_empty)-set_capacity)**2 vol = sp.optimize.minimize_scalar(min_func, bounds=(0, 1E16), method="bounded") if self.capacity(full_pressure, full_temperature, full_quality) <\ set_capacity: raise ValueError("Difference between full and empty" " conditions too small. Tank volume not" " converged (i.e. solution >1E16).") self.volume = vol.x
[docs] def capacity(self, p: float, T: float, q: float = 0, unit: str = "mol") -> float: """Return the amount of fluid stored in the tank at given conditions. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. Can vary between 0 and 1. The default is 0. unit : str, optional Unit of the capacity to be returned. Valid units are "mol" and "kg". The default is "mol". Returns ------- float Amount of fluid stored. """ if p == 0: return 0 fluid = self.stored_fluid.backend phase = self.stored_fluid.determine_phase(p, T) if phase == "Saturated": fluid.update(CP.QT_INPUTS, q, T) else: fluid.update(CP.PT_INPUTS, p, T) cap_mol = fluid.rhomolar() * self.volume if unit == "mol": return cap_mol elif unit == "kg": return cap_mol * self.stored_fluid.backend.molar_mass()
[docs] def capacity_bulk(self, p: float, T: float, q: float = 0, unit: str = "mol") -> float: """Calculate the amount of bulk fluid in the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. Can vary between 0 and 1. The default is 0. unit : str, optional Unit of the capacity to be returned. Valid units are "mol" and "kg". The default is "mol". Returns ------- float Amount of bulk fluid stored. """ return self.capacity(p, T, q, unit)
[docs] def find_quality_at_saturation_capacity(self, T: float, capacity: float) -> float: """Find vapor quality at the given temperature and capacity. Parameters ---------- T : float Temperature (K) capacity : float Amount of fluid in the tank (moles). Returns ------- float Vapor quality of the fluid being stored. This is assuming that the fluid is on the saturation line. """ fluid = self.stored_fluid.backend fluid.update(CP.QT_INPUTS, 0, T) rhol = fluid.rhomolar() fluid.update(CP.QT_INPUTS, 1, T) rhog = fluid.rhomolar() A = np.array([[1, 1], [1/rhog, 1/rhol]]) b = [capacity, self.volume] res = np.linalg.solve(A, b) return res[0]/(res[0]+res[1])
[docs] def internal_energy(self, p: float, T: float, q: float = 1) -> float: """Calculate the internal energy of the fluid inside of the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. The default is 1. Returns ------- float Internal energy of the fluid being stored (J). """ fluid = self.stored_fluid.backend phase = self.stored_fluid.determine_phase(p, T) if phase == "Saturated": fluid.update(CP.QT_INPUTS, q, T) else: fluid.update(CP.PT_INPUTS, p, T) ufluid = fluid.umolar() bulk_fluid_moles = fluid.rhomolar() * self.volume return ufluid * bulk_fluid_moles
[docs] def conditions_at_capacity_temperature(self, cap: float, T: float, p_guess: float, q_guess: float) -> OptimizeResult: """Find conditions corresponding to a given capacity and temperature. Parameters ---------- cap : float Amount of fluid inside the tank (moles). T : float Temperature (K). p_guess : float Initial guess for pressure value (Pa) to be optimized. q_guess : float Initial guess for vaport quality value to be optimized. Returns ------- OptimizeResult The optimization result represented as a OptimizeResult object. The relevant attribute for this method is x, the solution array. x[0] contains the pressure value and x[1] contains the vapor quality value. """ pmax = self.stored_fluid.backend.pmax() def optim(pres): return (self.capacity(pres, T, q_guess) - cap)**2 res = sp.optimize.minimize_scalar(optim, bounds=(1E-16, pmax), method='bounded') x = [res.x, q_guess] res.x = x if res.fun > 1: self.stored_fluid.backend.update(CP.QT_INPUTS, 0, T) psat = self.stored_fluid.backend.p() q = self.find_quality_at_saturation_capacity(T, cap) res.x[0] = psat res.x[1] = q return res
[docs] def conditions_at_capacity_pressure(self, cap: float, p: float, T_guess: float, q_guess: float) -> OptimizeResult: """Find conditions corresponding to a given capacity and temperature. Parameters ---------- cap : float Amount of fluid inside the tank (moles). P : float Pressure (Pa). T_guess : float Initial guess for temperature value (K) to be optimized. q_guess : float Initial guess for vaport quality value to be optimized. Returns ------- scipy.optimize.OptimizeResult The optimization result represented as a OptimizeResult object. The relevant attribute for this package is x, the solution array. x[0] contains the temperature value and x[1] contains the vapor quality value. """ fluid = self.stored_fluid.backend Tmin = fluid.Tmin() Tmax = fluid.Tmax() def optim(temper): return (self.capacity(p, temper, q_guess) - cap)**2 res = sp.optimize.minimize_scalar(optim, bounds=(Tmin, Tmax), method='bounded') x = [res.x, q_guess] res.x = x if res.fun > 1: self.stored_fluid.backend.update(CP.PQ_INPUTS, p, 0) Tsat = self.stored_fluid.backend.T() q = self.find_quality_at_saturation_capacity(Tsat, cap) res.x[0] = Tsat res.x[1] = q return res
[docs] def calculate_dormancy(self, p: float, T: float, heating_power: float, q: float = 0) -> pd.DataFrame: """Calculate dormancy time given a constant heating rate. Parameters ---------- p : float Initial tank pressure (Pa). T : float Initial tank temperature (K). heating_power : float The heating power going into the tank during parking (W). q : float, optional Initial vapor quality of the tank. The default is 0 (pure liquid). Returns ------- pd.DataFrame Pandas dataframe containing calculation conditions and results. Each key stores a floating point number. The dictionary keys and their respective values are: - "init pressure": initial pressure - "init temperature": initial temperature - "init quality": initial vapor quality - "dormancy time": time until tank needs to be vented in seconds - "final temperature": temperature of the tank as venting begins - "final quality": vapor quality at the time of venting - "final pressure": pressure at the time of venting - "capacity error": error between final and initial capacity - "total energy change": difference in internal energy between the initial and final conditions - "solid heat capacity contribution": the amount of heat absorbed by the tank walls """ init_cap = self.capacity(p, T, q) init_heat = self.internal_energy(p, T, q) vent_cond = self.conditions_at_capacity_pressure(init_cap, self.vent_pressure, T, q).x final_heat = self.internal_energy(self.vent_pressure, vent_cond[0], vent_cond[1]) final_cap = self.capacity(self.vent_pressure, vent_cond[0], vent_cond[1]) def heat_capacity_change(T1, T2): xgrid = np.linspace(T1, T2, 100) heatcapgrid = [self.heat_capacity(temper) for temper in xgrid] return sp.integrate.simps(heatcapgrid, xgrid) final_heat += heat_capacity_change(T, vent_cond[0]) return pd.DataFrame({"init pressure": p, "init temperature": T, "init quality": q, "dormancy time": (final_heat - init_heat)/heating_power, "final temperature": vent_cond[0], "final quality": vent_cond[1], "final pressure": self.vent_pressure, "capacity error": final_cap - init_cap, "total energy change": final_heat - init_heat, "solid heat capacity contribution": heat_capacity_change(T, vent_cond[0])}, index=[0])
[docs] def thermal_res(self, p: float, T: float, time: float, env_temp: float ) -> float: """Calculate the thermal resistance of the tank. Parameters ---------- p : float Pressure (Pa) of fluid inside tank. T : float Temperature (K) of fluid inside tank time : float Time elapsed in simulation (s). env_temp : float Temperature (K) of environment surrounding tank. Returns ------- float Thermal resistance of the tank (K/W). """ htc = self.heat_transfer_coefficient(p, T, time, env_temp) if htc*self.surface_area > 0: return max([1/(htc*self.surface_area), self.thermal_resistance(p, T, time, env_temp)]) else: return self.thermal_resistance(p, T, time, env_temp)
[docs]class SorbentTank(StorageTank): """Stores properties of a fluid storage tank filled with sorbents. Attributes ---------- volume : float Internal volume of the storage tank (m^3). sorbent_material : SorbentMaterial An object storing the properties of the sorbent material used in the tank. aluminum_mass : float, optional The mass of aluminum making up the tank walls (kg). The default is 0. carbon_fiber_mass : float, optional The mass of carbon fiber making up the tank walls (kg). The default is 0. steel_mass : float, optional The mass of steel making up the tank walls (kg). The default is 0. vent_pressure : float, optional Maximum pressure at which the tank has to be vented (Pa). The default is None. min_supply_pressure : float, optional The minimum supply pressure (Pa) for discharging simulations. The default is 1E5. thermal_resistance : Callable, optional The thermal resistance of the tank walls (K/W). The default is 0. surface_area : float, optional Outer surface area of the tank in contact with the environment (m^2). The default is 0. heat_transfer_coefficient : Callable, optional The heat transfer coefficient of the tank surface (W/(m^2 K)). The default is 0. """ def __init__(self, sorbent_material: SorbentMaterial, aluminum_mass: float = 0, carbon_fiber_mass: float = 0, steel_mass: float = 0, vent_pressure: float = None, min_supply_pressure: float = 1E5, thermal_resistance: float = 0, surface_area: float = 0, heat_transfer_coefficient: float = 0, volume: float = None, set_capacity: float = None, full_pressure: float = None, empty_pressure: float = None, full_temperature: float = None, empty_temperature: float = None, full_quality: float = 1, empty_quality: float = 1, set_sorbent_fill: float = 1 ): """Initialize a SorbentTank object. Parameters ---------- sorbent_material : SorbentMaterial An object storing the properties of the sorbent material used in the tank. aluminum_mass : float, optional The mass of aluminum making up the tank walls (kg). The default is 0. carbon_fiber_mass : float, optional The mass of carbon fiber making up the tank walls (kg). The default is 0. steel_mass : float, optional The mass of steel making up the tank walls (kg). The default is 0. vent_pressure : float, optional Maximum pressure at which the tank has to be vented (Pa). The default is None. min_supply_pressure : float, optional The minimum supply pressure (Pa) for discharging simulations. The default is 1E5. thermal_resistance : Callable or float, optional A function which returns the thermal resistance of the tank walls (K/W) as a function of tank pressure (Pa), tank temperature (K), time (s), and temperature of surroundings (K). The default is 0. If a float is provided, it will be converted to a function which returns that value everywhere. If both this and the arguments 'surface_area' and 'heat_transfer_coefficient' are passed, two values of thermal resistance will be calculated and the highest value between the two will be taken at each time step. Thus, to avoid confusion, one should either: (a) use the other two arguments together, or (b) use this one, but not both at the same time. If a callable is passed, it must have the signature:: def tr_function(p, T, time, env_temp): # 'p' is tank pressure (Pa) # 'T' is tank temperature (K) # 'time' is the time elapsed within the simulation (s) # 'env_temp' is the temperature of surroundings (K) .... # Returned is the thermal resistance (K/W) return tr_value surface_area : float, optional Outer surface area of the tank in contact with the environment (m^2). The default is 0. heat_transfer_coefficient : Callable or float, optional A function which returns the heat transfer coefficient of the tank walls (W/(m^2 K)) as a function of tank pressure (Pa), tank temperature (K), time (s), and temperature of surroundings (K). The default is 0. If a float is provided, it will be converted to a function which returns that value everywhere. If a callable is passed, it must have the signature:: def htc_function(p, T, time, env_temp): # 'p' is tank pressure (Pa) # 'T' is tank temperature (K) # 'time' is the time elapsed within the simulation (s) # 'env_temp' is the temperature of surroundings (K) .... # Returned is the heat transfer coefficient (W/(m^2 K)) return heat_transfer_coef volume : float, optional Internal volume of the storage tank (m^3). The default is None. This value is required unless the set capacity and operating conditions are defined, in which case the volume is calculated from the capacity and operating conditions. set_capacity : float, optional Set internal capacity of the storage tank (mol). The default is None. If specified, this will override the user-specified tank volume. full_pressure : float, optional Pressure (Pa) of the tank when it is considered full. The default is None. empty_pressure : float, optional Pressure (Pa) of the tank when it is considered empty. The default is None. full_temperature : float, optional Temperature (K) of the tank when it is considered full. The default is None. empty_temperature : float, optional Temperature (K) of the tank when it is considered empty. The default is None. full_quality : float, optional Vapor quality of the tank when it is considered full. The default is 1 (Gas). empty_quality : float, optional Vapor quality of the tank when it is considered empty. The default is 1 (Gas). set_sorbent_fill : float, optional Ratio of tank volume filled with sorbent. The default is 1 (completely filled with sorbent). Returns ------- SorbentTank Object which stores various properties of a storage tank containing sorbents. It also has some useful methods related to the tank, most notably dormancy calculation. """ stored_fluid = sorbent_material.model_isotherm.stored_fluid self.sorbent_material = sorbent_material super().__init__(volume=volume, aluminum_mass=aluminum_mass, stored_fluid=stored_fluid, carbon_fiber_mass=carbon_fiber_mass, min_supply_pressure=min_supply_pressure, vent_pressure=vent_pressure, thermal_resistance=thermal_resistance, surface_area=surface_area, steel_mass=steel_mass, heat_transfer_coefficient=heat_transfer_coefficient, set_capacity=set_capacity, full_pressure=full_pressure, empty_pressure=empty_pressure, full_temperature=full_temperature, empty_temperature=empty_temperature, full_quality=full_quality, empty_quality=empty_quality) self.heat_capacity = Cs_gen(mads=self.sorbent_material.mass, mcarbon=self.carbon_fiber_mass, malum=self.aluminum_mass, msteel=self.steel_mass, Tads=self.sorbent_material. Debye_temperature, MWads=self.sorbent_material.molar_mass, func=self.sorbent_material. heat_capacity_function) if set_capacity is not None: def min_func(v): self.volume = v sorbent_vol = set_sorbent_fill * v self.sorbent_material.mass = sorbent_vol *\ self.sorbent_material.bulk_density cap_full = self.capacity(full_pressure, full_temperature, full_quality) cap_empty = self.capacity(empty_pressure, empty_temperature, empty_quality) return ((cap_full-cap_empty)-set_capacity)**2 vol = sp.optimize.minimize_scalar(min_func, bounds=(0, 1E10), method="Bounded") self.volume = vol.x sorbent_vol = set_sorbent_fill * self.volume self.sorbent_material.mass = sorbent_vol *\ self.sorbent_material.bulk_density
[docs] def bulk_fluid_volume(self, p: float, T: float) -> float: """Calculate the volume of bulk fluid inside of the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature(K). Returns ------- float Bulk fluid volume within the tank (m^3). """ tankvol = self.volume mads = self.sorbent_material.mass rhoskel = self.sorbent_material.skeletal_density vads = self.sorbent_material.model_isotherm.v_ads outputraw = tankvol - mads/rhoskel - vads(p, T) * mads output = outputraw if outputraw >= 0 else 0 return output
[docs] def capacity(self, p: float, T: float, q: float = 0) -> float: """Return the amount of fluid stored in the tank at given conditions. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. Can vary between 0 and 1. The default is 0. Returns ------- float Amount of fluid stored (moles). """ if p == 0: return 0 fluid = self.stored_fluid.backend phase = self.stored_fluid.determine_phase(p, T) if phase == "Saturated": fluid.update(CP.QT_INPUTS, q, T) else: fluid.update(CP.PT_INPUTS, p, T) bulk_fluid_moles = fluid.rhomolar() * self.bulk_fluid_volume(p, T) adsorbed_moles = self.sorbent_material.model_isotherm.n_absolute(p, T)\ * self.sorbent_material.mass return bulk_fluid_moles + adsorbed_moles
[docs] def capacity_bulk(self, p: float, T: float, q: float = 0) -> float: """Calculate the amount of bulk fluid in the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. Can vary between 0 and 1. The default is 0. Returns ------- float Amount of bulk fluid stored (moles). """ fluid = self.stored_fluid.backend phase = self.stored_fluid.determine_phase(p, T) if phase == "Saturated": fluid.update(CP.QT_INPUTS, q, T) else: fluid.update(CP.PT_INPUTS, p, T) bulk_fluid_moles = fluid.rhomolar() * self.bulk_fluid_volume(p, T) return bulk_fluid_moles
[docs] def internal_energy(self, p: float, T: float, q: float = 1) -> float: """Calculate the internal energy of the fluid inside of the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. The default is 1. Returns ------- float Internal energy of the fluid being stored (J). """ fluid = self.stored_fluid.backend phase = self.stored_fluid.determine_phase(p, T) if phase == "Saturated": fluid.update(CP.QT_INPUTS, q, T) else: fluid.update(CP.PT_INPUTS, p, T) ufluid = fluid.umolar() bulk_fluid_moles = fluid.rhomolar() * self.bulk_fluid_volume(p, T) adsorbed_moles = self.sorbent_material.model_isotherm.n_absolute(p, T)\ * self.sorbent_material.mass uadsorbed = self.sorbent_material.model_isotherm.\ internal_energy_adsorbed(p, T) return ufluid * bulk_fluid_moles + adsorbed_moles * (uadsorbed)
[docs] def internal_energy_sorbent(self, p: float, T: float, q: float = 1) -> float: """Calculate the internal energy of the adsorbed fluid in the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. The default is 1. Returns ------- float Internal energy of the adsorbed fluid in the tank (J). """ adsorbed_moles = self.sorbent_material.model_isotherm.n_absolute(p, T)\ * self.sorbent_material.mass uadsorbed = self.sorbent_material.model_isotherm.\ internal_energy_adsorbed(p, T) return adsorbed_moles * (uadsorbed)
[docs] def internal_energy_bulk(self, p: float, T: float, q: float = 1) -> float: """Calculate the internal energy of the bulk fluid in the tank. Parameters ---------- p : float Pressure (Pa). T : float Temperature (K). q : float, optional Vapor quality of the fluid being stored. The default is 1. Returns ------- float Internal energy of the bulk fluid in the tank (J). """ fluid = self.stored_fluid.backend phase = self.stored_fluid.determine_phase(p, T) if phase == "Saturated": fluid.update(CP.QT_INPUTS, q, T) else: fluid.update(CP.PT_INPUTS, p, T) ufluid = fluid.umolar() bulk_fluid_moles = fluid.rhomolar() * self.bulk_fluid_volume(p, T) return ufluid * bulk_fluid_moles
[docs] def find_quality_at_saturation_capacity(self, T: float, capacity: float) -> float: """Find vapor quality at the given temperature and capacity. Parameters ---------- T : float Temperature (K) capacity : float Amount of fluid in the tank (moles). Returns ------- float Vapor quality of the fluid being stored. This is assuming that the fluid is on the saturation line. """ fluid = self.stored_fluid.backend fluid.update(CP.QT_INPUTS, 0, T) rhol = fluid.rhomolar() fluid.update(CP.QT_INPUTS, 1, T) rhog = fluid.rhomolar() p = fluid.p() bulk_capacity = capacity - self.sorbent_material.mass *\ self.sorbent_material.model_isotherm.n_absolute(p, T) A = np.array([[1, 1], [1/rhog, 1/rhol]]) b = [bulk_capacity, self.bulk_fluid_volume(p, T)] res = np.linalg.solve(A, b) q = res[0]/(res[0]+res[1]) return q
[docs] def find_temperature_at_saturation_quality(self, q: float, cap: float) -> OptimizeResult: """Find temperature at a given capacity and vapor quality value. Parameters ---------- q : float Vapor quality. Can vary between 0 and 1. cap : float Amount of fluid stored in the tank (moles). Returns ------- scipy.optimize.OptimizeResult The optimization result represented as a OptimizeResult object. The relevant attribute for this function is x, the optimized temperature value. """ def optim(x): self.stored_fluid.backend.update(CP.QT_INPUTS, q, x) p = self.stored_fluid.backend.p() return (self.capacity(p, x, q) - cap)**2 fluid = self.stored_fluid.backend Tmin = fluid.Tmin() Tmax = fluid.T_critical() res = sp.optimize.minimize_scalar(optim, method="bounded", bounds=(Tmin, Tmax)) return res
[docs] def calculate_dormancy(self, p: float, T: float, heating_power: float, q: float = 0) -> pd.DataFrame: """Calculate dormancy time given a constant heating rate. Parameters ---------- p : float Initial tank pressure (Pa). T : float Initial tank temperature (K). heating_power : float The heating power going into the tank during parking (W). q : float, optional Initial vapor quality of the tank. The default is 0 (pure liquid). Returns ------- pd.DataFrame Pandas dataframe containing calculation conditions and results. Each key stores a floating point number. The dictionary keys and their respective values are: - "init pressure": initial pressure - "init temperature": initial temperature - "init quality": initial vapor quality - "dormancy time": time until tank needs to be vented in seconds - "final temperature": temperature of the tank as venting begins - "final quality": vapor quality at the time of venting - "final pressure": pressure at the time of venting - "capacity error": error between final and initial capacity - "total energy change": difference in internal energy between the initial and final conditions - "sorbent energy contribution": the amount of heat taken by the adsorbed phase via desorption - "bulk energy contribution": the amount of heat absorbed by the bulk phase - "immersion heat contribution": how much heat has been absorbed by un-immersing the sorbent material in the fluid - "solid heat capacity contribution": the amount of heat absorbed by the tank walls """ init_pres = p init_cap = self.capacity(p, T, q) init_ene = self.internal_energy(p, T, q) init_ene_ads = self.internal_energy_sorbent(p, T, q) init_ene_bulk = self.internal_energy_bulk(p, T, q) vent_cond = self.conditions_at_capacity_pressure(init_cap, self.vent_pressure, T, q).x final_ene = self.internal_energy(self.vent_pressure, vent_cond[0], vent_cond[1]) final_cap = self.capacity(self.vent_pressure, vent_cond[0], vent_cond[1]) final_ene_ads = self.internal_energy_sorbent(self.vent_pressure, vent_cond[0], vent_cond[1]) final_ene_bulk = self.internal_energy_bulk(self.vent_pressure, vent_cond[0], vent_cond[1]) def heat_capacity_change(T1, T2): xgrid = np.linspace(T1, T2, 100) heatcapgrid = [self.heat_capacity(temper) for temper in xgrid] return sp.integrate.simps(heatcapgrid, xgrid) final_ene += heat_capacity_change(T, vent_cond[0]) res1 = self.find_temperature_at_saturation_quality(1, init_cap) res2 = self.find_temperature_at_saturation_quality(0, init_cap) if T > self.stored_fluid.backend.T_critical() or\ p > self.stored_fluid.backend.p_critical(): integ_res = 0 elif (res1.x > T and res1.fun < 1) or (res2.x > T and res2.fun < 1)\ or (vent_cond[1] != q): if vent_cond[1] != q: lower_bound = max(q, vent_cond[1]) upper_bound = min(q, vent_cond[1]) else: consider_res1 = True if res1.fun < 1 and \ T < res1.x < vent_cond[0] else False consider_res2 = True if res2.fun < 1 and \ T < res2.x < vent_cond[0] else False if consider_res1 and consider_res2: Tcheck = max(res1.x, res2.x) resfinal = 1 if res1.x > res2.x else 0 elif consider_res2 and (not consider_res1): Tcheck = res2.x resfinal = 0 elif consider_res1 and (not consider_res2): Tcheck = res1.x resfinal = 1 vent_cond[1] = resfinal if Tcheck > 0.998 * self.stored_fluid.backend.T_critical(): Tcheck = 0.998 * self.stored_fluid.backend.T_critical() resfinal = self.find_quality_at_saturation_capacity( Tcheck, init_cap) lower_bound = max(q, resfinal) upper_bound = min(q, resfinal) total_surface_area = self.sorbent_material.mass *\ self.sorbent_material.specific_surface_area * 1000 qgrid = np.linspace(lower_bound, upper_bound, 100) Agrid = np.zeros_like(qgrid) ygrid = np.zeros_like(qgrid) for i, qual in enumerate(qgrid): temper = self.find_temperature_at_saturation_quality(qual, init_cap)\ .x self.stored_fluid.backend.update(CP.QT_INPUTS, 0, temper) p = self.stored_fluid.backend.p() rhol = self.stored_fluid.backend.rhomolar() nl = (1 - qual) * self.capacity_bulk(p, temper, qual) vbulk = self.bulk_fluid_volume(p, temper) Agrid[i] = total_surface_area * (nl/(rhol * vbulk)) ygrid[i] = self.sorbent_material.model_isotherm.\ areal_immersion_energy(temper) integ_res = sp.integrate.simps(ygrid, Agrid) integ_res = -integ_res if q < resfinal else integ_res final_ene = final_ene + integ_res else: integ_res = 0 return pd.DataFrame( {"init pressure": init_pres, "init temperature": T, "init quality": q, "dormancy time": (final_ene - init_ene)/heating_power, "final temperature": vent_cond[0], "final quality": vent_cond[1], "final pressure": self.vent_pressure, "capacity error": final_cap - init_cap, "total energy change": final_ene - init_ene, "sorbent energy contribution": final_ene_ads - init_ene_ads, "bulk energy contribution": final_ene_bulk - init_ene_bulk, "immersion heat contribution": integ_res, "solid heat capacity contribution": heat_capacity_change( T, vent_cond[0])}, index=[0])