Source code for biosteam._heat_utility

# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2024, Yoel Cortes-Pena <yoelcortes@gmail.com>
# 
# This module is under the UIUC open-source license. See 
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""
from __future__ import annotations
from thermosteam.units_of_measure import (
    convert, DisplayUnits, AbsoluteUnitsOfMeasure, get_dimensionality,
    heat_utility_units_of_measure
)
from thermosteam.utils import unregistered, units_of_measure
from thermosteam import Thermo, Stream, ThermalCondition, settings
from .exceptions import DimensionError
from math import copysign
from collections import deque
from typing import Optional, TYPE_CHECKING, Iterable, Literal, Sequence
if TYPE_CHECKING: from biosteam import Unit

__all__ = ('HeatUtility', 'UtilityAgent')

# Costs from Table 17.1 in Warren D.  Seider et al.-Product and Process Design Principles Synthesis, Analysis and Evaluation-Wiley (2016)
# ^This table was made using data from Busche, 1995
# Entry temperature conditions of coolants taken from Table 12.1 in Warren, 2016

mol_basis_units = AbsoluteUnitsOfMeasure('kmol')
mass_basis_units = AbsoluteUnitsOfMeasure('kg')
energy_basis_units = AbsoluteUnitsOfMeasure('kJ')

# %% Utility agents

[docs] @unregistered class UtilityAgent(Stream): """ Create a UtilityAgent object that defines a utility option. Parameters ---------- ID : A unique identification. If ID is None, stream will not be registered. If no ID is given, stream will be registered with a unique ID. flow : All flow rates corresponding to defined chemicals. phase : 'g' for gas, 'l' for liquid, and 's' for solid. Defaults to 'l'. T : Temperature [K]. Defaults to 298.15. P : Pressure [Pa]. Defaults to 101325. units : Flow rate units of measure (only mass, molar, and volumetric flow rates are valid). Defaults to 'kmol/hr'. thermo : Thermo object to initialize input and output streams. Defaults to :meth:`settings.thermo <thermosteam._settings.ProcessSettings.thermo>`. T_limit : Temperature limit of outlet utility streams [K]. If no limit is given, phase change is assumed. If utility agent heats up, `T_limit` is the maximum temperature. If utility agent cools down, `T_limit` is the minimum temperature. heat_transfer_price : Price of transferred heat [USD/kJ]. Defautls to 1. regeneration_price : Price of regenerating the fluid for reuse [USD/kmol]. Defaults to 0. heat_transfer_efficiency : Fraction of heat transferred accounting for losses to the environment (must be between 0 to 1). Defaults to 1. isfuel : Whether to burn the agent as a isfuel for heat. dT : Minimum temperature change between inlet and outlet utility. A positive value prevents near infinite flows when utility agents use sensible heats. **chemical_flows : float ID - flow pairs. """ __slots__ = ('T_limit', '_heat_transfer_price', 'utility_stream_dump', '_regeneration_price', 'heat_transfer_efficiency', 'isfuel', 'dT') def __init__(self, ID: Optional[str]='', phase: Optional[str]='l', T: Optional[float]=298.15, P: Optional[float]=101325., units: Optional[str]=None, thermo: Optional[Thermo]=None, T_limit: Optional[float]=None, heat_transfer_price: float=0., regeneration_price: float=0., heat_transfer_efficiency: float=1., isfuel: bool=False, dT: Optional[float]=0, **chemical_flows: float): self._thermal_condition = ThermalCondition(T, P) thermo = self._load_thermo(thermo) self._init_indexer((), phase, thermo.chemicals, chemical_flows) if units is not None: name, factor = self._get_flow_name_and_factor(units) flow = getattr(self, name) flow[:] = self.mol / factor self.mol[:] /= self.mol.sum() # Total flow must be 1 kmol / hr self.mol.read_only = True # Flow rate cannot change anymore self._sink = self._source = None self.reset_cache() self._register(ID) self.T_limit = T_limit self.heat_transfer_price = heat_transfer_price self.regeneration_price = regeneration_price self.heat_transfer_efficiency = heat_transfer_efficiency self.isfuel = isfuel self.dT = dT self.utility_stream_dump = [] def _get_property(self, name, flow=False, nophase=False, T=None, P=None): # Take advantage of how composition is always constant and temperatures # and pressures are not expected to vary much. property_cache = self._property_cache thermal_condition = self._thermal_condition imol = self._imol composition = imol.data if not T: T = thermal_condition._T if not P: P = thermal_condition._P if nophase: literal = (T, P) else: phase = imol._phase literal = (phase, T, P) key = (name, literal) if key in property_cache: return property_cache[key] calculate = getattr(self.mixture, name) if nophase: property_cache[name] = value = calculate( composition, T, P ) else: property_cache[name] = value = calculate( phase, composition, T, P ) if len(property_cache) > 100: property_cache.pop(property_cache.__iter__().__next__()) return value @property def iscooling_agent(self) -> bool: """Whether the agent is a cooling agent.""" T_limit = self.T_limit return T_limit > self.T if T_limit else self.phase == 'l' @property def isheating_agent(self) -> bool: """Whether the agent is a heating agent.""" T_limit = self.T_limit return T_limit < self.T if T_limit else self.phase == 'g' @property def price(self): raise AttributeError(f"'{type(self).__name__}' object has no attribute 'price'") @price.setter def price(self, price): raise AttributeError(f"'{type(self).__name__}' object has no attribute 'price'") @property def cost(self): raise AttributeError(f"'{type(self).__name__}' object has no attribute 'cost'") @cost.setter def cost(self, cost): raise AttributeError(f"'{type(self).__name__}' object has no attribute 'cost'")
[docs] def to_stream(self, ID: Optional[str]=None): """ Return a copy as a :class:`~thermosteam.Stream` object. Examples -------- >>> import biosteam as bst >>> bst.settings.set_thermo(['Water', 'Ethanol']) >>> cooling_water = bst.HeatUtility.get_agent('cooling_water') >>> cooling_water_copy = cooling_water.to_stream('cooling_water_copy') >>> cooling_water_copy.show(flow='kg/hr') Stream: cooling_water_copy phase: 'l', T: 305.37 K, P: 101325 Pa flow (kg/hr): Water 18 """ new = Stream.__new__(Stream) new._sink = new._source = None new._thermo = self._thermo new._imol = self._imol.copy() new._thermal_condition = self._thermal_condition.copy() new.reset_cache() new._price = 0. new.characterization_factors = {} new._register(ID) return new
@property def heat_transfer_price(self) -> float: """Price of transfered heat [USD/kJ].""" return self._heat_transfer_price @heat_transfer_price.setter def heat_transfer_price(self, price: float): assert price >= 0, "heat transfer price cannot be negative" self._heat_transfer_price = price @property def regeneration_price(self) -> float: """Price of regenerating the fluid for reuse [USD/kmol].""" return self._regeneration_price @regeneration_price.setter def regeneration_price(self, price: float): assert price >= 0, "regeneration price cannot be negative" self._regeneration_price = price def _info_phaseTP(self, phase, units, notation): T_notation = notation['T'] P_notation = notation['P'] T_units = units['T'] P_units = units['P'] if self.T_limit is None: T_limit = "None" else: T_limit = convert(self.T_limit, 'K', T_units) T_limit = f"{T_limit:.3g} K" if T_limit else "None" T = convert(self.T, 'K', T_units) P = convert(self.P, 'Pa', P_units) s = '' if isinstance(phase, str) else 's' ht_price = self.heat_transfer_price rg_price = self.regeneration_price ht_eff = self.heat_transfer_efficiency return (f"heat_transfer_efficiency: {ht_eff:.3f}\n" f"heat_transfer_price: {ht_price:.3g} USD/kJ\n" f"regeneration_price: {rg_price:.3g} USD/kmol\n" f"T_limit: {T_limit}\n" f"phase{s}: {repr(phase)}\n" f"T: {T:{T_notation}} {T_units}\n" f"P: {P:{P_notation}} {P_units}\n" ) def __repr__(self): return f"<{type(self).__name__}: {self.ID}>"
# %%
[docs] @units_of_measure(heat_utility_units_of_measure) class HeatUtility: """ Create an HeatUtility object that can choose a utility stream and calculate utility requirements. It can calculate required flow rate, temperature change, or phase change of utility. Calculations assume counter current flow rate. Parameters ---------- heat_transfer_efficiency : Enforced fraction of heat transferred from utility (due to losses to environment). unit : Parent unit using this heat utility. hxn_ok : Whether heat utility can be satisfied within a heat exchanger network. Examples -------- Create a heat utility: >>> from biosteam import HeatUtility >>> hu = HeatUtility() >>> hu.show() HeatUtility: None duty: 0 flow: 0 cost: 0 Calculate utility requirement by calling it with a duty (kJ/hr), and entrance and exit temperature (K): >>> hu(1000, 300, 350) >>> hu.show() HeatUtility: low_pressure_steam duty: 1.05e+03 kJ/hr flow: 0.0272 kmol/hr cost: 0.00647 USD/hr All results are accessible: >>> hu.ID, hu.duty, hu.flow, hu.cost ('low_pressure_steam', 1052.6315789473686, 0.02721274387089031, 0.006471190492497716) All results are accessible: >>> hu.ID, hu.duty, hu.flow, hu.cost ('low_pressure_steam', 1052.6315789473686, 0.02721274387089031, 0.006471190492497716) """ __slots__ = ( 'inlet_utility_stream', 'outlet_utility_stream', 'agent', 'duty', 'flow', 'cost', 'heat_transfer_efficiency', 'unit', 'hxn_ok', 'unit_duty', 'oxygen_rich_inlet', # Only for fuels ) #: Minimum approach temperature difference. Used to assign #: the pinch temperature of the utility stream. dT: float = 5 #: Units of measure for IPython display. display_units: DisplayUnits = DisplayUnits(duty='kJ/hr', flow='kmol/hr', cost='USD/hr') #: Used broadly throughout utilities. thermo_water: Thermo = Thermo(['Water']) #: Used for refrigeration utility. thermo_propane: Thermo = Thermo(['Propane']) #: Used for refrigeration utility. thermo_propylene: Thermo = Thermo(['Propylene']) #: Used for refrigeration utility. thermo_ethylene: Thermo = Thermo(['Ethylene']) #: Used exclusively for furnaces. thermo_natural_gas: Thermo = Thermo(['Methane', 'N2', 'CO2', 'O2', 'H2O']) #: Characterization factor data (value and units) by agent ID and impact key. characterization_factors: dict[tuple[str, str], tuple[float, AbsoluteUnitsOfMeasure]] = {} #: All cooling utilities available. cooling_agents: list[UtilityAgent] #: All heating utilities available. heating_agents: list[UtilityAgent]
[docs] @classmethod def set_CF(self, ID: str, key: str, value: float, basis: Optional[str]=None, units: Optional[str]=None): """ Set the characterization factor of a utility agent for a given impact key. Parameters ---------- ID : ID of utility agent. key : Key of impact indicator. value : Characterization factor value. basis : Basis of characterization factor. Valid dimensions include weight, molar, and energy (e.g. 'kg', 'kmol', 'kJ'). Defaults to 'kg'. units : Units of impact indicator. Before using this argument, the default units of the impact indicator should be defined with :meth:`settings.define_impact_indicator <thermosteam._settings.ProcessSettings.define_impact_indicator>`. Units must also be dimensionally consistent with the default units. Raises ------ ValueError When the duty characterization factor for a cooling agent is positive (should be negative). DimensionError When characterization factor is not given in dimensions of weight, molar, or energy. See Also -------- HeatUtility.get_CF Examples -------- Set the GWP characterization factor for low pressure steam at 88.44 kg CO2e / mmBtu (GREET 2020; Steam Production via Small Boiler from North American Natural Gas): >>> import biosteam as bst >>> bst.HeatUtility.set_CF('low_pressure_steam', 'GWP [kg CO2e]', 88.44, basis='MMBtu') Retrieve the GWP characterization factor for low pressure steam on a Btu basis: >>> bst.HeatUtility.get_CF('low_pressure_steam', 'GWP [kg CO2e]', basis='Btu') 8.844e-05 """ agent = self.get_agent(ID) if units is not None: original_units = settings.get_impact_indicator_units(key) value = original_units.unconvert(value, units) if basis is None: basis_units = mass_basis_units else: dim = get_dimensionality(basis) if dim == mol_basis_units.dimensionality: basis_units = mol_basis_units elif dim == mass_basis_units.dimensionality: basis_units = mass_basis_units elif dim == energy_basis_units.dimensionality: basis_units = energy_basis_units if agent.iscooling_agent and value > 0.: raise ValueError( 'duty characterization factor must be negative for cooling agents' ) else: raise DimensionError( "dimensions for characterization factors must be in a molar, " f"mass or energy basis, not '{dim}'" ) value *= basis_units.conversion_factor(basis) self.characterization_factors[agent.ID, key] = value, basis_units
[docs] @classmethod def get_CF(self, ID: str, key: str, basis: Optional[str]=None, units: Optional[str]=None): """ Return the characterization factor of a utility agent for a given impact key. Parameters ---------- ID : ID of utility agent. key : Key of impact indicator. basis : Basis of characterization factor. Valid dimensions include weight, molar, and energy (e.g. 'kg', 'kmol', 'kJ'). Defaults to 'kg'. units : Units of impact indicator. Before using this argument, the default units of the impact indicator should be defined with :meth:`settings.define_impact_indicator <thermosteam._settings.ProcessSettings.define_impact_indicator>`. Units must also be dimensionally consistent with the default units. Raises ------ DimensionalityError When the characterization factor cannot be converted to the given basis due to inconsistent dimensions with the original basis. See Also -------- HeatUtility.set_CF """ try: value, basis_units = self.characterization_factors[ID, key] except KeyError: if basis is None: return 0., None else: return 0. if units is not None: original_units = settings.get_impact_indicator_units(key) value = original_units.convert(value, units) if basis is None: return value, basis_units.units else: return value / basis_units.conversion_factor(basis)
def get_impact(self, key: str): agent = self.agent CF, basis_units = self.get_CF(agent.ID, key) if CF == 0.: return 0. if basis_units == 'kg': return self.flow * agent.MW * CF elif basis_units == 'mol': return self.flow * CF elif basis_units == 'kJ': return self.duty * CF else: raise RuntimeError("unknown error") def get_inventory(self, key: str): agent = self.agent CF, basis_units = self.get_CF(agent.ID, key) if basis_units == 'kg': return self.flow * agent.MW, basis_units elif basis_units == 'mol': return self.flow, basis_units elif basis_units == 'kJ': return self.duty, basis_units else: raise RuntimeError("unknown error") def __init__(self, heat_transfer_efficiency: Optional[float]=None, unit: Optional[Unit]=None, hxn_ok: Optional[bool]=False, ): #: Enforced fraction of heat transferred from utility (due #: to losses to environment). self.heat_transfer_efficiency: float = heat_transfer_efficiency #: Parent unit using this heat utility. self.unit: Unit|None = unit #: Whether heat utility can be satisfied within a heat exchanger network. self.hxn_ok: bool = hxn_ok #: Total heat transferred from utility to both the process and the environment [kJ/hr]. self.duty: float = 0. #: Effective heat transferred from utility to the unit operation [kJ/hr]. self.unit_duty: float = 0. #: Flow rate of utility [kmol/hr]. self.flow: float = 0. #: Cost of utility [USD/hr]. self.cost: float = 0. #: Utility agent being used. self.agent: UtilityAgent|None = None #: Fresh utility stream self.inlet_utility_stream: Stream|None = None #: Used utility stream self.outlet_utility_stream: Stream|None = None def __bool__(self) -> bool: return bool(self.agent) @property def ID(self) -> str: """ID of utility agent being used.""" agent = self.agent return agent.ID if agent else "" @ID.setter def ID(self, ID: str): """ID of utility agent being used.""" self.agent = self.get_agent(ID)
[docs] @classmethod def default_agents(cls): """Reset all agents back to BioSTEAM's defaults.""" cls.default_heating_agents() cls.default_cooling_agents()
[docs] @classmethod def default_heating_agents(cls): """Reset all heating agents back to BioSTEAM's defaults.""" thermo_water = cls.thermo_water low_pressure_steam = UtilityAgent( 'low_pressure_steam', Water=1, T=412.189, P=344738.0, phase='g', thermo=thermo_water, regeneration_price = 0.2378, heat_transfer_efficiency = 0.95, ) medium_pressure_steam = UtilityAgent( 'medium_pressure_steam', Water=1, T=454.770, P=1.041e+6, phase='g', thermo=thermo_water, regeneration_price = 0.2756, heat_transfer_efficiency = 0.90, ) high_pressure_steam = UtilityAgent( 'high_pressure_steam', Water=1, T=508.991, P=3.11e+6, phase='g', thermo=thermo_water, regeneration_price = 0.3171, heat_transfer_efficiency = 0.85, ) natural_gas = UtilityAgent( 'natural_gas', Methane=1, T=298.15, P=200 * 101325, phase='g', thermo=cls.thermo_natural_gas, heat_transfer_efficiency = 0.90, # Heat loss to environment regeneration_price=3.49672, T_limit=405, # Must be reasonably higher than the emission's dew point at 500 psig (401 K) isfuel=True, ) cls.heating_agents = [low_pressure_steam, medium_pressure_steam, high_pressure_steam, natural_gas]
[docs] @classmethod def default_cooling_agents(cls): """Reset all cooling agents back to BioSTEAM's defaults.""" thermo_water = cls.thermo_water cooling_water = UtilityAgent( 'cooling_water', Water=1, T=305.372, P=101325, thermo=thermo_water, T_limit = 324.817, regeneration_price = 4.8785e-4, dT=2., ) chilled_water = UtilityAgent( 'chilled_water', Water=1, T=280.372, P=101325, thermo=thermo_water, T_limit = 300.372, heat_transfer_price = 5e-6, dT=2., ) chilled_brine = UtilityAgent( 'chilled_brine', Water=1, T=255.372, P=101325, thermo=thermo_water, T_limit = 275.372, heat_transfer_price = 8.145e-6, dT=2., ) propane = UtilityAgent( 'propane', Propane=1, thermo=cls.thermo_propane, T=238.70, P=cls.thermo_propane.chemicals.Propane.Psat(238.70), heat_transfer_price = 13.17e-6, phase='l', dT=1., ) propylene = UtilityAgent( 'propylene', Propylene=1, thermo=cls.thermo_propylene, T=227.59, P=cls.thermo_propylene.chemicals.Propylene.Psat(227.59), heat_transfer_price = 16.54e-6, # Lever rule with -30 and -90 F prices phase='l', dT=1., ) ethylene = UtilityAgent( 'ethylene', Ethylene=1, thermo=cls.thermo_ethylene, T=172.04, P=cls.thermo_ethylene.chemicals.Ethylene.Psat(172.04), heat_transfer_price = 33.2e-06, phase='l', dT=1., ) cls.cooling_agents = [cooling_water, chilled_water, chilled_brine, propane, propylene, ethylene]
def copy(self) -> HeatUtility: hu = HeatUtility() hu.copy_like(self) return hu
[docs] def copy_like(self, other: HeatUtility): """Copy all data from another heat utility.""" if other.agent: self.load_agent(other.agent) self.inlet_utility_stream = other.inlet_utility_stream.copy() self.outlet_utility_stream = other.outlet_utility_stream.copy() self.flow = other.flow self.duty = other.duty self.unit_duty = other.unit_duty self.cost = other.cost self.heat_transfer_efficiency = other.heat_transfer_efficiency
[docs] def scale(self, factor: float): """Scale utility data.""" self.flow *= factor self.duty *= factor self.cost *= factor self.inlet_utility_stream.mol *= factor if self.agent.isfuel: self.outlet_utility_stream.mol *= factor self.oxygen_rich_inlet.mol *= factor
# No need to factor the outlet utility stream # because it shares the same flow rate data as the inlet
[docs] def empty(self): """Remove utility requirements.""" if self.agent: agent = self.agent dump = agent.utility_stream_dump if len(dump) < 1000: if agent.isfuel: dump.append( (self.inlet_utility_stream, self.outlet_utility_stream, self.oxygen_rich_inlet) ) else: dump.append( (self.inlet_utility_stream, self.outlet_utility_stream) ) self.cost = self.flow = self.duty = self.unit_duty = 0 self.outlet_utility_stream = self.inlet_utility_stream = self.agent = None
def set_utility_by_flow_rate(self, agent: Optional[UtilityAgent], F_mol: float): if F_mol == 0.: self.empty() return self.load_agent(agent) heat_transfer_efficiency = self.heat_transfer_efficiency or agent.heat_transfer_efficiency if agent.T_limit or agent.isfuel: raise ValueError('agent must work by latent heat to set by flow rate') if self.inlet_utility_stream.phase == 'l' : self.outlet_utility_stream.phase = 'g' dh = -agent._get_property('Hvap', nophase=True) else: dh = agent._get_property('Hvap', nophase=True) self.outlet_utility_stream.phase = 'l' self.duty = duty = dh * F_mol self.unit_duty = duty * heat_transfer_efficiency self.outlet_utility_stream.mol[:] = F_mol self.flow = F_mol self.cost = agent._heat_transfer_price * abs(duty) + agent._regeneration_price * F_mol
[docs] def __call__(self, unit_duty: float, T_in: float, T_out: Optional[float]=None, agent: Optional[UtilityAgent]=None): """ Calculate utility requirements given the essential parameters. Parameters ---------- unit_duty : Unit duty requirement [kJ/hr] T_in : Inlet process stream temperature [K] T_out : Outlet process stream temperature [K] agent : Utility agent to use. Defaults to a suitable agent from predefined heating/cooling utility agents. """ if unit_duty == 0: self.empty() return T_out = T_out or T_in iscooling = unit_duty < 0. #: Whether the utility is cooling the process. # Note: These are pinch temperatures at the utility inlet and outlet. # Not to be confused with the inlet and outlet of the process stream. # If cooling, T_pinch_in is the minimum utility temperature required. # If heating, T_pinch_in is the maximum utility temperature required. T_pinch_in, T_pinch_out = self.get_inlet_and_outlet_pinch_temperature( iscooling, T_in, T_out ) ## Select heat transfer agent ## if agent: if agent is not self.agent: self.load_agent(agent) else: agent = (self.get_suitable_cooling_agent if iscooling else self.get_suitable_heating_agent)(T_pinch_in) self.load_agent(agent) ## Calculate utility requirement ## heat_transfer_efficiency = self.heat_transfer_efficiency or agent.heat_transfer_efficiency duty = unit_duty / heat_transfer_efficiency if agent.isfuel: T_emissions = self.get_outlet_temperature( T_pinch_out, agent.T_limit, iscooling ) feed = self.inlet_utility_stream emissions = self.outlet_utility_stream emissions.copy_like(feed) emissions.P = 101325 reactions = agent.chemicals.get_combustion_reactions() reactions.force_reaction(emissions) O2_consumption = -emissions.imol['O2'] oxygen_rich_gas = self.oxygen_rich_inlet z_O2 = oxygen_rich_gas.imol['O2'] / oxygen_rich_gas.F_mol oxygen_rich_gas.F_mol = O2_consumption / z_O2 emissions.mol += oxygen_rich_gas.mol F_emissions = emissions.F_mass z_CO2 = emissions.imass['CO2'] / F_emissions z_CO2_target = 0.055 # Usually between 4 - 7 for biomass and natural gas (https://www.sciencedirect.com/science/article/pii/S0957582021005127) F_emissions_new = z_CO2 * F_emissions / z_CO2_target dF_emissions = F_emissions_new - F_emissions oxygen_rich_gas.F_mass = F_mass_O2_new = oxygen_rich_gas.F_mass + dF_emissions emissions.mol += oxygen_rich_gas.mol * (dF_emissions / F_mass_O2_new) emissions.T = T_emissions emissions.P = 3548325.0 # 500 psig dh = feed.Hnet - emissions.Hnet elif agent.T_limit: # Temperature change self.outlet_utility_stream.T = T_outlet = self.get_outlet_temperature( T_pinch_out, agent.T_limit, iscooling ) dh = agent._get_property('H') - agent._get_property('H', T=T_outlet) else: # Phase change if agent.phase == 'l': self.outlet_utility_stream.phase = 'g' dh = -agent._get_property('Hvap', nophase=True) else: dh = agent._get_property('Hvap', nophase=True) self.outlet_utility_stream.phase = 'l' # Update utility flow F_mol = duty / dh self.inlet_utility_stream.mol[:] *= F_mol if agent.isfuel: emissions.mol[:] *= F_mol oxygen_rich_gas.mol[:] *= F_mol # Update results self.unit_duty = unit_duty self.flow = F_mol self.duty = duty self.cost = agent._heat_transfer_price * abs(duty) + agent._regeneration_price * F_mol
@property def inlet_process_stream(self) -> Stream: """If a heat exchanger is available, this stream is the inlet process stream to the heat exchanger.""" heat_exchanger = self.unit if heat_exchanger: return heat_exchanger.inlet else: raise AttributeError('no heat exchanger available ' 'to retrieve process stream') @property def outlet_process_stream(self) -> Stream: """If a heat exchanger is available, this stream is the outlet process stream to the heat exchanger.""" heat_exchanger = self.unit if heat_exchanger: return heat_exchanger.outlet else: raise AttributeError('no heat exchanger available ' 'to retrieve process stream')
[docs] @staticmethod def heat_utilities_by_agent(heat_utilities: Iterable[HeatUtility]): """Return a dictionary of heat utilities sorted by agent ID.""" heat_utilities = [i for i in heat_utilities if i.agent] heat_utilities_by_agent = {i.ID: [] for i in heat_utilities} for i in heat_utilities: heat_utilities_by_agent[i.agent.ID].append(i) return heat_utilities_by_agent
[docs] @classmethod def sum(cls, heat_utilities: Iterable[HeatUtility]): """Return a HeatUtility object that reflects the sum of heat utilities.""" heat_utility = cls() heat_utility.mix_from(heat_utilities) return heat_utility
[docs] @classmethod def sum_by_agent(cls, heat_utilities: Iterable[HeatUtility]): """Return a list of heat utilities that reflect the sum of heat utilities by agent.""" heat_utilities_by_agent = cls.heat_utilities_by_agent(heat_utilities) return [cls.sum(i) for i in heat_utilities_by_agent.values()]
[docs] @classmethod def get_agent(cls, ID: str): """Return utility agent with given ID.""" for agent in cls.heating_agents + cls.cooling_agents: if agent.ID == ID: return agent raise LookupError(ID)
[docs] @classmethod def get_heating_agent(cls, ID: str): """Return heating agent with given ID.""" for agent in cls.heating_agents: if agent.ID == ID: return agent raise LookupError(ID)
[docs] @classmethod def get_cooling_agent(cls, ID: str): """Return cooling agent with given ID.""" for agent in cls.cooling_agents: if agent.ID == ID: return agent raise LookupError(ID)
[docs] @classmethod def get_suitable_heating_agent(cls, T_pinch: float): """ Return a heating agent that works at the pinch temperature. Parameters ---------- T_pinch : Pinch temperature [K]. """ for agent in cls.heating_agents: if T_pinch < agent.T - agent.dT or agent.isfuel: return agent raise RuntimeError(f'no heating agent that can heat over {T_pinch} K')
[docs] @classmethod def get_suitable_cooling_agent(cls, T_pinch: float): """Return a cooling agent that works at the pinch temperature. Parameters ---------- T_pinch : Pinch temperature [K]. """ for agent in cls.cooling_agents: if T_pinch > agent.T + agent.dT: return agent raise RuntimeError(f'no cooling agent that can cool under {T_pinch} K')
[docs] def load_agent(self, agent: UtilityAgent): """Initialize utility streams with given agent.""" if self.agent is agent: self.inlet_utility_stream.copy_like(agent) if not agent.T_limit: self.outlet_utility_stream.copy_thermal_condition(agent) return elif self.agent: self.empty() if agent.utility_stream_dump: if agent.isfuel: self.inlet_utility_stream, self.outlet_utility_stream, self.oxygen_rich_inlet = agent.utility_stream_dump.pop() self.oxygen_rich_inlet.reset_flow(O2=21, N2=79, phase='g', units='kg/hr') else: self.inlet_utility_stream, self.outlet_utility_stream = agent.utility_stream_dump.pop() # Prevent errors where utility streams are altered self.inlet_utility_stream.copy_like(agent) if not agent.T_limit: self.outlet_utility_stream.copy_thermal_condition(agent) else: self.inlet_utility_stream = agent.to_stream() if agent.isfuel: self.outlet_utility_stream = agent.to_stream() self.oxygen_rich_inlet = Stream(O2=21, N2=79, phase='g', units='kg/hr', thermo=agent.thermo) else: self.outlet_utility_stream = self.inlet_utility_stream.flow_proxy() self.agent = agent
[docs] def mix_from(self, heat_utilities: Iterable[HeatUtility]): """Mix all heat utilities to this heat utility.""" heat_utilities = [i for i in heat_utilities if i.agent] N_heat_utilities = len(heat_utilities) if N_heat_utilities == 0: self.empty() elif N_heat_utilities == 1: self.copy_like(heat_utilities[0]) else: heat_utility, *other_heat_utilities = heat_utilities agent = heat_utility.agent ID = agent.ID for i in other_heat_utilities: if i.agent.ID != ID: raise ValueError( "utility agent must be the same to mix heat utilities" ) self.load_agent(agent) self.flow = self.inlet_utility_stream.F_mol = sum([i.flow for i in heat_utilities]) self.outlet_utility_stream.mix_from([i.outlet_utility_stream for i in heat_utilities]) self.duty = sum([i.duty for i in heat_utilities]) self.unit_duty = sum([i.unit_duty for i in heat_utilities]) self.cost = sum([i.cost for i in heat_utilities]) self.heat_transfer_efficiency = None
[docs] def reverse(self): """Reverse direction of utility. If utility is being consumed, the utility is produced instead, and vice-versa.""" self.flow *= -1 self.duty *= -1 self.unit_duty *= -1 self.cost *= -1 if self.duty: self.inlet_utility_stream, self.outlet_utility_stream = self.outlet_utility_stream, self.inlet_utility_stream
# Subcalculations
[docs] @staticmethod def get_outlet_temperature(T_pinch: float, T_limit: float, iscooling: bool): """ Return outlet temperature of the utility in a counter current heat exchanger Parameters ---------- T_pinch : Pinch temperature of utility stream [K]. iscooling : True if utility is loosing energy. """ if iscooling: return T_limit if T_limit and T_limit < T_pinch else T_pinch else: return T_limit if T_limit and T_limit > T_pinch else T_pinch
[docs] @classmethod def get_inlet_and_outlet_pinch_temperature(cls, iscooling: bool, T_in: float, T_out: float): """Return pinch inlet and outlet temperature of utility.""" dT = cls.dT if iscooling: if T_in + 1e-1 < T_out: raise ValueError("inlet must be hotter than outlet if cooling") T_pinch_in = T_out - dT T_pinch_out = T_in - dT else: if T_in > T_out + 1e-1: raise ValueError("inlet must be cooler than outlet if heating") T_pinch_in = T_out + dT T_pinch_out = T_in + dT return T_pinch_in, T_pinch_out
# Representation def _info_data(self, duty: None, flow: None, cost: None): # Get units of measure su = self.display_units duty_units = duty or su.duty flow_units = flow or su.flow cost_units = cost or su.cost # Change units and return info string flow = self.inlet_utility_stream.get_total_flow(flow_units) duty = convert(self.duty, 'kJ/hr', duty_units) cost = convert(self.cost, 'USD/hr', cost_units) return duty, copysign(flow, self.flow), cost, duty_units, flow_units, cost_units def __repr__(self): if self.agent: duty, flow, cost, duty_units, flow_units, cost_units = self._info_data(None, None, None) return f'<{self.ID}: {self.duty:.3g} {duty_units}, {self.flow:.3g} {flow_units}, {self.cost:.3g} {cost_units}>' else: return f'<{type(self).__name__}: None>' def _info(self, duty: str|None, flow: str|None, cost: str|None): """Return string related to specifications""" if not self.agent: return (f'{type(self).__name__}: None\n' +' duty: 0\n' +' flow: 0\n' +' cost: 0') else: (duty, flow, cost, duty_units, flow_units, cost_units) = self._info_data(duty, flow, cost) return (f'{type(self).__name__}: {self.ID}\n' +f'duty:{duty: .3g} {duty_units}\n' +f'flow:{flow: .3g} {flow_units}\n' +f'cost:{cost: .3g} {cost_units}')
[docs] def show(self, duty: Optional[str]=None, flow: Optional[str]=None, cost: Optional[str]=None): """Print all specifications""" print(self._info(duty, flow, cost))
_ipython_display_ = show def __add__(self, other: HeatUtility) -> HeatUtility: if other == 0: return self # Special case to get Python built-in sum to work return self.__class__.sum([self, other]) def __radd__(self, other: HeatUtility) -> HeatUtility: return self.__add__(other)
HeatUtility.default_agents() settings_cls = settings.__class__ settings_cls.get_agent = HeatUtility.get_agent settings_cls.get_cooling_agent = HeatUtility.get_cooling_agent settings_cls.get_heating_agent = HeatUtility.get_heating_agent settings_cls.set_utility_agent_CF = HeatUtility.set_CF del settings_cls