diff --git a/biosteam/_system.py b/biosteam/_system.py index badf7ab5c..64df128b3 100644 --- a/biosteam/_system.py +++ b/biosteam/_system.py @@ -4650,23 +4650,6 @@ def save_report( reactions = report.unit_reaction_tables(units) report.tables_to_excel(reactions, writer, 'Reactions') - if 'Specifications' in sheets: - if stage: - with self.stage_configuration() as conf: - specifications = [ - u._mass_and_energy_balance_specifications_table() - for u in conf.stages - ] - else: - specifications = [ - u._mass_and_energy_balance_specifications_table() - for u in units - ] - report.tables_to_excel( - specifications, writer, - 'Specifications' - ) - if 'Water mass balance' in sheets: water_mass_balance = report.water_mass_balance_table(self) report.tables_to_excel( diff --git a/biosteam/_unit.py b/biosteam/_unit.py index 48a7305d0..b9ece2896 100644 --- a/biosteam/_unit.py +++ b/biosteam/_unit.py @@ -26,7 +26,7 @@ from thermosteam.base import display_asfunctor from flexsolve import IQ_interpolation from numpy.typing import NDArray -from typing import Callable, Optional, TYPE_CHECKING, Sequence, Iterable, Tuple, Any +from typing import Callable, Optional, TYPE_CHECKING, Sequence, Iterable, Tuple, Any, Container from thermosteam.base.sparse import SparseVector, sum_sparse_vectors import numpy as np import thermosteam as tmo @@ -38,7 +38,7 @@ streams = Optional[Sequence[Stream|str]] -__all__ = ('Unit', 'ProcessSpecification', 'BoundedNumericalSpecification') +__all__ = ('Unit', 'design_input', 'ProcessSpecification', 'BoundedNumericalSpecification') int_types = (int, np.int32) @@ -125,46 +125,337 @@ def __init__(self, port, source=None): self.port = port self._source = source +# %% Defining design inputs + +def design_input( + name: Optional[str]=None, + units: Optional[str]=None, + *, + default: Optional[float|str]=NotImplemented, + lb: Optional[float]=None, + ub: Optional[float]=None, + catalog: Optional[Container[str]]=None, + purchase_item: Optional[str]=None, + catalog_type: Optional[str]=None, + fget: Optional[Callable]=None, + fset: Optional[Callable]=None, + fdel: Optional[Callable]=None, + doc: Optional[str]=None, + ): + """ + Return a design input descriptor. Design inputs behave like + property objects (and are documented as such by Sphinx), but also + validate values and are reported in :meth:`Unit.results`. -# %% Unit Operation + Parameters + ---------- + name : + Name of design input. Defaults to the attribute name. + units : + Units of measure for numerical design input. + default : + Default value applied when design input is set to None. + lb : + Lower bound for numerical design input. + ub : + Upper bound for numerical design input. + catalog : + Valid options for categorical design input. If a dictionary, + values are the cost factors of each option. + purchase_item : + Name of purchase cost item to apply cost factors. + catalog_type : + Type of cost factor: 'Material', 'Design', or 'Module'. + fget : + Getter. Defaults to returning the value stored. + fset : + Setter (called after validation). Defaults to storing the value. + fdel : + Deleter. + doc : + Docstring. Defaults to getter's docstring or " [].", if none available. -def phenomena_based_run(self): - if not (self._recycle_system and self._system.algorithm == 'Phenomena based'): - Unit.run(self) - return - ins = self.ins - outs = self.outs - Ts = [i.T for i in outs] - Q = self.Hnet - Unit.run(self) - new = sum( - [i.mol for i in outs], - -sum([i.mol for i in ins], 0) - ) - if hasattr(self, '_dmol'): - old = self._dmol - f = bst.PhasePartition.dmol_relaxation_factor - self._dmol = dmol = f * old + (1 - f) * new + Examples + -------- + >>> import biosteam as bst + >>> class Vessel(bst.Unit): + ... tau = bst.design_input('Residence time', 'hr', lb=0) + ... orientation = bst.design_input(catalog={'Vertical', 'Horizontal'}, default='Vertical') + ... + ... @bst.design_input(units='m3', lb=0) + ... def volume(self): + ... return 2 * self.tau + ... + ... @volume.setter + ... def volume(self, volume): + ... self.tau = volume / 2 + ... + + >>> Vessel.tau + NumericalDesignInput('tau', 'Residence time', 'hr', lb=0) + >>> Vessel.orientation + CategoricalDesignInput('orientation', default='Vertical', catalog={'Horizontal', 'Vertical'}) + >>> Vessel.volume + NumericalDesignInput('volume', 'm3', lb=0) + + """ + if catalog is None: + if catalog_type is not None: + raise ValueError('cannot define catalog_type without catalog') + if purchase_item is not None: + raise ValueError('cannot define purchase_item without catalog') + if units is None: + units = '' + return NumericalDesignInput(name, units, default, lb, ub, fget, fset, fdel, doc) else: - self._dmol = dmol = new - self._dmol[np.abs(dmol) < 1e-9] = 0. - self._duty = self.Hnet - Ts_new = [i.T for i in outs] - if all([i == j for i, j in zip(Ts_new, Ts)]): # T constant - self._energy_variable = None - elif self.Hnet == Q: # Q constant - self._energy_variable = 'T' + N_none = (catalog_type is None) + (purchase_item is None) + if N_none == 0: + catalog_type = catalog_type.capitalize() + if catalog_type == 'Material': + catalog_type = 'F_M' + elif catalog_type == 'Design': + catalog_type = 'F_D' + elif catalog_type == 'Module': + catalog_type = 'F_BM' + else: + raise ValueError(f"{catalog_type!r} is not a recognised catalog_type ; only 'Material', 'Design', or 'Module' are valid") + reference = (purchase_item, catalog_type) + elif N_none == 2: + reference = None + else: + raise ValueError('must define both catalog_type and purchase_item, or none of them; not either') + if units is not None: + raise ValueError('units cannot be defined together with catalog') + if lb is not None: + raise ValueError('lower bound (lb) cannot be defined together with catalog') + if ub is not None: + raise ValueError('upper bound (ub) cannot be defined together with catalog') + return CategoricalDesignInput(name, default, catalog, reference, fget, fset, fdel, doc) + + +def _get_class_annotation(owner, key): + try: + import annotationlib + except ImportError: # Python < 3.14 + annotations = owner.__dict__.get('__annotations__', {}) else: - self._energy_variable = None + try: + annotations = annotationlib.get_annotations( + owner, format=annotationlib.Format.FORWARDREF + ) + except Exception: + return None + return annotations.get(key) + + +class DesignInput(property): + """ + Abstract class that represent design inputs as property objects + which store values at "_" by default. + + Subclasses must implement a `_copy(self, fget, fset, fdel)`, + `validate(self, value)` and a `__init__(self, ...)` method. + + """ + + def __set_name__(self, owner, key): + self.key = key + if self.name is None: self.name = key.replace('_', ' ').capitalize() + default = self.default + if default is not NotImplemented: self.default = default = self.validate(default) + doc = self.__doc__ + if doc is None: + units = self.units + if default is NotImplemented: + default_note = '' + elif units: + default_note = f' Defaults to {default!r} [{units}].' + else: + default_note = f' Defaults to {default!r}.' + self.__doc__ = doc = f"{self.name} [{units}].{default_note}" if units else f"{self.name}.{default_note}" + fset = self.fset + fget = self.fget + N_none = (fget is None) + (fset is None) + if N_none == 2: + # Document like a property with a getter named after the attribute + attr = '_' + key + fget = lambda unit: getattr(unit, attr, None) + fget._ = None + fget.__name__ = self.__name__ = key + fget.__qualname__ = f"{owner.__qualname__}.{key}" + fget.__module__ = owner.__module__ + fget.__doc__ = doc + annotation = _get_class_annotation(owner, key) + if annotation is None: annotation = self._default_annotation + fget.__annotations__ = {'return': annotation} + fset = lambda unit, value: setattr(unit, attr, value) + super().__init__(fget, fset, self.fdel, doc) + elif N_none: + raise RuntimeError('design inputs must implement both getter and setter methods') + else: + fget = self.fget + try: + annotations = fget.__annotations__ + except AttributeError: # Not a function (e.g., a callable object) + pass + else: + if 'return' not in annotations: + annotations['return'] = self._default_annotation + + def __call__(self, fget): + # Used as a decorator right after creation (e.g., @design_input(...)), + # so the getter is set in place instead of creating a copy + property.__init__(self, fget, self.fset, self.fdel, self.__doc__) + return self + + def getter(self, fget): + return self._copy(fget, self.fset, self.fdel) + + def setter(self, fset): + return self._copy(None if hasattr(self.fget, '_') else self.fget, fset, self.fdel) + + def deleter(self, fdel): + return self._copy(None if hasattr(self.fget, '_') else self.fget, self.fset, fdel) + + def __delete__(self, unit): + if self.fdel is None: raise AttributeError(f'cannot delete design input {self.key!r}') + self.fdel(unit) + + +class NumericalDesignInput(DesignInput): + _default_annotation = float + + def __init__(self, name, units, default, lb, ub, fget, fset, fdel, doc): + self.name = name + self.units = units + self.default = default + self.lb = lb + self.ub = ub + super().__init__(fget, fset, fdel, doc) + + def validate(self, value): + if self.lb is not None and value < self.lb: + units = self.units + units = ' ' + units if units else '' + try: + msg = f"{self.key} ({value:.4g}{units}) cannot be below {self.lb:.4g}{units}" + except: # Handle format errors + msg = f"{self.key} ({value}{units}) cannot be below {self.lb}{units}" + elif self.ub is not None and value > self.ub: + units = self.units + units = ' ' + units if units else '' + try: + msg = f"{self.key} ({value:.4g}{units}) cannot be above {self.ub:.4g}{units}" + except: # Handle format errors + msg = f"{self.key} ({value}{units}) cannot be above {self.ub}{units}" + else: + return value + raise AttributeError(msg) + + def _copy(self, fget, fset, fdel): + return NumericalDesignInput(self.name, self.units, self.default, self.lb, self.ub, fget, fset, fdel, self.__doc__) + + def __set__(self, unit, value): + self.validate(value) + self.fset(unit, value) + + def __repr__(self): + name = '' if self.name == self.key.capitalize().replace('_', ' ') else f', {self.name!r}' + default = '' if self.default is None else f', default={self.default!r}' + units = f', {self.units!r}' if self.units else '' + lb = '' if self.lb is None else f', lb={self.lb}' + ub = '' if self.ub is None else f', ub={self.ub}' + return f"{type(self).__name__}({self.key!r}{name}{units}{default}{lb}{ub})" + + +class CategoricalDesignInput(DesignInput): + units = '' + _default_annotation = str + + def __init__(self, name, default, catalog, reference, fget, fset, fdel, doc): + self.name = name + self.default = default + self.catalog = catalog + self.reference = reference + super().__init__(fget, fset, fdel, doc) + + def validate(self, value): + catalog = self.catalog + if value not in catalog: + try: + # Assume value is a string + value = value.capitalize() + except: + # If its not (e.g., could be an int), that is fine + pass + else: + if value in catalog: return value + N = len(catalog) + if N > 2: + *names, last = list(catalog) + names = ', '.join([repr(i) for i in names]) + ', and ' + repr(last) + msg = f"{value!r} is not a recognised {self.key}; only {names} are valid" + elif N == 2: + left, right = catalog + msg = f"{value!r} is not a recognised {self.key}; only {left!r} and {right!r} are valid" + elif N == 1: + name = list(catalog)[0] + msg = f"{value!r} is not a recognised {self.key}; only {name} is valid" + else: + msg = f"no valid {self.key} available" + else: + return value + raise AttributeError(msg) + + def _copy(self, fget, fset, fdel): + return CategoricalDesignInput(self.name, self.default, self.catalog, self.reference, fget, fset, fdel, self.__doc__) + + def __set__(self, unit, value): + if value is None: value = self.default + if value is None: + if self.reference is not None: + item, name = self.reference + getattr(unit, name).pop(item, None) + self.fset(unit, value) + return + value = self.validate(value) + if self.reference is not None: + item, name = self.reference + getattr(unit, name)[item] = self.catalog[value] + self.fset(unit, value) + + def __repr__(self): + name = '' if self.name == self.key.capitalize().replace('_', ' ') else f', {self.name!r}' + catalog = self.catalog + if isinstance(catalog, (set, frozenset)): + catalog = ', catalog={' + str(sorted(catalog))[1:-1] + '}' + elif catalog is None: + catalog = '' + else: + catalog = f', catalog={catalog}' + reference = '' if self.reference is None else f', reference={self.reference}' + default = '' if self.default is None else f', default={self.default!r}' + return f"{type(self).__name__}({self.key!r}{name}{default}{catalog}{reference})" + + +# %% Unit Operation @thermo_user @registered(ticket_name='U') class Unit: """ - Abstract class for Unit objects. Child objects must contain + Abstract class for Unit objects. Subclasses must define :attr:`~Unit._run`, :attr:`~Unit._design` and :attr:`~Unit._cost` methods to estimate stream outlets of a Unit and find design and cost information. - + + For advanced phenomena based simulation, subclasses may define + :attr:`~Unit._update_nonlinearities`, + :attr:`~Unit._create_bulk_balance_equations`, + :attr:`~Unit._create_material_balance_equations`, + :attr:`~Unit._create_energy_balance_equations`, + :attr:`~Unit._update_energy_coefficient`, + and :attr:`~Unit._update_variable` methods. + Parameters ---------- ID : @@ -228,26 +519,13 @@ class Unit: """ - #: Class for initiallizing streams. - Stream = Stream - max_parallel_units = int(10e3) - - #: Whether unit leverages information from other units within the system - #: regardless of whether or not they are directly connected. - _universal = False - - #: Whether streams do not mix, but do interact. - _interaction = False - - #: Class for initiallizing missing streams. - MissingStream = MissingStream + _design_inputs = [] - #: Initialize unit operation with key-word arguments. - _init = AbstractMethod + ### Abstract Attributes ### - #: Run mass and energy balances and update outlet streams (without user-defined specifications). - _run = AbstractMethod + #: **class-attribute** Units of measure for :attr:`~Unit.design_results` dictionary. + _units: dict[str, str] = {} #: **class-attribute** Expected number of inlet streams. Defaults to 1. _N_ins: int = 1 @@ -261,13 +539,31 @@ class Unit: #: **class-attribute** Whether the number of streams in :attr:`~Unit.outs` is fixed. _outs_size_is_fixed: bool = True - #: **class-attribute** Used for piping warnings. - _stacklevel: int = 6 - #: **class-attribute** Name denoting the type of Unit class. Defaults to the class #: name of the first child class line: str = 'Unit' + #: **class-attribute** Default bare-module factors for each purchase cost item. + #: Items in this dictionary are copied to the :attr:`~Unit.F_BM` attribute during + #: initialization. + _F_BM_default: dict[str, float] = {} + + #: **class-attribute** Lifetime of equipment. Defaults to lifetime of + #: production venture. Use an integer to specify the lifetime for all + #: items in the unit purchase costs. Use a dictionary to specify the + #: lifetime of each purchase cost item. + _default_equipment_lifetime: int|dict[str, int] = {} + + #: **class-attribute** Name of attributes that are auxiliary units. These units + #: will be accounted for in the purchase and installed equipment costs + #: without having to add these costs in the :attr:`~Unit.baseline_purchase_costs` dictionary. + #: Heat and power utilities are also automatically accounted for. + auxiliary_unit_names: tuple[str, ...] = () + + #: **class-attribute** Cost items that need to be summed across operation modes for + #: flexible operation (e.g., filtration membranes). + _materials_and_maintenance: frozenset[str] = frozenset() + #: **class-attribute** Settings for diagram representation. Defaults to a #: box with the same number of inlet and outlet edges as :attr:`~Unit._N_ins` #: and :attr:`~Unit._N_outs`. @@ -279,61 +575,86 @@ class Unit: #: and all outlet streams are emptied. _skip_simulation_when_inlets_are_empty = False - #: **class-attribute** Name of attributes that are auxiliary units. These units - #: will be accounted for in the purchase and installed equipment costs - #: without having to add these costs in the :attr:`~Unit.baseline_purchase_costs` dictionary. - #: Heat and power utilities are also automatically accounted for. - auxiliary_unit_names: tuple[str, ...] = () + #: **class-attribute** Whether to link inlet and outlet streams. + _link_streams: bool = False + #: Whether unit leverages information from other units within the system + #: regardless of whether or not they are directly connected. + _universal = False + + #: Whether streams do not mix, but do interact. + _interaction = False + #: **class-attribute** Index for auxiliary inlets to parent unit for graphviz diagram settings. _auxin_index = {} #: **class-attribute** Index for auxiliary outlets to parent unit for graphviz diagram settings. _auxout_index = {} + + #: **class-attribute** Used for piping warnings. + _stacklevel: int = 6 + + ### Abstract methods ### + + #: Create auxiliary components. + _load_components = AbstractMethod + + #: Add design requirements to the :attr:`~Unit.design_results` dictionary. + _design = AbstractMethod + + #: Add itemized purchase costs to the :attr:`~Unit.baseline_purchase_costs` dictionary. + _cost = AbstractMethod + #: Add embodied emissions (e.g., unit construction) in LCA. + _lca = AbstractMethod + + #: Initialize unit operation with key-word arguments. + _init = AbstractMethod + + #: Run mass and energy balances and update outlet streams (without user-defined specifications). + _run = AbstractMethod + def __init_subclass__(cls, isabstract=False, new_graphics=True, does_nothing=None, - default_phenomena=None, ): dct = cls.__dict__ + # Collect design inputs through the class history (including mixins); + # attributes that override an inherited design input remove it. + design_input_names = set() + design_inputs = [] + for base in reversed(cls.__mro__): + for key, obj in base.__dict__.items(): + if isinstance(obj, DesignInput): + design_input_names.add(key) + if obj.key is None: obj.__set_name__(base, key) + design_inputs.append(obj) + elif key in design_inputs: + design_input_names.remove(key) + cls._design_inputs = [i.key for i in design_inputs] # Preserve order + + # Extend annotation if '__init__' in dct and '_init' not in dct: init = dct['__init__'] - if hasattr(init, 'extension'): cls._init = init.extension + if hasattr(init, 'extension'): + cls._init = init.extension elif dct.get('_init'): _init = dct['_init'] - cls.__init__ = extended_signature(cls.__init__, _init) - cls.__init__.extension = _init + cls.__init__ = extended_signature(cls.__init__, _init, design_inputs) if '__init__' in dct or '_init' in dct: init = dct['__init__'] annotations = init.__annotations__ for i in ('ins', 'outs'): if i not in annotations: annotations[i] = streams if '_stacklevel' not in dct: cls._stacklevel += 1 + if does_nothing: return dct = cls.__dict__ if 'run' in dct: raise UnitInheritanceError( "the 'run' method cannot be overridden; implement `_run` instead" ) - if default_phenomena is None or default_phenomena: - methods = ( - '_update_nonlinearities', - '_update_energy_coefficient', - '_update_variable', - '_create_material_balance_equations', - '_create_energy_balance_equations', - '_create_bulk_balance_equations', - '_energy_variable', - ) - if all([getattr(cls, i) is getattr(Unit, i) for i in methods]): - cls.run = phenomena_based_run - else: - for i in methods: - if getattr(cls, i) is not getattr(Unit, i): continue - setattr(cls, i, AbstractMethod) - cls.run = Unit.run if '_N_heat_utilities' in dct: warn("'_N_heat_utilities' class attribute is scheduled for deprecation; " "use the `add_heat_utility` method instead", @@ -366,31 +687,12 @@ def __init_subclass__(cls, # Set new graphics for specified line cls._graphics = UnitGraphics.box(cls._N_ins, cls._N_outs) if not isabstract: - if hasattr(cls, '_BM'): - raise UnitInheritanceError( - 'cannot set `_BM`; implement `_F_BM_default` instead' - ) - elif hasattr(cls, '_F_BM_defaults'): - raise UnitInheritanceError( - 'cannot set `_F_BM_defaults`; implement ' - '`_F_BM_default` instead' - ) - elif cls._F_BM_default is Unit._F_BM_default: + if cls._F_BM_default is Unit._F_BM_default: cls._F_BM_default = {} - - if hasattr(cls, '_equipment_lifetime'): - raise UnitInheritanceError( - 'cannot set `_equipment_lifetime`; ' - 'implement `_default_equipment_lifetime` instead' - ) - elif hasattr(cls, '_default_equipment_lifetimes'): - raise UnitInheritanceError( - 'cannot set `_default_equipment_lifetimes`; implement ' - '`_default_equipment_lifetime` instead' - ) - elif cls._default_equipment_lifetime is Unit._default_equipment_lifetime: + if cls._default_equipment_lifetime is Unit._default_equipment_lifetime: cls._default_equipment_lifetime = {} - if cls._units is Unit._units: cls._units = {} + if cls._units is Unit._units: + cls._units = {} if not cls._run: if cls._N_ins == 1 and cls._N_outs == 1: static(cls) @@ -410,45 +712,6 @@ def __init_subclass__(cls, bst.units.__dict__[name] = cls if name not in bst.__dict__: bst.__dict__[name] = cls - - ### Abstract Attributes ### - #: **class-attribute** Units of measure for :attr:`~Unit.design_results` dictionary. - _units: dict[str, str] = {} - - #: **class-attribute** Default bare-module factors for each purchase cost item. - #: Items in this dictionary are copied to the :attr:`~Unit.F_BM` attribute during - #: initialization. - _F_BM_default: dict[str, float] = {} - - #: **class-attribute** Cost items that need to be summed across operation modes for - #: flexible operation (e.g., filtration membranes). - _materials_and_maintenance: frozenset[str] = frozenset() - - #: **class-attribute** Whether to link inlet and outlet streams. - _link_streams: bool = False - - #: **class-attribute** Lifetime of equipment. Defaults to lifetime of - #: production venture. Use an integer to specify the lifetime for all - #: items in the unit purchase costs. Use a dictionary to specify the - #: lifetime of each purchase cost item. - _default_equipment_lifetime: int|dict[str, int] = {} - - #: [str] The energy variable for phenomena-based simulation. - _energy_variable: str = None - - ### Abstract methods ### - - #: Create auxiliary components. - _load_components = AbstractMethod - - #: Add design requirements to the :attr:`~Unit.design_results` dictionary. - _design = AbstractMethod - - #: Add itemized purchase costs to the :attr:`~Unit.baseline_purchase_costs` dictionary. - _cost = AbstractMethod - - #: Add embodied emissions (e.g., unit construction) in LCA. - _lca = AbstractMethod def _collect_variables(self, key): self._system_collect_variables((key, self.ID)) @@ -621,7 +884,7 @@ def _create_energy_balance_equations(self): phenomena-based simulation. """ fresh_inlets, process_inlets, equations = self._begin_energy_equations() - if self._energy_variable == 'T': + if self.specified_variable == 'Q': dmol = self._dmol if dmol.any(): dH = self._duty + sum([i.Hf for i in self.outs]) - sum([i.Hf for i in self.ins]) @@ -775,9 +1038,6 @@ def _begin_energy_equations(self): process_inlets.append(i) return fresh_inlets, process_inlets, equations - Inlets = piping.Inlets - Outlets = piping.Outlets - def __init__(self, ID: Optional[str]='', ins: streams=None, @@ -901,17 +1161,29 @@ def __init__(self, #: Auxiliary unit operation names. self.auxiliary_unit_names = list(self.auxiliary_unit_names) + # Set defaults for design results. + cls = type(self) + for i in self._design_inputs: + if i in kwargs: + setattr(self, i, kwargs.pop(i)) + else: + value = getattr(cls, i).default + if value is NotImplemented: continue + setattr(self, i, value) + self._init(**kwargs) self._assert_compatible_property_package() + __init__._init_inputs = frozenset() + def _init_inlets(self, ins): - self._ins = self.Inlets( + self._ins = piping.Inlets( self, self._N_ins, ins, self._thermo, self._ins_size_is_fixed, self._stacklevel, ) def _init_outlets(self, outs, **kwargs): - self._outs = self.Outlets( + self._outs = piping.Outlets( self, self._N_outs, outs, self._thermo, self._outs_size_is_fixed, self._stacklevel, ) @@ -1095,7 +1367,7 @@ def get_nested_auxiliary_units_with_names(self, depth=-1) -> list[Unit]: def _unit_auxins(self, N_streams, streams, thermo): if streams is None or streams == (): - return [self.auxin(self.Stream(None, thermo=thermo)) for i in range(N_streams)] + return [self.auxin(Stream(None, thermo=thermo)) for i in range(N_streams)] elif isinstance(streams, (Stream, MissingStream)) or streams.__class__ is str: return self.auxin(streams, thermo=thermo) else: @@ -1103,7 +1375,7 @@ def _unit_auxins(self, N_streams, streams, thermo): def _unit_auxouts(self, N_streams, streams, thermo): if streams is None or streams == (): - return [self.auxout(self.Stream(None, thermo=thermo)) for i in range(N_streams)] + return [self.auxout(Stream(None, thermo=thermo)) for i in range(N_streams)] elif isinstance(streams, (Stream, MissingStream)) or streams.__class__ is str: return self.auxout(streams, thermo=thermo) else: @@ -1175,7 +1447,6 @@ def auxlet(self, stream: Stream, thermo=None): from this unit. """ - Stream = self.Stream if thermo is None: thermo = self.thermo if stream is None: stream = Stream(None, thermo=thermo) @@ -1209,7 +1480,6 @@ def auxin(self, stream: Stream, thermo=None): from this unit. """ - Stream = self.Stream if thermo is None: thermo = self.thermo if stream is None: stream = Stream(None, thermo=thermo) @@ -1240,7 +1510,6 @@ def auxout(self, stream: Stream, thermo=None): from this unit. """ - Stream = self.Stream if thermo is None: thermo = self.thermo if stream is None: stream = Stream(None, thermo=thermo) @@ -2328,6 +2597,88 @@ def _get_tooltip_string(self, format=None, full=None): if format == 'html': tooltip = ' ' + tooltip return tooltip + def get_design_input_units(self, key): + design_input = getattr(self.__class__, key) + return design_input.units + + def get_design_input(self, key: str, units: str): + """ + Return design input in a new set of units of measure. + + Parameters + ---------- + key : + Name of design input. + units : + Units of measure. + + Examples + -------- + >>> import biosteam as bst + >>> bst.settings.set_thermo(['Water'], cache=True) + >>> feed = bst.Stream(None, Water=100) + >>> tank = bst.StorageTank(None, feed, tau=672) + >>> tank.get_design_input('tau', 'day') + 28.0 + + """ + return convert(getattr(self, key), self.get_design_input_units(key), units) + + def set_design_input(self, key: str, units: str, value: float): + """ + Set design result in given the units of measure. + + Parameters + ---------- + key : + Name of design input. + units : + Units of measure. + value: + Value of the design input. + + Examples + -------- + >>> import biosteam as bst + >>> bst.settings.set_thermo(['Water'], cache=True) + >>> feed = bst.Stream(None, Water=100) + >>> tank = bst.StorageTank(None, feed) + >>> tank.set_design_input('tau', 'day', 7) + 168 + >>> tank.get_design_input('tau', 'hr') + 168 + + """ + value = convert(value, units, self.get_design_input_units(key)) + setattr(self, key, value) + return value + + def convert_design_input(self, key, units, value): + """ + Convert design input in given units to the stored units of measure. + + Parameters + ---------- + key : + Name of design input. + units : + Units of measure. + value: + Value of the design input. + + Examples + -------- + >>> import biosteam as bst + >>> bst.settings.set_thermo(['Water'], cache=True) + >>> feed = bst.Stream(None, Water=100) + >>> tank = bst.StorageTank(None, feed) + >>> tank.simulate() + >>> tank.convert_design_input('tau', 'day', 7) + 168 + + """ + return convert(value, units, self.get_design_input_units(key)) + def get_design_result(self, key: str, units: str): """ Return design result in a new set of units of measure. @@ -2354,7 +2705,6 @@ def get_design_result(self, key: str, units: str): """ return convert(self.design_results[key], self._units[key], units) - def set_design_result(self, key: str, units: str, value: float): """ Set design result in given the units of measure. @@ -2413,8 +2763,17 @@ def convert_design_result(self, key, units, value): def reset_cache(self, isdynamic=None): pass - def _get_design_info(self): - return () + def _get_design_input_info(self): + cls = self.__class__ + design_inputs = [] + for key in self._design_inputs: + value = getattr(self, key) + if value is None: continue # Decision not made + dd = getattr(cls, key) + design_inputs.append( + (dd.name, value, dd.units) + ) + return design_inputs def _load_stream_links(self): if self._link_streams: self._outs[0].link_with(self._ins[0]) @@ -2571,26 +2930,16 @@ def simulate(self, self.run() self._summary(design_kwargs, cost_kwargs) - def _mass_and_energy_balance_specifications(self): - return (self.line, ()) - - def _mass_and_energy_balance_specifications_table(self): - title, specs = self._mass_and_energy_balance_specifications() - index = [] - values = [] - for name, value, units in specs: - index.append(name) - values.append([value, units]) - df = pd.DataFrame( - values, index, ('Values', 'Units') - ) - df.columns.name = f"{title} - {self.ID}" - return df - - def results(self, with_units=True, include_utilities=True, - include_total_cost=True, include_installed_cost=True, - include_zeros=True, external_utilities=None, key_hook=None, - basis=None): + def results(self, + with_units=True, + include_utilities=True, + include_total_cost=True, + include_installed_cost=True, + include_zeros=True, + external_utilities=None, + key_hook=None, + basis=None + ): """ Return key results from simulation as a DataFrame if `with_units` is True or as a Series otherwise. @@ -2666,32 +3015,26 @@ def addcapex(key): ureg = tmo.units_of_measure.ureg ureg.default_system = basis Quantity = ureg.Quantity + def to_basis(vi, ui): + if not ui or ui == 'hr' or isinstance(vi, str): return ui, vi + try: + q = Quantity(vi, ui).to_base_units() + except Exception: # Units not defined in registry (e.g., psig) + return ui, vi + return q.units, q.magnitude + for ki, vi, ui in self._get_design_input_info(): + addkey(('Design inputs', ki)) + addval(to_basis(vi, ui)) for ki, vi in self.design_results.items(): - addkey(('Design', ki)) - if ki in units: - ui = units[ki] - if ui != 'hr': - q = Quantity(vi, ui) - q = q.to_base_units() - vi = q.magnitude - ui = q.units - else: - ui = '' - addval((ui, vi)) - for ki, vi, ui in self._get_design_info(): - addkey(('Design', ki)) - q = Quantity(vi, ui) - q = q.to_base_units() - vi = q.magnitude - ui = q.units - addval((ui, vi)) + addkey(('Design results', ki)) + addval(to_basis(vi, units.get(ki, ''))) else: + for ki, vi, ui in self._get_design_input_info(): + addkey(('Design inputs', ki)) + addval((ui, vi)) for ki, vi in self.design_results.items(): - addkey(('Design', ki)) + addkey(('Design results', ki)) addval((units.get(ki, ''), vi)) - for ki, vi, ui in self._get_design_info(): - addkey(('Design', ki)) - addval((ui, vi)) for ki, vi in Cost.items(): addcapex(('Purchase cost', ki)) addval(('USD', vi)) @@ -2744,12 +3087,11 @@ def addcapex(key): addval(flow) addkey((ID, 'Cost')) addval(-flow * stream_prices[name]) - - for ki, vi in self.design_results.items(): - addkey(('Design', ki)) + for ki, vi, ui in self._get_design_input_info(): + addkey(('Design inputs', ki)) addval(vi) - for ki, vi, ui in self._get_design_info(): - addkey(('Design', ki)) + for ki, vi in self.design_results.items(): + addkey(('Design results', ki)) addval(vi) for ki, vi in self.purchase_costs.items(): addcapex(('Purchase cost', ki)) diff --git a/biosteam/exceptions.py b/biosteam/exceptions.py index d4b6e364b..dbe8b54a9 100644 --- a/biosteam/exceptions.py +++ b/biosteam/exceptions.py @@ -15,8 +15,8 @@ 'DesignError', 'Converged', 'FailedScenario', + 'DesignWarning', 'UnitWarning', - 'DesignWarning', 'CostWarning', 'lb_warning', 'ub_warning', @@ -52,7 +52,7 @@ def __init__(self, exception, parameters, sample): f"[{type(exception).__name__}] {exception}" ) -class UnitWarning(Warning): +class DesignWarning(Warning): """Warning regarding unit operations.""" @classmethod @@ -60,12 +60,9 @@ def from_source(cls, source, msg): """Return a DesignWarning object with source description.""" msg = message_with_object_stamp(source, msg) return cls(msg) - -class DesignWarning(UnitWarning): - """Warning regarding design constraints.""" - -class CostWarning(UnitWarning): - """Warning regarding design constraints.""" + +# Backwards-compatible aliases +UnitWarning = CostWarning = DesignWarning # %% Bounds checking @@ -77,22 +74,22 @@ def design_warning_with_source(source, msg): # pragma: no cover def lb_warning(source, key, value, units, lb, stacklevel=2): # pragma: no cover units = ' ' + units if units else '' try: - msg = f"{key} ({value:.4g}{units}) is out of bounds (minimum {lb:.4g}{units})." + msg = f"{key} ({value:.4g}{units}) is out of recommended bounds (minimum {lb:.4g}{units})." except: # Handle format errors - msg = f"{key} ({value:.4g}{units}) is out of bounds (minimum {lb}{units})." + msg = f"{key} ({value:.4g}{units}) is out of recommended bounds (minimum {lb}{units})." warn(DesignWarning.from_source(source, msg), stacklevel=stacklevel) def ub_warning(source, key, value, units, ub, stacklevel=2): # pragma: no cover units = ' ' + units if units else '' try: - msg = f"{key} ({value:.4g}{units}) is out of bounds (maximum {ub:.4g}{units})." + msg = f"{key} ({value:.4g}{units}) is out of recommended bounds (maximum {ub:.4g}{units})." except: # Handle format errors - msg = f"{key} ({value:.4g}{units}) is out of bounds (maximum {ub}{units})." + msg = f"{key} ({value:.4g}{units}) is out of recommended bounds (maximum {ub}{units})." warn(DesignWarning.from_source(source, msg), stacklevel=stacklevel) -def bounds_warning(source, key, value, units, bounds, kind='design'): # pragma: no cover +def bounds_warning(source, key, value, units, bounds): # pragma: no cover """Issue a warning if value is out of bounds. Parameters @@ -113,16 +110,9 @@ def bounds_warning(source, key, value, units, bounds, kind='design'): # pragma: if not (lb <= value <= ub): units = ' ' + units if units else '' try: - msg = f"{key} ({value:.4g}{units}) is out of bounds ({lb:.4g} to {ub:.4g}{units})" + msg = f"{key} ({value:.4g}{units}) is out of recommended bounds ({lb:.4g} to {ub:.4g}{units})" except: # Handle format errors - msg = f"{key} ({value:.4g}{units}) is out of bounds ({lb} to {ub}{units})" - if kind == 'design': - Warning = DesignWarning - msg += ' for design algorithm' - elif kind == 'cost': - msg += ' for cost correlation' - Warning = CostWarning - else: - raise ValueError(f"kind must be either 'design' or 'cost', not {repr(kind)}") - warning = Warning.from_source(source, msg) + msg = f"{key} ({value:.4g}{units}) is out of recommended bounds ({lb} to {ub}{units})" + msg += ' for design/costing' + warning = DesignWarning.from_source(source, msg) warn(warning, stacklevel=3) \ No newline at end of file diff --git a/biosteam/units/Gibbs_equilibrium_reactor.py b/biosteam/units/Gibbs_equilibrium_reactor.py index 0d82ffe0b..5073754b7 100644 --- a/biosteam/units/Gibbs_equilibrium_reactor.py +++ b/biosteam/units/Gibbs_equilibrium_reactor.py @@ -7,6 +7,7 @@ """ import biosteam as bst from .abstract_stirred_tank_reactor import AbstractStirredTankReactor +from .._unit import design_input from .design_tools.Gibbs_equilibrium_reaction import minimize_Gibbs_free_energy from typing import Optional, Iterable, Callable @@ -119,25 +120,37 @@ class GibbsEquilibriumReactor(AbstractStirredTankReactor): NO2 11.9 >>> EqR.results() - Equilibrium reactor Units EqR - Low pressure steam Duty kJ/hr 3.58e+05 - Flow kmol/hr 9.24 - Cost USD/hr 2.2 - Design Reactor volume m3 25.1 - Number of reactors 1 - Residence time hr 0.1 - Vessel type Vertical - Length ft 21.6 - Diameter ft 7.22 - Weight lb 3.32e+04 - Wall thickness in 0.705 - Jacketed diameter ft 7.6 - Vessel material Stainless steel 316 - Purchase cost Vertical pressure vessel (jacketed) USD 1.02e+05 - Platform and ladders USD 1.56e+04 - Total purchase cost USD 1.17e+05 - Installed equipment cost USD 4.39e+05 - Utility cost USD/hr 2.2 + Gibbs equilibrium reactor Units EqR + Low pressure steam Duty kJ/hr 3.58e+05 + Flow kmol/hr 9.25 + Cost USD/hr 2.2 + Design inputs Operating pressure Pa 1.01e+06 + Operating temperature K 298 + Maximum reactor volume m3 500 + Working volume fraction 0.8 + Loop temperature change K 5 + Jacket annular diameter m 0.1 + Agitator power kW/m3 0 + Length to diameter ratio 3 + Optimization method differential evolution + Residence time hr 0.1 + Cleaning and unloading time hr 3 + Residence time phases lg + Vessel material Stainless steel 316 + Vessel type Vertical + Design results Reactor volume m3 26.6 + Number of reactors 1 + Residence time hr 0.1 + Length ft 22.1 + Diameter ft 7.36 + Weight lb 3.5e+04 + Wall thickness in 0.716 + Jacketed diameter ft 7.74 + Purchase cost Pressure vessel USD 2.21e+05 + Platform and ladders USD 1.6e+04 + Total purchase cost USD 2.37e+05 + Installed equipment cost USD 5.7e+05 + Utility cost USD/hr 2.2 """ _N_ins = 1 @@ -164,10 +177,15 @@ def _init(self, **kwargs ): AbstractStirredTankReactor._init(self, **kwargs) - self.phases = phases - self.products = products + + #: Phases of products. Defaults to the phases of inlets. + self.phases = phases + + #: Optimization method. Valid options include 'COBYLA' and 'differential evolution'. self.method = self.method_default if method is None else method + self.phase_hook = phase_hook + self.products = products def _get_duty(self): return self.Hnet diff --git a/biosteam/units/_batch_crystallizer.py b/biosteam/units/_batch_crystallizer.py index c073236d9..42a46cbd4 100644 --- a/biosteam/units/_batch_crystallizer.py +++ b/biosteam/units/_batch_crystallizer.py @@ -9,6 +9,7 @@ """ import numpy as np from .. import Unit +from .._unit import design_input from .design_tools import size_batch from .decorators import cost import flexsolve as flx @@ -109,21 +110,24 @@ class BatchCrystallizer(Unit): >>> C1.results() Batch crystallizer Units C1 - Electricity Power kW 2.65 - Cost USD/hr 0.207 + Electricity Power kW 3.54 + Cost USD/hr 0.277 Cooling water Duty kJ/hr -1.69e+06 Flow kmol/hr 8.06e+03 Cost USD/hr 3.93 - Design Crystallizer volume m3 132 - Number of crystallizers 3 - Batch time hr 21 - Loading time hr 0 - Cleaning and unloading time hr 1 + Design inputs Operating temperature K 313 Working volume fraction 0.9 - Purchase cost Crystallizer (x3) USD 1.56e+06 - Total purchase cost USD 1.56e+06 - Installed equipment cost USD 3.13e+06 - Utility cost USD/hr 4.14 + Power per volume kW/gal 0.00746 + Crystallization time hr 20 + Cleaning and unloading time hr 1 + Design results Crystallizer volume m3 132 + Number of crystallizers 4 + Batch time hr 28 + Loading time hr 7 + Purchase cost Crystallizer (x4) USD 2.09e+06 + Total purchase cost USD 2.09e+06 + Installed equipment cost USD 4.17e+06 + Utility cost USD/hr 4.21 References ---------- @@ -159,19 +163,23 @@ class BatchCrystallizer(Unit): _N_ins = _N_outs = 1 - #: [float] Cleaning and unloading time (hr). - tau_0 = 1 - - #: Optional[float] Loading time (hr). - tau_loading = None - - #: [float] Fraction of filled tank to total tank volume. - V_wf = 0.9 - - def _get_design_info(self): - return (('Cleaning and unloading time', self.tau_0, 'hr'), - ('Working volume fraction', self.V_wf, '')) - + tau: float = design_input('Crystallization time', 'hr', lb=0) + tau_0: float = design_input('Cleaning and unloading time', 'hr', lb=0) + tau_loading: float = design_input( + 'Loading time', 'hr', lb=0, + doc="Loading time of batch crystallizer [hr]. If not given, it will " + "assume each vessel is constantly being filled.", + ) + V_wf: float = design_input('Working volume fraction', lb=0, ub=1) + T: float = design_input('Operating temperature', 'K', lb=0) + kW: float = design_input( + 'Power per volume', 'kW/gal', lb=0, + doc="Electricity usage per volume in kW/gal. Defaults to 0.00746, " + "a heuristic value for suspension of solids.", + ) + _tau_0 = 1 # Default cleaning and unloading time [hr] + _V_wf = 0.9 # Default fraction of filled tank to total tank volume + def _init(self, tau=None, N=None, V=None, T=305.15, Nmin=2, Nmax=36, vessel_material='Carbon steel', @@ -179,7 +187,6 @@ def _init(self, ): self._N = N; self._V = V - #: [float] Reaction time [hr]. self.tau = tau #: [int] Number of vessels @@ -188,7 +195,6 @@ def _init(self, #: [float] Target volume of a vessel if V: self.V = V - #: [float] Operating temperature of reactor [K]. self.T = T #: [int] Minimum number of vessels @@ -197,7 +203,6 @@ def _init(self, #: [int] Maximum number of vessels self.Nmax = Nmax - #: [float] Electricity usage per volume in kW/gal self.kW = kW # [str] Vessel material @@ -258,13 +263,6 @@ def V(self, V): assert not self._N, 'cannot specify both crystallizer volume and number of crystallizers' self._V = V - @property - def tau(self): - return self._tau - @tau.setter - def tau(self, tau): - self._tau = tau - def _design(self): effluent = self.effluent v_0 = effluent.F_vol diff --git a/biosteam/units/_carbon_capture.py b/biosteam/units/_carbon_capture.py index 2cd3e3d26..ec68383c5 100755 --- a/biosteam/units/_carbon_capture.py +++ b/biosteam/units/_carbon_capture.py @@ -6,7 +6,7 @@ # github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt # for license details. -from .._unit import Unit +from .._unit import Unit, design_input from .decorators import cost from .design_tools.cost_index import CEPCI_by_year @@ -103,7 +103,10 @@ class AmineAbsorption(Unit): Low pressure steam Duty kJ/hr 4.36e+08 Flow kmol/hr 1.13e+04 Cost USD/hr 2.68e+03 - Design Total flow kmol/hr 2.41e+04 + Design inputs CO2 recovery 0.9 + Net MEA usage kg/t 1.5 + Duty per kg CO2 kJ/kg 3.61e+03 + Design results Total flow kmol/hr 2.41e+04 CO2 flow kmol/hr 2.61e+03 Purchase cost Makeup tank USD 2.5e+05 Cooler USD 9.78e+04 @@ -137,6 +140,12 @@ class AmineAbsorption(Unit): _N_outs = 2 _units = {'Total flow': 'kmol/hr', 'CO2 flow':'kmol/hr'} + CO2_recovery: float = design_input('CO2 recovery', lb=0, ub=1) + MEA_to_CO2: float = design_input( + 'Net MEA usage', 'kg/t', lb=0, + doc="Net usage of MEA (kg pure MEA/metric tonne CO2 captured).", + ) + heat_ratio: float = design_input('Duty per kg CO2', 'kJ/kg') def _init(self, CO2_recovery=0.9, MEA_to_CO2=1.5, heat_ratio=3611): self.CO2_recovery = CO2_recovery @@ -202,7 +211,7 @@ class CO2Compression(Unit): CO2Compression Units U1 Electricity Power kW 360 Cost USD/hr 28.2 - Design Total flow kmol/hr 2.61e+03 + Design results Total flow kmol/hr 2.61e+03 Purchase cost Compressor USD 1.45e+06 Total purchase cost USD 1.45e+06 Installed equipment cost USD 4.8e+06 diff --git a/biosteam/units/_enzyme_treatment.py b/biosteam/units/_enzyme_treatment.py index 077accb6a..7def42faa 100644 --- a/biosteam/units/_enzyme_treatment.py +++ b/biosteam/units/_enzyme_treatment.py @@ -9,6 +9,7 @@ """ from .tank import MixTank from .heat_exchange import HXutility +from .._unit import design_input __all__ = ('EnzymeTreatment',) @@ -16,12 +17,13 @@ class EnzymeTreatment(MixTank): """Create an EnzymeTreatment unit that is cost as a MixTank with a heat exchanger.""" _N_outs = 1 auxiliary_unit_names = ('heat_exchanger',) + T: float = design_input('Operating temperature', 'K', lb=0) def _init(self, vessel_type='Conventional', tau=1.0, V_wf=0.9, vessel_material='Stainless steel', T=None): MixTank._init(self, vessel_type=vessel_type, tau=tau, V_wf=V_wf, vessel_material=vessel_material) - self.T = T #: Operating temperature (if any) + self.T = T self.heat_exchanger = HXutility(None, None, None, T=T, thermo=self.thermo) def _run(self): diff --git a/biosteam/units/_flash.py b/biosteam/units/_flash.py index 1766a7b47..0d03b4741 100644 --- a/biosteam/units/_flash.py +++ b/biosteam/units/_flash.py @@ -8,6 +8,7 @@ """ """ from .. import Unit, PowerUtility +from .._unit import design_input from thermosteam import MultiStream, separations from math import pi import numpy as np @@ -113,22 +114,26 @@ class Flash(design.PressureVessel, Unit): flow (kmol/hr): Water 42.4 Glycerol 298 >>> F1.results() - Flash Units F1 - Medium pressure steam Duty kJ/hr 4.81e+07 - Flow kmol/hr 1.33e+03 - Cost USD/hr 366 - Design Vessel type Horizontal - Length ft 8.46 - Diameter ft 5.5 - Weight lb 2.51e+03 - Wall thickness in 0.312 - Vessel material Carbon steel - Purchase cost Horizontal pressure vessel USD 1.47e+04 - Platform and ladders USD 3.22e+03 - Heat exchanger - Floating head USD 4.48e+04 - Total purchase cost USD 6.26e+04 - Installed equipment cost USD 1.89e+05 - Utility cost USD/hr 366 + Flash Units F1 + Medium pressure steam Duty kJ/hr 4.81e+07 + Flow kmol/hr 1.33e+03 + Cost USD/hr 366 + Design inputs Operating temperature K 410 + Holdup time min 15 + Surge time min 7.5 + Vacuum system preference Liquid-ring pump + Vessel material Carbon steel + Vessel type Horizontal + Design results Length ft 8.46 + Diameter ft 5.5 + Weight lb 2.51e+03 + Wall thickness in 0.312 + Purchase cost Pressure vessel USD 1.47e+04 + Platform and ladders USD 3.22e+03 + Heat exchanger - Floating head USD 4.48e+04 + Total purchase cost USD 6.26e+04 + Installed equipment cost USD 1.89e+05 + Utility cost USD/hr 366 References @@ -166,6 +171,27 @@ class Flash(design.PressureVessel, Unit): **design.PressureVessel._F_BM_default} _graphics = vertical_vessel_graphics _N_outs = 2 + V: float = design_input( + 'Molar vapor fraction', lb=0, ub=1, + ) + T: float = design_input( + 'Operating temperature', 'K', lb=0, + ) + Q: float = design_input( + 'Duty', 'kJ/hr', + ) + vacuum_system_preference: str = design_input( + doc="If a vacuum system is needed, it will choose one according to this " + "preference ('Liquid-ring pump', 'Steam-jet ejector', or 'Dry-vacuum pump').", + ) + holdup_time: float = design_input( + doc="Time it takes to raise liquid to half full [min].", + units='min', lb=0, + ) + surge_time: float = design_input( + doc="Time it takes to reach from normal to maximum liquid level [min].", + units='min', lb=0, + ) def _init(self, V=None, T=None, Q=None, P=None, y=None, x=None, @@ -180,10 +206,7 @@ def _init(self, flash_inlet=True, has_vapor_condenser=None, ): - #: Enforced molar vapor fraction self.V = V - - #: Enforced operating temperature (K) self.T = T #: [array_like] Molar composition of vapor (for binary mixture) @@ -192,7 +215,6 @@ def _init(self, #: [array_like] Molar composition of liquid (for binary mixture) self.x = x - #: Enforced duty (kJ/hr) self.Q = Q #: Operating pressure (Pa) @@ -201,7 +223,6 @@ def _init(self, #: [str] Vessel construction material self.vessel_material = vessel_material - #: [str] If a vacuum system is needed, it will choose one according to this preference. self.vacuum_system_preference = vacuum_system_preference #: [bool] True if glycol groups are present in the mixture @@ -213,10 +234,8 @@ def _init(self, #: [str] 'Horizontal', 'Vertical', or 'Default' self.vessel_type = vessel_type - #: [float] Time it takes to raise liquid to half full (min) self.holdup_time = holdup_time - #: [float] Time it takes to reach from normal to maximum liquied level (min) self.surge_time = surge_time #: [bool] True if using a mist eliminator pad @@ -292,8 +311,9 @@ def _size_flash_vessel(self): if self.no_vessel_needed: self.design_results.clear() else: + if self.vessel_type is None: self.vessel_type = self._default_vessel_type() vessel_type = self.vessel_type - if vessel_type == 'Vertical': + if vessel_type == 'Vertical': args = self._vertical_vessel_pressure_diameter_and_length() elif vessel_type == 'Horizontal': args = self._horizontal_vessel_pressure_diameter_and_length() @@ -530,14 +550,13 @@ def _init(self, split, self.has_vapor_condenser = has_vapor_condenser self._load_components() - self.T = T #: Operating temperature (K) + self.T = T self.P = P #: Operating pressure (Pa) - self.Q = Q #: Duty (kJ/hr) + self.Q = Q #: [str] Vessel construction material self.vessel_material = vessel_material - #: [str] If a vacuum system is needed, it will choose one according to this preference. self.vacuum_system_preference = vacuum_system_preference #: [bool] True if glycol groups are present in the mixture @@ -549,10 +568,8 @@ def _init(self, split, #: [str] 'Horizontal', 'Vertical', or 'Default' self.vessel_type = vessel_type - #: [float] Time it takes to raise liquid to half full (min) self.holdup_time = holdup_time - #: [float] Time it takes to reach from normal to maximum liquied level (min) self.surge_time = surge_time #: [bool] True if using a mist eliminator pad diff --git a/biosteam/units/_multi_effect_evaporator.py b/biosteam/units/_multi_effect_evaporator.py index 5a50be50f..1b7cdd8a6 100644 --- a/biosteam/units/_multi_effect_evaporator.py +++ b/biosteam/units/_multi_effect_evaporator.py @@ -10,6 +10,7 @@ import numpy as np import biosteam as bst from .. import Unit +from .._unit import design_input from .mixing import Mixer from .heat_exchange import HXutility from ._flash import Flash, Evaporator @@ -128,7 +129,8 @@ class MultiEffectEvaporator(Unit): Cooling water Duty kJ/hr -3.5e+05 Flow kmol/hr 239 Cost USD/hr 0.117 - Design Area m^2 11 + Design inputs Molar fraction evaporated 0.1 + Design results Area m^2 11 Volume m^3 3.51 Purchase cost Evaporators (x5) USD 9.56e+03 Condenser - Double pipe USD 5.36e+03 @@ -183,7 +185,8 @@ class MultiEffectEvaporator(Unit): Cooling water Duty kJ/hr -1.15e+05 Flow kmol/hr 78.8 Cost USD/hr 0.0384 - Design Area m^2 1.62 + Design inputs Molar fraction evaporated 0.1 + Design results Area m^2 1.62 Volume m^3 3.07 Purchase cost Evaporators (x3) USD 2.74e+03 Condenser - Double pipe USD 3.9e+03 @@ -238,7 +241,8 @@ class MultiEffectEvaporator(Unit): Cooling water Duty kJ/hr -1.15e+05 Flow kmol/hr 78.8 Cost USD/hr 0.0384 - Design Area m^2 1.62 + Design inputs Molar fraction evaporated 0.1 + Design results Area m^2 1.62 Volume m^3 3.07 Purchase cost Evaporators (x3) USD 2.74e+03 Condenser - Double pipe USD 3.9e+03 @@ -263,6 +267,12 @@ class MultiEffectEvaporator(Unit): # Data for simmulation and costing _evap_data = evaporators[_Type] + + V: float = design_input( + 'Molar fraction evaporated', lb=0, ub=1, + doc="Molar fraction evaporated as specified in `V_definition` " + "(either overall or in the first effect).", + ) @property def Type(self): @@ -297,7 +307,7 @@ def V_definition(self, V_definition): def _init(self, P, V, V_definition='Overall', flash=True, chemical='7732-18-5'): self.P = P #: tuple[float] Pressures describing each evaporator (Pa). - self.V = V #: [float] Molar fraction evaporated. + self.V = V self.V_definition = V_definition self.flash = flash #: [bool] Whether to perform a flash calculation to account for volatile components. self._V_first_effect = None @@ -476,7 +486,7 @@ def _design(self): LMTD = Th - Tc A = compute_heat_transfer_area(LMTD, U, Q, 1.) As.append(A) - bounds_warning(self, 'heat transfer area requirement', A, 'ft2', A_range, 'cost') + bounds_warning(self, 'heat transfer area requirement', A, 'ft2', A_range) evap_costs.append(C_func(A, CE)) self._As = As Design['Area'] = A = sum(As) diff --git a/biosteam/units/_pump.py b/biosteam/units/_pump.py index 7fbb9293b..15c3f1ac2 100644 --- a/biosteam/units/_pump.py +++ b/biosteam/units/_pump.py @@ -88,21 +88,23 @@ class Pump(Unit): 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 - Installed equipment cost USD 1.53e+04 - Utility cost USD/hr 0.0225 + Pump Units P1 + Electricity Power kW 0.288 + Cost USD/hr 0.0225 + Design inputs Outlet pressure Pa 2e+05 + Design pressure change Pa 101325 + Design results 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 + Installed equipment cost USD 1.53e+04 + Utility cost USD/hr 0.0225 References ---------- @@ -119,6 +121,14 @@ class Pump(Unit): 'Flow rate': 'gpm'} _F_BM_default = {'Pump': 3.3, 'Motor': 3.3} + P: float = bst.design_input( + 'Outlet pressure', 'Pa', + doc="Pressure of output stream [Pa]. If None or the same as the inlet, " + "the pump is designed to increase the pressure by `dP_design`.", + ) + dP_design: float = bst.design_input( + 'Design pressure change', 'Pa', lb=0, + ) @property def pump_type(self): diff --git a/biosteam/units/abstract_stirred_tank_reactor.py b/biosteam/units/abstract_stirred_tank_reactor.py index 5430eb249..f34b7cb00 100644 --- a/biosteam/units/abstract_stirred_tank_reactor.py +++ b/biosteam/units/abstract_stirred_tank_reactor.py @@ -17,6 +17,7 @@ """ from .. import Unit +from .._unit import design_input from typing import Optional from math import ceil from biosteam.units.design_tools.geometry import cylinder_diameter_from_volume @@ -184,16 +185,27 @@ class AbstractStirredTankReactor(PressureVessel, Unit, isabstract=True): Chilled water Duty kJ/hr -1.41e+07 Flow kmol/hr 9.42e+03 Cost USD/hr 70.3 - Design Reactor volume m3 319 - Number of reactors 4 + Design inputs Operating pressure Pa 1.01e+05 + Operating temperature K 305 + Maximum reactor volume m3 355 + Working volume fraction 0.8 + Loop temperature change K 5 + Jacket annular diameter m 0.05 + Agitator power kW/m3 0.985 + Length to diameter ratio 3 Residence time hr 8 + Cleaning and unloading time hr 3 + Residence time phases l + Vessel material Stainless steel 316 Vessel type Vertical + Design results Reactor volume m3 319 + Number of reactors 4 + Residence time hr 8 Length ft 50.5 Diameter ft 16.8 Weight lb 5.39e+04 Wall thickness in 0.363 - Vessel material Stainless steel 316 - Purchase cost Vertical pressure vessel (x4) USD 1.18e+06 + Purchase cost Pressure vessel (x4) USD 1.18e+06 Platform and ladders (x4) USD 2.12e+05 Heat exchanger - Floating head (x4) USD 1.61e+05 Recirculation pump - Pump (x4) USD 3.9e+04 @@ -272,6 +284,32 @@ class AbstractStirredTankReactor(PressureVessel, Unit, isabstract=True): #: Basis of residence time. Defaults to inlet flow rates tau_basis: str = 'ins' # Alternatively, 'outs' + T: float = design_input('Operating temperature', 'K', lb=0) + P: float = design_input('Operating pressure', 'Pa', lb=0) + tau: float = design_input('Residence time', 'hr', lb=0) + V_wf: float = design_input('Working volume fraction', lb=0, ub=1) + V_max: float = design_input('Maximum reactor volume', 'm3', lb=0) + length_to_diameter: float = design_input('Length to diameter ratio', lb=0) + kW_per_m3: float = design_input('Agitator power', 'kW/m3', lb=0) + tau_0: float = design_input('Cleaning and unloading time', 'hr', lb=0) + N: int = design_input('Number of reactors') + dT_hx_loop: float = design_input( + 'Loop temperature change', 'K', lb=0, + doc="Maximum change in temperature for the heat exchanger loop [K].", + ) + jacket_annular_diameter: float = design_input( + 'Jacket annular diameter', 'm', lb=0, + ) + tau_phases: str = design_input( + 'Residence time phases', + doc="Phases accounted for in residence time.", + ) + loading_time: float = design_input( + 'Loading time', 'hr', lb=0, + doc="Loading time of batch reactor [hr]. If not given, it will assume " + "each vessel is constantly being filled.", + ) + @property def effluent(self): return self.outs[-1] diff --git a/biosteam/units/adsorption.py b/biosteam/units/adsorption.py index c25d0e66f..2597dedf0 100644 --- a/biosteam/units/adsorption.py +++ b/biosteam/units/adsorption.py @@ -264,23 +264,34 @@ class SingleComponentAdsorptionColumn(PressureVessel, bst.Unit): :alt: Adsorption column fluid concentration profile over time. >>> A1.results() - Single component adsorption column Units A1 - Electricity Power kW 0.241 - Cost USD/hr 0.0188 - Design Diameter ft 1.22 - Length ft 12.2 - Vessel type Vertical - Weight lb 521 - Wall thickness in 0.25 - Pressure drop Pa 103 - Vessel material Stainless steel 316 - Purchase cost Vertical pressure vessel (x3) USD 6.12e+04 - Platform and ladders (x3) USD 8.37e+03 - Pump - Pump (x3) USD 1.31e+04 - Pump - Motor (x3) USD 174 - Total purchase cost USD 8.28e+04 - Installed equipment cost USD 2.05e+05 - Utility cost USD/hr 0.0188 + Single component adsorption column Units A1 + Electricity Power kW 0.241 + Cost USD/hr 0.0188 + Design inputs Scaled final concentration 0.05 + Number of columns 3 + Operating pressure Pa 101325 + Adsorbate Adsorbate + Adsorbent ActivatedCarbon + Cycle time hr 1000 + Mass transfer coefficient 1/hr 0.3 + Particle diameter m 0.004 + Adsorbent density kg/m3 380 + Superficial velocity m/hr 9.2 + Vessel material Stainless steel 316 + Vessel type Vertical + Void fraction 0.525 + Design results Diameter ft 1.22 + Length ft 12.2 + Weight lb 521 + Wall thickness in 0.25 + Pressure drop Pa 103 + Purchase cost Pressure vessel (x3) USD 6.12e+04 + Platform and ladders (x3) USD 8.37e+03 + Pump - Pump (x3) USD 1.31e+04 + Pump - Motor (x3) USD 174 + Total purchase cost USD 8.28e+04 + Installed equipment cost USD 2.05e+05 + Utility cost USD/hr 0.0188 >>> A1.show('wt') SingleComponentAdsorptionColumn: A1 @@ -338,6 +349,53 @@ class SingleComponentAdsorptionColumn(PressureVessel, bst.Unit): 'Langmuir': equilibrium_loading_Langmuir_isotherm, 'Freundlich': equilibrium_loading_Freundlich_isotherm, } + adsorbate: str = bst.design_input() + adsorbent: str = bst.design_input( + doc="Name of adsorbent. Defaults to 'ActivatedCarbon'.", + ) + N_columns: int = bst.design_input( + 'Number of columns', lb=2, ub=3, + doc="Number of columns can be either 2 or 3. The 3-column configuration " + "minimizes cost of adsorbent regeneration. The 2-column configuration " + "minimizes capital cost.", + ) + cycle_time: float = bst.design_input( + 'Cycle time', 'hr', lb=0, + doc="Time at which the receiving vessel is switched [hr]. " + "Note that 1-2 hours required for thermal-swing-adsorption " + "(TSA) for silica gels.", + ) + superficial_velocity: float = bst.design_input( + 'Superficial velocity', 'm/hr', lb=0, + doc="Superficial velocity of the feed [m/hr]. The diameter of the receiving " + "vessel adjusts accordingly. Typical velocities are 4 to 14.4 m/hr for " + "liquids. Common velocity range for gasses is 504-2160 m/hr.", + ) + P: float = bst.design_input('Operating pressure', 'Pa', lb=0) + k: float = bst.design_input( + 'Mass transfer coefficient', '1/hr', lb=0, + doc="Mass transfer coefficient [1/hr]. If not given, mass transfer " + "model is ignored.", + ) + k_regeneration: float = bst.design_input( + 'Regen. mass transfer coefficient', '1/hr', lb=0, + doc="Mass transfer coefficient of the regeneration solvent [1/hr].", + ) + void_fraction: float = bst.design_input(lb=0, ub=1) + rho_adsorbent: float = bst.design_input('Adsorbent density', 'kg/m3', lb=0) + C_final_scaled: float = bst.design_input( + 'Scaled final concentration', lb=0, ub=1, + doc="Final outlet concentration at breakthrough relative to inlet.", + ) + particle_diameter: float = bst.design_input( + 'Particle diameter', 'm', lb=0, + doc="Particle diameter assumed for pressure drop estimation [m].", + ) + LUB_forced: float = bst.design_input( + 'Length of unused bed', 'm', lb=0, + doc="Length of unused bed [m]. Only used if no mass transfer coefficient " + "is given (defaults to 0.6096 m in that case).", + ) def _init(self, cycle_time, @@ -421,7 +479,7 @@ def _init(self, self.C_final_scaled = C_final_scaled self.vessel_material = vessel_material self.vessel_type = vessel_type - self.N_slices = N_slices + self.N_slices = N_slices #: Number of slices to model mass transfer." self.particle_diameter = particle_diameter self.adsorbent = adsorbent self.LUB_forced = LUB_forced diff --git a/biosteam/units/aerated_bioreactor.py b/biosteam/units/aerated_bioreactor.py index 9578b3ea8..6d6b5c65d 100644 --- a/biosteam/units/aerated_bioreactor.py +++ b/biosteam/units/aerated_bioreactor.py @@ -18,6 +18,7 @@ """ import biosteam as bst from .abstract_stirred_tank_reactor import AbstractStirredTankReactor +from .._unit import design_input from math import pi from scipy.constants import g import flexsolve as flx @@ -190,6 +191,22 @@ class AeratedBioreactor(AbstractStirredTankReactor): 'Stirred tank': 'Riet', 'Bubble column': 'Dewes', } + compressor_isentropic_efficiency: float = design_input( + 'Compressor isentropic efficiency', lb=0, ub=1, + ) + cooler_pressure_drop: float = design_input( + 'Cooler pressure drop', 'Pa', lb=0, + ) + theta_O2: float = design_input( + 'O2 saturation fraction', lb=0, ub=1, + doc="Average concentration of O2 in the liquid as a fraction of saturation.", + ) + Q_O2_consumption: float = design_input( + 'Duty per O2 consumed', 'kJ/kmol', + doc="Forced duty per O2 consumed [kJ/kmol]. If not given, the duty " + "is computed from the energy balance.", + ) + def _init( self, theta_O2=0.5, Q_O2_consumption=None, optimize_power=None, design=None, method=None, kLa_kwargs=None, @@ -197,13 +214,11 @@ def _init( **kwargs, ): if compressor_isentropic_efficiency is None: compressor_isentropic_efficiency = 0.85 - #: Isentropic efficiency of the compressor. Defaults to 0.85. self.compressor_isentropic_efficiency = compressor_isentropic_efficiency - #: Pressure drop at the cooler [Pa]. Defaults to 20684.28 Pa, a heuristic value for a gas. self.cooler_pressure_drop = 20684.28 if cooler_pressure_drop is None else cooler_pressure_drop AbstractStirredTankReactor._init(self, **kwargs) - self.theta_O2 = theta_O2 # Average concentration of O2 in the liquid as a fraction of saturation. - self.Q_O2_consumption = Q_O2_consumption # Forced duty per O2 consummed [kJ/kmol]. + self.theta_O2 = theta_O2 + self.Q_O2_consumption = Q_O2_consumption if design is None: if self.kW_per_m3 == 0: design = 'Bubble column' diff --git a/biosteam/units/anaerobic_bioreactor.py b/biosteam/units/anaerobic_bioreactor.py index aa8a7c4c9..89b443fe7 100644 --- a/biosteam/units/anaerobic_bioreactor.py +++ b/biosteam/units/anaerobic_bioreactor.py @@ -147,28 +147,39 @@ class AnaerobicBioreactor(AbstractStirredTankReactor): Yeast 532 >>> F1.results() - Anaerobic bioreactor Units F1 - Electricity Power kW 10.2 - Cost USD/hr 0.798 - Chilled water Duty kJ/hr -1.4e+07 - Flow kmol/hr 9.35e+03 - Cost USD/hr 69.8 - Design Reactor volume m3 1.28e+03 - Number of reactors 1 - Residence time hr 8 - Vessel type Vertical - Length ft 80.2 - Diameter ft 26.7 - Weight lb 3.35e+05 - Wall thickness in 0.508 - Jacketed diameter ft 27.1 - Vessel material Stainless steel 316 - Purchase cost Vertical pressure vessel (jacketed) USD 5.3e+05 - Platform and ladders USD 1.03e+05 - Agitator - Agitator USD 1.83e+04 - Total purchase cost USD 6.52e+05 - Installed equipment cost USD 2.33e+06 - Utility cost USD/hr 70.5 + Anaerobic bioreactor Units F1 + Electricity Power kW 10.2 + Cost USD/hr 0.798 + Chilled water Duty kJ/hr -1.4e+07 + Flow kmol/hr 9.35e+03 + Cost USD/hr 69.8 + Design inputs Operating pressure Pa 101325 + Operating temperature K 305 + Maximum reactor volume m3 3785 + Working volume fraction 0.8 + Loop temperature change K 5 + Jacket annular diameter m 0.1 + Agitator power kW/m3 0.01 + Length to diameter ratio 3 + Residence time hr 8 + Cleaning and unloading time hr 3 + Residence time phases l + Vessel material Stainless steel 316 + Vessel type Vertical + Design results Reactor volume m3 1.28e+03 + Number of reactors 1 + Residence time hr 8 + Length ft 80.2 + Diameter ft 26.7 + Weight lb 3.35e+05 + Wall thickness in 0.508 + Jacketed diameter ft 27.1 + Purchase cost Pressure vessel USD 1.11e+06 + Platform and ladders USD 1.03e+05 + Agitator - Agitator USD 1.83e+04 + Total purchase cost USD 1.23e+06 + Installed equipment cost USD 2.91e+06 + Utility cost USD/hr 70.5 """ _N_ins = 1 diff --git a/biosteam/units/auxiliary_pressure_vessel.py b/biosteam/units/auxiliary_pressure_vessel.py index 7efe4f348..d59e7d73a 100644 --- a/biosteam/units/auxiliary_pressure_vessel.py +++ b/biosteam/units/auxiliary_pressure_vessel.py @@ -55,9 +55,9 @@ def __init__(self, if orientation == 'Horizontal': key = 'Vertical pressure vessel' bounds_warning(self, 'Horizontal vessel weight', weight, 'lb', - self._bounds['Horizontal vessel weight'], 'cost') + self._bounds['Horizontal vessel weight']) bounds_warning(self, 'Horizontal vessel diameter', diameter, 'ft', - self._bounds['Horizontal vessel diameter'], 'cost') + self._bounds['Horizontal vessel diameter']) self.F_BM[key] = 3.05 self.F_M[key] = pressure_vessel_material_factors[material] baseline_purchase_costs[key] = design.compute_horizontal_vessel_purchase_cost(weight) @@ -65,9 +65,9 @@ def __init__(self, elif orientation == 'Vertical': key = 'Vertical pressure vessel' bounds_warning(self, 'Vertical vessel weight', weight, 'lb', - self._bounds['Vertical vessel weight'], 'cost') + self._bounds['Vertical vessel weight']) bounds_warning(self, 'Vertical vessel length', length, 'ft', - self._bounds['Vertical vessel length'], 'cost') + self._bounds['Vertical vessel length']) self.F_BM[key] = 4.16 self.F_M[key] = pressure_vessel_material_factors[material] baseline_purchase_costs[key] = design.compute_vertical_vessel_purchase_cost(weight) diff --git a/biosteam/units/compressor.py b/biosteam/units/compressor.py index 83a752283..804c956ed 100644 --- a/biosteam/units/compressor.py +++ b/biosteam/units/compressor.py @@ -73,19 +73,6 @@ class Compressor(Unit, isabstract=True): '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( @@ -128,82 +115,80 @@ class Compressor(Unit, isabstract=True): CE=567, ), } + P: float = bst.design_input( + 'Outlet pressure', 'Pa', lb=0, + ) + eta: float = bst.design_input( + 'Isentropic efficiency', lb=0, ub=1, + ) + driver_efficiency: float = bst.design_input( + lb=0, ub=1, + doc="Efficiency of driver (e.g., steam turbine or electric motor). " + "If not defined, a heuristic efficiency will be selected based " + "on the compressor type and the driver.", + ) + compressor_type: str = bst.design_input( + catalog=baseline_cost_algorithms, + doc="Type of compressor. If not defined, the type will be determined " + "based on the outlet pressure.", + ) + driver: str = bst.design_input( + purchase_item='Compressor(s)', + catalog_type='Design', + catalog={ + 'Electric motor': 1.0, + 'Steam turbine': 1.15, + 'Gas turbine': 1.25, + }, + doc="Driver of compressor. If not defined, the driver will be determined " + "based on type of compressor used. Centrifugal compressors default to " + "steam turbines while reciprocating and screw compressors default to " + "electric motors.", + ) + material: str = bst.design_input( + purchase_item='Compressor(s)', + catalog_type='Material', + catalog={ + 'Carbon steel': 1.0, + 'Stainless steel': 2.5, + 'Nickel alloy': 5.0, + }, + doc="Construction material. Defaults to 'Carbon steel'.", + ) + _F_BM_default = { + 'Compressor(s)': 2.15, + } def _init(self, P, eta=0.7, vle=False, compressor_type=None, - driver=None, material=None, driver_efficiency=None + driver=None, material=None, driver_efficiency=None, ): - self.P = P #: Outlet pressure [Pa]. - self.eta = eta #: Isentropic efficiency. + self.P = P + self.eta = eta - #: Whether to perform phase equilibrium calculations on the outflow. + #: [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. 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)}" - ) + + if compressor_type is None: + self.default_compressor_type = True 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 + self.compressor_type = compressor_type + self.default_compressor_type = False + + if driver is None: + self.default_driver = True + else: + self.driver = driver + self.default_driver = False + + if driver_efficiency is None: + self.default_driver_efficiency = True + else: + self.driver_efficiency = driver_efficiency + self.default_driver_efficiency = False def _determine_compressor_type(self): psig = (self.P - 101325.) * 14.6959 / 101325. @@ -224,13 +209,11 @@ def _calculate_ideal_power_and_duty(self): 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 + driver = self.driver + if self.default_driver_efficiency: + alg = self.baseline_cost_algorithms[self.compressor_type] + self.driver_efficiency = alg.efficiencies[driver] + driver_efficiency = self.driver_efficiency if driver == 'Electric motor': self.power_utility(power / driver_efficiency) else: @@ -253,29 +236,27 @@ def _set_power(self, power): 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] + if self.default_compressor_type: + self.compressor_type = self._determine_compressor_type() + alg = self.baseline_cost_algorithms[self.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 + if self.default_driver: self.driver = alg.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']] + alg = self.baseline_cost_algorithms[self.compressor_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') + bounds_warning(self, 'power', Pc, 'hp', alg.hp_bounds) 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']] class IsothermalCompressor(Compressor, new_graphics=False): @@ -338,12 +319,15 @@ class IsothermalCompressor(Compressor, new_graphics=False): 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 + Design inputs Outlet pressure Pa 3.5e+07 + Compressor type Reciprocating Driver Electric motor + Driver efficiency 0.85 + Isentropic efficiency 1 + Material Carbon steel + Design results Compressors in parallel 1 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 Installed equipment cost USD 1.01e+03 @@ -425,20 +409,23 @@ class IsentropicCompressor(Compressor, new_graphics=False): High pressure steam Duty kJ/hr 12.7 Flow kmol/hr 0.000396 Cost USD/hr 0.000125 - Design Ideal power kW 4.92 - Ideal duty kJ/hr 0 - Type Centrifugal - Compressors in parallel 1 + Design inputs Outlet pressure Pa 5e+06 + Compressor type Centrifugal Driver Steam turbine Driver efficiency 0.65 + Isentropic efficiency 0.7 + Material Carbon steel + Design results Ideal power kW 4.92 + Ideal duty kJ/hr 0 + Compressors in parallel 1 Purchase cost Compressor(s) USD 5.87e+04 Total purchase cost USD 5.87e+04 Installed equipment cost USD 1.17e+05 Utility cost USD/hr 0.000125 - 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`: + 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"]) @@ -464,91 +451,20 @@ class IsentropicCompressor(Compressor, new_graphics=False): 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 + Design inputs Outlet pressure Pa 1e+07 + Compressor type Centrifugal Driver Steam turbine Driver efficiency 0.65 - Purchase cost Compressor(s) USD 4.98e+04 - Total purchase cost USD 4.98e+04 - Installed equipment cost USD 9.97e+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 + Isentropic efficiency 1 + Material Carbon steel + Design results 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 Installed equipment cost USD 9.97e+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 - Installed equipment cost USD 9.97e+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 - Installed equipment cost USD 9.97e+04 - Utility cost USD/hr 9.66e-05 """ equation_node_names = ( @@ -556,8 +472,7 @@ class IsentropicCompressor(Compressor, new_graphics=False): 'energy_balance_node', 'isentropic_compression_phenomenode', ) - _energy_variable = 'T' - + @property def T_node(self): if hasattr(self, '_T_node'): return self._T_node @@ -618,22 +533,10 @@ def _run(self): out.H = feed.H + dH_actual if self.vle is True: out.vle(H=out.H, P=out.P) - def _mass_and_energy_balance_specifications(self): - return 'Compressor', [ - ('Isentropic efficiency', 100 * self.eta, '%'), - ('P', self.P, 'Pa'), - ] - 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 (stream, -stream.C) - def _update_nonlinearities(self): feed = self.ins[0] out = self.outs[0] @@ -647,32 +550,17 @@ def _update_nonlinearities(self): self.design_results['Ideal duty'] = 0. self.Q = dH_isentropic / self.eta - def _create_energy_departure_equations(self): - feed = self.ins[0] - out = self.outs[0] - coeff = {self: out.C} - feed._update_energy_departure_coefficient(coeff) - return [(coeff, feed.H - out.H + self.Q)] - - 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 + def _create_energy_balance_equations(self): + fresh_inlets, process_inlets, equations = self._begin_energy_equations() + outlet = self.outs[0] + C = outlet.C + coeff = {(self.outs[0], 'T'): C, + (outlet, 'F_mol'): outlet.h} + Q = self.Q + outlet.T * C + sum([i.H for i in fresh_inlets]) + for i in process_inlets: + coeff[i, 'F_mol'] = -i.h + Q -= i._update_energy_coefficient(coeff) + return [(coeff, Q)] class PolytropicCompressor(Compressor, new_graphics=False): @@ -728,18 +616,22 @@ class PolytropicCompressor(Compressor, new_graphics=False): 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 - Installed equipment cost USD 3.48e+03 - Utility cost USD/hr 0.529 + Polytropic compressor Units K1 + Electricity Power kW 6.76 + Cost USD/hr 0.529 + Design inputs Outlet pressure Pa 3.5e+07 + Compressor type Reciprocating + Driver Electric motor + Driver efficiency 0.85 + Isentropic efficiency 0.7 + Material Carbon steel + Number of integration steps 100 + Design results Polytropic work 2.07e+04 + Compressors in parallel 1 + Purchase cost Compressor(s) USD 1.62e+03 + Total purchase cost USD 1.62e+03 + Installed equipment cost USD 3.48e+03 + Utility cost USD/hr 0.529 Repeat using Hundseid method [4]_: @@ -757,31 +649,39 @@ class PolytropicCompressor(Compressor, new_graphics=False): phase: 'g', T: 958.08 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 - Installed equipment cost USD 3.3e+03 - Utility cost USD/hr 0.507 + Polytropic compressor Units K1 + Electricity Power kW 6.48 + Cost USD/hr 0.507 + Design inputs Outlet pressure Pa 3.5e+07 + Compressor type Reciprocating + Driver Electric motor + Driver efficiency 0.85 + Isentropic efficiency 0.7 + Material Carbon steel + Number of integration steps 200 + Design results Polytropic work 1.98e+04 + Compressors in parallel 1 + Purchase cost Compressor(s) USD 1.54e+03 + Total purchase cost USD 1.54e+03 + Installed equipment cost USD 3.3e+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 - Installed equipment cost USD 3.3e+03 - Utility cost USD/hr 0.507 + Polytropic compressor Units K1 + Electricity Power kW 6.48 + Cost USD/hr 0.507 + Design inputs Outlet pressure Pa 3.5e+07 + Compressor type Reciprocating + Driver Electric motor + Driver efficiency 0.85 + Isentropic efficiency 0.7 + Material Carbon steel + Number of integration steps 200 + Design results Polytropic work 1.98e+04 + Compressors in parallel 1 + Purchase cost Compressor(s) USD 1.54e+03 + Total purchase cost USD 1.54e+03 + Installed equipment cost USD 3.3e+03 + Utility cost USD/hr 0.507 Repeat using Hundseid method [4]_: @@ -800,22 +700,31 @@ class PolytropicCompressor(Compressor, new_graphics=False): 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 - Installed equipment cost USD 3.3e+03 - Utility cost USD/hr 0.507 + Polytropic compressor Units K1 + Electricity Power kW 6.48 + Cost USD/hr 0.507 + Design inputs Outlet pressure Pa 3.5e+07 + Compressor type Reciprocating + Driver Electric motor + Driver efficiency 0.85 + Isentropic efficiency 0.7 + Material Carbon steel + Number of integration steps 200 + Design results Polytropic work 1.98e+04 + Compressors in parallel 1 + Purchase cost Compressor(s) USD 1.54e+03 + Total purchase cost USD 1.54e+03 + Installed equipment cost USD 3.3e+03 + Utility cost USD/hr 0.507 """ available_methods = {'schultz', 'hundseid'} + n_steps: int = bst.design_input( + 'Number of integration steps', lb=1, + doc="Number of virtual steps used in numerical integration for hundseid method.", + ) + 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) @@ -963,28 +872,31 @@ class MultistageCompressor(Unit): 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 - Installed equipment cost USD 1.27e+05 - Utility cost USD/hr 0.056 + 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 inputs Isentropic efficiency 0.7 + Number of stages 4 + Pressure ratio 2 + Design results 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 + Installed equipment cost USD 1.27e+05 + Utility cost USD/hr 0.056 Show the fluid state at the outlet of each heat exchanger: @@ -1040,7 +952,7 @@ class MultistageCompressor(Unit): Cooling water Duty kJ/hr -7.15e+03 Flow kmol/hr 4.88 Cost USD/hr 0.00238 - Design Type Multistage compressor + Design results Type Multistage compressor Area ft^2 1.14 Purchase cost K2 k1 - Compressor(s) USD 1.11e+04 K2 h1 - Double pipe USD 297 @@ -1133,7 +1045,7 @@ class MultistageCompressor(Unit): Cooling water Duty kJ/hr -7.15e+03 Flow kmol/hr 4.88 Cost USD/hr 0.00238 - Design Type Multistage compressor + Design results Type Multistage compressor Area ft^2 1.14 Purchase cost K2 k1 - Compressor(s) USD 1.11e+04 K2 h1 - Double pipe USD 297 @@ -1158,6 +1070,23 @@ class MultistageCompressor(Unit): **HX._units, } + pr: float = bst.design_input( + 'Pressure ratio', lb=0, + doc="Pressure ratio between isentropic stages (setup option 1).", + ) + n_stages: int = bst.design_input( + 'Number of stages', lb=1, + doc="Number of isentropic stages (setup option 1).", + ) + eta: float = bst.design_input( + 'Isentropic efficiency', lb=0, ub=1, + doc="Isentropic efficiency of each stage (setup option 1).", + ) + compressor_type: str = bst.design_input( + doc="Type of compressor (setup option 1): blower/centrifugal/reciprocating. " + "If None, the type will be determined automatically.", + ) + def _init( self, pr=None, n_stages=None, eta=0.7, vle=False, compressor_type=None, compressors=None, hxs=None, diff --git a/biosteam/units/decorators/__init__.py b/biosteam/units/decorators/__init__.py index 7236b8f1f..aa3105855 100644 --- a/biosteam/units/decorators/__init__.py +++ b/biosteam/units/decorators/__init__.py @@ -7,13 +7,13 @@ # for license details. """ """ -__all__ = [] - from ._cost import * from ._design import * from . import _cost from . import _design -__all__.extend(_cost.__all__) -__all__.extend(_design.__all__) \ No newline at end of file +__all__ = ( + *_cost.__all__, + *_design.__all__, +) \ No newline at end of file diff --git a/biosteam/units/decorators/_design.py b/biosteam/units/decorators/_design.py index 11f855810..c4e605cd1 100644 --- a/biosteam/units/decorators/_design.py +++ b/biosteam/units/decorators/_design.py @@ -56,20 +56,6 @@ def define(self, design_basis): functions[name] = design_basis return design_basis - def __call__(self, name, units): - """ - Return a Unit class decorator that adds a size/design requirement to the class. - - Parameters - ---------- - name : str - Name of design item - units : str - Units of measure of design item. - - """ - return lambda cls: self.add_design_basis_to_cls(cls, name, units) - def add_design_basis_to_cls(self, cls, name, units, annual): """ Add size/design requirement to class. diff --git a/biosteam/units/design_tools/pressure_vessel.py b/biosteam/units/design_tools/pressure_vessel.py index d3128a289..cd8231b73 100644 --- a/biosteam/units/design_tools/pressure_vessel.py +++ b/biosteam/units/design_tools/pressure_vessel.py @@ -7,6 +7,7 @@ # for license details. """ """ +from ..._unit import design_input from .specification_factors import (pressure_vessel_material_factors, material_densities_lb_per_ft3) from . import flash_vessel_design as design @@ -15,13 +16,12 @@ __all__ = ('PressureVessel',) -allowed_vessel_types = {'Vertical', 'Horizontal', None} + class PressureVessel: """Abstract class for pressure vessels.""" - _units = {'Vertical vessel weight': 'lb', - 'Horizontal vessel weight': 'lb', + _units = {'Vessel weight': 'lb', 'Length': 'ft', 'Diameter': 'ft', 'Jacketed diameter': 'ft', @@ -36,49 +36,24 @@ class PressureVessel: # Bare module factors _F_BM_default = { - 'Horizontal pressure vessel': 3.05, - 'Vertical pressure vessel': 4.16, - 'Horizontal pressure vessel (jacketed)': 3.05, - 'Vertical pressure vessel (jacketed)': 4.16, 'Platform and ladders': 1. } - @property - def vessel_type(self): - vessel_type = self._vessel_type - if not vessel_type: - self._vessel_type = vessel_type = self._default_vessel_type() - return vessel_type - @vessel_type.setter - def vessel_type(self, vessel_type): - if vessel_type not in allowed_vessel_types: - raise ValueError("vessel type must be either 'Vertical', " - "'Horizontal', or None") - self._vessel_type = vessel_type - - @property - def vessel_material(self): - """Vessel construction material.""" - return self._vessel_material - @vessel_material.setter - def vessel_material(self, material): - try: - F_M = self.F_M - F_M['Vertical pressure vessel'] = F_M['Horizontal pressure vessel'] = pressure_vessel_material_factors[material] - except KeyError: - raise ValueError(f"no material factor available for '{material}'; " - "only the following materials are available: " - f"{', '.join(pressure_vessel_material_factors)}") - self._vessel_material = material - - def _get_design_info(self): - try: - return (('Vessel material', self._vessel_material, ''),) - except: - return () - + vessel_type = design_input( + purchase_item='Pressure vessel', + catalog_type='Module', + catalog={'Horizontal': 3.05, + 'Vertical': 4.16}, + ) + + vessel_material = design_input( + purchase_item='Pressure vessel', + catalog_type='Material', + catalog=pressure_vessel_material_factors, + ) + def _default_vessel_type(self): - return None + raise NotImplementedError('must define _default_vessel_type method') def _design_and_cost_pressure_vessel( self, pressure, diameter, length, @@ -99,6 +74,7 @@ def _design_and_cost_pressure_vessel( self.baseline_purchase_costs.update(purchase_costs) def _vessel_design(self, pressure, diameter, length, annular_diameter=0, check=True) -> dict: + if self.vessel_type is None: self.vessel_type = self._default_vessel_type() vessel_type = self.vessel_type if vessel_type == 'Horizontal': method = self._horizontal_vessel_design @@ -116,15 +92,18 @@ def _horizontal_vessel_design(self, pressure, diameter, length, annular_diameter ) if check: if pressure < 14.68: - warn('vacuum pressure vessel ASME codes not implemented yet; ' - 'wall thickness may be inaccurate and stiffening rings may be ' - 'required', category=DesignWarning) - bounds_warning(self, 'Horizontal vessel weight', VW, 'lb', - self._bounds['Horizontal vessel weight'], 'cost') - bounds_warning(self, 'Horizontal vessel diameter', diameter, 'ft', - self._bounds['Horizontal vessel diameter'], 'cost') + warning = DesignWarning.from_source( + self, + 'vacuum pressure vessel ASME codes not implemented yet; ' + 'wall thickness may be inaccurate and stiffening rings may be ' + 'required' + ) + warn(warning) + bounds_warning(self, 'Vessel weight', VW, 'lb', + self._bounds['Horizontal vessel weight']) + bounds_warning(self, 'Vessel diameter', diameter, 'ft', + self._bounds['Horizontal vessel diameter']) Design = {} - Design['Vessel type'] = 'Horizontal' Design['Length'] = length # ft Design['Diameter'] = diameter # ft Design['Weight'] = VW # lb @@ -140,16 +119,17 @@ def _vertical_vessel_design(self, pressure, diameter, length, annular_diameter=0 Design = {} if check: if pressure < 14.68: - warn('vacuum pressure vessel ASME codes not implemented yet; ' - 'wall thickness may be inaccurate and stiffening rings may be ' - 'required', category=DesignWarning) - bounds_warning(self, 'Vertical vessel weight', VW, 'lb', - self._bounds['Vertical vessel weight'], - 'cost') - bounds_warning(self, 'Vertical vessel length', length, 'ft', - self._bounds['Vertical vessel length'], - 'cost') - Design['Vessel type'] = 'Vertical' + warning = DesignWarning.from_source( + self, + 'vacuum pressure vessel ASME codes not implemented yet; ' + 'wall thickness may be inaccurate and stiffening rings may be ' + 'required' + ) + warn(warning) + bounds_warning(self, 'Vessel weight', VW, 'lb', + self._bounds['Vertical vessel weight']) + bounds_warning(self, 'Vessel length', length, 'ft', + self._bounds['Vertical vessel length']) Design['Length'] = length # ft Design['Diameter'] = diameter # ft Design['Weight'] = VW # lb @@ -168,25 +148,14 @@ def _vessel_purchase_cost(self, weight, diameter, length, jacketed=None) -> dict return method(weight, diameter, length, jacketed) def _horizontal_vessel_purchase_cost(self, weight, diameter, length=None, jacketed=None) -> dict: - if jacketed: - return { - 'Horizontal pressure vessel (jacketed)': design.compute_horizontal_vessel_purchase_cost(weight), - 'Platform and ladders': design.compute_horizontal_vessel_platform_and_ladders_purchase_cost(diameter) - } - else: - return { - 'Horizontal pressure vessel': design.compute_horizontal_vessel_purchase_cost(weight), - 'Platform and ladders': design.compute_horizontal_vessel_platform_and_ladders_purchase_cost(diameter) - } + return { + 'Pressure vessel': design.compute_horizontal_vessel_purchase_cost(weight), + 'Platform and ladders': design.compute_horizontal_vessel_platform_and_ladders_purchase_cost(diameter) + } def _vertical_vessel_purchase_cost(self, weight, diameter, length, jacketed=None) -> dict: - if jacketed: - return { - 'Vertical pressure vessel (jacketed)': design.compute_vertical_vessel_purchase_cost(weight), - 'Platform and ladders': design.compute_vertical_vessel_platform_and_ladders_purchase_cost(diameter, length) - } - else: - return { - 'Vertical pressure vessel': design.compute_vertical_vessel_purchase_cost(weight), - 'Platform and ladders': design.compute_vertical_vessel_platform_and_ladders_purchase_cost(diameter, length) - } \ No newline at end of file + return { + 'Pressure vessel': design.compute_vertical_vessel_purchase_cost(weight), + 'Platform and ladders': design.compute_vertical_vessel_platform_and_ladders_purchase_cost(diameter, length) + } + \ No newline at end of file diff --git a/biosteam/units/design_tools/tank_design.py b/biosteam/units/design_tools/tank_design.py index 091f2e161..5edba9b00 100644 --- a/biosteam/units/design_tools/tank_design.py +++ b/biosteam/units/design_tools/tank_design.py @@ -106,7 +106,7 @@ def compute_number_of_tanks_and_purchase_cost(total_volume, if V_total < V_min: warn(f"volume ({V_total:.5g} {V_units}) is below " f"the lower bound ({V_min:.5g} {V_units}) for purchase " - "cost estimation", bst.exceptions.CostWarning) + "cost estimation", bst.exceptions.DesignWarning) N = ceil(V_total / purchase_cost_algorithm.V_max) if N: V = V_total / N diff --git a/biosteam/units/distillation.py b/biosteam/units/distillation.py index ce0f49a9b..a7365fdaa 100644 --- a/biosteam/units/distillation.py +++ b/biosteam/units/distillation.py @@ -48,6 +48,7 @@ from . import design_tools as design from thermosteam import separations from .. import Unit +from .._unit import design_input from .splitting import MockSplitter from .._graphics import vertical_column_graphics from scipy.optimize import brentq @@ -108,6 +109,14 @@ class RefluxDrum(design.PressureVessel, Unit): _default_vessel_type = Flash._default_vessel_type vapor = Flash.vapor liquid = Flash.liquid + holdup_time: float = design_input( + doc="Time it takes to raise liquid to half full [min].", + units='min', lb=0, + ) + surge_time: float = design_input( + doc="Time it takes to reach from normal to maximum liquid level [min].", + units='min', lb=0, + ) def _init(self, vessel_material='Carbon steel', @@ -130,10 +139,7 @@ def _init(self, #: [str] 'Horizontal', 'Vertical', or 'Default' self.vessel_type = vessel_type - #: [float] Time it takes to raise liquid to half full (min) self.holdup_time = holdup_time - - #: [float] Time it takes to reach from normal to maximum liquied level (min) self.surge_time = surge_time #: [bool] True if using a mist eliminator pad @@ -150,6 +156,7 @@ def _design(self): if self.no_vessel_needed: self.design_results.clear() else: + if self.vessel_type is None: self.vessel_type = self._default_vessel_type() vessel_type = self.vessel_type if vessel_type == 'Vertical': args = self._vertical_vessel_pressure_diameter_and_length() @@ -299,6 +306,44 @@ class Distillation(Unit, isabstract=True): composition_sensitive = False use_overall_energy_balance = True + k: float = design_input( + 'Reflux to minimum reflux ratio', lb=0, + ) + P: float = design_input( + 'Operating pressure', 'Pa', lb=0, + ) + Rmin: float = design_input( + 'Enforced minimum reflux ratio', lb=0, + doc="User enforced minimum reflux ratio. If the actual minimum reflux " + "ratio is more than `Rmin`, this enforced value is ignored.", + ) + tray_spacing: float = design_input( + 'Tray spacing', 'mm', lb=0, + ) + stage_efficiency: float = design_input( + lb=0, ub=1, + doc="User enforced stage efficiency. If None, stage efficiency is " + "calculated by the O'Connell correlation.", + ) + velocity_fraction: float = design_input( + lb=0, ub=1, + doc="Fraction of actual velocity to maximum velocity allowable before " + "flooding.", + ) + open_tray_area: float = design_input( + 'Open tray area fraction', lb=0, ub=1, + doc="Ratio of open area to active area of a tray.", + ) + downcomer_area_fraction: float = design_input( + lb=0, ub=1, + doc="Enforced fraction of downcomer area to net (total) area of a tray. " + "If None, the fraction is estimated based on heuristics.", + ) + vacuum_system_preference: str = design_input( + doc="If a vacuum system is needed, it will choose one according to this " + "preference ('Liquid-ring pump', 'Steam-jet ejector', or 'Dry-vacuum pump').", + ) + def _init(self, LHK, k, P=101325, @@ -352,36 +397,7 @@ def _init(self, self.vacuum_system_preference = vacuum_system_preference self._load_components(partial_condenser, condenser_thermo, reboiler_thermo) self.LHK = LHK - - def _mass_and_energy_balance_specifications(self): - spec = self.product_specification_format - specs = [] - if spec == 'Composition': - self._Lr = self._Hr = None - elif spec == 'Recovery': - self._y_top = self._x_bot = None - specs.append( - ('Partial condenser', self._partial_condenser, '-'), - ) - if spec == 'Composition': - specs.extend([ - ('Distillate light key fraction', 100 * self._y_top, '%'), - ('Bottoms product heavy key fraction', 100 * self._x_bot, '%'), - ]) - elif spec == 'Recovery': - specs.extend([ - ('Light key recovery', 100 * self._Lr, '%'), - ('Heavy key recovery', 100 * self._Hr, '%'), - ]) - else: - raise RuntimeError('invalid product specification format') - if isinstance(self, ShortcutColumn): - return 'Shortcut column', specs - elif isinstance(self, BinaryDistillation): - return 'Binary distillation', specs - else: - raise NotImplementedError('unknown name for distillation class') - + def _reset_thermo(self, thermo): super()._reset_thermo(thermo) self.LHK = self._LHK @@ -547,14 +563,6 @@ def LHK(self, LHK): self._gases_index = get_index(gases) self._solids_index = get_index(solids) - @property - def Rmin(self): - """User enforced minimum reflux ratio. If the actual minimum reflux ratio is less than `Rmin`. This enforced value is ignored.""" - return self._Rmin - @Rmin.setter - def Rmin(self, Rmin): - self._Rmin = Rmin - @property def y_top(self): """Light key composition of at the distillate.""" @@ -621,30 +629,6 @@ def weir_height(self, WS): ) self._WH = WS - @property - def tray_spacing(self): - return self._TS - @tray_spacing.setter - def tray_spacing(self, TS): - """Tray spacing (225-600 mm).""" - self._TS = TS - - @property - def stage_efficiency(self): - """Enforced user defined stage efficiency.""" - return self._E_eff - @stage_efficiency.setter - def stage_efficiency(self, E_eff): - self._E_eff = E_eff - - @property - def velocity_fraction(self): - """Fraction of actual velocity to maximum velocity allowable before flooding.""" - return self._f - @velocity_fraction.setter - def velocity_fraction(self, f): - self._f = f - @property def foaming_factor(self): """Foaming factor (0 to 1).""" @@ -655,23 +639,6 @@ def foaming_factor(self, F_F): raise ValueError(f"foaming_factor must be between 0 and 1, ({F_F} given).") self._F_F = F_F - @property - def open_tray_area(self): - """Ratio of open area, A_h, to active area, A_a.""" - return self._A_ha - @open_tray_area.setter - def open_tray_area(self, A_ha): - self._A_ha = A_ha - - @property - def downcomer_area_fraction(self): - """Enforced fraction of downcomer area to net (total) area. - If None, the fraction is estimated based on heuristics.""" - return self._A_dn - @downcomer_area_fraction.setter - def downcomer_area_fraction(self, A_dn): - self._A_dn = A_dn - @property def tray_type(self): """Default 'Sieve'""" @@ -1003,7 +970,7 @@ def _complete_distillation_column_design(self): R = Design['Reflux'] Rstages, Sstages = self._compute_N_stages() is_divided = self.is_divided - TS = self._TS + TS = self._tray_spacing ### Get diameter of rectifying section based on top plate ### @@ -1018,12 +985,12 @@ def _complete_distillation_column_design(self): F_LV = design.compute_flow_parameter(L, V, rho_V, rho_L) C_sbf = design.compute_max_capacity_parameter(TS, F_LV) F_F = self._F_F - A_ha = self._A_ha + A_ha = self._open_tray_area U_f = design.compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha) - A_dn = self._A_dn + A_dn = self._downcomer_area_fraction if A_dn is None: A_dn = design.compute_downcomer_area_fraction(F_LV) - f = self._f + f = self._velocity_fraction R_diameter = design.compute_tower_diameter(V_vol, U_f, f, A_dn) * 3.28 ### Get diameter of stripping section based on feed plate ### @@ -1037,7 +1004,7 @@ def _complete_distillation_column_design(self): C_sbf = design.compute_max_capacity_parameter(TS, F_LV) sigma = condensate.get_property('sigma', 'dyn/cm') U_f = design.compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha) - A_dn = self._A_dn + A_dn = self._downcomer_area_fraction if A_dn is None: A_dn = design.compute_downcomer_area_fraction(F_LV) S_diameter = design.compute_tower_diameter(V_vol, U_f, f, A_dn) * 3.28 @@ -1403,44 +1370,51 @@ class BinaryDistillation(Distillation, new_graphics=False): Glycerol 23.9 -------- 105 kmol/hr >>> D1.results() - Divided Distillation Column Units D1 - Electricity Power kW 0.663 - Cost USD/hr 0.0518 - Cooling water Duty kJ/hr -4.88e+06 - Flow kmol/hr 3.33e+03 - Cost USD/hr 1.63 - Low pressure steam Duty kJ/hr 9.06e+06 - Flow kmol/hr 234 - Cost USD/hr 55.7 - Design Theoretical feed stage 9 - Theoretical stages 13 - Minimum reflux Ratio 0.687 - Reflux Ratio 1.37 - Rectifier stages 15 - Stripper stages 13 - Rectifier height ft 34.7 - Stripper height ft 31.7 - Rectifier diameter ft 3.93 - Stripper diameter ft 3.19 - Rectifier wall thickness in 0.312 - Stripper wall thickness in 0.312 - Rectifier weight lb 6e+03 - Stripper weight lb 4.43e+03 - Purchase cost Rectifier trays USD 1.5e+04 - Stripper trays USD 1.25e+04 - Rectifier tower USD 4.56e+04 - Rectifier platform and ladders USD 1.39e+04 - Stripper tower USD 3.83e+04 - Stripper platform and ladders USD 1.14e+04 - Condenser - Floating head USD 3.33e+04 - Reflux drum - Horizontal pressur... USD 1.02e+04 - Reflux drum - Platform and ladders USD 3.02e+03 - Pump - Pump USD 4.37e+03 - Pump - Motor USD 370 - Reboiler - Floating head USD 3.52e+04 - Total purchase cost USD 2.23e+05 - Installed equipment cost USD 7.74e+05 - Utility cost USD/hr 57.4 + Divided Distillation Column Units D1 + Electricity Power kW 0.663 + Cost USD/hr 0.0518 + Cooling water Duty kJ/hr -4.88e+06 + Flow kmol/hr 3.33e+03 + Cost USD/hr 1.63 + Low pressure steam Duty kJ/hr 9.06e+06 + Flow kmol/hr 234 + Cost USD/hr 55.7 + Design inputs Operating pressure Pa 101325 + Enforced minimum reflux ratio 0.01 + Reflux to minimum reflux ratio 2 + Open tray area fraction 0.1 + Tray spacing mm 450 + Vacuum system preference Liquid-ring pump + Velocity fraction 0.8 + Design results Theoretical feed stage 9 + Theoretical stages 13 + Minimum reflux Ratio 0.687 + Reflux Ratio 1.37 + Rectifier stages 15 + Stripper stages 13 + Rectifier height ft 34.7 + Stripper height ft 31.7 + Rectifier diameter ft 3.93 + Stripper diameter ft 3.19 + Rectifier wall thickness in 0.312 + Stripper wall thickness in 0.312 + Rectifier weight lb 6e+03 + Stripper weight lb 4.43e+03 + Purchase cost Rectifier trays USD 1.5e+04 + Stripper trays USD 1.25e+04 + Rectifier tower USD 4.56e+04 + Rectifier platform and ladders USD 1.39e+04 + Stripper tower USD 3.83e+04 + Stripper platform and ladders USD 1.14e+04 + Condenser - Floating head USD 3.33e+04 + Reflux drum - Pressure vessel USD 1.02e+04 + Reflux drum - Platform and ladders USD 3.02e+03 + Pump - Pump USD 4.37e+03 + Pump - Motor USD 370 + Reboiler - Floating head USD 3.52e+04 + Total purchase cost USD 2.23e+05 + Installed equipment cost USD 7.74e+05 + Utility cost USD/hr 57.4 Binary distillation with full-condenser @@ -1967,44 +1941,51 @@ class ShortcutColumn(Distillation, new_graphics=False): Glycerol 23.9 -------- 105 kmol/hr >>> D1.results() - Divided Distillation Column Units D1 - Electricity Power kW 0.787 - Cost USD/hr 0.0615 - Cooling water Duty kJ/hr -7.54e+06 - Flow kmol/hr 5.15e+03 - Cost USD/hr 2.51 - Medium pressure steam Duty kJ/hr 1.25e+07 - Flow kmol/hr 346 - Cost USD/hr 95.4 - Design Theoretical feed stage 8 - Theoretical stages 16 - Minimum reflux Ratio 1.06 - Reflux Ratio 2.12 - Rectifier stages 13 - Stripper stages 26 - Rectifier height ft 31.7 - Stripper height ft 50.9 - Rectifier diameter ft 4.52 - Stripper diameter ft 3.64 - Rectifier wall thickness in 0.312 - Stripper wall thickness in 0.312 - Rectifier weight lb 6.45e+03 - Stripper weight lb 7.93e+03 - Purchase cost Rectifier trays USD 1.52e+04 - Stripper trays USD 2.01e+04 - Rectifier tower USD 4.76e+04 - Rectifier platform and ladders USD 1.42e+04 - Stripper tower USD 5.38e+04 - Stripper platform and ladders USD 1.81e+04 - Condenser - Floating head USD 4.07e+04 - Reflux drum - Horizontal pressur... USD 1.03e+04 - Reflux drum - Platform and ladders USD 3.02e+03 - Pump - Pump USD 4.36e+03 - Pump - Motor USD 381 - Reboiler - Floating head USD 2.59e+04 - Total purchase cost USD 2.54e+05 - Installed equipment cost USD 8.82e+05 - Utility cost USD/hr 97.9 + Divided Distillation Column Units D1 + Electricity Power kW 0.787 + Cost USD/hr 0.0615 + Cooling water Duty kJ/hr -7.54e+06 + Flow kmol/hr 5.15e+03 + Cost USD/hr 2.51 + Medium pressure steam Duty kJ/hr 1.25e+07 + Flow kmol/hr 346 + Cost USD/hr 95.4 + Design inputs Operating pressure Pa 101325 + Enforced minimum reflux ratio 0.01 + Reflux to minimum reflux ratio 2 + Open tray area fraction 0.1 + Tray spacing mm 450 + Vacuum system preference Liquid-ring pump + Velocity fraction 0.8 + Design results Theoretical feed stage 8 + Theoretical stages 16 + Minimum reflux Ratio 1.06 + Reflux Ratio 2.12 + Rectifier stages 13 + Stripper stages 26 + Rectifier height ft 31.7 + Stripper height ft 50.9 + Rectifier diameter ft 4.52 + Stripper diameter ft 3.64 + Rectifier wall thickness in 0.312 + Stripper wall thickness in 0.312 + Rectifier weight lb 6.45e+03 + Stripper weight lb 7.93e+03 + Purchase cost Rectifier trays USD 1.52e+04 + Stripper trays USD 2.01e+04 + Rectifier tower USD 4.76e+04 + Rectifier platform and ladders USD 1.42e+04 + Stripper tower USD 5.38e+04 + Stripper platform and ladders USD 1.81e+04 + Condenser - Floating head USD 4.07e+04 + Reflux drum - Pressure vessel USD 1.03e+04 + Reflux drum - Platform and ladders USD 3.02e+03 + Pump - Pump USD 4.36e+03 + Pump - Motor USD 381 + Reboiler - Floating head USD 2.59e+04 + Total purchase cost USD 2.54e+05 + Installed equipment cost USD 8.82e+05 + Utility cost USD/hr 97.9 """ line = 'Distillation' @@ -2371,19 +2352,23 @@ class AdiabaticMultiStageVLEColumn(MultiStageEquilibrium): MTBE 0.000309 >>> stripper.results() - Stripper Units - Design Theoretical stages 2 - Actual stages 4 - Height ft 19.9 - Diameter ft 3 - Wall thickness in 0.312 - Weight lb 2.71e+03 - Purchase cost Trays USD 5.59e+03 - Tower USD 2.91e+04 - Platform and ladders USD 7.52e+03 - Total purchase cost USD 4.23e+04 - Installed equipment cost USD 1.57e+05 - Utility cost USD/hr 0 + Stripper Units + Design inputs Number of stages 2 + Open tray area fraction 0.1 + Tray spacing mm 450 + Velocity fraction 0.8 + Design results Theoretical stages 2 + Actual stages 4 + Height ft 19.9 + Diameter ft 3 + Wall thickness in 0.312 + Weight lb 2.71e+03 + Purchase cost Trays USD 5.59e+03 + Tower USD 2.91e+04 + Platform and ladders USD 7.52e+03 + Total purchase cost USD 4.23e+04 + Installed equipment cost USD 1.57e+05 + Utility cost USD/hr 0 """ _graphics = vertical_column_graphics @@ -2542,10 +2527,10 @@ def _get_relative_volatilities(self): def _design(self): vapor_out, liquid_out = self.outs Design = self.design_results - TS = self._TS - A_ha = self._A_ha + TS = self._tray_spacing + A_ha = self._open_tray_area F_F = self._F_F - f = self._f + f = self._velocity_fraction ### Get diameter of column based on outlets (assuming they are comparable to each stages) ### rho_L = liquid_out.rho @@ -2557,7 +2542,7 @@ def _design(self): C_sbf = design.compute_max_capacity_parameter(TS, F_LV) sigma = liquid_out.get_property('sigma', 'dyn/cm') U_f = design.compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha) - A_dn = self._A_dn + A_dn = self._downcomer_area_fraction if A_dn is None: A_dn = design.compute_downcomer_area_fraction(F_LV) diameter = design.compute_tower_diameter(V_vol, U_f, f, A_dn) * 3.28 @@ -2677,33 +2662,38 @@ class MESHDistillation(MultiStageEquilibrium, new_graphics=False): 0.69 >>> D1.results() - Distillation Units - Electricity Power kW 0.575 - Cost USD/hr 0.045 - Cooling water Duty kJ/hr -2.98e+06 - Flow kmol/hr 2.03e+03 - Cost USD/hr 0.992 - Low pressure steam Duty kJ/hr 7.8e+06 - Flow kmol/hr 202 - Cost USD/hr 47.9 - Design Theoretical stages 5 - Actual stages 7 - Height ft 24.3 - Diameter ft 3.32 - Wall thickness in 0.312 - Weight lb 3.63e+03 - Purchase cost Trays USD 8.11e+03 - Tower USD 3.43e+04 - Platform and ladders USD 9.44e+03 - Condenser - Floating head USD 2.36e+04 - Reflux drum - Vertical pressure ... USD 1.29e+04 - Reflux drum - Platform and ladders USD 3.89e+03 - Pump - Pump USD 4.35e+03 - Pump - Motor USD 358 - Reboiler - Floating head USD 2.34e+04 - Total purchase cost USD 1.2e+05 - Installed equipment cost USD 4.15e+05 - Utility cost USD/hr 49 + Distillation Units + Electricity Power kW 0.574 + Cost USD/hr 0.0449 + Cooling water Duty kJ/hr -2.98e+06 + Flow kmol/hr 2.03e+03 + Cost USD/hr 0.992 + Low pressure steam Duty kJ/hr 7.8e+06 + Flow kmol/hr 202 + Cost USD/hr 48 + Design inputs Number of stages 5 + Open tray area fraction 0.1 + Tray spacing mm 450 + Vacuum system preference Liquid-ring pump + Velocity fraction 0.8 + Design results Theoretical stages 5 + Actual stages 7 + Height ft 24.3 + Diameter ft 3.32 + Wall thickness in 0.312 + Weight lb 3.63e+03 + Purchase cost Trays USD 8.11e+03 + Tower USD 3.43e+04 + Platform and ladders USD 9.43e+03 + Condenser - Floating head USD 2.36e+04 + Reflux drum - Pressure vessel USD 1.29e+04 + Reflux drum - Platform and ladders USD 3.89e+03 + Pump - Pump USD 4.35e+03 + Pump - Motor USD 358 + Reboiler - Floating head USD 2.34e+04 + Total purchase cost USD 1.2e+05 + Installed equipment cost USD 4.15e+05 + Utility cost USD/hr 49 Simulate distillation column with a full condenser, 5 stages, a 0.673 reflux ratio, 2.57 boilup ratio, and feed at stage 2: @@ -2774,6 +2764,7 @@ class MESHDistillation(MultiStageEquilibrium, new_graphics=False): tray_type = Distillation.tray_type tray_material = Distillation.tray_material vessel_material = Distillation.vessel_material + vacuum_system_preference = Distillation.vacuum_system_preference def _init(self, N_stages, feed_stages, @@ -2990,7 +2981,7 @@ def _actual_stages(self): def update_liquid_holdup(self): diameter = self.estimate_diameter() - hw = weir_height = self._TS * self.weir_height * 0.0393701 # mm to inches + hw = weir_height = self._tray_spacing * self.weir_height * 0.0393701 # mm to inches area = diameter * diameter * pi / 4 b = 2/3 stages = self.stages @@ -3019,10 +3010,10 @@ def update_liquid_holdup(self): def estimate_diameter(self): # ft diameters = [] - TS = self._TS - A_ha = self._A_ha + TS = self._tray_spacing + A_ha = self._open_tray_area F_F = self._F_F - f = self._f + f = self._velocity_fraction for i in self.stages: vapor, liquid = i.partition.outs if liquid.isempty() or vapor.isempty(): continue @@ -3035,7 +3026,7 @@ def estimate_diameter(self): # ft F_LV = design.compute_flow_parameter(L, V, rho_V, rho_L) C_sbf = design.compute_max_capacity_parameter(TS, F_LV) U_f = design.compute_max_vapor_velocity(C_sbf, sigma, rho_L, rho_V, F_F, A_ha) - A_dn = self._A_dn + A_dn = self._downcomer_area_fraction if A_dn is None: i.partition.downcomer_area_fraction = A_dn = design.compute_downcomer_area_fraction(F_LV) diameters.append( design.compute_tower_diameter(V_vol, U_f, f, A_dn) @@ -3057,7 +3048,7 @@ def _design(self): 'wall thickness may be inaccurate and stiffening rings may be ' 'required', category=RuntimeWarning) N_theoretical, N_actual = self._actual_stages() - TS = self._TS + TS = self._tray_spacing Design['Theoretical stages'] = N_theoretical Design['Actual stages'] = actual_stages = N_actual - (bool(self.condenser) + bool(self.reboiler)) Design['Height'] = H = design.compute_tower_height(TS, actual_stages) * 3.28 diff --git a/biosteam/units/drying.py b/biosteam/units/drying.py index c8f83770a..21d6878d6 100644 --- a/biosteam/units/drying.py +++ b/biosteam/units/drying.py @@ -29,7 +29,7 @@ cylinder_area, ) from .decorators import cost -from .._unit import Unit +from .._unit import Unit, design_input from math import exp, log from thermosteam import separations from warnings import warn @@ -44,6 +44,9 @@ class SprayDryer(Unit): _units = {'Evaporation rate': 'lb/hr'} _N_ins = 1 _N_outs = 2 + moisture_content: float = design_input( + 'Moisture content of solids', lb=0, ub=1, + ) def _init(self, moisture_content=0.90): self.moisture_content = moisture_content @@ -170,20 +173,26 @@ class DrumDryer(Unit): ----- 1.22e+04 kg/hr >>> dryer.results() - Drum dryer Units D610 - Electricity Power kW 845 - Cost USD/hr 66 - Natural gas (inlet) Flow kg/hr 2.45e+03 - Cost USD/hr 534 - Design Evaporation kg/hr 2.09e+04 - Volume 1.05e+03 - Diameter m 3.76 - Length 94 - Peripheral drum area m2 1.11e+03 - Purchase cost Drum dryer USD 1.2e+06 - Total purchase cost USD 1.2e+06 - Installed equipment cost USD 2.46e+06 - Utility cost USD/hr 600 + Drum dryer Units D610 + Electricity Power kW 845 + Cost USD/hr 66 + Natural gas (inlet) Flow kg/hr 2.45e+03 + Cost USD/hr 534 + Design inputs Specific evaporation rate kg/hr/m3 20 + Operating pressure Pa 1.01e+06 + Relative humidity of hot gas 0.8 + Operating temperature K 343 + Length to diameter ratio 25 + Moisture content of solids 0.1 + Design results Evaporation kg/hr 2.09e+04 + Volume 1.05e+03 + Diameter m 3.76 + Length 94 + Peripheral drum area m2 1.11e+03 + Purchase cost Drum dryer USD 1.2e+06 + Total purchase cost USD 1.2e+06 + Installed equipment cost USD 2.46e+06 + Utility cost USD/hr 600 """ # auxiliary_unit_names = ('heat_exchanger',) @@ -192,6 +201,21 @@ class DrumDryer(Unit): 'Diameter': 'm'} _N_ins = 3 _N_outs = 3 + P: float = design_input('Operating pressure', 'Pa', lb=0) + T: float = design_input('Operating temperature', 'K', lb=0) + RH: float = design_input( + 'Relative humidity of hot gas', lb=0, ub=1, + ) + H: float = design_input( + 'Specific evaporation rate', 'kg/hr/m3', lb=0, + ) + length_to_diameter: float = design_input( + 'Length to diameter ratio', lb=0, + doc="Length to diameter ratio of the drum. Note that the drum is horizontal.", + ) + moisture_content: float = design_input( + 'Moisture content of solids', lb=0, ub=1, + ) @property def isplit(self): @@ -327,6 +351,8 @@ class ThermalOxidizer(Unit): _N_outs = 1 max_volume = 20. # m3 _F_BM_default = {'Vessels': 2.06} # Assume same as dryer + tau: float = design_input('Residence time', 'hr', lb=0) + duty_per_kg: float = design_input('Duty per kg of feed', 'kJ/kg') @property def natural_gas(self): diff --git a/biosteam/units/fluidized_catalytic_cracking.py b/biosteam/units/fluidized_catalytic_cracking.py index 1acc9535e..f1030a245 100644 --- a/biosteam/units/fluidized_catalytic_cracking.py +++ b/biosteam/units/fluidized_catalytic_cracking.py @@ -2,6 +2,7 @@ """ """ import biosteam as bst +from .._unit import design_input from math import sqrt, pi import numpy as np @@ -42,6 +43,59 @@ class FluidizedCatalyticCracking(bst.Unit): 'pump', 'feed_preheater', 'air_compressor', 'condenser', 'riser', 'stripper', 'regenerator', 'reactor', ) + vessel_material: str = design_input() + feed_pressure: float = design_input('Feed pressure', 'Pa', lb=0) + feed_vapor_fraction: float = design_input( + 'Feed vapor fraction', lb=0, ub=1, + ) + catalyst_to_feed_ratio: float = design_input( + 'Catalyst to feed ratio', lb=0, + doc="Catalyst to feed ratio by weight.", + ) + spent_catalyst_to_feed_ratio: float = design_input( + 'Spent catalyst to feed ratio', lb=0, + doc="Spent catalyst to feed ratio by weight.", + ) + product_loss: float = design_input( + 'Product loss', lb=0, ub=1, + doc="Fraction of product lost (combusted in the regenerator).", + ) + riser_product_residence_time: float = design_input( + 'Riser product residence time', 'hr', lb=0, + ) + riser_length_to_diameter: float = design_input( + 'Riser length to diameter ratio', lb=0, + ) + reactor_vessel_dissengagement_height: float = design_input( + 'Reactor vessel disengagement height', 'm', lb=0, + ) + reactor_vessel_exit_velocity: float = design_input( + 'Reactor vessel exit velocity', 'm/hr', lb=0, + ) + stripper_steam_rate: float = design_input( + 'Stripper steam rate', lb=0, + doc="Steam to catalyst ratio in the stripper by weight.", + ) + stripper_catalyst_residence_time: float = design_input( + 'Stripper catalyst residence time', 'hr', lb=0, + ) + stripper_length_to_diameter: float = design_input( + 'Stripper length to diameter ratio', lb=0, + ) + CO2_concentration: float = design_input( + 'CO2 concentration', lb=0, ub=1, + doc="Maximum CO2 mass fraction in the flue gas; air flow is increased " + "to meet this concentration.", + ) + regenerator_catalyst_residence_time: float = design_input( + 'Regenerator catalyst residence time', 'hr', lb=0, + ) + regenerator_length_to_diameter: float = design_input( + 'Regenerator length to diameter ratio', lb=0, + ) + regenerator_pressure: float = design_input( + 'Regenerator pressure', 'Pa', lb=0, + ) def _init( self, diff --git a/biosteam/units/gas_fed_bioreactor.py b/biosteam/units/gas_fed_bioreactor.py index 16374c3f3..fb10abacd 100644 --- a/biosteam/units/gas_fed_bioreactor.py +++ b/biosteam/units/gas_fed_bioreactor.py @@ -19,6 +19,7 @@ import biosteam as bst from .abstract_stirred_tank_reactor import AbstractStirredTankReactor from .aerated_bioreactor import AeratedBioreactor +from .._unit import design_input from math import pi import numpy as np from scipy.constants import g @@ -247,6 +248,22 @@ class GasFedBioreactor(AbstractStirredTankReactor): default_methods = AeratedBioreactor.default_methods get_kLa = AeratedBioreactor.get_kLa get_agitation_power = AeratedBioreactor.get_agitation_power + compressor_isentropic_efficiency: float = design_input( + 'Compressor isentropic efficiency', lb=0, ub=1, + ) + cooler_pressure_drop: float = design_input( + 'Cooler pressure drop', 'Pa', lb=0, + ) + theta: float = design_input( + 'Gas substrate saturation fraction', lb=0, ub=1, + doc="Average concentration of gas substrate in the liquid as a " + "fraction of saturation.", + ) + Q_consumption: float = design_input( + 'Duty per gas substrate consumed', 'kJ/kmol', + doc="Forced duty per gas substrate consumed [kJ/kmol]. If not given, " + "the duty is computed from the energy balance.", + ) def _init(self, gas_substrates=None, @@ -274,11 +291,10 @@ def _init(self, gas_substrates = [i for i in gas_substrates if i in aeration.H_coefficients] self.gas_substrates = gas_substrates - #: Isentropic efficiency of the compressor. Defaults to 0.85. self.compressor_isentropic_efficiency = compressor_isentropic_efficiency self.cooler_pressure_drop = 20684.28 if cooler_pressure_drop is None else cooler_pressure_drop - self.theta = theta # Average concentration of gas substrate in the liquid as a fraction of saturation. - self.Q_consumption = Q_consumption # Forced duty per gas substrate consummed [kJ/kmol]. + self.theta = theta + self.Q_consumption = Q_consumption self.kLa_kwargs = {} if kLa_kwargs is None else kLa_kwargs self.controlled_feeds = controlled_feeds # list[int|Stream] Feed index or stream. self.titer = titer # dict[str, float] g / L diff --git a/biosteam/units/heat_exchange.py b/biosteam/units/heat_exchange.py index eefda1e70..33d3bc751 100644 --- a/biosteam/units/heat_exchange.py +++ b/biosteam/units/heat_exchange.py @@ -312,7 +312,10 @@ class HXutility(HX): Low pressure steam Duty kJ/hr 1.01e+06 Flow kmol/hr 26.2 Cost USD/hr 6.22 - Design Area ft^2 59.9 + Design inputs Number of shells 2 + Outlet temperature K 323 + Furnace pressure psig 500 + Design results Area ft^2 59.9 Overall heat transfer coefficient kW/m^2/K 0.5 Log-mean temperature difference K 101 Fouling correction factor 1 @@ -346,7 +349,10 @@ class HXutility(HX): Low pressure steam Duty kJ/hr 1.94e+07 Flow kmol/hr 500 Cost USD/hr 119 - Design Area ft^2 716 + Design inputs Number of shells 2 + Outlet vapor fraction 1 + Furnace pressure psig 500 + Design results Area ft^2 716 Overall heat transfer coefficient kW/m^2/K 1 Log-mean temperature difference K 80.8 Fouling correction factor 1 @@ -366,7 +372,11 @@ class HXutility(HX): Low pressure steam Duty kJ/hr 1.84e+07 Flow kmol/hr 475 Cost USD/hr 113 - Design Area ft^2 680 + Design inputs Number of shells 2 + Outlet vapor fraction 1 + Furnace pressure psig 500 + Heat transfer efficiency 1 + Design results Area ft^2 680 Overall heat transfer coefficient kW/m^2/K 1 Log-mean temperature difference K 80.8 Fouling correction factor 1 @@ -400,7 +410,10 @@ class HXutility(HX): Low pressure steam Duty kJ/hr 1.94e+07 Flow kmol/hr 500 Cost USD/hr 119 - Design Area ft^2 716 + Design inputs Number of shells 2 + Outlet vapor fraction 1 + Furnace pressure psig 500 + Design results Area ft^2 716 Overall heat transfer coefficient kW/m^2/K 1 Log-mean temperature difference K 80.8 Fouling correction factor 1 @@ -420,7 +433,11 @@ class HXutility(HX): Low pressure steam Duty kJ/hr 1.84e+07 Flow kmol/hr 475 Cost USD/hr 113 - Design Area ft^2 680 + Design inputs Number of shells 2 + Outlet vapor fraction 1 + Furnace pressure psig 500 + Heat transfer efficiency 1 + Design results Area ft^2 680 Overall heat transfer coefficient kW/m^2/K 1 Log-mean temperature difference K 80.8 Fouling correction factor 1 @@ -434,6 +451,37 @@ class HXutility(HX): """ line = 'Heat exchanger' _graphics = utility_heat_exchanger_graphics + T: float = bst.design_input('Outlet temperature', 'K', lb=0) + V: float = bst.design_input('Outlet vapor fraction', lb=0, ub=1) + H: float = bst.design_input('Outlet enthalpy', 'kJ/hr') + U: float = bst.design_input( + 'Overall heat transfer coefficient', 'kW/m^2/K', lb=0, + doc="Enforced overall heat transfer coefficient [kW/m^2/K]. " + "If None, it is estimated heuristically.", + ) + N_shells: int = bst.design_input( + 'Number of shells', lb=1, + doc="Number of shells for LMTD correction factor method.", + ) + ft: float = bst.design_input( + 'LMTD correction factor', + doc="User imposed LMTD correction factor. If None, it is estimated " + "using the number of shells.", + ) + dP: float = bst.design_input( + 'Pressure drop', 'Pa', doc="Pressure drop along the process fluid [Pa].", + ) + heat_transfer_efficiency: float = bst.design_input( + lb=0, ub=1, + doc="User enforced heat transfer efficiency. A value less than 1 " + "means that a fraction of heat transfered is lost to the environment. " + "If value is None, it defaults to the heat transfer efficiency of the " + "heat utility.", + ) + furnace_pressure: float = bst.design_input( + 'Furnace pressure', 'psig', + doc="Internal pressure of combustion gas [psig]. Defaults to 500 psig.", + ) def _init(self, T=None, V=None, rigorous=False, U=None, H=None, @@ -449,20 +497,15 @@ def _init(self, furnace_pressure=None, # [Pa] equivalent to 500 psig utility_agent=None, ): - self.T = T # : [float] Temperature of outlet stream (K). - self.V = V # : [float] Vapor fraction of outlet stream. - self.H = H # : [float] Enthalpy of outlet stream. + self.T = T + self.V = V + self.H = H #: [bool] If true, calculate vapor liquid equilibrium self.rigorous = rigorous - #: [float] Enforced overall heat transfer coefficent (kW/m^2/K) self.U = U - - #: [int] Number of shells for LMTD correction factor method. self.N_shells = N_shells - - #: [float] User imposed correction factor. self.ft = ft #: [bool] If True, heat exchanger can only heat. @@ -474,20 +517,12 @@ def _init(self, self.material = material self.heat_exchanger_type = heat_exchanger_type - #: Optional[float] Pressure drop along the process fluid. self.dP = dP #: [bool] Whether to estimate pressure drop if none given. self.estimate_pressure_drop = estimate_pressure_drop - #: [bool] User enforced heat transfer efficiency. A value less than 1 - #: means that a fraction of heat transfered is lost to the environment. - #: If value is None, it defaults to the heat transfer efficiency of the - #: heat utility. self.heat_transfer_efficiency = heat_transfer_efficiency - - #: Optional[float] Internal pressure of combustion gas. Defaults - #: 500 psig (equivalent to 3548325.0 Pa) self.furnace_pressure = 500 if furnace_pressure is None else furnace_pressure #: Optional[UtilityAgent] Utility agent proving heating/cooling duty. @@ -818,7 +853,11 @@ class HXprocess(HX): >>> hx.results() Heat exchanger Units hx - Design Area ft^2 213 + Design inputs Number of shells 2 + Pressure drop 0 Pa 0 + Pressure drop 1 Pa 0 + Pinch temperature difference K 5 + Design results Area ft^2 213 Overall heat transfer coefficient kW/m^2/K 0.5 Log-mean temperature difference K 20.4 Fouling correction factor 1 @@ -858,7 +897,13 @@ class HXprocess(HX): flow (kmol/hr): Ethanol 200 >>> hx.results() Heat exchanger Units hx - Design Area ft^2 369 + Design inputs Number of shells 2 + Pressure drop 0 Pa 0 + Pressure drop 1 Pa 0 + Pinch temperature difference K 5 + Outlet phase 0 l + Outlet phase 1 l + Design results Area ft^2 369 Overall heat transfer coefficient kW/m^2/K 0.5 Log-mean temperature difference K 11.4 Fouling correction factor 1 @@ -870,7 +915,13 @@ class HXprocess(HX): Utility cost USD/hr 0 >>> hx.results() Heat exchanger Units hx - Design Area ft^2 369 + Design inputs Number of shells 2 + Pressure drop 0 Pa 0 + Pressure drop 1 Pa 0 + Pinch temperature difference K 5 + Outlet phase 0 l + Outlet phase 1 l + Design results Area ft^2 369 Overall heat transfer coefficient kW/m^2/K 0.5 Log-mean temperature difference K 11.4 Fouling correction factor 1 @@ -982,6 +1033,56 @@ def _create_material_balance_equations(self, composition_sensitive): ) return equations + U: float = bst.design_input( + 'Overall heat transfer coefficient', 'kW/m^2/K', lb=0, + doc="Enforced overall heat transfer coefficient [kW/m^2/K]. " + "If None, it is estimated heuristically.", + ) + N_shells: int = bst.design_input( + 'Number of shells', lb=1, + doc="Number of shells for LMTD correction factor method.", + ) + ft: float = bst.design_input( + 'LMTD correction factor', + doc="User imposed LMTD correction factor. If None, it is estimated " + "using the number of shells.", + ) + dT: float = bst.design_input( + 'Pinch temperature difference', 'K', lb=0, + ) + T_lim0: float = bst.design_input( + 'Outlet temperature limit 0', 'K', lb=0, + doc="Temperature limit of outlet stream at index 0 [K].", + ) + T_lim1: float = bst.design_input( + 'Outlet temperature limit 1', 'K', lb=0, + doc="Temperature limit of outlet stream at index 1 [K].", + ) + H_lim0: float = bst.design_input( + 'Outlet enthalpy limit 0', 'kJ/hr', + doc="Enthalpy limit of outlet stream at index 0 [kJ/hr].", + ) + H_lim1: float = bst.design_input( + 'Outlet enthalpy limit 1', 'kJ/hr', + doc="Enthalpy limit of outlet stream at index 1 [kJ/hr].", + ) + phase0: str = bst.design_input( + 'Outlet phase 0', + doc="Enforced phase of outlet at index 0 ('l' or 'g').", + ) + phase1: str = bst.design_input( + 'Outlet phase 1', + doc="Enforced phase of outlet at index 1 ('l' or 'g').", + ) + dP0: float = bst.design_input( + 'Pressure drop 0', 'Pa', + doc="Pressure drop along fluid at index 0 [Pa].", + ) + dP1: float = bst.design_input( + 'Pressure drop 1', 'Pa', + doc="Pressure drop along fluid at index 1 [Pa].", + ) + def _init(self, U=None, dT=5., T_lim0=None, T_lim1=None, material="Carbon steel/carbon steel", @@ -995,47 +1096,26 @@ def _init(self, dP1=None, # Defaults to 0 estimate_pressure_drop=False, ): - #: [float] Enforced overall heat transfer coefficent (kW/m^2/K) self.U = U #: [float] Total heat transfered in kJ/hr (not including losses). self.total_heat_transfer = None - #: Number of shells for LMTD correction factor method. self.N_shells = N_shells - - #: User imposed correction factor. self.ft = ft - - #: [float] Pinch temperature difference. self.dT = dT - - #: [float] Temperature limit of outlet stream at index 0. self.T_lim0 = T_lim0 - - #: [float] Temperature limit of outlet stream at index 1. self.T_lim1 = T_lim1 - - #: [float] Enthalpy limit of outlet stream at index 0. self.H_lim0 = H_lim0 - - #: [float] Enthalpy limit of outlet stream at index 1. self.H_lim1 = H_lim1 - - #: [str] Enforced phase of outlet at index 0 self.phase0 = phase0 - - #: [str] Enforced phase of outlet at index 1 self.phase1 = phase1 self.material = material self.heat_exchanger_type = heat_exchanger_type self.reset_streams_at_setup = False - #: Optional[float] Pressure drop along the fluid [0]. self.dP0 = 0 if dP0 is None else dP0 - - #: Optional[float] Pressure drop along fluid [1]. self.dP1 = 0 if dP1 is None else dP1 #: [bool] Whether to estimate pressure drop if none given. diff --git a/biosteam/units/liquid_liquid_extraction.py b/biosteam/units/liquid_liquid_extraction.py index cdbde6ae7..2e3f9d210 100644 --- a/biosteam/units/liquid_liquid_extraction.py +++ b/biosteam/units/liquid_liquid_extraction.py @@ -48,6 +48,7 @@ from .stage import MultiStageEquilibrium from .._graphics import mixer_settler_graphics from .. import Unit +from .._unit import design_input from thermosteam import separations as sep import numpy as np @@ -111,12 +112,15 @@ class LLEUnit(bst.Unit, isabstract=True): """ _N_outs = 2 + efficiency: float = design_input( + lb=0, ub=1, + doc="Fraction of feed in liquid-liquid equilibrium. " + "The rest of the feed is divided equally between phases.", + ) def _init(self, top_chemical=None, efficiency=1.0, forced_split_IDs=None, forced_split=None): #: [str] Identifier of chemical that will be favored in the low density phase. self.top_chemical = top_chemical - #: [float] Fraction of feed in liquid-liquid equilibrium. - #: The rest of the feed is divided equally between phases. self.efficiency = efficiency #: array[float] Forced splits to 0th stream for given IDs. self.forced_split = forced_split @@ -308,7 +312,8 @@ class LLECentrifuge(LLEUnit, LiquidsCentrifuge): Liquids centrifuge Units C1 Electricity Power kW 17.5 Cost USD/hr 1.37 - Design Flow rate m^3/hr 12.5 + Design inputs Efficiency 1 + Design results Flow rate m^3/hr 12.5 Purchase cost Liquids centrifuge USD 1.29e+05 Total purchase cost USD 1.29e+05 Installed equipment cost USD 2.62e+05 @@ -385,7 +390,9 @@ class SLLECentrifuge(Unit): 3-Phase decanter centrifuge Units C1 Electricity Power kW 0.0101 Cost USD/hr 0.000792 - Design Flow rate L/min 250 + Design inputs Efficiency 1 + Retentate moisture content 0.5 + Design results Flow rate L/min 250 Purchase cost 3-Phase decanter centrifuge USD 2.88e+05 Total purchase cost USD 2.88e+05 Installed equipment cost USD 5.84e+05 @@ -397,6 +404,14 @@ class SLLECentrifuge(Unit): _N_outs = 3 solvent = LLEUnit.solvent + efficiency: float = design_input( + lb=0, ub=1, + doc="Fraction of feed in liquid-liquid equilibrium. " + "The rest of the feed is divided equally between phases.", + ) + moisture_content: float = design_input( + 'Retentate moisture content', lb=0, ub=1, + ) @property def solids_split(self): @@ -413,11 +428,7 @@ def _init(self, solids_split, top_chemical=None, efficiency=1.0, #: [str] Identifier of chemical that will be favored in the low density phase. self.top_chemical = top_chemical - #: Fraction of feed in liquid-liquid equilibrium. - #: The rest of the feed is divided equally between phases. self.efficiency = efficiency - - #: Moisture content of retentate self.moisture_content = moisture_content assert self._solids_isplit['7732-18-5'] == 0, 'cannot define water split, only moisture content' @@ -498,7 +509,8 @@ class SolidLiquidsSplitCentrifuge(Unit): 3-Phase decanter centrifuge Units C1 Electricity Power kW 0.0101 Cost USD/hr 0.000792 - Design Flow rate L/min 250 + Design inputs Retentate moisture content 0.5 + Design results Flow rate L/min 250 Purchase cost 3-Phase decanter centrifuge USD 2.88e+05 Total purchase cost USD 2.88e+05 Installed equipment cost USD 5.84e+05 @@ -508,6 +520,9 @@ class SolidLiquidsSplitCentrifuge(Unit): line = SLLECentrifuge.line _N_ins = 1 _N_outs = 3 + moisture_content: float = design_input( + 'Retentate moisture content', lb=0, ub=1, + ) @property def solids_split(self): @@ -530,7 +545,6 @@ def _init(self, aqueous_split, solids_split, moisture_content=0.5): # [ChemicalIndexer] Splits to 2th outlet stream (solids) from aqueous/heavy phase. self._solids_isplit = self.thermo.chemicals.isplit(solids_split) - #: Moisture content of retentate self.moisture_content = moisture_content assert self._solids_isplit['7732-18-5'] == 0, 'cannot define water split to solids, only moisture content' @@ -575,6 +589,13 @@ class LiquidsMixingTank(PressureVessel, bst.Unit): _ins_size_is_fixed = False _N_ins = 3 _N_outs = 1 + tau: float = design_input('Residence time', 'hr', lb=0) + agitator_kW_per_m3: float = design_input( + 'Agitator power per volume', 'kW/m3', lb=0, + ) + length_to_diameter: float = design_input( + 'Length to diameter ratio', lb=0, + ) def _init(self, tau=0.022, agitator_kW_per_m3=1.0, length_to_diameter=1, vessel_material='Carbon steel', vessel_type='Vertical'): @@ -633,6 +654,13 @@ class LiquidsSettler(PressureVessel, bst.Unit, isabstract=True): """ _N_ins = 1 _N_outs = 2 + length_to_diameter: float = design_input( + 'Length to diameter ratio', lb=0, + ) + area_to_feed: float = design_input( + 'Area to feed ratio', 'ft2/gpm', lb=0, + doc="Diameter * length per gpm of feed [ft2/gpm].", + ) def _init(self, area_to_feed=0.1, length_to_diameter=4, @@ -640,8 +668,8 @@ def _init(self, area_to_feed=0.1, vessel_type='Horizontal'): self.vessel_material = vessel_material self.vessel_type = vessel_type - self.length_to_diameter = length_to_diameter #: Length to diameter ratio - self.area_to_feed = area_to_feed #: [ft2/gpm] Diameter * length per gpm of feed + self.length_to_diameter = length_to_diameter + self.area_to_feed = area_to_feed @staticmethod def _default_vessel_type(): diff --git a/biosteam/units/mixing.py b/biosteam/units/mixing.py index d141cb16d..1c26e8e2b 100644 --- a/biosteam/units/mixing.py +++ b/biosteam/units/mixing.py @@ -13,7 +13,7 @@ .. autoclass:: biosteam.units.mixing.MockMixer """ -from .._unit import Unit +from .._unit import Unit, design_input from .._graphics import mixer_graphics import flexsolve as flx import biosteam as bst @@ -330,6 +330,12 @@ class SteamMixer(Unit): _ins_size_is_fixed = False _graphics = mixer_graphics installation_cost = purchase_cost = 0. + P: float = design_input('Outlet pressure', 'Pa', lb=0) + T: float = design_input('Outlet temperature', 'K', lb=0) + solids_loading: float = design_input( + lb=0, ub=1, doc="Final solids loading (by wt) after mixing in process water." + ) + def _init(self, P, T=None, solids_loading=None, liquid_IDs=['7732-18-5'], solid_IDs=None, solids_loading_includes_steam=None): diff --git a/biosteam/units/molecular_sieve.py b/biosteam/units/molecular_sieve.py index a504a8292..30749ebf4 100644 --- a/biosteam/units/molecular_sieve.py +++ b/biosteam/units/molecular_sieve.py @@ -73,7 +73,7 @@ class MolecularSieve(Splitter): Cooling water Duty kJ/hr -1.18e+05 Flow kmol/hr 80.9 Cost USD/hr 0.0395 - Design Flow rate kg/hr 2.13e+03 + Design results Flow rate kg/hr 2.13e+03 Purchase cost Column USD 6.85e+05 Total purchase cost USD 6.85e+05 Installed equipment cost USD 1.23e+06 diff --git a/biosteam/units/nrel_bioreactor.py b/biosteam/units/nrel_bioreactor.py index c0412b2b1..c1f8d4bd4 100644 --- a/biosteam/units/nrel_bioreactor.py +++ b/biosteam/units/nrel_bioreactor.py @@ -42,6 +42,7 @@ """ import numpy as np from .. import Unit +from .._unit import design_input from .design_tools import size_batch from .decorators import cost from math import ceil @@ -121,22 +122,23 @@ class NRELAnaerobicBatchBioreactor(Unit, isabstract=True): _N_outs = 2 _N_ins = 1 - #: [float] Cleaning and unloading time (hr). - tau_0 = 3 - - #: [float] Fraction of filled tank to total tank volume. - V_wf = 0.9 - - def _get_design_info(self): - return (('Cleaning and unloading time', self.tau_0, 'hr'), - ('Working volume fraction', self.V_wf, '')) - + tau: float = design_input('Reaction time', 'hr', lb=0) + tau_0: float = design_input('Cleaning and unloading time', 'hr', lb=0) + V_wf: float = design_input('Working volume fraction', lb=0, ub=1) + T: float = design_input('Operating temperature', 'K', lb=0) + loading_time: float = design_input( + 'Loading time', 'hr', lb=0, + doc="Loading time of batch reactor [hr]. If not given, it will assume " + "each vessel is constantly being filled.", + ) + _tau_0 = 3 # Default cleaning and unloading time [hr] + _V_wf = 0.9 # Default fraction of filled tank to total tank volume + def _init(self, tau=None, N=None, V_max=None, T=305.15, P=101325, Nmin=2, Nmax=36, loading_time=None, reactions=None, tau_0=None, V_wf=None): self._N = N; self._V_max = V_max - #: [float] Reaction time [hr]. self.tau = tau #: [int] Number of batch reactors. @@ -145,7 +147,6 @@ def _init(self, tau=None, N=None, V_max=None, T=305.15, P=101325, #: [float] Maximum volume of a fermentor [m3]. if V_max: self.V_max = V_max - #: [float] Operating temperature of reactor [K]. self.T = T #: [float] Operating pressure of reactor [Pa]. @@ -157,8 +158,6 @@ def _init(self, tau=None, N=None, V_max=None, T=305.15, P=101325, #: [int] Maximum number of fermentors. self.Nmax = Nmax - #: [int] Loading time of batch reactor. - #: If not given, it will assume each vessel is constantly being filled. self.loading_time = loading_time #: [Reaction|ReactionSystem] Stoichiomentric reactions. @@ -212,13 +211,6 @@ def V_max(self, V_max): assert not self._N, 'cannot specify both reactor volume and number of reactors' self._V_max = V_max - @property - def tau(self): - return self._tau - @tau.setter - def tau(self, tau): - self._tau = tau - def _run(self): vent, effluent = self.outs effluent.mix_from(self.ins) @@ -335,14 +327,16 @@ class NRELEthanolFermentation(NRELAnaerobicBatchBioreactor): Chilled water Duty kJ/hr -1.41e+07 Flow kmol/hr 9.42e+03 Cost USD/hr 70.3 - Design Reactor volume m3 247 + Design inputs Operating temperature K 305 + Working volume fraction 0.9 + Reaction time hr 8 + Cleaning and unloading time hr 3 + Design results Reactor volume m3 247 Batch time hr 12.6 Loading time hr 1.57 Number of reactors 8 Recirculation flow rate m3/hr 17.7 Reactor duty kJ/hr -1.41e+07 - Cleaning and unloading time hr 3 - Working volume fraction 0.9 Purchase cost Heat exchangers (x8) USD 1.57e+05 Reactors (x8) USD 1.87e+06 Agitators (x8) USD 1.17e+05 diff --git a/biosteam/units/size_enlargement.py b/biosteam/units/size_enlargement.py index 74350d303..37438ae24 100644 --- a/biosteam/units/size_enlargement.py +++ b/biosteam/units/size_enlargement.py @@ -13,7 +13,7 @@ """ from .decorators import cost -from .._unit import Unit +from .._unit import Unit, design_input from warnings import warn __all__ = ('PelletMill', 'BagassePelletMill') @@ -45,6 +45,7 @@ class PelletMill(Unit): """ _N_ins = 1 _N_outs = 1 + kW: float = design_input('Power requirement', 'kWh/kg', lb=0) def _init(self, kW=0.01): self.kW = kW diff --git a/biosteam/units/solids_separation.py b/biosteam/units/solids_separation.py index 3e41ec1c3..645c15f4f 100644 --- a/biosteam/units/solids_separation.py +++ b/biosteam/units/solids_separation.py @@ -64,6 +64,9 @@ class SolidsSeparator(Splitter): """ _N_ins = 1 _ins_size_is_fixed = False + moisture_content: float = bst.design_input( + 'Retentate moisture content', lb=0, ub=1, + ) def _init(self, split, order=None, moisture_content=None, @@ -71,7 +74,6 @@ def _init(self, split, strict_moisture_content=None ): Splitter._init(self, order=order, split=split) - #: Moisture content of retentate self.moisture_content = moisture_content self.strict_moisture_content = strict_moisture_content if moisture_content is not None: diff --git a/biosteam/units/splitting.py b/biosteam/units/splitting.py index 0020b9cef..e47d61570 100644 --- a/biosteam/units/splitting.py +++ b/biosteam/units/splitting.py @@ -337,6 +337,8 @@ class Separator(Unit): _N_ins = 1 _N_outs = 2 _ins_size_is_fixed = False + T: float = bst.design_input('Operating temperature', 'K', lb=0) + P: float = bst.design_input('Operating pressure', 'Pa', lb=0) @property def equation_node_names(self): @@ -411,26 +413,7 @@ def _init(self, split, order=None, T=None, P=None, phases=None): self.T = T self.P = P self.phases = phases - - def _mass_and_energy_balance_specifications(self): - isplit = self._isplit - specs = [ - (i.ID + ' split', j * 100, '%') for i, j in zip(isplit.chemicals, isplit.data) - ] - if self.T is not None: - specs.append( - ('T', self.T, 'K') - ) - if self.phases is not None: - specs.append( - ('Phases', self.phases, '-') - ) - if self.P is not None: - specs.append( - ('P', self.P, 'Pa') - ) - return 'Separator', specs - + def _run(self): ins = self._ins top, bottom = self._outs diff --git a/biosteam/units/stage.py b/biosteam/units/stage.py index c31f17582..d9cb6edea 100644 --- a/biosteam/units/stage.py +++ b/biosteam/units/stage.py @@ -31,6 +31,7 @@ from scipy.spatial.distance import cdist from collections import deque from .. import Unit +from .._unit import design_input from .design_tools import MESH from numba import float64, int16, types, njit from typing import NamedTuple, Iterable @@ -2209,6 +2210,7 @@ class MultiStageEquilibrium(Unit): sequential_runs_init = 0 phenomena_runs_init = 0 + N_stages: int = design_input('Number of stages', lb=1) def __init_subclass__(cls, *args, **kwargs): super().__init_subclass__(cls, *args, **kwargs) diff --git a/biosteam/units/tank.py b/biosteam/units/tank.py index 933556c00..c379d0d08 100644 --- a/biosteam/units/tank.py +++ b/biosteam/units/tank.py @@ -35,7 +35,7 @@ storage_tank_purchase_cost_algorithms, mix_tank_purchase_cost_algorithms) from ..utils import ExponentialFunctor -from .._unit import Unit +from .._unit import Unit, design_input from .mixing import Mixer __all__ = ('Tank', 'MixTank', 'StorageTank', 'tank_factory') @@ -106,13 +106,15 @@ def tank_factory(name, *, CE, cost, S, tau, n=0.6, kW_per_m3=0., V_wf=0.9, phase: 'l', T: 298.15 K, P: 101325 Pa flow (kmol/hr): Corn 4.64e+04 >>> T101.results() - Corn storage Units T101 - Design Residence time hr 259 - Total volume m^3 1.33e+04 - Purchase cost Tank USD 7.62e+04 - Total purchase cost USD 7.62e+04 - Installed equipment cost USD 1.75e+05 - Utility cost USD/hr 0 + Corn storage Units T101 + Design inputs Working volume fraction 0.9 + Electricity requirement kW/m^3 0 + Residence time hr 259 + Design results Total volume m^3 1.33e+04 + Purchase cost Tank USD 7.62e+04 + Total purchase cost USD 7.62e+04 + Installed equipment cost USD 1.75e+05 + Utility cost USD/hr 0 """ dct = { @@ -187,11 +189,14 @@ class Tank(Unit, isabstract=True): """ _default_kW_per_m3 = 0. - _units = {'Total volume': 'm^3', - 'Residence time': 'hr'} + _units = {'Total volume': 'm^3'} _F_BM_default = {'Tank': 2.3} _N_outs = 1 + tau: float = design_input('Residence time', 'hr', lb=0) + V_wf: float = design_input('Working volume fraction', lb=0, ub=1) + kW_per_m3: float = design_input('Electricity requirement', 'kW/m^3', lb=0) + def _init(self, vessel_type=None, tau=None, V_wf=None, vessel_material=None, kW_per_m3=0. @@ -199,16 +204,13 @@ def _init(self, # [str] Vessel type. self.vessel_type = vessel_type or self._default_vessel_type - #: [float] Residence time in hours. self.tau = tau or self._default_tau - #: [float] Fraction of working volume to total volume. self.V_wf = V_wf or self._default_V_wf #: [str] Vessel construction material. self.vessel_material = vessel_material or self._default_vessel_material - # [float] Electricity requirement per unit volume [kW/m^3]. self.kW_per_m3 = kW_per_m3 or self._default_kW_per_m3 def __init_subclass__(cls, isabstract=False): @@ -259,8 +261,7 @@ def vessel_type(self, vessel_type): def _design(self): design_results = self.design_results - design_results['Residence time'] = tau = self.tau - design_results['Total volume'] = tau * self.F_vol_out / self.V_wf + design_results['Total volume'] = self.tau * self.F_vol_out / self.V_wf def _cost(self): design_results = self.design_results @@ -335,13 +336,15 @@ class StorageTank(Tank): phase: 'l', T: 298.15 K, P: 101325 Pa flow (kg/hr): Ethanol 2.3e+04 >>> T1.results() - Storage tank Units T1 - Design Residence time hr 168 - Total volume m^3 4.92e+03 - Purchase cost Tank (x2) USD 8.42e+05 - Total purchase cost USD 8.42e+05 - Installed equipment cost USD 1.94e+06 - Utility cost USD/hr 0 + Storage tank Units T1 + Design inputs Working volume fraction 1 + Electricity requirement kW/m^3 0 + Residence time hr 168 + Design results Total volume m^3 4.92e+03 + Purchase cost Tank (x2) USD 8.42e+05 + Total purchase cost USD 8.42e+05 + Installed equipment cost USD 1.94e+06 + Utility cost USD/hr 0 """ _outs_size_is_fixed = _ins_size_is_fixed = True diff --git a/biosteam/units/turbine.py b/biosteam/units/turbine.py index f210d3d14..faab8ea34 100644 --- a/biosteam/units/turbine.py +++ b/biosteam/units/turbine.py @@ -31,7 +31,6 @@ #: TODO: #: * Implement estimate of isentropic efficiency when not given (is this possible?). - class TurbineCostAlgorithm(NamedTuple): #: Defines preliminary correlation algorithm for a turbine type psig_max: float #: Maximum achievable pressure in psig (to autodermine turbine type and/or issue warning) @@ -51,15 +50,7 @@ class Turbine(Unit, isabstract=True): _graphics = turbine_graphics _N_ins = 1 _N_outs = 1 - _units = { - 'Ideal power': 'kW', - 'Ideal duty': 'kJ/hr', - } - material_factors = { - 'Carbon steel': 1.0, - 'Stainless steel': 2.5, - 'Nickel alloy': 5.0, - } + _F_BM_default = { 'Turbine(s)': 2.15, } @@ -73,11 +64,19 @@ class Turbine(Unit, isabstract=True): CE=567, ), } + material_factors = { + 'Carbon steel': 1.0, + 'Stainless steel': 2.5, + 'Nickel alloy': 5.0 + } + + P: float = bst.design_input('Outlet pressure', 'Pa', lb=0) + eta: float = bst.design_input('Isentropic efficiency', lb=0, ub=1) def _init(self, P, eta=0.3, vle=False, turbine_type=None, material=None, efficiency=None): - self.P = P #: Outlet pressure [Pa]. - self.eta = eta #: Isentropic efficiency. + self.P = P + self.eta = eta #: Whether to perform phase equilibrium calculations on the outflow. #: If False, the outlet will be assumed to be the same phase as the inlet. @@ -165,7 +164,7 @@ def _cost(self): Pc = abs(self.power_utility.get_property('rate', 'hp')) N = design_results['Turbines in parallel'] Pc_per_turbine = Pc / N - bounds_warning(self, 'power', Pc, 'hp', alg.hp_bounds, 'cost') + bounds_warning(self, 'power', Pc, 'hp', alg.hp_bounds) self.baseline_purchase_costs['Turbines(s)'] = N * bst.CE / alg.CE * alg.cost(Pc_per_turbine) diff --git a/biosteam/units/vacuum_system.py b/biosteam/units/vacuum_system.py index 8f3323255..ad472903f 100644 --- a/biosteam/units/vacuum_system.py +++ b/biosteam/units/vacuum_system.py @@ -82,7 +82,7 @@ class VacuumSystem(Auxiliary): Cooling water Duty kJ/hr -9.37e+06 Flow kmol/hr 6.4e+03 Cost USD/hr 3.12 - Design Total volume m3 7.23e+04 + Design results Total volume m3 7.23e+04 Purchase cost Vacuum system - Steam-jet ejecto... USD 8.18e+04 Total purchase cost USD 8.18e+04 Installed equipment cost USD 8.18e+04 diff --git a/biosteam/units/valve.py b/biosteam/units/valve.py index 45252a99a..f5262d40c 100644 --- a/biosteam/units/valve.py +++ b/biosteam/units/valve.py @@ -12,7 +12,7 @@ .. autoclass:: biosteam.units.valve.IsenthalpicValve """ -from .. import Unit +from .. import Unit, design_input from .._graphics import valve_graphics from warnings import warn @@ -62,9 +62,10 @@ class IsenthalpicValve(Valve, new_graphics=False): (which serve preliminary techno-economic analysis purposes). """ + P: float = design_input('Outlet pressure', 'Pa', lb=0) def _init(self, P, vle=False): - self.P: float = P #: Outlet pressure [Pa]. + self.P = P #: Whether to perform phase equilibrium calculations on the outflow. #: If False, the outlet will be assumed to be the same phase as the inlet. diff --git a/docs/conf.py b/docs/conf.py index 57631ffb4..fcb5b902b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -326,7 +326,37 @@ def format_annotation(annotation, config, *args, **kwargs): myst_heading_anchors = 3 - - - - +# -- Design inputs ----------------------------------------------------------- + +# Design inputs are property objects, so autodoc documents them as +# properties (name, docstring, and type annotation). The following directive +# renders them with a "design_input" prefix instead of "property". +from sphinx import addnodes +from sphinx.domains.python import PyProperty +from inspect import getattr_static +from importlib import import_module +from biosteam._unit import DesignInput + +def is_design_input(modname, classname, name): + try: + obj = import_module(modname) + for i in classname.split('.'): obj = getattr(obj, i) + return isinstance(getattr_static(obj, name), DesignInput) + except Exception: + return False + +class PyDesignInputAwareProperty(PyProperty): + + def get_signature_prefix(self, sig): + prefix = super().get_signature_prefix(sig) + modname = self.options.get('module', self.env.ref_context.get('py:module')) + classname = self.env.ref_context.get('py:class') + if modname and classname: + name = sig.split(':', 1)[0].strip().rsplit('.', 1)[-1] + if is_design_input(modname, classname, name): + # Replace the "property" keyword (followed by a space) + prefix[-2] = addnodes.desc_sig_keyword('', 'design_input') + return prefix + +def setup(app): + app.add_directive_to_domain('py', 'property', PyDesignInputAwareProperty, override=True) diff --git a/docs/tutorial/Distillation.ipynb b/docs/tutorial/Distillation.ipynb index b4331f37e..0aaf67986 100644 --- a/docs/tutorial/Distillation.ipynb +++ b/docs/tutorial/Distillation.ipynb @@ -197,7 +197,43 @@ " 8.63\n", " \n", " \n", - " Design\n", + " Design inputs\n", + " Operating pressure\n", + " Pa\n", + " 101325\n", + " \n", + " \n", + " Enforced minimum reflux ratio\n", + " \n", + " 0.01\n", + " \n", + " \n", + " Reflux to minimum reflux ratio\n", + " \n", + " 1.25\n", + " \n", + " \n", + " Open tray area fraction\n", + " \n", + " 0.1\n", + " \n", + " \n", + " Tray spacing\n", + " mm\n", + " 450\n", + " \n", + " \n", + " Vacuum system preference\n", + " \n", + " Liquid-ring pump\n", + " \n", + " \n", + " Velocity fraction\n", + " \n", + " 0.8\n", + " \n", + " \n", + " Design results\n", " Theoretical feed stage\n", " \n", " 24\n", @@ -304,7 +340,7 @@ " 5.19e+03\n", " \n", " \n", - " Reflux drum - Vertical pressure vessel\n", + " Reflux drum - Pressure vessel\n", " USD\n", " 7.58e+03\n", " \n", @@ -351,44 +387,51 @@ "" ], "text/plain": [ - "Divided Distillation Column Units McCabeThiele\n", - "Electricity Power kW 0.139\n", - " Cost USD/hr 0.0109\n", - "Cooling water Duty kJ/hr -6.37e+05\n", - " Flow kmol/hr 435\n", - " Cost USD/hr 0.212\n", - "Low pressure steam Duty kJ/hr 1.4e+06\n", - " Flow kmol/hr 36.3\n", - " Cost USD/hr 8.63\n", - "Design Theoretical feed stage 24\n", - " Theoretical stages 29\n", - " Minimum reflux Ratio 0.8\n", - " Reflux Ratio 1\n", - " Rectifier stages 41\n", - " Stripper stages 10\n", - " Rectifier height ft 73\n", - " Stripper height ft 27.3\n", - " Rectifier diameter ft 3\n", - " Stripper diameter ft 3\n", - " Rectifier wall thickness in 0.5\n", - " Stripper wall thickness in 0.312\n", - " Rectifier weight lb 9.16e+03\n", - " Stripper weight lb 3.6e+03\n", - "Purchase cost Rectifier trays USD 2.92e+04\n", - " Stripper trays USD 1.03e+04\n", - " Rectifier tower USD 5.86e+04\n", - " Rectifier platform and ladders USD 2.13e+04\n", - " Stripper tower USD 3.41e+04\n", - " Stripper platform and ladders USD 9.69e+03\n", - " Condenser - Double pipe USD 5.19e+03\n", - " Reflux drum - Vertical pressure ... USD 7.58e+03\n", - " Reflux drum - Platform and ladders USD 1.85e+03\n", - " Pump - Pump USD 4.36e+03\n", - " Pump - Motor USD 141\n", - " Reboiler - Double pipe USD 5.2e+03\n", - "Total purchase cost USD 1.88e+05\n", - "Installed equipment cost USD 6.63e+05\n", - "Utility cost USD/hr 8.85" + "Divided Distillation Column Units McCabeThiele\n", + "Electricity Power kW 0.139\n", + " Cost USD/hr 0.0109\n", + "Cooling water Duty kJ/hr -6.37e+05\n", + " Flow kmol/hr 435\n", + " Cost USD/hr 0.212\n", + "Low pressure steam Duty kJ/hr 1.4e+06\n", + " Flow kmol/hr 36.3\n", + " Cost USD/hr 8.63\n", + "Design inputs Operating pressure Pa 101325\n", + " Enforced minimum reflux ratio 0.01\n", + " Reflux to minimum reflux ratio 1.25\n", + " Open tray area fraction 0.1\n", + " Tray spacing mm 450\n", + " Vacuum system preference Liquid-ring pump\n", + " Velocity fraction 0.8\n", + "Design results Theoretical feed stage 24\n", + " Theoretical stages 29\n", + " Minimum reflux Ratio 0.8\n", + " Reflux Ratio 1\n", + " Rectifier stages 41\n", + " Stripper stages 10\n", + " Rectifier height ft 73\n", + " Stripper height ft 27.3\n", + " Rectifier diameter ft 3\n", + " Stripper diameter ft 3\n", + " Rectifier wall thickness in 0.5\n", + " Stripper wall thickness in 0.312\n", + " Rectifier weight lb 9.16e+03\n", + " Stripper weight lb 3.6e+03\n", + "Purchase cost Rectifier trays USD 2.92e+04\n", + " Stripper trays USD 1.03e+04\n", + " Rectifier tower USD 5.86e+04\n", + " Rectifier platform and ladders USD 2.13e+04\n", + " Stripper tower USD 3.41e+04\n", + " Stripper platform and ladders USD 9.69e+03\n", + " Condenser - Double pipe USD 5.19e+03\n", + " Reflux drum - Pressure vessel USD 7.58e+03\n", + " Reflux drum - Platform and ladders USD 1.85e+03\n", + " Pump - Pump USD 4.36e+03\n", + " Pump - Motor USD 141\n", + " Reboiler - Double pipe USD 5.2e+03\n", + "Total purchase cost USD 1.88e+05\n", + "Installed equipment cost USD 6.63e+05\n", + "Utility cost USD/hr 8.85" ] }, "execution_count": 2, diff --git a/docs/tutorial/Getting_started.ipynb b/docs/tutorial/Getting_started.ipynb index d72fa9603..3dae4b605 100644 --- a/docs/tutorial/Getting_started.ipynb +++ b/docs/tutorial/Getting_started.ipynb @@ -599,12 +599,38 @@ " 3.06\n", " \n", " \n", - " Design\n", + " Design inputs\n", + " Molar vapor fraction\n", + " \n", + " 0.1\n", + " \n", + " \n", + " Holdup time\n", + " s\n", + " 900\n", + " \n", + " \n", + " Surge time\n", + " s\n", + " 450\n", + " \n", + " \n", + " Vacuum system preference\n", + " \n", + " Liquid-ring pump\n", + " \n", + " \n", + " Vessel material\n", + " \n", + " Carbon steel\n", + " \n", + " \n", " Vessel type\n", " \n", " Horizontal\n", " \n", " \n", + " Design results\n", " Length\n", " m\n", " 1.96\n", @@ -625,13 +651,8 @@ " 0.00794\n", " \n", " \n", - " Vessel material\n", - " \n", - " Carbon steel\n", - " \n", - " \n", " Purchase cost\n", - " Horizontal pressure vessel\n", + " Pressure vessel\n", " USD\n", " 1.12e+04\n", " \n", @@ -668,22 +689,26 @@ "" ], "text/plain": [ - "Flash Units flash\n", - "Low pressure steam Duty kJ/hr 5.92e+05\n", - " Flow kmol/hr 15.3\n", - " Cost USD/hr 3.06\n", - "Design Vessel type Horizontal\n", - " Length m 1.96\n", - " Diameter m 1.22\n", - " Weight kg 624\n", - " Wall thickness m 0.00794\n", - " Vessel material Carbon steel\n", - "Purchase cost Horizontal pressure vessel USD 1.12e+04\n", - " Platform and ladders USD 3.21e+03\n", - " Heat exchanger - Double pipe USD 4.3e+03\n", - "Total purchase cost USD 1.87e+04\n", - "Installed equipment cost USD 4.36e+04\n", - "Utility cost USD/hr 3.06" + "Flash Units flash\n", + "Low pressure steam Duty kJ/hr 5.92e+05\n", + " Flow kmol/hr 15.3\n", + " Cost USD/hr 3.06\n", + "Design inputs Molar vapor fraction 0.1\n", + " Holdup time s 900\n", + " Surge time s 450\n", + " Vacuum system preference Liquid-ring pump\n", + " Vessel material Carbon steel\n", + " Vessel type Horizontal\n", + "Design results Length m 1.96\n", + " Diameter m 1.22\n", + " Weight kg 624\n", + " Wall thickness m 0.00794\n", + "Purchase cost Pressure vessel USD 1.12e+04\n", + " Platform and ladders USD 3.21e+03\n", + " Heat exchanger - Double pipe USD 4.3e+03\n", + "Total purchase cost USD 1.87e+04\n", + "Installed equipment cost USD 4.36e+04\n", + "Utility cost USD/hr 3.06" ] }, "execution_count": 20, diff --git a/docs/tutorial/Gotchas.ipynb b/docs/tutorial/Gotchas.ipynb index 4cc3cf38c..070aa155f 100644 --- a/docs/tutorial/Gotchas.ipynb +++ b/docs/tutorial/Gotchas.ipynb @@ -267,20 +267,23 @@ "phase: 'l', T: 298.15 K, P: 101325 Pa\n", "flow (kmol/hr): Water 55.5\n", "\n", - "Pump Units P1\n", - "Electricity Power kW 0.321\n", - " Cost USD/hr 0.0251\n", - "Design Type Centrifugal\n", - " Ideal power hp 0.151\n", - " Flow rate gpm 4.42\n", - " Efficiency 0.352\n", - " Power hp 0.43\n", - " Head ft 386\n", - " Motor size hp 0.5\n", - "Purchase cost Pump USD 4.32e+03\n", - " Motor USD 289\n", - "Total purchase cost USD 4.61e+03\n", - "Utility cost USD/hr 0.0251\n" + "Pump Units P1\n", + "Electricity Power kW 0.321\n", + " Cost USD/hr 0.0251\n", + "Design inputs Outlet pressure Pa 101325\n", + " Design pressure change Pa 405300\n", + "Design results Type Centrifugal\n", + " Ideal power hp 0.151\n", + " Flow rate gpm 4.42\n", + " Efficiency 0.352\n", + " Power hp 0.43\n", + " Head ft 386\n", + " Motor size hp 0.5\n", + "Purchase cost Pump USD 4.32e+03\n", + " Motor USD 289\n", + "Total purchase cost USD 4.61e+03\n", + "Installed equipment cost USD 1.52e+04\n", + "Utility cost USD/hr 0.0251\n" ] } ], diff --git a/docs/tutorial/Inheriting_from_Unit.ipynb b/docs/tutorial/Inheriting_from_Unit.ipynb index 348fc32ec..fc3af5af7 100644 --- a/docs/tutorial/Inheriting_from_Unit.ipynb +++ b/docs/tutorial/Inheriting_from_Unit.ipynb @@ -353,7 +353,7 @@ " 50.4\n", " \n", " \n", - " Design\n", + " Design results\n", " Area\n", " m^2\n", " 24.4\n", @@ -371,6 +371,12 @@ " 1.02e+04\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 2.49e+04\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -381,14 +387,15 @@ "" ], "text/plain": [ - "Boiler Units B1\n", - "Low pressure steam Duty kJ/hr 8.21e+06\n", - " Flow kmol/hr 212\n", - " Cost USD/hr 50.4\n", - "Design Area m^2 24.4\n", - "Purchase cost Boiler USD 1.02e+04\n", - "Total purchase cost USD 1.02e+04\n", - "Utility cost USD/hr 50.4" + "Boiler Units B1\n", + "Low pressure steam Duty kJ/hr 8.21e+06\n", + " Flow kmol/hr 212\n", + " Cost USD/hr 50.4\n", + "Design results Area m^2 24.4\n", + "Purchase cost Boiler USD 1.02e+04\n", + "Total purchase cost USD 1.02e+04\n", + "Installed equipment cost USD 2.49e+04\n", + "Utility cost USD/hr 50.4" ] }, "execution_count": 4, @@ -451,7 +458,7 @@ " 5.04e+03\n", " \n", " \n", - " Design\n", + " Design results\n", " Area\n", " m^2\n", " 610\n", @@ -469,6 +476,12 @@ " 2.38e+05\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 5.84e+05\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -479,14 +492,15 @@ "" ], "text/plain": [ - "Boiler Units B1\n", - "Low pressure steam Duty kJ/hr 8.21e+08\n", - " Flow kmol/hr 2.12e+04\n", - " Cost USD/hr 5.04e+03\n", - "Design Area m^2 610\n", - "Purchase cost Boiler (x4) USD 2.38e+05\n", - "Total purchase cost USD 2.38e+05\n", - "Utility cost USD/hr 5.04e+03" + "Boiler Units B1\n", + "Low pressure steam Duty kJ/hr 8.21e+08\n", + " Flow kmol/hr 2.12e+04\n", + " Cost USD/hr 5.04e+03\n", + "Design results Area m^2 610\n", + "Purchase cost Boiler (x4) USD 2.38e+05\n", + "Total purchase cost USD 2.38e+05\n", + "Installed equipment cost USD 5.84e+05\n", + "Utility cost USD/hr 5.04e+03" ] }, "execution_count": 5, @@ -860,7 +874,7 @@ " 469\n", " \n", " \n", - " Design\n", + " Design results\n", " Flow rate\n", " kg/hr\n", " 1e+06\n", @@ -878,6 +892,12 @@ " 4.03e+06\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 5.6e+06\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -888,13 +908,14 @@ "" ], "text/plain": [ - "Shredder Units shredder\n", - "Electricity Power kW 6e+03\n", - " Cost USD/hr 469\n", - "Design Flow rate kg/hr 1e+06\n", - "Purchase cost Shredder USD 4.03e+06\n", - "Total purchase cost USD 4.03e+06\n", - "Utility cost USD/hr 469" + "Shredder Units shredder\n", + "Electricity Power kW 6e+03\n", + " Cost USD/hr 469\n", + "Design results Flow rate kg/hr 1e+06\n", + "Purchase cost Shredder USD 4.03e+06\n", + "Total purchase cost USD 4.03e+06\n", + "Installed equipment cost USD 5.6e+06\n", + "Utility cost USD/hr 469" ] }, "execution_count": 14, @@ -1064,7 +1085,7 @@ " 469\n", " \n", " \n", - " Design\n", + " Design results\n", " Flow rate\n", " kg/hr\n", " 1e+06\n", @@ -1082,6 +1103,12 @@ " 4.83e+06\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 6.72e+06\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -1092,13 +1119,14 @@ "" ], "text/plain": [ - "Shredder Units shredder\n", - "Electricity Power kW 6e+03\n", - " Cost USD/hr 469\n", - "Design Flow rate kg/hr 1e+06\n", - "Purchase cost Shredder USD 4.83e+06\n", - "Total purchase cost USD 4.83e+06\n", - "Utility cost USD/hr 469" + "Shredder Units shredder\n", + "Electricity Power kW 6e+03\n", + " Cost USD/hr 469\n", + "Design results Flow rate kg/hr 1e+06\n", + "Purchase cost Shredder USD 4.83e+06\n", + "Total purchase cost USD 4.83e+06\n", + "Installed equipment cost USD 6.72e+06\n", + "Utility cost USD/hr 469" ] }, "execution_count": 18, @@ -1158,7 +1186,7 @@ " 469\n", " \n", " \n", - " Design\n", + " Design results\n", " Flow rate\n", " kg/hr\n", " 1e+06\n", @@ -1176,6 +1204,12 @@ " 6.38e+06\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 8.87e+06\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -1186,13 +1220,14 @@ "" ], "text/plain": [ - "Shredder Units shredder\n", - "Electricity Power kW 6e+03\n", - " Cost USD/hr 469\n", - "Design Flow rate kg/hr 1e+06\n", - "Purchase cost Shredder (x2) USD 6.38e+06\n", - "Total purchase cost USD 6.38e+06\n", - "Utility cost USD/hr 469" + "Shredder Units shredder\n", + "Electricity Power kW 6e+03\n", + " Cost USD/hr 469\n", + "Design results Flow rate kg/hr 1e+06\n", + "Purchase cost Shredder (x2) USD 6.38e+06\n", + "Total purchase cost USD 6.38e+06\n", + "Installed equipment cost USD 8.87e+06\n", + "Utility cost USD/hr 469" ] }, "execution_count": 19, @@ -1292,12 +1327,38 @@ " 173\n", " \n", " \n", - " Design\n", + " Design inputs\n", + " Molar vapor fraction\n", + " \n", + " 0.5\n", + " \n", + " \n", + " Holdup time\n", + " min\n", + " 15\n", + " \n", + " \n", + " Surge time\n", + " min\n", + " 7.5\n", + " \n", + " \n", + " Vacuum system preference\n", + " \n", + " Liquid-ring pump\n", + " \n", + " \n", + " Vessel material\n", + " \n", + " Carbon steel\n", + " \n", + " \n", " Vessel type\n", " \n", " Vertical\n", " \n", " \n", + " Design results\n", " Length\n", " ft\n", " 12.5\n", @@ -1323,13 +1384,8 @@ " 3.74e+04\n", " \n", " \n", - " Vessel material\n", - " \n", - " Carbon steel\n", - " \n", - " \n", " Purchase cost\n", - " Vertical pressure vessel\n", + " Pressure vessel\n", " USD\n", " 4.47e+04\n", " \n", @@ -1355,6 +1411,12 @@ " 1.34e+05\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 3.75e+05\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -1365,25 +1427,30 @@ "" ], "text/plain": [ - "Flash with agitator Units F1\n", - "Electricity Power kW 25.2\n", - " Cost USD/hr 1.97\n", - "Low pressure steam Duty kJ/hr 2.82e+07\n", - " Flow kmol/hr 728\n", - " Cost USD/hr 173\n", - "Design Vessel type Vertical\n", - " Length ft 12.5\n", - " Diameter ft 8.5\n", - " Weight lb 8.14e+03\n", - " Wall thickness in 0.438\n", - " Flow rate kg/hr 3.74e+04\n", - " Vessel material Carbon steel\n", - "Purchase cost Vertical pressure vessel USD 4.47e+04\n", - " Platform and ladders USD 1.27e+04\n", - " Agitator USD 4e+04\n", - " Heat exchanger - Floating head USD 3.65e+04\n", - "Total purchase cost USD 1.34e+05\n", - "Utility cost USD/hr 175" + "Flash with agitator Units F1\n", + "Electricity Power kW 25.2\n", + " Cost USD/hr 1.97\n", + "Low pressure steam Duty kJ/hr 2.82e+07\n", + " Flow kmol/hr 728\n", + " Cost USD/hr 173\n", + "Design inputs Molar vapor fraction 0.5\n", + " Holdup time min 15\n", + " Surge time min 7.5\n", + " Vacuum system preference Liquid-ring pump\n", + " Vessel material Carbon steel\n", + " Vessel type Vertical\n", + "Design results Length ft 12.5\n", + " Diameter ft 8.5\n", + " Weight lb 8.14e+03\n", + " Wall thickness in 0.438\n", + " Flow rate kg/hr 3.74e+04\n", + "Purchase cost Pressure vessel USD 4.47e+04\n", + " Platform and ladders USD 1.27e+04\n", + " Agitator USD 4e+04\n", + " Heat exchanger - Floating head USD 3.65e+04\n", + "Total purchase cost USD 1.34e+05\n", + "Installed equipment cost USD 3.75e+05\n", + "Utility cost USD/hr 175" ] }, "execution_count": 21, @@ -1440,98 +1507,92 @@ { "data": { "image/svg+xml": [ - "\n", + "\n", "\n", - "\n", + "\n", "\n", "\n", - "166809148505:c->166806313900:w\n", - "\n", - "\n", - " s4\n", - "\n", - "\n", + "80883549693:c->80883594096:w\n", + "\n", "\n", "\n", "\n", - "166809149156:c->166810615049:c\n", - "\n", - "\n", + "80883549672:c->80883564165:c\n", + "\n", + "\n", "\n", - " \n", + " \n", "\n", "\n", "\n", "\n", "\n", - "166810615049:c->166809148505:c\n", - "\n", - "\n", + "80883564165:c->80883549693:c\n", + "\n", + "\n", "\n", - " \n", + " \n", "\n", "\n", "\n", "\n", "\n", - "166806313564:e->166809149156:c\n", - "\n", - "\n", - " feed\n", - "\n", - "\n", + "80883594120:e->80883549672:c\n", + "\n", "\n", "\n", "\n", - "166809148505\n", + "80883549693\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "HT1\n", - "Mix tank\n", + "\n", + "HT1\n", + "Mix tank\n", "\n", "\n", "\n", "\n", "\n", - "166809149156\n", + "80883549672\n", "\n", "\n", - "\n", + "\n", "\n", "\n", "\n", "\n", - "\n", - "HT1\n", - "Pump\n", + "\n", + "HT1\n", + "Pump\n", "\n", "\n", "\n", "\n", "\n", - "166810615049\n", + "80883564165\n", "\n", - "\n", - "HT1\n", - "Heat exchanger\n", + "\n", + "HT1\n", + "Heat exchanger\n", "\n", "\n", "\n", "\n", "\n", - "166806313900\n", - "\n", + "80883594096\n", + "\n", + " s4\n", "\n", "\n", "\n", - "166806313564\n", - "\n", + "80883594120\n", + "\n", + " feed\n", "\n", "\n", "" @@ -1640,6 +1701,12 @@ " 3.94e+04\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 7.25e+04\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -1650,18 +1717,19 @@ "" ], "text/plain": [ - "Heated tank Units HT1\n", - "Electricity Power kW 0.736\n", - " Cost USD/hr 0.0575\n", - "Low pressure steam Duty kJ/hr 1.88e+05\n", - " Flow kmol/hr 4.86\n", - " Cost USD/hr 1.15\n", - "Purchase cost Mix tank - Tank USD 3.07e+04\n", - " Pump - Pump USD 4.64e+03\n", - " Pump - Motor USD 290\n", - " Heat exchanger - Double pipe USD 3.84e+03\n", - "Total purchase cost USD 3.94e+04\n", - "Utility cost USD/hr 1.21" + "Heated tank Units HT1\n", + "Electricity Power kW 0.736\n", + " Cost USD/hr 0.0575\n", + "Low pressure steam Duty kJ/hr 1.88e+05\n", + " Flow kmol/hr 4.86\n", + " Cost USD/hr 1.15\n", + "Purchase cost Mix tank - Tank USD 3.07e+04\n", + " Pump - Pump USD 4.64e+03\n", + " Pump - Motor USD 290\n", + " Heat exchanger - Double pipe USD 3.84e+03\n", + "Total purchase cost USD 3.94e+04\n", + "Installed equipment cost USD 7.25e+04\n", + "Utility cost USD/hr 1.21" ] }, "execution_count": 22, diff --git a/docs/tutorial/Unit_operation_results.ipynb b/docs/tutorial/Unit_operation_results.ipynb index 6849d7a35..bb82a1201 100644 --- a/docs/tutorial/Unit_operation_results.ipynb +++ b/docs/tutorial/Unit_operation_results.ipynb @@ -545,7 +545,43 @@ " 380\n", " \n", " \n", - " Design\n", + " Design inputs\n", + " Operating pressure\n", + " Pa\n", + " 101325\n", + " \n", + " \n", + " Enforced minimum reflux ratio\n", + " \n", + " 0.01\n", + " \n", + " \n", + " Reflux to minimum reflux ratio\n", + " \n", + " 1.25\n", + " \n", + " \n", + " Open tray area fraction\n", + " \n", + " 0.1\n", + " \n", + " \n", + " Tray spacing\n", + " mm\n", + " 450\n", + " \n", + " \n", + " Vacuum system preference\n", + " \n", + " Liquid-ring pump\n", + " \n", + " \n", + " Velocity fraction\n", + " \n", + " 0.8\n", + " \n", + " \n", + " Design results\n", " Theoretical feed stage\n", " \n", " 26\n", @@ -632,7 +668,7 @@ " 1.54e+05\n", " \n", " \n", - " Stripper platform and ladders\n", + " Rectifier platform and ladders\n", " USD\n", " 4.86e+04\n", " \n", @@ -642,7 +678,7 @@ " 6.97e+04\n", " \n", " \n", - " Rectifier platform and ladders\n", + " Stripper platform and ladders\n", " USD\n", " 1.7e+04\n", " \n", @@ -652,7 +688,7 @@ " 6.58e+04\n", " \n", " \n", - " Reflux drum - Vertical pressure vessel\n", + " Reflux drum - Pressure vessel\n", " USD\n", " 4.09e+04\n", " \n", @@ -683,6 +719,12 @@ " 5.88e+05\n", " \n", " \n", + " Installed equipment cost\n", + " \n", + " USD\n", + " 2.12e+06\n", + " \n", + " \n", " Utility cost\n", " \n", " USD/hr\n", @@ -693,43 +735,51 @@ "" ], "text/plain": [ - "Divided Distillation Column Units D1\n", - "Electricity Power kW 2.49\n", - " Cost USD/hr 0.195\n", - "Cooling water Duty kJ/hr -2.84e+07\n", - " Flow kmol/hr 1.94e+04\n", - " Cost USD/hr 9.45\n", - "Low pressure steam Duty kJ/hr 6.19e+07\n", - " Flow kmol/hr 1.6e+03\n", - " Cost USD/hr 380\n", - "Design Theoretical feed stage 26\n", - " Theoretical stages 31\n", - " Minimum reflux Ratio 0.771\n", - " Reflux Ratio 0.964\n", - " Rectifier stages 45\n", - " Stripper stages 9\n", - " Rectifier height ft 78.9\n", - " Stripper height ft 25.8\n", - " Rectifier diameter ft 9.96\n", - " Stripper diameter ft 7.84\n", - " Rectifier wall thickness in 0.438\n", - " Stripper wall thickness in 0.375\n", - " Rectifier weight lb 4.18e+04\n", - " Stripper weight lb 1.22e+04\n", - "Purchase cost Rectifier trays USD 9.02e+04\n", - " Stripper trays USD 1.95e+04\n", - " Rectifier tower USD 1.54e+05\n", - " Stripper platform and ladders USD 4.86e+04\n", - " Stripper tower USD 6.97e+04\n", - " Rectifier platform and ladders USD 1.7e+04\n", - " Condenser - Floating head USD 6.58e+04\n", - " Reflux drum - Vertical pressure ... USD 4.09e+04\n", - " Reflux drum - Platform and ladders USD 1.07e+04\n", - " Pump - Pump USD 4.53e+03\n", - " Pump - Motor USD 441\n", - " Reboiler - Floating head USD 6.67e+04\n", - "Total purchase cost USD 5.88e+05\n", - "Utility cost USD/hr 390" + "Divided Distillation Column Units D1\n", + "Electricity Power kW 2.49\n", + " Cost USD/hr 0.195\n", + "Cooling water Duty kJ/hr -2.84e+07\n", + " Flow kmol/hr 1.94e+04\n", + " Cost USD/hr 9.45\n", + "Low pressure steam Duty kJ/hr 6.19e+07\n", + " Flow kmol/hr 1.6e+03\n", + " Cost USD/hr 380\n", + "Design inputs Operating pressure Pa 101325\n", + " Enforced minimum reflux ratio 0.01\n", + " Reflux to minimum reflux ratio 1.25\n", + " Open tray area fraction 0.1\n", + " Tray spacing mm 450\n", + " Vacuum system preference Liquid-ring pump\n", + " Velocity fraction 0.8\n", + "Design results Theoretical feed stage 26\n", + " Theoretical stages 31\n", + " Minimum reflux Ratio 0.771\n", + " Reflux Ratio 0.964\n", + " Rectifier stages 45\n", + " Stripper stages 9\n", + " Rectifier height ft 78.9\n", + " Stripper height ft 25.8\n", + " Rectifier diameter ft 9.96\n", + " Stripper diameter ft 7.84\n", + " Rectifier wall thickness in 0.438\n", + " Stripper wall thickness in 0.375\n", + " Rectifier weight lb 4.18e+04\n", + " Stripper weight lb 1.22e+04\n", + "Purchase cost Rectifier trays USD 9.02e+04\n", + " Stripper trays USD 1.95e+04\n", + " Rectifier tower USD 1.54e+05\n", + " Rectifier platform and ladders USD 4.86e+04\n", + " Stripper tower USD 6.97e+04\n", + " Stripper platform and ladders USD 1.7e+04\n", + " Condenser - Floating head USD 6.58e+04\n", + " Reflux drum - Pressure vessel USD 4.09e+04\n", + " Reflux drum - Platform and ladders USD 1.07e+04\n", + " Pump - Pump USD 4.53e+03\n", + " Pump - Motor USD 441\n", + " Reboiler - Floating head USD 6.67e+04\n", + "Total purchase cost USD 5.88e+05\n", + "Installed equipment cost USD 2.12e+06\n", + "Utility cost USD/hr 390" ] }, "execution_count": 13, diff --git a/tests/test_compressor.py b/tests/test_compressor.py index c82058b6d..36e2949cf 100644 --- a/tests/test_compressor.py +++ b/tests/test_compressor.py @@ -28,10 +28,10 @@ def test_isentropic_hydrogen_compressor(): rtol=1e-3, ) # check compressor design - assert K.design_results["Type"] == "Centrifugal" - assert K.design_results["Driver"] == "Steam turbine" + assert K.compressor_type == "Centrifugal" + assert K.driver == "Steam turbine" ideal_power = 4.920933056717339 - eta_motor = K.baseline_cost_algorithms[K.design_results["Type"]].efficiencies["Steam turbine"] + eta_motor = K.baseline_cost_algorithms[K.compressor_type].efficiencies["Steam turbine"] expected_power = ideal_power / eta / eta_motor actual_power = K.power_utility.consumption assert_allclose( @@ -60,10 +60,10 @@ def test_isentropic_two_phase_steam_compressor(): rtol=1e-3, ) # check compressor design - assert K.design_results["Type"] == "Centrifugal" - assert K.design_results["Driver"] == "Steam turbine" + assert K.compressor_type == "Centrifugal" + assert K.driver == "Steam turbine" ideal_power = 5.4152214427108305 - eta_motor = K.baseline_cost_algorithms[K.design_results["Type"]].efficiencies["Steam turbine"] + eta_motor = K.baseline_cost_algorithms[K.compressor_type].efficiencies["Steam turbine"] expected_power = ideal_power / eta / eta_motor actual_power = K.power_utility.consumption assert_allclose( @@ -97,9 +97,9 @@ def test_isothermal_hydrogen_compressor(): rtol=1e-3, ) # check compressor design - assert K.design_results["Type"] == "Reciprocating" - assert K.design_results["Driver"] == "Electric motor" - eta_motor = K.baseline_cost_algorithms[K.design_results["Type"]].efficiencies["Electric motor"] + assert K.compressor_type == "Reciprocating" + assert K.driver == "Electric motor" + eta_motor = K.baseline_cost_algorithms[K.compressor_type].efficiencies["Electric motor"] expected_power = ideal_power / eta / eta_motor actual_power = K.power_utility.rate assert_allclose( @@ -128,9 +128,9 @@ def test_isothermal_hydrogen_compressor(): rtol=1e-3, ) # check compressor design - assert K.design_results["Type"] == "Reciprocating" - assert K.design_results["Driver"] == "Electric motor" - eta_motor = K.baseline_cost_algorithms[K.design_results["Type"]].efficiencies["Electric motor"] + assert K.compressor_type == "Reciprocating" + assert K.driver == "Electric motor" + eta_motor = K.baseline_cost_algorithms[K.compressor_type].efficiencies["Electric motor"] expected_power = ideal_power / eta / eta_motor actual_power = K.power_utility.rate assert_allclose( @@ -194,8 +194,8 @@ def test_polytropic_hydrogen_compressor(): rtol=1e-3, ) # check compressor design - assert K.design_results["Type"] == "Reciprocating" - assert K.design_results["Driver"] == "Electric motor" + assert K.compressor_type == "Reciprocating" + assert K.driver == "Electric motor" assert len(K.heat_utilities) == 0 and K.net_duty == 0. expected_power = 6.484199755124557 actual_power = K.power_utility.rate @@ -331,7 +331,7 @@ def test_compressor_design(): def compressor_type_at_psig(psig): K.P = psig_to_pascal(psig) K.simulate() - return K.design_results["Type"] + return K.compressor_type # Pressures [psig] in the same order of magnitude as upper bound for each # compressor type. @@ -359,10 +359,18 @@ def get_compressors_in_parallel(psig, acfm): K.compressor_type = "Centrifugal" K.driver = 'Electric motor' K.driver_efficiency = 0.9 + K.default_compressor_type = K.default_driver = K.default_driver_efficiency = False K.simulate() - assert K.design_results['Type'] == 'Centrifugal' - assert K.design_results['Driver'] == 'Electric motor' - assert K.design_results['Driver efficiency'] == 0.9 + assert K.compressor_type == 'Centrifugal' + assert K.driver == 'Electric motor' + assert K.driver_efficiency == 0.9 + assert K.F_D['Compressor(s)'] == 1.0 # Electric motor design factor + + # Test design inputs are given in results + results = K.results(with_units=False) + assert results[('Design inputs', 'Compressor type')] == 'Centrifugal' + assert results[('Design inputs', 'Driver')] == 'Electric motor' + assert results[('Design inputs', 'Driver efficiency')] == 0.9 # Test capital cost correlations and factors def installed_equipment_cost_at_psig(psig): @@ -370,7 +378,7 @@ def installed_equipment_cost_at_psig(psig): K.simulate() return K.installed_cost - K.driver = K.driver_efficiency = K.compressor_type = 'Default' + K.default_compressor_type = K.default_driver = K.default_driver_efficiency = True assert_allclose([installed_equipment_cost_at_psig(i) for i in pressures], [ 2267264.256152, 5179043.514644, 10479970.002024], rtol=1e-3) diff --git a/tests/test_unit.py b/tests/test_unit.py index f86e5bc99..915f78cac 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -436,34 +436,214 @@ def _run(self): pass S.ins[0].disconnect_sink() assert len(S.ins) == 1 and isinstance(S.ins[0], bst.MissingStream) - # Test outs size not fixed + # Test outs size not fixed S.outs[0].disconnect_source() assert len(S.outs) == 2 and isinstance(S.outs[0], bst.MissingStream) and isinstance(S.outs[1], bst.Stream) + +def test_design_inputs(): + bst.settings.set_thermo(['Water'], cache=True) + + class Vessel(bst.Unit): + _F_BM_default = {'Vessel': 2.0} + + #: Residence time [hr]. + tau = bst.design_input('Residence time', 'hr', lb=0) + + #: Fractional efficiency. + efficiency = bst.design_input(lb=0, ub=1) + + #: Construction material. + material = bst.design_input( + purchase_item='Vessel', + catalog_type='Material', + catalog={'Carbon steel': 1.0, 'Stainless steel': 2.5}, + ) + + #: Orientation (with design factor). + orientation = bst.design_input( + purchase_item='Vessel', + catalog_type='Design', + catalog={'Vertical': 1.0, 'Horizontal': 1.2}, + ) + + #: Type with no cost factor. + vessel_type = bst.design_input(catalog=('Open', 'Closed')) -# def test_design_costing_decorators(): -# @design_decision( -# name='k', -# hard_bounds=(1, None), -# ) -# @design_result( -# name='T', -# hard_bounds=(0, None), -# units='K', -# ) -# @design_result( -# name='Weight', -# soft_bounds=(1, 10), -# hard_bounds=(0, None), -# units='lb', -# ) -# @cost_factors( -# 'Pressure vessel', -# F_P=('Pressure', F_P), -# F_D=('vessel_orientation', F_D), -# F_M=('material', F_M), -# ) + @bst.design_input('Volume', 'm3', lb=0) + def volume(self): + return self.tau * self.feed.F_vol + + @volume.setter + def volume(self, volume): + self.tau = volume / self.feed.F_vol + + def _init(self, tau=1, efficiency=0.9, material='Carbon steel', + orientation='Vertical', vessel_type='Closed'): + self.tau = tau + self.efficiency = efficiency + self.material = material + self.orientation = orientation + self.vessel_type = vessel_type + + def _run(self): + self.outs[0].copy_like(self.ins[0]) + + def _cost(self): + self.baseline_purchase_costs['Vessel'] = 1000. + # Design inputs are registered (sorted) and named + assert Vessel._design_inputs == [ + 'efficiency', 'material', 'orientation', 'tau', 'vessel_type', 'volume' + ] + assert Vessel.tau.key == 'tau' + assert Vessel.tau.name == 'Residence time' + assert Vessel.efficiency.name == 'Efficiency' # Default name from key + assert Vessel.vessel_type.name == 'Vessel type' + assert Vessel.volume.units == 'm3' + assert Vessel.efficiency.units == '' + + # Design inputs are documented like property objects + import inspect + assert all([isinstance(getattr(Vessel, i), property) for i in Vessel._design_inputs]) + assert Vessel.tau.__name__ == 'tau' + assert Vessel.tau.__doc__ == 'Residence time [hr].' + assert Vessel.efficiency.__doc__ == 'Efficiency.' + assert Vessel.material.__doc__ == 'Material.' + assert Vessel.volume.__name__ == 'volume' + assert Vessel.volume.__doc__ == 'Volume [m3].' # No docstring in getter + + class DocumentedVessel(bst.Unit): + T: float = bst.design_input('Temperature', 'K', lb=0) + + @bst.design_input('Volume', 'm3', lb=0) + def volume(self) -> float: + """Total volume.""" + return self._volume + + @volume.setter + def volume(self, volume): + self._volume = volume + + tau = bst.design_input('Residence time', 'hr', doc='Time in vessel.') + + assert DocumentedVessel.T.__name__ == 'T' + assert inspect.signature(DocumentedVessel.T.fget).return_annotation is float + assert DocumentedVessel.volume.__doc__ == 'Total volume.' + assert inspect.signature(DocumentedVessel.volume.fget).return_annotation is float + assert DocumentedVessel.volume.lb == 0 # Setter keeps configuration + assert DocumentedVessel.tau.__doc__ == 'Time in vessel.' + assert DocumentedVessel._design_inputs == ['T', 'tau', 'volume'] + + feed = bst.Stream(None, Water=100) + V = Vessel(None, feed) + + # Numerical design inputs + assert V.tau == 1 + V.tau = 2 + assert V.tau == 2 + assert V.get_design_input('tau', 'min') == 120 + assert V.set_design_input('tau', 'day', 1) == 24 + assert V.tau == 24 + assert V.convert_design_input('tau', 'min', 60) == 1 + assert V.get_design_input_units('tau') == 'hr' + V.tau = 2 + + # Bounds are inclusive and enforced + V.efficiency = 1 + V.efficiency = 0 + with pytest.raises(AttributeError): + V.efficiency = 1.1 + with pytest.raises(AttributeError): + V.efficiency = -0.1 + with pytest.raises(AttributeError): + V.tau = -1 + assert V.tau == 2 # Value unchanged after failed set + + # Design input with user-defined getter and setter + assert V.volume == V.tau * feed.F_vol + V.volume = 4 * feed.F_vol + assert V.tau == 4 + with pytest.raises(AttributeError): + V.volume = -1 # Bounds are enforced before calling the setter + assert V.tau == 4 + V.tau = 2 + + # Categorical design inputs update cost factors + assert V.F_M['Vessel'] == 1.0 + assert V.F_D['Vessel'] == 1.0 + V.material = 'Stainless steel' + V.orientation = 'horizontal' # Capitalization is handled + assert V.material == 'Stainless steel' + assert V.orientation == 'Horizontal' + assert V.F_M['Vessel'] == 2.5 + assert V.F_D['Vessel'] == 1.2 + with pytest.raises(AttributeError): + V.material = 'Unobtanium' + assert V.material == 'Stainless steel' + V.vessel_type = 'Open' + with pytest.raises(AttributeError): + V.vessel_type = 'Ajar' + + # Design inputs cannot be deleted unless a deleter is defined + with pytest.raises(AttributeError): + del V.tau + with pytest.raises(AttributeError): + del V.material + + # Cost factors from design inputs are used in costing + V.simulate() + assert V.purchase_costs['Vessel'] == 1000. * 2.5 * 1.2 + + # Design inputs show up in results + df = V.results() + for key in Vessel._design_inputs: + name = getattr(Vessel, key).name + assert ('Design inputs', name) in df.index + series = V.results(with_units=False) + assert series[('Design inputs', 'Residence time')] == 2 + assert series[('Design inputs', 'Material')] == 'Stainless steel' + + # Design inputs are inherited and can be extended in subclasses + class AgitatedVessel(Vessel): + agitator_power = bst.design_input('Agitator power', 'kW', lb=0) + + assert AgitatedVessel._design_inputs == [ + 'agitator_power', 'efficiency', 'material', 'orientation', 'tau', + 'vessel_type', 'volume' + ] + assert Vessel._design_inputs == [ + 'efficiency', 'material', 'orientation', 'tau', 'vessel_type', 'volume' + ] + + # Invalid definitions + with pytest.raises(ValueError): + bst.design_input(catalog_type='Material') + with pytest.raises(ValueError): + bst.design_input(purchase_item='Vessel') + with pytest.raises(ValueError): + bst.design_input(catalog={'A': 1}, purchase_item='Vessel') + with pytest.raises(ValueError): + bst.design_input(catalog={'A': 1}, purchase_item='Vessel', catalog_type='Other') + with pytest.raises(ValueError): + bst.design_input(catalog=('A', 'B'), units='kg') + with pytest.raises(ValueError): + bst.design_input(catalog=('A', 'B'), lb=0) + with pytest.raises(ValueError): + bst.design_input(catalog=('A', 'B'), ub=1) + + # Both getter and setter must be defined (or none of them) + with pytest.raises(RuntimeError): + class GetterOnly(bst.Unit): + @bst.design_input('Volume', 'm3') + def volume(self): return 1. + with pytest.raises(RuntimeError): + class SetterOnly(bst.Unit): + volume = bst.design_input('Volume', 'm3') + @volume.setter + def volume(self, volume): pass + if __name__ == '__main__': + test_design_inputs() test_disconnect() test_auxiliary_unit_owners() test_unit_convinience_properties() diff --git a/thermosteam b/thermosteam index 1faa312d7..18af9df1c 160000 --- a/thermosteam +++ b/thermosteam @@ -1 +1 @@ -Subproject commit 1faa312d7f8233eee73404f83d6583d58ecb42b7 +Subproject commit 18af9df1cb1096cb70347158da70c392c112c5f5