Source code for CADETProcess.processModel.flowSheet

from functools import wraps
from warnings import warn

import numpy as np
from addict import Dict

from CADETProcess import CADETProcessError
from CADETProcess.dataStructure import frozen_attributes
from CADETProcess.dataStructure import Structure, UnsignedInteger, String
from .componentSystem import ComponentSystem
from .unitOperation import UnitBaseClass
from .unitOperation import Inlet, Outlet, Cstr
from .binding import NoBinding


[docs] @frozen_attributes class FlowSheet(Structure): """Class to design process flow sheet. In this class, UnitOperation models are added and connected in a flow sheet. Attributes ---------- n_comp : UnsignedInteger Number of components of the units in the flow sheet. name : String Name of the FlowSheet. units : list UnitOperations in the FlowSheet. connections : dict Connections of UnitOperations. output_states : dict Split ratios of outgoing streams of UnitOperations. """ name = String() def __init__(self, component_system, name=None): self.component_system = component_system self.name = name self._units = [] self._feed_inlets = [] self._eluent_inlets = [] self._product_outlets = [] self._connections = Dict() self._output_states = Dict() self._flow_rates = Dict() self._parameters = Dict() self._sized_parameters = Dict() self._polynomial_parameters = Dict() self._section_dependent_parameters = Dict() @property def component_system(self): return self._component_system @component_system.setter def component_system(self, component_system): if not isinstance(component_system, ComponentSystem): raise TypeError('Expected ComponentSystem') self._component_system = component_system @property def n_comp(self): return self.component_system.n_comp
[docs] def unit_name_decorator(func): @wraps(func) def wrapper(self, unit, *args, **kwargs): """Enable calling functions with unit object or unit name.""" if isinstance(unit, str): try: unit = self.units_dict[unit] except KeyError: raise CADETProcessError('Not a valid unit') return func(self, unit, *args, **kwargs) return wrapper
[docs] def origin_destination_name_decorator(func): @wraps(func) def wrapper(self, origin, destination, *args, **kwargs): """Enable calling origin and destination using unit names.""" if isinstance(origin, str): try: origin = self.units_dict[origin] except KeyError: raise CADETProcessError('Not a valid unit') if isinstance(destination, str): try: destination = self.units_dict[destination] except KeyError: raise CADETProcessError('Not a valid unit') return func(self, origin, destination, *args, **kwargs) return wrapper
[docs] def update_parameters(self): for unit in self.units: self._parameters[unit.name] = unit.parameters self._section_dependent_parameters[unit.name] = \ unit.section_dependent_parameters self._polynomial_parameters[unit.name] = unit.polynomial_parameters self._sized_parameters[unit.name] = unit.sized_parameters self._parameters['output_states'] = { unit.name: self.output_states[unit] for unit in self.units } self._sized_parameters['output_states'] = { unit.name: self.output_states[unit] for unit in self.units } self._section_dependent_parameters['output_states'] = { unit.name: self.output_states[unit] for unit in self.units }
[docs] def update_parameters_decorator(func): @wraps(func) def wrapper(self, *args, **kwargs): """Update parameters dict to save time.""" results = func(self, *args, **kwargs) self.update_parameters() return results return wrapper
@property def units(self): """list: list of all unit_operations in the flow sheet.""" return self._units @property def units_dict(self): """dict: Unit operation names and objects.""" return {unit.name: unit for unit in self.units} @property def unit_names(self): """list: Names of unit operations.""" return [unit.name for unit in self.units] @property def number_of_units(self): """int: Number of unit operations in the FlowSheet.""" return len(self._units)
[docs] @unit_name_decorator def get_unit_index(self, unit): """Return the unit index of the unit. Parameters ---------- unit : UnitBaseClass UnitBaseClass object of which the index is to be returned. Raises ------ CADETProcessError If unit does not exist in the current flow sheet. Returns ------- unit_index : int Returns the unit index of the unit_operation. """ if unit not in self.units: raise CADETProcessError('Unit not in flow sheet') return self.units.index(unit)
@property def inlets(self): """list: All Inlets in the system.""" return [unit for unit in self._units if isinstance(unit, Inlet)] @property def outlets(self): """list: All Outlets in the system.""" return [unit for unit in self._units if isinstance(unit, Outlet)] @property def cstrs(self): """list: All Cstrs in the system.""" return [unit for unit in self._units if isinstance(unit, Cstr)] @property def units_with_binding(self): """list: UnitOperations with binding models.""" return [unit for unit in self._units if not isinstance(unit.binding_model, NoBinding)]
[docs] @update_parameters_decorator def add_unit( self, unit, feed_inlet=False, eluent_inlet=False, product_outlet=False): """Add unit to the flow sheet. Parameters ---------- unit : UnitBaseClass UnitBaseClass object to be added to the flow sheet. feed_inlet : bool If True, add unit to feed inlets. eluent_inlet : bool If True, add unit to eluent inlets. product_outlet : bool If True, add unit to product outlets. Raises ------ TypeError If unit is no instance of UnitBaseClass. CADETProcessError If unit already exists in flow sheet. If n_comp does not match with FlowSheet. See Also -------- remove_unit """ if not isinstance(unit, UnitBaseClass): raise TypeError('Expected UnitOperation') if unit in self._units or unit.name in self.unit_names: raise CADETProcessError('Unit already part of System') if unit.component_system is not self.component_system: raise CADETProcessError('Component systems do not match.') self._units.append(unit) self._connections[unit] = Dict({ 'origins': [], 'destinations': [], }) self._output_states[unit] = [] self._flow_rates[unit] = [] super().__setattr__(unit.name, unit) if feed_inlet: self.add_feed_inlet(unit) if eluent_inlet: self.add_eluent_inlet(unit) if product_outlet: self.add_product_outlet(unit)
[docs] @unit_name_decorator @update_parameters_decorator def remove_unit(self, unit): """Remove unit from flow sheet. Removes unit from the list. Tries to remove units which are twice located as desinations. For this the origins and destinations are deleted for the unit. Raises a CADETProcessError if an ValueError is excepted. If the unit is specified as feed_inlet, eluent_inlet or product_outlet, the corresponding attributes are deleted. Parameters ---------- unit : UnitBaseClass UnitBaseClass object to be removed to the flow sheet. Raises ------ CADETProcessError If unit does not exist in the flow sheet. See Also -------- add_unit feed_inlet eluent_inlet product_outlet """ if unit not in self.units: raise CADETProcessError('Unit not in flow sheet') if unit is self.feed_inlets: self.remove_feed_inlet(unit) if unit is self.eluent_inlets: self.remove_eluent_inlet(unit) if unit is self.product_outlets: self.remove_product_outlet(unit) origins = self.connections[unit].origins.copy() for origin in origins: self.remove_connection(origin, unit) destinations = self.connections[unit].destinations.copy() for destination in destinations: self.remove_connection(unit, destination) self._units.remove(unit) self._connections.pop(unit) self._output_states.pop(unit) self.__dict__.pop(unit.name)
@property def connections(self): """dict: In- and outgoing connections for each unit. See Also -------- add_connection remove_connection """ return self._connections
[docs] @origin_destination_name_decorator @update_parameters_decorator def add_connection(self, origin, destination): """Add connection between units 'origin' and 'destination'. Parameters ---------- origin : UnitBaseClass UnitBaseClass from which the connection originates. destination : UnitBaseClass UnitBaseClass where the connection terminates. Raises ------ CADETProcessError If origin OR destination do not exist in the current flow sheet. If connection already exists in the current flow sheet. See Also -------- connections remove_connection output_state """ if origin not in self._units: raise CADETProcessError('Origin not in flow sheet') if destination not in self._units: raise CADETProcessError('Destination not in flow sheet') if destination in self.connections[origin].destinations: raise CADETProcessError('Connection already exists') self._connections[origin].destinations.append(destination) self._connections[destination].origins.append(origin) self.set_output_state(origin, 0)
[docs] @origin_destination_name_decorator @update_parameters_decorator def remove_connection(self, origin, destination): """Remove connection between units 'origin' and 'destination'. Parameters ---------- origin : UnitBaseClass UnitBaseClass from which the connection originates. destination : UnitBaseClass UnitBaseClass where the connection terminates. Raises ------ CADETProcessError If origin OR destination do not exist in the current flow sheet. If connection does not exists in the current flow sheet. See Also -------- connections add_connection """ if origin not in self._units: raise CADETProcessError('Origin not in flow sheet') if destination not in self._units: raise CADETProcessError('Destination not in flow sheet') try: self._connections[origin].destinations.remove(destination) self._connections[destination].origins.remove(origin) except KeyError: raise CADETProcessError('Connection does not exist.')
[docs] @origin_destination_name_decorator def connection_exists(self, origin, destination): """bool: check if connection exists in flow sheet. Parameters ---------- origin : UnitBaseClass UnitBaseClass from which the connection originates. destination : UnitBaseClass UnitBaseClass where the connection terminates. """ if destination in self._connections[origin].destinations \ and origin in self._connections[destination].origins: return True return False
[docs] def check_connections(self): """Validate that units are connected correctly. Raises ------ Warning If Inlets have ingoing streams. If Outlets have outgoing streams. If Units (other than Cstr) are not fully connected. Returns ------- flag : bool True if all units are connected correctly. False otherwise. """ flag = True for unit, connections in self.connections.items(): if isinstance(unit, Inlet): if len(connections.origins) != 0: flag = False warn("Inlet unit cannot have ingoing stream.") if len(connections.destinations) == 0: flag = False warn(f" Unit '{unit.name}' does not have outgoing stream.") elif isinstance(unit, Outlet): if len(connections.destinations) != 0: flag = False warn("Outlet unit cannot have outgoing stream.") if len(connections.origins) == 0: flag = False warn(f"Unit '{unit.name}' does not have ingoing stream.") elif isinstance(unit, Cstr): if unit.flow_rate is not None and len(connections.destinations) == 0: flag = False warn("Cstr cannot have flow rate without outgoing stream.") else: if len(connections.origins) == 0: flag = False warn(f"Unit '{unit.name}' does not have ingoing stream.") if len(connections.destinations) == 0: flag = False warn(f" Unit '{unit.name}' does not have outgoing stream.") return flag
@property def missing_parameters(self): missing_parameters = [] for unit in self.units: missing_parameters += [ f'{unit.name}.{param}' for param in unit.missing_parameters ] return missing_parameters
[docs] def check_units_config(self): """Check if units are configured correctly. Returns ------- flag : bool True if units are configured correctly. False otherwise. """ flag = True for unit in self.units: if not unit.check_required_parameters(): flag = False return flag
@property def output_states(self): return self._output_states
[docs] @unit_name_decorator @update_parameters_decorator def set_output_state(self, unit, state): """Set split ratio of outgoing streams for UnitOperation. Parameters ---------- unit : UnitBaseClass UnitOperation of flowsheet. state : int or list of floats or dict new output state of the unit. Raises ------ CADETProcessError If unit not in FlowSheet If state is integer and the state >= the state_length. If the length of the states is unequal the state_length. If the sum of the states is not equal to 1. """ if unit not in self._units: raise CADETProcessError('Unit not in flow sheet') state_length = len(self.connections[unit].destinations) if state_length == 0: output_state = [] if isinstance(state, (int, np.integer)): if state >= state_length: raise CADETProcessError('Index exceeds destinations') output_state = [0.0] * state_length output_state[state] = 1.0 elif isinstance(state, dict): output_state = [0.0] * state_length for dest, value in state.items(): try: assert self.connection_exists(unit, dest) except AssertionError: raise CADETProcessError(f'{unit} does not connect to {dest}.') dest = self[dest] index = self.connections[unit].destinations.index(dest) output_state[index] = value elif isinstance(state, list): if len(state) != state_length: raise CADETProcessError(f'Expected length {state_length}.') output_state = state else: raise TypeError("Output state must be integer, list or dict.") if state_length != 0 and not np.isclose(sum(output_state), 1): raise CADETProcessError('Sum of fractions must be 1') self._output_states[unit] = output_state
[docs] def get_flow_rates(self, state=None): """ Calculate the volumetric flow rate for all connections in the process. Parameters ---------- state : Dict, optional Updated flow rates and output states for process sections. Default is None. Returns ------- Dict Volumetric flow rate for each unit operation. Notes ----- To calculate the flow rates, a system of equations is set up: .. math:: Q_i = \sum_{j=1}^{n_{units}} q_{ji} = \sum_{j=1}^{n_{units}} Q_j * w_{ji}, where :math:`Q_i` is the total flow exiting unit :math:`i`, and :math:`w_{ij}` is the percentile of the total flow of unit :math:`j` directed to unit :math:`i`. If the unit is an `Inlet` or a `Cstr` with a given flow rate, :math:`Q_i` is given and the system is simplified. This system is solved using `numpy.linalg.solve`. Then, the individual flows :math:`q_{ji}` are extracted. Raises ------ CADETProcessError If flow sheet connectivity matrix is singular, indicating a potential issue in flow sheet configuration. References ---------- Forum discussion on flow rate calculation: https://forum.cadet-web.de/t/improving-the-flowrate-calculation/795 """ flow_rates = { unit.name: unit.flow_rate for unit in (self.inlets + self.cstrs) if unit.flow_rate is not None } output_states = self.output_states if state is not None: for param, value in state.items(): param = param.split('.') unit_name = param[1] param_name = param[-1] if param_name == 'flow_rate': flow_rates[unit_name] = value[0] elif unit_name == 'output_states': unit = self.units_dict[param_name] output_states[unit] = list(value.ravel()) n_units = self.number_of_units # Setup matrix with output states. w_out = np.zeros((n_units, n_units)) for unit in self.units: unit_index = self.get_unit_index(unit) if unit.name in flow_rates: w_out[unit_index, unit_index] = 1 else: for origin in self.connections[unit]['origins']: o_index = self.get_unit_index(origin) local_d_index = self.connections[origin].destinations.index(unit) w_out[unit_index, o_index] = output_states[origin][local_d_index] w_out[unit_index, unit_index] += -1 # Check for a singular matrix before the loop if np.linalg.cond(w_out) == np.inf: raise CADETProcessError( "Flow sheet connectivity matrix is singular, which may be due to " "unconnected units or missing flow rates. Please ensure all units are " "correctly connected and all necessary flow rates are set." ) # Solve system of equations for each polynomial coefficient total_flow_rate_coefficents = np.zeros((4, n_units)) for i in range(4): if len(flow_rates) == 0: continue coeffs = np.array(list(flow_rates.values()), ndmin=2)[:, i] if not np.any(coeffs): continue Q_vec = np.zeros(n_units) for unit_name in flow_rates: unit_index = self.get_unit_index(self.units_dict[unit_name]) Q_vec[unit_index] = flow_rates[unit_name][i] try: total_flow_rate_coefficents[i, :] = np.linalg.solve(w_out, Q_vec) except np.linalg.LinAlgError: raise CADETProcessError( "Unexpected error in flow rate calculation. " "Please check the flow sheet setup." ) # w_out_help is the same as w_out but it contains the origin flow for every unit w_out_help = np.zeros((n_units, n_units)) for unit in self.connections: unit_index = self.get_unit_index(unit) for origin in self.connections[unit]['origins']: o_index = self.get_unit_index(origin) local_d_index = self.connections[origin].destinations.index(unit) w_out_help[unit_index, o_index] = output_states[origin][local_d_index] # Calculate total_in as a matrix in "one" step rather than iterating manually. total_in_matrix = w_out_help @ total_flow_rate_coefficents.T # Generate output dict return_flow_rates = Dict() for index, unit in enumerate(self.units): unit_solution_dict = Dict() if not isinstance(unit, Inlet): unit_solution_dict['total_in'] = list(total_in_matrix[index]) if not isinstance(unit, Outlet): unit_solution_dict['total_out'] = list(total_flow_rate_coefficents[:, index]) if not isinstance(unit, Inlet): unit_solution_dict['origins'] = Dict( { origin.name: list( total_flow_rate_coefficents[:, self.get_unit_index(origin)] * w_out_help[index, self.get_unit_index(origin)] ) for origin in self.connections[unit].origins } ) if not isinstance(unit, Outlet): unit_solution_dict['destinations'] = Dict( { destination.name: list( total_flow_rate_coefficents[:, index] * w_out_help[self.get_unit_index(destination), index] ) for destination in self.connections[unit].destinations } ) return_flow_rates[unit.name] = unit_solution_dict return return_flow_rates
[docs] def check_flow_rates(self, state=None): flow_rates = self.get_flow_rates(state) for unit, q in flow_rates.items(): if isinstance(unit, (Inlet, Outlet)): continue elif isinstance(unit, Cstr) and Cstr.flow_rate is not None: continue if not np.all(q.total_in == q.total_out): raise CADETProcessError(f"Unbalanced flow rate for unit '{unit}'.")
@property def feed_inlets(self): """list: Inlets considered for calculating recovery yield.""" return self._feed_inlets
[docs] @unit_name_decorator def add_feed_inlet(self, feed_inlet): """Add inlet to list of units to be considered for recovery. Parameters ---------- feed_inlet : SourceMixin Unit to be added to list of feed inlets. Raises ------ CADETProcessError If unit is not an Inlet. If unit is already marked as feed inlet. """ if feed_inlet not in self.inlets: raise CADETProcessError('Expected Inlet') if feed_inlet in self._feed_inlets: raise CADETProcessError( f'Unit \'{feed_inlet}\' is already a feed inlet.' ) self._feed_inlets.append(feed_inlet)
[docs] @unit_name_decorator def remove_feed_inlet(self, feed_inlet): """Remove inlet from list of units to be considered for recovery. Parameters ---------- feed_inlet : SourceMixin Unit to be removed from list of feed inlets. """ if feed_inlet not in self._feed_inlets: raise CADETProcessError( f'Unit \'{feed_inlet}\' is not a feed inlet.' ) self._feed_inlets.remove(feed_inlet)
@property def eluent_inlets(self): """list: Inlets to be considered for eluent consumption.""" return self._eluent_inlets
[docs] @unit_name_decorator def add_eluent_inlet(self, eluent_inlet): """Add inlet to list of units to be considered for eluent consumption. Parameters ---------- eluent_inlet : SourceMixin Unit to be added to list of eluent inlets. Raises ------ CADETProcessError If unit is not an Inlet. If unit is already marked as eluent inlet. """ if eluent_inlet not in self.inlets: raise CADETProcessError('Expected Inlet') if eluent_inlet in self._eluent_inlets: raise CADETProcessError( f'Unit \'{eluent_inlet}\' is already an eluent inlet' ) self._eluent_inlets.append(eluent_inlet)
[docs] @unit_name_decorator def remove_eluent_inlet(self, eluent_inlet): """Remove inlet from list of units considered for eluent consumption. Parameters ---------- eluent_inlet : SourceMixin Unit to be added to list of eluent inlets. Raises ------ CADETProcessError If unit is not in eluent inlets. """ if eluent_inlet not in self._eluent_inlets: raise CADETProcessError( f'Unit \'{eluent_inlet}\' is not an eluent inlet.' ) self._eluent_inlets.remove(eluent_inlet)
@property def product_outlets(self): """list: Outlets to be considered for fractionation.""" return self._product_outlets
[docs] @unit_name_decorator def add_product_outlet(self, product_outlet): """Add outlet to list of units considered for fractionation. Parameters ---------- product_outlet : Outlet Unit to be added to list of product outlets. Raises ------ CADETProcessError If unit is not an Outlet. If unit is already marked as product outlet. """ if product_outlet not in self.outlets: raise CADETProcessError('Expected Outlet') if product_outlet in self._product_outlets: raise CADETProcessError( f'Unit \'{product_outlet}\' is already a product outlet' ) self._product_outlets.append(product_outlet)
[docs] @unit_name_decorator def remove_product_outlet(self, product_outlet): """Remove outlet from list of units to be considered for fractionation. Parameters ---------- product_outlet : Outlet Unit to be added to list of product outlets. Raises ------ CADETProcessError If unit is not a product outlet. """ if product_outlet not in self._product_outlets: raise CADETProcessError( f'Unit \'{product_outlet}\' is not a product outlet.' ) self._product_outlets.remove(product_outlet)
@property def parameters(self): return self._parameters @parameters.setter def parameters(self, parameters): try: output_states = parameters.pop('output_states') for unit, state in output_states.items(): unit = self.units_dict[unit] self.set_output_state(unit, state) except KeyError: pass for unit, params in parameters.items(): if unit not in self.units_dict: raise CADETProcessError('Not a valid unit') self.units_dict[unit].parameters = params self.update_parameters() @property def sized_parameters(self): return self._sized_parameters @property def polynomial_parameters(self): return self._polynomial_parameters @property def section_dependent_parameters(self): return self._section_dependent_parameters @property def initial_state(self): initial_state = {unit.name: unit.initial_state for unit in self.units} return initial_state @initial_state.setter def initial_state(self, initial_state): for unit, st in initial_state.items(): if unit not in self.units_dict: raise CADETProcessError('Not a valid unit') self.units_dict[unit].initial_state = st
[docs] def __getitem__(self, unit_name): """Make FlowSheet substriptable s.t. units can be used as keys. Parameters ---------- unit_name : str Name of the unit. Returns ------- unit : UnitBaseClass UnitOperation of FlowSheet. Raises ------ KeyError If unit not in FlowSheet """ try: return self.units_dict[unit_name] except KeyError: raise KeyError('Not a valid unit')
def __contains__(self, item): """Check if UnitOperation is part of the FlowSheet. Parameters ---------- item : UnitBaseClass item to be searched Returns ------- Bool : True if item is in units, otherwise False. """ if (item in self._units) or (item in self.unit_names): return True else: return False def __iter__(self): yield from self.units