Source code for biosteam.units.compressor

# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2022, Yoel Cortes-Pena <yoelcortes@gmail.com>, Ben Portner <github.com/BenPortner>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
.. contents:: :local:

.. autoclass:: biosteam.units.compressor.Compressor
.. autoclass:: biosteam.units.compressor.IsothermalCompressor
.. autoclass:: biosteam.units.compressor.IsentropicCompressor
.. autoclass:: biosteam.units.compressor.PolytropicCompressor
.. autoclass:: biosteam.units.compressor.MultistageCompressor

References
----------
.. [1] Seider, W. D., Lewin,  D. R., Seader, J. D., Widagdo, S., Gani, R.,
    & Ng, M. K. (2017). Product and Process Design Principles. Wiley.
    Cost Accounting and Capital Cost Estimation (Chapter 16)
.. [2] Sinnott, R. and Towler, G (2019). "Chemical Engineering Design: SI Edition (Chemical Engineering Series)". 6th Edition. Butterworth-Heinemann.
.. [3] Schultz, J. (1962). "The Polytropic Analysis of Centrifugal Compressors". J. Eng. Power., 84(1): 69-82 (14 pages)
.. [4] Hundseid, O., Bakken, L. E. and Helde, T. (2006). “A Revised Compressor Polytropic Performance Analysis,” Proceedings of ASME GT2006, Paper Number 91033, ASME Turbo Expo 2006.

"""
import biosteam as bst
import numpy as np
from warnings import warn
from math import log, exp, ceil
from typing import NamedTuple, Tuple, Callable, Dict
from thermosteam.constants import R
from .heat_exchange import HX
from ..utils import list_available_names
from ..exceptions import DesignWarning, bounds_warning
from .. import Unit
from thermosteam._graphics import compressor_graphics

__all__ = (
    'Compressor',
    'IsentropicCompressor', 
    'IsothermalCompressor', 
    'PolytropicCompressor',
    'MultistageCompressor'
)

#: TODO:
#: * Implement estimate of isentropic efficiency when not given (is this possible?).

class CompressorCostAlgorithm(NamedTuple): 
    #: Defines preliminary correlation algorithm for a compressor type
    psig_max: float #: Maximum achievable pressure in psig (to autodermine compressor type and/or issue warning)
    hp_bounds: Tuple[float, float] #: Horse power per machine (not a hard limit for costing, but included here for completion)
    acfm_bounds: Tuple[float, float] #: Actual cubic feet per minute (hard limit for parallel units)
    cost: Callable #: function(horse_power) -> Baseline purchase cost
    efficiencies: Dict[str, float] #: Heuristic efficiencies at 1,000 hp.
    driver: float #: Default driver (e.g., electric motor, steam turbine or gas turbine).
    CE: float #: Chemical engineering price cost index.


[docs] class Compressor(Unit, isabstract=True): """ Abstract class for compressors that includes design and costing. Child classes should implement the `_run` method for mass and energy balances. Preliminary design and costing is estimated according to [1]_. """ _graphics = compressor_graphics _N_ins = 1 _N_outs = 1 _units = { 'Ideal power': 'kW', 'Ideal duty': 'kJ/hr', } design_factors = { 'Electric motor': 1.0, 'Steam turbine': 1.15, 'Gas turbine': 1.25, } material_factors = { 'Carbon steel': 1.0, 'Stainless steel': 2.5, 'Nickel alloy': 5.0, } _F_BM_default = { 'Compressor(s)': 2.15, } #: dict[str, CompressorCostAlgorithm] Cost algorithms by compressor type. baseline_cost_algorithms = { 'Screw': CompressorCostAlgorithm( psig_max=400., acfm_bounds=(800., 2e4), hp_bounds=(10., 750.), cost=lambda Pc: exp(8.2496 + 0.7243 * log(Pc)), efficiencies={ 'Electric motor': 0.80, 'Steam turbine': 0.65, 'Gas turbine': 0.35, }, driver='Electric motor', CE=567, ), 'Centrifugal': CompressorCostAlgorithm( psig_max=5e3, acfm_bounds=(1e3, 1.5e5), hp_bounds=(200., 3e4), cost=lambda Pc: exp(9.1553 + 0.63 * log(Pc)), efficiencies={ 'Electric motor': 0.80, 'Steam turbine': 0.65, 'Gas turbine': 0.35, }, driver='Steam turbine', CE=567, ), 'Reciprocating': CompressorCostAlgorithm( psig_max=1e5, acfm_bounds=(5., 7000.), hp_bounds=(100., 20e3), cost=lambda Pc: exp(4.6762 + 1.23 * log(Pc)), efficiencies={ 'Electric motor': 0.85, 'Steam turbine': 0.65, 'Gas turbine': 0.35, }, driver='Electric motor', CE=567, ), } def _init(self, P, eta=0.7, vle=False, compressor_type=None, driver=None, material=None, driver_efficiency=None ): self.P = P #: Outlet pressure [Pa]. self.eta = eta #: Isentropic efficiency. #: Whether to perform phase equilibrium calculations on the outflow. #: If False, the outlet will be assumed to be the same phase as the inlet. self.vle = vle self.material = 'Carbon steel' if material is None else material self.compressor_type = 'Default' if compressor_type is None else compressor_type self.driver = 'Default' if driver is None else driver self.driver_efficiency = 'Default' if driver_efficiency is None else driver_efficiency @property def compressor_type(self): return self._compressor_type @compressor_type.setter def compressor_type(self, compressor_type): """[str] Type of compressor. If 'Default', the type will be determined based on the outlet pressure.""" compressor_type = compressor_type.capitalize() if compressor_type not in self.baseline_cost_algorithms and compressor_type != 'Default': raise ValueError( f"compressor type {repr(compressor_type)} not available; " f"only {list_available_names(self.baseline_cost_algorithms)} are available" ) self._compressor_type = compressor_type @property def driver(self): return self._driver @driver.setter def driver(self, driver): """[str] Type of compressor. If 'Default', the type will be determined based on type of compressor used. Centrifugal compressors default to steam turbines while reciprocating and screw compressors default to electric motors.""" driver = driver.capitalize() if driver not in self.design_factors and driver != 'Default': raise ValueError( f"driver {repr(driver)} not available; " f"only {list_available_names(self.design_factors)} are available" ) self._driver = driver @property def driver_efficiency(self): return self._driver_efficiency @driver_efficiency.setter def driver_efficiency(self, driver_efficiency): """[str] Efficiency of driver (e.g., steam turbine or electric motor). If 'Default', a heuristic efficiency will be selected based on the compressor type and the driver.""" if isinstance(driver_efficiency, str): if driver_efficiency != 'Default': raise ValueError( f"driver efficiency must be a number or 'Default'; not {repr(driver_efficiency)}" ) else: driver_efficiency = float(driver_efficiency) self._driver_efficiency = driver_efficiency @property def material(self): """[str]. Construction material. Defaults to 'Carbon steel'.""" return self._material @material.setter def material(self, material): try: self.F_M['Compressor(s)'] = self.material_factors[material] except KeyError: raise AttributeError("material must be one of the following: " f"{list_available_names(self.material_factors)}") self._material = material def _determine_compressor_type(self): psig = (self.P - 101325.) * 14.6959 / 101325. cost_algorithms = self.baseline_cost_algorithms for name, alg in cost_algorithms.items(): if psig < alg.psig_max: return name warn('no compressor available that is recommended for a pressure of ' f'{self.P:.5g}; defaulting to {name.lower()} compressor', DesignWarning) return name def _calculate_ideal_power_and_duty(self): feed = self.ins[0] out = self.outs[0] if feed.P > out.P: raise RuntimeError('inlet pressure is above outlet') dH = out.H - feed.H Q = TdS = feed.T * (out.S - feed.S) # Duty [kJ/hr] power_ideal = (dH - TdS) / 3600. # Power [kW] return power_ideal, Q def _set_power(self, power): driver = self.design_results['Driver'] driver_efficiency = self._driver_efficiency if driver_efficiency == 'Default': compressor_type = self.design_results['Type'] alg = self.baseline_cost_algorithms[compressor_type] driver_efficiency = alg.efficiencies[driver] self.design_results['Driver efficiency'] = driver_efficiency if driver == 'Electric motor': self.power_utility(power / driver_efficiency) else: # The turbine produces the power that the compressor consumes. # This may not be the most elegant way showing this, but it # makes it easy for design and costing. self.power_utility.consumption = self.power_utility.production = power / driver_efficiency if driver == 'Steam turbine': # Use high pressure steam utility as the driver. # Assume that the recondenser cost is negligible and that # heat integration is used (which are commonly the case). hps = bst.settings.get_heating_agent('high_pressure_steam') self.add_heat_utility(self.power_utility.consumption, T_in=298.15, agent=hps) elif driver == 'Gas turbine': # TODO: Possibly have an optional inlet stream that can work # as either steam or gas feed to the turbine. raise RuntimeError('gas turbine driver is not yet available in BioSTEAM') else: raise RuntimeError(f"invalid driver '{driver}'") def _design(self): design_results = self.design_results compressor_type = self.compressor_type if compressor_type == 'Default': compressor_type = self._determine_compressor_type() design_results['Type'] = compressor_type alg = self.baseline_cost_algorithms[compressor_type] acfm_lb, acfm_ub = alg.acfm_bounds acfm = self.ins[0].get_total_flow('cfm') design_results['Compressors in parallel'] = ceil(acfm / acfm_ub) if acfm > acfm_ub else 1 design_results['Driver'] = alg.driver if self._driver == 'Default' else self._driver def _cost(self): # Note: Must run `_set_power` before running parent cost algorithm design_results = self.design_results alg = self.baseline_cost_algorithms[design_results['Type']] acfm_lb, acfm_ub = alg.acfm_bounds Pc = self.power_utility.get_property('consumption', 'hp') N = design_results['Compressors in parallel'] Pc_per_compressor = Pc / N bounds_warning(self, 'power', Pc, 'hp', alg.hp_bounds, 'cost') if Pc_per_compressor < 1.: self.baseline_purchase_costs['Compressor(s)'] = 0. else: self.baseline_purchase_costs['Compressor(s)'] = N * bst.CE / alg.CE * alg.cost(Pc_per_compressor) self.F_D['Compressor(s)'] = self.design_factors[design_results['Driver']]
[docs] class IsothermalCompressor(Compressor, new_graphics=False): """ Create an isothermal compressor. Parameters ---------- ins : Inlet fluid. outs : Outlet fluid. P : float Outlet pressure [Pa]. eta : float Isothermal efficiency. vle : bool Whether to perform phase equilibrium calculations on the outflow. If False, the outlet will be assumed to be the same phase as the inlet. type: str Type of compressor : blower/centrifugal/reciprocating. If None, the type will be determined automatically. Notes ----- Default compressor selection, design and cost algorithms are adapted from [2]_. Examples -------- Simulate reversible isothermal compression of gaseous hydrogen. Note that we set `include_excess_energies=True` to correctly account for the non-ideal behavior of hydrogen at high pressures. We further use the Soave-Redlich-Kwong (SRK) equation of state instead of the default Peng-Robinson (PR) because it is more accurate in this regime. >>> import biosteam as bst >>> from thermo import SRK >>> thermo = bst.Thermo([bst.Chemical('H2', eos=SRK)]) >>> thermo.mixture.include_excess_energies = True >>> bst.settings.set_thermo(thermo) >>> feed = bst.Stream('feed', H2=1, T=298.15, P=20e5, phase='g') >>> K = bst.units.IsothermalCompressor('K', ins=feed, outs='outlet', P=350e5, eta=1) >>> K.simulate() >>> K.show() IsothermalCompressor: K ins... [0] feed phase: 'g', T: 298.15 K, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 298.15 K, P: 3.5e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Isothermal compressor Units K Electricity Power kW 2.47 Cost USD/hr 0.193 Chilled water Duty kJ/hr -7.26e+03 Flow kmol/hr 7.53 Cost USD/hr 0.0363 Design Type Reciprocating Compressors in parallel 1 Driver Electric motor Ideal power kW 2.1 Ideal duty kJ/hr -7.26e+03 Driver efficiency 0.85 Purchase cost Compressor(s) USD 470 Total purchase cost USD 470 Utility cost USD/hr 0.23 """ def _run(self): feed = self.ins[0] out = self.outs[0] out.copy_like(feed) out.P = self.P out.T = feed.T if self.vle is True: out.vle(T=out.T, P=out.P) self.ideal_power, self.ideal_duty = self._calculate_ideal_power_and_duty() def _design(self): super()._design() feed = self.ins[0] outlet = self.outs[0] ideal_power, ideal_duty = self._calculate_ideal_power_and_duty() Q = ideal_duty / self.eta self.add_heat_utility(unit_duty=Q, T_in=feed.T, T_out=outlet.T) self.design_results['Ideal power'] = ideal_power # kW self.design_results['Ideal duty'] = ideal_duty # kJ / hr self._set_power(ideal_power / self.eta)
[docs] class IsentropicCompressor(Compressor, new_graphics=False): """ Create an isentropic compressor. Parameters ---------- ins : Inlet fluid. outs : Outlet fluid. P : float Outlet pressure [Pa]. eta : float Isentropic efficiency. vle : bool Whether to perform phase equilibrium calculations on the outflow. If False, the outlet will be assumed to be the same phase as the inlet. type: str Type of compressor : blower/centrifugal/reciprocating. If None, the type will be determined automatically. Notes ----- Default compressor selection, design and cost algorithms are adapted from [2]_. Examples -------- Simulate isentropic compression of gaseous hydrogen with 70% efficiency: >>> import biosteam as bst >>> bst.settings.set_thermo(["H2"]) >>> feed = bst.Stream('feed', H2=1, T=25 + 273.15, P=101325, phase='g') >>> K = bst.units.IsentropicCompressor('K1', ins=feed, outs='outlet', P=50e5, eta=0.7) >>> K.simulate() >>> K.show() IsentropicCompressor: K1 ins... [0] feed phase: 'g', T: 298.15 K, P: 101325 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 1152 K, P: 5e+06 Pa flow (kmol/hr): H2 1 >>> K.results() Isentropic compressor Units K1 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 12.7 Flow kmol/hr 0.000396 Cost USD/hr 0.000126 Design Ideal power kW 4.92 Ideal duty kJ/hr 0 Type Centrifugal Compressors in parallel 1 Driver Steam turbine Driver efficiency 0.65 Purchase cost Compressor(s) USD 5.87e+04 Total purchase cost USD 5.87e+04 Utility cost USD/hr 0.000126 Per default, the outlet phase is assumed to be the same as the inlet phase. If phase changes are to be accounted for, set `vle=True`: >>> import biosteam as bst >>> bst.settings.set_thermo(["H2O"]) >>> feed = bst.MultiStream('feed', T=372.75, P=1e5, l=[('H2O', 0.1)], g=[('H2O', 0.9)]) >>> K = bst.units.IsentropicCompressor('K2', ins=feed, outs='outlet', P=100e5, eta=1.0, vle=True) >>> K.simulate() >>> K.show() IsentropicCompressor: K2 ins... [0] feed phases: ('g', 'l'), T: 372.75 K, P: 100000 Pa flow (kmol/hr): (g) H2O 0.9 (l) H2O 0.1 outs... [0] outlet phases: ('g', 'l'), T: 798.63 K, P: 1e+07 Pa flow (kmol/hr): (g) H2O 1 >>> K.results() Isentropic compressor Units K2 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 9.8 Flow kmol/hr 0.000305 Cost USD/hr 9.66e-05 Design Ideal power kW 5.42 Ideal duty kJ/hr 0 Type Centrifugal Compressors in parallel 1 Driver Steam turbine Driver efficiency 0.65 Purchase cost Compressor(s) USD 4.98e+04 Total purchase cost USD 4.98e+04 Utility cost USD/hr 9.66e-05 >>> K.results() Isentropic compressor Units K2 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 9.8 Flow kmol/hr 0.000305 Cost USD/hr 9.66e-05 Design Ideal power kW 5.42 Ideal duty kJ/hr 0 Type Centrifugal Compressors in parallel 1 Driver Steam turbine Driver efficiency 0.65 Purchase cost Compressor(s) USD 4.98e+04 Total purchase cost USD 4.98e+04 Utility cost USD/hr 9.66e-05 >>> K.results() Isentropic compressor Units K2 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 9.8 Flow kmol/hr 0.000305 Cost USD/hr 9.66e-05 Design Ideal power kW 5.42 Ideal duty kJ/hr 0 Type Centrifugal Compressors in parallel 1 Driver Steam turbine Driver efficiency 0.65 Purchase cost Compressor(s) USD 4.98e+04 Total purchase cost USD 4.98e+04 Utility cost USD/hr 9.66e-05 Per default, the outlet phase is assumed to be the same as the inlet phase. If phase changes are to be accounted for, set `vle=True`: >>> import biosteam as bst >>> bst.settings.set_thermo(["H2O"]) >>> feed = bst.MultiStream('feed', T=372.75, P=1e5, l=[('H2O', 0.1)], g=[('H2O', 0.9)]) >>> K = bst.units.IsentropicCompressor('K2', ins=feed, outs='outlet', P=100e5, eta=1.0, vle=True) >>> K.simulate() >>> K.show() IsentropicCompressor: K2 ins... [0] feed phases: ('g', 'l'), T: 372.75 K, P: 100000 Pa flow (kmol/hr): (g) H2O 0.9 (l) H2O 0.1 outs... [0] outlet phases: ('g', 'l'), T: 798.63 K, P: 1e+07 Pa flow (kmol/hr): (g) H2O 1 >>> K.results() Isentropic compressor Units K2 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 9.8 Flow kmol/hr 0.000305 Cost USD/hr 9.66e-05 Design Ideal power kW 5.42 Ideal duty kJ/hr 0 Type Centrifugal Compressors in parallel 1 Driver Steam turbine Driver efficiency 0.65 Purchase cost Compressor(s) USD 4.98e+04 Total purchase cost USD 4.98e+04 Utility cost USD/hr 9.66e-05 """ _energy_variable = 'T' def _run(self): feed = self.ins[0] out = self.outs[0] out.copy_like(feed) out.P = self.P out.S = feed.S if self.vle is True: out.vle(S=out.S, P=out.P) T_isentropic = out.T else: T_isentropic = out.T self.T_isentropic = T_isentropic dH_isentropic = out.H - feed.H self.design_results['Ideal power'] = dH_isentropic / 3600. # kW self.design_results['Ideal duty'] = 0. dH_actual = dH_isentropic / self.eta out.H = feed.H + dH_actual if self.vle is True: out.vle(H=out.H, P=out.P) if self.system and self.system.algorithm == 'Phenomena oriented': self._coeffs = {self: feed.T, feed.source: out.T} # dT_out = (T_out/T_in) * dT_in def _design(self): super()._design() self._set_power(self.design_results['Ideal power'] / self.eta) def _get_energy_departure_coefficient(self, stream): feed = self.ins[0] source = feed.source if source is None or source._energy_variable != 'T': return None return (self, -stream.C) def _update_nonlinearities(self): feed = self.ins[0] product = self.outs[0] if len(product.phases) > 1: raise NotImplementedError('energy departure equation with multiple phase not yet implemented for isentropic compressors') source = feed.source if source is None or source._energy_variable != 'T': return [] data = product.get_data() self._run() self._coeffs = {self: feed.T, source: product.T} # dT_out = (T_out/T_in) * dT_in product.set_data(data) def _create_energy_departure_equations(self): # Special case where T_out = f(T_in) # 0 = Cp * log(T/T0) - R * log(P/P0) # log(T/T0) = R / Cp * log(P/P0) # T = T0 * exp(R / Cp * log(P/P0)) # T = T0 * P/P0 * exp(R/Cp) feed = self.ins[0] product = self.outs[0] if len(product.phases) > 1: raise NotImplementedError('energy departure equation with multiple phase not yet implemented for isentropic compressors') source = feed.source # TODO: add method <Stream>.energy_variable -> 'T'|'B' if source is None or source._energy_variable != 'T': return [] return [(self._coeffs, 0)] def _create_material_balance_equations(self, composition_sensitive): fresh_inlets, process_inlets, equations = self._begin_equations(composition_sensitive) outlet, = self.outs if len(outlet.phases) > 1: raise NotImplementedError('energy departure equation with multiple phase not yet implemented for isentropic compressors') ones = np.ones(self.chemicals.size) minus_ones = -ones zeros = np.zeros(self.chemicals.size) # Overall flows eq_overall = {outlet: ones} for i in process_inlets: eq_overall[i] = minus_ones equations.append( (eq_overall, sum([i.mol for i in fresh_inlets], zeros)) ) return equations def _update_energy_variable(self, departure): self.outs[0].T += departure
[docs] class PolytropicCompressor(Compressor, new_graphics=False): """ Create a polytropic compressor. Parameters ---------- ins : Inlet fluid. outs : Outlet fluid. P : float Outlet pressure [Pa]. eta : float Polytropic efficiency. vle : bool Whether to perform phase equilibrium calculations on the outflow. If False, the outlet will be assumed to be the same phase as the inlet. method: str 'schultz'/'hundseid'. Calculation method for polytropic work. 'hundseid' is recommend for real gases at high pressure ratios. n_steps: int Number of virtual steps used in numerical integration for hundseid method. compressor_type: str Type of compressor : blower/centrifugal/reciprocating. If None, the type will be determined automatically. Notes ----- Default compressor selection, design and cost algorithms are adapted from [2]_. Examples -------- Simulate polytropic compression of hydrogen with 70% efficiency using the Schultz method [3]_: >>> import biosteam as bst >>> thermo = bst.Thermo([bst.Chemical('H2')]) >>> thermo.mixture.include_excess_energies = True >>> bst.settings.set_thermo(thermo) >>> feed = bst.Stream('feed', H2=1, T=25 + 273.15, P=20e5, phase='g') >>> K = bst.units.PolytropicCompressor('K1', ins=feed, outs='outlet', P=350e5, eta=0.7, method='schultz') >>> K.simulate() >>> K.show(T='degC:.3g') PolytropicCompressor: K1 ins... [0] feed phase: 'g', T: 25 degC, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 713 degC, P: 3.5e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Polytropic compressor Units K1 Electricity Power kW 6.76 Cost USD/hr 0.529 Design Polytropic work 2.07e+04 Type Reciprocating Compressors in parallel 1 Driver Electric motor Driver efficiency 0.85 Purchase cost Compressor(s) USD 1.62e+03 Total purchase cost USD 1.62e+03 Utility cost USD/hr 0.529 Repeat using Hundseid method [4]_: >>> K = bst.units.PolytropicCompressor('K1', ins=feed, outs='outlet', P=350e5, eta=0.7, method='hundseid', n_steps=200) >>> K.simulate() >>> K.show() PolytropicCompressor: K1 ins... [0] feed phase: 'g', T: 298.15 K, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 958.07 K, P: 3.5e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Polytropic compressor Units K1 Electricity Power kW 6.48 Cost USD/hr 0.507 Design Polytropic work 1.98e+04 Type Reciprocating Compressors in parallel 1 Driver Electric motor Driver efficiency 0.85 Purchase cost Compressor(s) USD 1.54e+03 Total purchase cost USD 1.54e+03 Utility cost USD/hr 0.507 >>> K.results() Polytropic compressor Units K1 Electricity Power kW 6.48 Cost USD/hr 0.507 Design Polytropic work 1.98e+04 Type Reciprocating Compressors in parallel 1 Driver Electric motor Driver efficiency 0.85 Purchase cost Compressor(s) USD 1.54e+03 Total purchase cost USD 1.54e+03 Utility cost USD/hr 0.507 Repeat using Hundseid method [4]_: >>> K = bst.units.PolytropicCompressor('K1', ins=feed, outs='outlet', P=350e5, eta=0.7, method='hundseid', n_steps=200) >>> K.simulate() >>> K.show() PolytropicCompressor: K1 ins... [0] feed phase: 'g', T: 298.15 K, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 958.07 K, P: 3.5e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Polytropic compressor Units K1 Electricity Power kW 6.48 Cost USD/hr 0.507 Design Polytropic work 1.98e+04 Type Reciprocating Compressors in parallel 1 Driver Electric motor Driver efficiency 0.85 Purchase cost Compressor(s) USD 1.54e+03 Total purchase cost USD 1.54e+03 Utility cost USD/hr 0.507 """ available_methods = {'schultz', 'hundseid'} def _init(self, P, eta=0.7, vle=False, compressor_type=None, method=None, n_steps=None): Compressor._init(self, P=P, eta=eta, vle=vle, compressor_type=compressor_type) self.method = "schultz" if method is None else method self.n_steps = 100 if n_steps is None else n_steps @property def method(self): return self._method @method.setter def method(self, method): method = method.lower() if method not in self.available_methods: raise ValueError( f"method {repr(method)} not available; " f"only {list_available_names(self.available_methods)} are available" ) self._method = method def _schultz(self): # calculate polytropic work using Schultz method feed = self.ins[0] out = self.outs[0] # calculate polytropic exponent and real gas correction factor out.P = self.P out.S = feed.S k = log(out.P / feed.P) / log(feed.V / out.V) n_1_n = (k - 1) / k # n: polytropic exponent W_poly = feed.P * feed.V / n_1_n * ((out.P/feed.P)**n_1_n - 1) # kJ/kmol W_isen = (out.H - feed.H) # kJ/kmol f = W_isen/W_poly # f: correction factor for real gases # calculate non-reversible polytropic work (accounting for eta) n_1_n = n_1_n / self.eta W_actual = f * feed.P * feed.V / n_1_n * ((out.P/feed.P)**n_1_n - 1) / self.eta * out.F_mol # kJ/kmol -> kJ/hr # calculate outlet state out.H = feed.H + W_actual # kJ/hr return W_actual # kJ/hr def _hundseid(self): # calculate polytropic work using Hundseid method feed = self.ins[0].copy() out = self.outs[0] n_steps = self.n_steps pr = (self.P / feed.P) ** (1 / n_steps) # pressure ratio between discrete steps W_actual = 0 for i in range(n_steps): # isentropic pressure change out.P = feed.P * pr out.S = feed.S dH_isen_i = out.H - feed.H # efficiency correction dH_i = dH_isen_i / self.eta out.H = feed.H + dH_i W_actual += dH_i # kJ/hr # next step feed.P = out.P feed.T = out.T return W_actual # kJ/hr def _run(self): feed = self.ins[0] out = self.outs[0] out.copy_like(feed) name = '_' + self._method method = getattr(self, name) self.design_results['Polytropic work'] = method() # Polytropic work [kJ/hr] if self.vle is True: out.vle(H=out.H, P=out.P) def _design(self): super()._design() self._set_power(self.design_results['Polytropic work'] / 3600) # kJ/hr -> kW
[docs] class MultistageCompressor(Unit): """ Create a multistage compressor. Models multistage polytropic or isentropic compression with intermittent cooling. There are two setup options: * Option 1: Define `pr` and `n_stages` (optionally `eta`, `vle`, `type`). Creates `n_stages` identical isentropic compressors. Each compressor is followed by a heat exchanger, which cools the effluent to inlet temperature. * Option 2: Define `compressors` and `hxs`. Takes a list of pre-defined compressors and heat exchangers and connects them in series. This option allows more flexibility in terms of the type of compressor (isentropic/polytropic) and parameterization (e.g. each stage can have different efficiencies and outlet temperatures). Parameters ---------- ins : Inlet fluid. outs : Outlet fluid. pr : float (setup option 1) Pressure ratio between isentropic stages. n_stages: float (setup option 1) Number of isentropic stages. eta : float (setup option 1) Isentropic efficiency. vle : bool (setup option 1) Whether to perform phase equilibrium calculations on the outflow of each stage. If False, the outlet will be assumed to be the same phase as the inlet. compressor_type: str (setup option 1) Type of compressor : blower/centrifugal/reciprocating. If None, the type will be determined automatically. compressors: list[_CompressorBase] (setup option 2) List of compressors to use for each stage. hxs: list[HX] (setup option 2) List of heat exchangers to use for each stage. Notes ----- Default compressor selection, design and cost algorithms are adapted from [2]_. Examples -------- Simulate multistage compression of gaseous hydrogen (simple setup). Hydrogen is compressed isentropically (with an efficiency of 70%) from 20 bar to 320 bar in four stages (pressure ratio of two in each stage): >>> import biosteam as bst >>> thermo = bst.Thermo([bst.Chemical('H2')]) >>> thermo.mixture.include_excess_energies = True >>> bst.settings.set_thermo(thermo) >>> feed = bst.Stream('feed', H2=1, T=298.15, P=20e5, phase='g') >>> K = bst.units.MultistageCompressor('K', ins=feed, outs='outlet', pr=2, n_stages=4, eta=0.7) >>> K.simulate() >>> K.show() MultistageCompressor: K ins... [0] feed phase: 'g', T: 298.15 K, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 298.15 K, P: 3.2e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Multistage compressor Units K Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 5.68 Flow kmol/hr 0.000177 Cost USD/hr 5.6e-05 Chilled water Duty kJ/hr -1.12e+04 Flow kmol/hr 7.42 Cost USD/hr 0.0559 Design Type Multistage compressor Area ft^2 1.54 Purchase cost K k1 - Compressor(s) USD 1.45e+04 K h1 - Double pipe USD 568 K k2 - Compressor(s) USD 1.46e+04 K h2 - Double pipe USD 675 K k3 - Compressor(s) USD 1.48e+04 K h3 - Double pipe USD 956 K k4 - Compressor(s) USD 1.52e+04 K h4 - Double pipe USD 1.78e+03 Total purchase cost USD 6.3e+04 Utility cost USD/hr 0.056 Show the fluid state at the outlet of each heat exchanger: >>> for hx in K.hxs: ... hx.outs[0].show() Stream: K_H1__K_K2 from <HXutility: K_H1> to <IsentropicCompressor: K_K2> phase: 'g', T: 298.15 K, P: 4e+06 Pa flow (kmol/hr): H2 1 Stream: K_H2__K_K3 from <HXutility: K_H2> to <IsentropicCompressor: K_K3> phase: 'g', T: 298.15 K, P: 8e+06 Pa flow (kmol/hr): H2 1 Stream: K_H3__K_K4 from <HXutility: K_H3> to <IsentropicCompressor: K_K4> phase: 'g', T: 298.15 K, P: 1.6e+07 Pa flow (kmol/hr): H2 1 Stream: outlet from <MultistageCompressor: K> phase: 'g', T: 298.15 K, P: 3.2e+07 Pa flow (kmol/hr): H2 1 If we want to setup more complex multistage compression schemes, we can pre-define the compressors and heat exchangers and pass them as a list to `MultistageCompressor`: >>> ks = [ ... bst.units.IsentropicCompressor(P=30e5, eta=0.6), ... bst.units.PolytropicCompressor(P=50e5, eta=0.65), ... bst.units.IsentropicCompressor(P=90e5, eta=0.70), ... bst.units.PolytropicCompressor(P=170e5, eta=0.75), ... bst.units.IsentropicCompressor(P=320e5, eta=0.80), ... ] >>> hxs = [bst.units.HXutility(T=T) for T in [310, 350, 400, 350, 298]] >>> K = bst.units.MultistageCompressor('K2', ins=feed, outs='outlet', compressors=ks, hxs=hxs) >>> K.simulate() >>> K.show() MultistageCompressor: K2 ins... [0] feed phase: 'g', T: 298.15 K, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 298 K, P: 3.2e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Multistage compressor Units K2 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 6.48 Flow kmol/hr 0.000202 Cost USD/hr 6.39e-05 Chilled water Duty kJ/hr -5.63e+03 Flow kmol/hr 3.73 Cost USD/hr 0.0282 Cooling water Duty kJ/hr -7.15e+03 Flow kmol/hr 4.88 Cost USD/hr 0.00238 Design Type Multistage compressor Area ft^2 1.14 Purchase cost K2 k1 - Compressor(s) USD 1.11e+04 K2 h1 - Double pipe USD 297 K2 k2 - Compressor(s) USD 1.3e+04 K2 h2 - Double pipe USD 199 K2 k3 - Compressor(s) USD 1.44e+04 K2 h3 - Double pipe USD 129 K2 k4 - Compressor(s) USD 1.64e+04 K2 h4 - Double pipe USD 753 K2 k5 - Compressor(s) USD 1.45e+04 K2 h5 - Double pipe USD 2.03e+03 Total purchase cost USD 7.27e+04 Utility cost USD/hr 0.0306 Show the fluid state at the outlet of each heat exchanger: >>> for hx in K.hxs: ... hx.outs[0].show() Stream: K2_H1__K2_K2 from <HXutility: K2_H1> to <PolytropicCompressor: K2_K2> phase: 'g', T: 310 K, P: 3e+06 Pa flow (kmol/hr): H2 1 Stream: K2_H2__K2_K3 from <HXutility: K2_H2> to <IsentropicCompressor: K2_K3> phase: 'g', T: 350 K, P: 5e+06 Pa flow (kmol/hr): H2 1 Stream: K2_H3__K2_K4 from <HXutility: K2_H3> to <PolytropicCompressor: K2_K4> phase: 'g', T: 400 K, P: 9e+06 Pa flow (kmol/hr): H2 1 Stream: K2_H4__K2_K5 from <HXutility: K2_H4> to <IsentropicCompressor: K2_K5> phase: 'g', T: 350 K, P: 1.7e+07 Pa flow (kmol/hr): H2 1 Stream: outlet from <MultistageCompressor: K2> phase: 'g', T: 298 K, P: 3.2e+07 Pa flow (kmol/hr): H2 1 Show the fluid state at the outlet of each heat exchanger: >>> for hx in K.hxs: ... hx.outs[0].show() Stream: K2_H1__K2_K2 from <HXutility: K2_H1> to <PolytropicCompressor: K2_K2> phase: 'g', T: 310 K, P: 3e+06 Pa flow (kmol/hr): H2 1 Stream: K2_H2__K2_K3 from <HXutility: K2_H2> to <IsentropicCompressor: K2_K3> phase: 'g', T: 350 K, P: 5e+06 Pa flow (kmol/hr): H2 1 Stream: K2_H3__K2_K4 from <HXutility: K2_H3> to <PolytropicCompressor: K2_K4> phase: 'g', T: 400 K, P: 9e+06 Pa flow (kmol/hr): H2 1 Stream: K2_H4__K2_K5 from <HXutility: K2_H4> to <IsentropicCompressor: K2_K5> phase: 'g', T: 350 K, P: 1.7e+07 Pa flow (kmol/hr): H2 1 Stream: outlet from <MultistageCompressor: K2> phase: 'g', T: 298 K, P: 3.2e+07 Pa flow (kmol/hr): H2 1 If we want to setup more complex multistage compression schemes, we can pre-define the compressors and heat exchangers and pass them as a list to `MultistageCompressor`: >>> ks = [ ... bst.units.IsentropicCompressor(P=30e5, eta=0.6), ... bst.units.PolytropicCompressor(P=50e5, eta=0.65), ... bst.units.IsentropicCompressor(P=90e5, eta=0.70), ... bst.units.PolytropicCompressor(P=170e5, eta=0.75), ... bst.units.IsentropicCompressor(P=320e5, eta=0.80), ... ] >>> hxs = [bst.units.HXutility(T=T) for T in [310, 350, 400, 350, 298]] >>> K = bst.units.MultistageCompressor('K2', ins=feed, outs='outlet', compressors=ks, hxs=hxs) >>> K.simulate() >>> K.show() MultistageCompressor: K2 ins... [0] feed phase: 'g', T: 298.15 K, P: 2e+06 Pa flow (kmol/hr): H2 1 outs... [0] outlet phase: 'g', T: 298 K, P: 3.2e+07 Pa flow (kmol/hr): H2 1 >>> K.results() Multistage compressor Units K2 Electricity Power kW 0 Cost USD/hr 0 High pressure steam Duty kJ/hr 6.48 Flow kmol/hr 0.000202 Cost USD/hr 6.39e-05 Chilled water Duty kJ/hr -5.63e+03 Flow kmol/hr 3.73 Cost USD/hr 0.0282 Cooling water Duty kJ/hr -7.15e+03 Flow kmol/hr 4.88 Cost USD/hr 0.00238 Design Type Multistage compressor Area ft^2 1.14 Purchase cost K2 k1 - Compressor(s) USD 1.11e+04 K2 h1 - Double pipe USD 297 K2 k2 - Compressor(s) USD 1.3e+04 K2 h2 - Double pipe USD 199 K2 k3 - Compressor(s) USD 1.44e+04 K2 h3 - Double pipe USD 129 K2 k4 - Compressor(s) USD 1.64e+04 K2 h4 - Double pipe USD 753 K2 k5 - Compressor(s) USD 1.45e+04 K2 h5 - Double pipe USD 2.03e+03 Total purchase cost USD 7.27e+04 Utility cost USD/hr 0.0306 """ _N_ins = 1 _N_outs = 1 _units = { **Compressor._units, **HX._units, } def _init( self, pr=None, n_stages=None, eta=0.7, vle=False, compressor_type=None, compressors=None, hxs=None, ): # setup option 1: list of compressors and list of heat exchangers if compressors is not None and hxs is not None: if not isinstance(compressors[0], Compressor): print(compressors[0].__class__) raise RuntimeError(f"invalid parameterization of {self.ID}: `compressors` must " f"be a list of compressor objects.") elif not isinstance(hxs[0], bst.HX): raise RuntimeError(f"invalid parameterization of {self.ID}: `hxd` must " f"be a list of heat exchanger objects.") elif len(compressors) != len(hxs): raise RuntimeError(f"invalid parameterization of {self.ID}: `compressors` and `hxs` " f"must have the same length.") else: self.compressors = tuple(compressors) self.hxs = tuple(hxs) self.pr = None self.n_stages = None # setup option 2: fixed pressure ratio and number of stages elif pr is not None and n_stages is not None: self.pr = pr self.n_stages = n_stages self.eta = eta self.vle=vle self.compressor_type=compressor_type self.compressors = None self.hxs = None else: raise RuntimeError(f"invalid parameterization of {self.ID}: Must specify `pr` and " f"`n_stages` or `compressors` and `hxs`.") self._old_specifications = None def _overwrite_subcomponent_id(self, subcomponent, i_stage): # overwrite subcomponent id ID = f"{self.ID}_{subcomponent.ticket_name}{i_stage}" subcomponent.ID = ID # overwrite inlet id if not multistage inlet if i_stage == 1 and isinstance(subcomponent, Compressor): pass else: subcomponent.ins[0].ID = f"{subcomponent.ins[0].ID}__{ID}" # overwrite outlet id if not multistage outlet if i_stage == (self.n_stages or len(self.compressors)) and isinstance(subcomponent, bst.HX): pass else: subcomponent.outs[0].ID = f"{ID}" def reset_cache(self, **kwargs): super().reset_cache(**kwargs) self._old_specifications = None def _setup(self): super()._setup() feed = self._ins[0] compressors = self.compressors hxs = self.hxs pr = self.pr n_stages = self.n_stages specifications = (pr, n_stages, compressors, hxs) if (specifications == self._old_specifications): return # Skip setup (already done) # setup option 1: rewire compressors and heat exchangers if pr is None and n_stages is None: last_hx = None for n, (c, hx) in enumerate(zip(compressors, hxs)): if last_hx is not None: c.ins[0] = last_hx.outs[0] hx.ins[0] = c.outs[0] last_hx = hx self._overwrite_subcomponent_id(c, n+1) self._overwrite_subcomponent_id(hx, n+1) # setup option 2: create connected compressor and hx objects else: T = feed.T compressors = [] hxs = [] # Temporarily register all units/streams in this flowsheet # (instead of the main flowsheet) to prevent system creation problems self.flowsheet = bst.Flowsheet('Multistage_compressor_' + self.ID) with self.flowsheet.temporary(): hx = None; P = feed.P for n in range(self.n_stages): inflow = hx.outs[0] if hx else None P *= pr c = IsentropicCompressor( ins=inflow, P=P, eta=self.eta, vle=self.vle, compressor_type=self.compressor_type ) self._overwrite_subcomponent_id(c, n+1) hx = bst.HXutility( ins=c.outs[0], T=T, rigorous=self.vle ) self._overwrite_subcomponent_id(hx, n+1) compressors.append(c) hxs.append(hx) self.compressors = compressors = tuple(compressors) self.hxs = hxs = tuple(hxs) # set inlet and outlet reference compressors[0]._ins = self._ins hxs[-1]._outs = self._outs # set auxillary units units = [u for t in zip(compressors, hxs) for u in t] self.auxiliary_unit_names = tuple([u.ID for u in units]) for u in units: self.__setattr__(u.ID, u) self._old_specifications = (pr, n_stages, compressors, hxs) def _run(self): # calculate results # helper variables units = [u for t in zip(self.compressors, self.hxs) for u in t] # simulate all subcomponents for u in units: u._setup() u._run() def _design(self): self.design_results["Type"] = "Multistage compressor" # design all subcomponents units = [u for t in zip(self.compressors, self.hxs) for u in t] for u in units: u._summary() # sum up design values sum_fields = [ "Power", "Duty", "Area", "Tube side pressure drop", "Shell side pressure drop" ] for u in units: for k,v in u.design_results.items(): if k in sum_fields: if k in self.design_results: self.design_results[k] += v else: self.design_results[k] = v