Source code for biosteam.units._pump

# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, 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.
"""
"""
import numpy as np
from .design_tools.mechanical import (
    calculate_NPSH,
    pump_efficiency, 
    nearest_NEMA_motor_size,
    nema_sizes_hp,
    electric_motor_cost
)
from .design_tools.specification_factors import (
    pump_material_factors,
    pump_centrifugal_factors)
from .._unit import Unit
from ..utils import static_flow_and_phase
from math import ceil
from warnings import warn
import biosteam as bst

__all__ = ('Pump',)

ln = np.log
exp = np.exp

# %% Data

max_hp = nema_sizes_hp[-1]
pump_types = ('Default', 'CentrifugalSingle', 'CentrifugalDouble', 'Gear')
    

# %% Classes

# TODO: Fix pump selection to include NPSH available and required.
[docs] @static_flow_and_phase class Pump(Unit): """ Create a pump that sets the pressure of the outlet. Parameters ---------- ins : Inlet. outs : Outlet. P=101325 : float, optional Pressure of output stream (Pa). If the pressure of the outlet is the same as the inlet, pump is designed to increase of pressure to `dP_design`. pump_type='Default' : str If 'Default', a pump type is selected based on heuristics. material : str, optional Contruction material of pump. Defaults to 'Cast iron'. dP_design=101325 : float Design pressure change for pump. ignore_NPSH=True : bool Whether to take into consideration NPSH in the selection of a pump type. Notes ----- Default pump selection and design and cost algorithms are based on [1]_. Examples -------- Simulate Pump for pressure increase: >>> from biosteam import Stream, settings >>> from biosteam.units import Pump >>> settings.set_thermo(['Water', 'Ethanol'], cache=True) >>> feed = Stream('feed', Water=200, T=350) >>> P1 = Pump('P1', ins=feed, outs='out', P=2e5) >>> P1.simulate() >>> P1.show() Pump: P1 ins... [0] feed phase: 'l', T: 350 K, P: 101325 Pa flow (kmol/hr): Water 200 outs... [0] out phase: 'l', T: 350 K, P: 200000 Pa flow (kmol/hr): Water 200 >>> P1.results() Pump Units P1 Electricity Power kW 0.288 Cost USD/hr 0.0225 Design Type Centrifugal Ideal power hp 0.136 Flow rate gpm 16.3 Efficiency 0.352 Power hp 0.386 Head ft 96.3 Motor size hp 0.5 Purchase cost Pump USD 4.37e+03 Motor USD 273 Total purchase cost USD 4.64e+03 Utility cost USD/hr 0.0225 References ---------- .. [1] Seider, Warren D., et al. (2017). "Cost Accounting and Capital Cost Estimation". In Product and Process Design Principles: Synthesis, Analysis, and Evaluation (pp. 450-455). New York: Wiley. """ _units = {'Ideal power': 'hp', 'Motor size': 'hp', 'Power': 'hp', 'Head': 'ft', 'NPSH': 'ft', 'Flow rate': 'gpm'} _F_BM_default = {'Pump': 3.3, 'Motor': 3.3} @property def pump_type(self): """Pump type""" return self._pump_type @pump_type.setter def pump_type(self, pump_type): if pump_type not in pump_types: raise ValueError('Type must be one of the following: {", ".join(pump_types)}') self._pump_type = pump_type @property def material(self): """Pump material""" return self._material @material.setter def material(self, material): try: self.F_M['Pump'] = pump_material_factors[material] except KeyError: raise ValueError("material must be one of the following: " f"{', '.join(pump_material_factors)}") self._material = material def _init(self, P=None, pump_type='Default', material='Cast iron', dP_design=101325, ignore_NPSH=True ): self.P = P self.pump_type = pump_type self.material = material self.dP_design = dP_design self.ignore_NPSH = ignore_NPSH def _run(self): s_in, = self.ins s_out, = self.outs s_out.copy_like(s_in) if self.P: s_out.P = self.P def _design(self): Design = self.design_results si, = self.ins so, = self.outs if si.isempty(): self.design_results.clear() return Pi = si.P Po = so.P Qi = si.F_vol mass = si.F_mass nu = si.nu dP = Po - Pi if dP < 1: dP = self.dP_design power_ideal = Qi*dP*3.725e-7 # hp q = Qi*4.403 # gpm N = 1 # Note: # A pump motor can have maximally `max_hp` power, so need to first solve # for how many pumps with maximally `max_hp` are needed. # Assume 100% efficiency to estimate the number of pumps. # Additionally, pump types have their own maximum size. N_guess = -1 while N != N_guess: N_guess = N power_ideal_i = power_ideal / N_guess q_i = q / N_guess efficiency = pump_efficiency(q_i, power_ideal_i) power_i = power_ideal_i / efficiency head_i = N * power_i / mass * 897806 # ft # Note that: # head = power / (mass * gravity) # head [ft] = power[hp]/mass[kg/hr]/9.81[m/s^2] * conversion_factor # and 897806 = (conversion_factor/9.81) if self.ignore_NPSH: NPSH_satisfied = True else: Design['NPSH'] = NPSH = calculate_NPSH(Pi, si.P_vapor, si.rho) NPSH_satisfied = NPSH > 1.52 pump_type = self.pump_type if pump_type == 'Default': if (15.24 <= head_i <= 3200 and nu <= 0.00002 and NPSH_satisfied): pump_type = 'Centrifugal' elif (head_i <= 914.4 and 0.00001 <= nu <= 0.252): N = max(ceil(q_i / 1500), N) pump_type = 'Gear' elif (head_i <= 20000 # and power_i <= 200 and 1 <= power_i <= 200 and nu <= 0.01): N = max(ceil(q_i / 500), N) pump_type = 'MeteringPlunger' else: NPSH = calculate_NPSH(Pi, si.P_vapor, si.rho) warn(f'{repr(self)} no pump type available at current power ' f'({power_i:.3g} hp), head ({head_i:.3g} ft), kinematic ' f'viscosity ({nu:.3g} m2/s), and NPSH ({NPSH:.3g} ft); ' 'assuming centrigugal pump', RuntimeWarning) pump_type = 'Centrifugal' Design['Type'] = pump_type Design['Ideal power'] = power_ideal_i = power_ideal / N Design['Flow rate'] = q_i = q / N_guess Design['Efficiency'] = pump_efficiency(q_i, power_ideal_i) Design['Power'] = power_i = power_ideal_i / efficiency Design['Head'] = N * power_i / mass * 897806 # ft Design['Motor size'] = motor_rating = nearest_NEMA_motor_size(power_i) self.add_power_utility(power_i / 1.341) # Add power for individual pump in kW self.parallel['self'] = N # BioSTEAM will multiply all costs (both capital and utility) by this number self.parallel['Motor'] = N * ceil(power_i / motor_rating) # Multiple motors per pump is posible def _cost(self): Design = self.design_results if not Design: return Cost = self.baseline_purchase_costs pump_type = Design['Type'] q = Design['Flow rate'] h = Design['Head'] p = Design['Power'] I = bst.CE/567 # TODO: Add cost equation for small pumps # Head and flow rate is too small, so make conservative estimate on cost if q < 50: q = 50 if h < 50: h = 50 F_T = 1 # Assumption # Cost pump if 'Centrifugal' in pump_type: # Find pump factor F_Tdict = pump_centrifugal_factors if p < 75 and 50 <= q <= 900 and 50 <= h <= 400: F_T = F_Tdict['VSC3600'] elif p < 200 and 50 <= q <= 3500 and 50 <= h <= 2000: F_T = F_Tdict['VSC1800'] elif p < 150 and 100 <= q <= 1500 and 100 <= h <= 450: F_T = F_Tdict['HSC3600'] elif p < 250 and 250 <= q <= 5000 and 50 <= h <= 500: F_T = F_Tdict['HSC1800'] elif p < 250 and 50 <= q <= 1100 and 300 <= h <= 1100: F_T = F_Tdict['2HSC3600'] elif p < 1450 and 100 <= q <= 1500 and 650 <= h <= 3200: F_T = F_Tdict['2+HSC3600'] elif p < 1450 and 1500 <= q <= 5000 and 650 <= h <= 3200: # TODO: This flow rate is allowable in the design section # but not in the estimation of purchase cost. # For now, we ignore this problem, but additional code on # selecting number of pumps should be added. F_T = F_Tdict['2+HSC3600'] else: # TODO: This flow rate is allowable in the design section # but not in the estimation of purchase cost. # For now, we ignore this problem, but additional code on # selecting number of pumps should be added. F_T = F_Tdict['2+HSC3600'] # raise NotImplementedError(f'no centrifugal pump available at current power ({p:.3g} hp), flow rate ({q:.3g} gpm), and head ({h:.3g} ft)') S = q*h**0.5 # Size factor S_new = S if S > 400 else 400 lnS = ln(S_new) Cb = exp(12.1656-1.1448*lnS+0.0862*lnS**2) Cb *= S/S_new Cost['Pump'] = Cb*I elif pump_type == 'Gear': q_new = q if q > 50 else 50 lnq = ln(q_new) Cb = exp(8.2816 - 0.2918*lnq + 0.0743*lnq**2) Cb *= q/q_new Cost['Pump'] = Cb*I elif pump_type == 'MeteringPlunger': lnp = ln(p) lnp2 = lnp * lnp Cb = exp(7.9361 + 0.26986*lnp + 0.06718*lnp2) Cost['Pump'] = Cb*I self.F_D['Pump'] = F_T # Cost electric motor Cost['Motor'] = electric_motor_cost(p)