class SolverTimeIntegratorParametersGroup(ParametersGroup): """Class for defining the solver time integrator parameters for cadet. See also -------- ParametersGroup """ abstol = UnsignedFloat(default=1e-8) algtol = UnsignedFloat(default=1e-12) reltol = UnsignedFloat(default=1e-6) reltol_sens = UnsignedFloat(default=1e-12) init_step_size = UnsignedFloat(default=1e-6) max_steps = UnsignedInteger(default=1000000) max_step_size = UnsignedInteger(default=1000000) errortest_sens = Bool(default=False) max_newton_iter = UnsignedInteger(default=1000000) max_errtest_fail = UnsignedInteger(default=1000000) max_convtest_fail = UnsignedInteger(default=1000000) max_newton_iter_sens = UnsignedInteger(default=1000000) _parameters = [ 'abstol', 'algtol', 'reltol', 'reltol_sens', 'init_step_size', 'max_steps', 'max_step_size', 'errortest_sens', 'max_newton_iter', 'max_errtest_fail', 'max_convtest_fail', 'max_newton_iter_sens' ]
class FillRegion(Structure): color_index = Integer() start = UnsignedFloat() end = UnsignedFloat() y_max = UnsignedFloat() text = String()
class NelderMead(SciPyInterface): """ """ maxiter = UnsignedInteger() maxfev = UnsignedInteger() initial_simplex = None xatol = UnsignedFloat(default=0.01) fatol = UnsignedFloat(default=0.01) adaptive = Bool(default=True) _options = [ 'maxiter', 'maxfev', 'initial_simplex', 'xatol', 'fatol', 'adaptive' ]
class SLSQP(SciPyInterface): """Class from scipy for optimization with SLSQP as method for the solver. This class is a wrapper for the method SLSQP from the optimization suite of the scipy interface. It defines the solver options in the local variable options as a dictionary and implements the abstract method run for running the optimization. """ ftol = UnsignedFloat(default=1e-2) eps = UnsignedFloat(default=1e-6) disp = Bool(default=False) _options = ['ftol', 'eps', 'disp']
class COBYLA(SciPyInterface): """Class from scipy for optimization with COBYLA as method for the solver. This class is a wrapper for the method COBYLA from the optimization suite of the scipy interface. It defines the solver options in the local variable options as a dictionary and implements the abstract method run for running the optimization. """ rhobeg = UnsignedFloat(default=1) maxiter = UnsignedInteger(default=1000) disp = Bool(default=False) catol = UnsignedFloat(default=0.0002) _options = ['rhobeg', 'maxiter', 'disp', 'catol']
class KumarMultiComponentLangmuir(BindingBaseClass): """Kumar Multi Component Langmuir adsoprtion isotherm. Attributes ---------- adsorption_rate : Parameter Adsorption rate constants. desorption_rate : Parameter Desorption rate constants. maximum_adsorption_capacity : Parameter Maximum adsoprtion capacities. characteristic_charge: Parameter Salt exponents/characteristic charges. activation_temp : Parameter Activation temperatures. temperature : unsigned float. Temperature. """ adsorption_rate = DependentlySizedUnsignedList(dep='n_comp') desorption_rate = DependentlySizedUnsignedList(dep='n_comp', default=1) maximum_adsorption_capacity = DependentlySizedUnsignedList(dep='n_comp') characteristic_charge = DependentlySizedUnsignedList(dep='n_comp', default=1) activation_temp = DependentlySizedUnsignedList(dep='n_comp') temperature = UnsignedFloat() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._parameter_names += [ 'adsorption_rate', 'desorption_rate', 'maximum_adsorption_capacity', 'characteristic_charge', 'activation_temp', 'temperature' ]
class Fraction(metaclass=StructMeta): mass = Vector() volume = UnsignedFloat() def __init__(self, mass, volume): self.mass = mass self.volume = volume @property def n_comp(self): return self.mass.size @property def fraction_mass(self): """np.Array: Cumulative mass all species in the fraction. See Also -------- mass purity concentration """ return sum(self.mass) @property def purity(self): """np.Array: Purity of the fraction. Invalid values are replaced by zero. See Also -------- mass fraction_mass concentration """ with np.errstate(divide='ignore', invalid='ignore'): purity = self.mass / self.fraction_mass return np.nan_to_num(purity) @property def concentration(self): """np.Array: Component concentrations of the fraction. Invalid values are replaced by zero. See Also -------- mass volume """ with np.errstate(divide='ignore', invalid='ignore'): concentration = self.mass / self.volume return np.nan_to_num(concentration) def __repr__(self): return "%s(mass=np.%r,volume=%r)" % (self.__class__.__name__, self.mass, self.volume)
class LumpedRateModelWithoutPores(TubularReactor): """Parameters for a lumped rate model without pores. Attributes ---------- total_porosity : UnsignedFloat between 0 and 1. Total porosity of the column. q : List of unsinged floats. Length depends on n_comp Initial concentration of the bound phase. Notes ----- Although technically the LumpedRateModelWithoutPores does not have particles, the particle reactions interface is used to support reactions in the solid phase and cross-phase reactions. """ supports_bulk_reaction = False supports_particle_reaction = True total_porosity = UnsignedFloat(ub=1) _parameters = TubularReactor._parameter_names + ['total_porosity'] q = DependentlySizedUnsignedList(dep=('n_comp', '_n_bound_states'), default=0) _initial_state = TubularReactor._initial_state + ['q'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._discretization = LRMDiscretizationFV()
class TrustConstr(SciPyInterface): """Class from scipy for optimization with trust-constr as method for the solver. This class is a wrapper for the method trust-constr from the optimization suite of the scipy interface. It defines the solver options in the local variable options as a dictionary and implements the abstract method run for running the optimization. """ gtol = UnsignedFloat(default=1e-6) xtol = UnsignedFloat(default=1e-8) barrier_tol = UnsignedFloat(default=1e-8) initial_constr_penalty = UnsignedFloat(default=1.0) initial_tr_radius = UnsignedFloat(default=1.0) initial_barrier_parameter = UnsignedFloat(default=0.01) initial_barrier_tolerance = UnsignedFloat(default=0.01) factorization_method = None maxiter = UnsignedInteger(default=1000) verbose = UnsignedInteger(default=0) disp = Bool(default=False) _options = [ 'gtol', 'xtol', 'barrier_tol', 'finite_diff_rel_step', 'initial_constr_penalty', 'initial_tr_radius', 'initial_barrier_parameter', 'initial_barrier_tolerance', 'factorization_method', 'maxiter','verbose', 'disp' ] jac = Switch(default='3-point', valid=['2-point', '3-point', 'cs']) def __str__(self): return 'trust-constr'
class StericMassAction(BindingBaseClass): """Parameters for Steric Mass Action Law binding model. Attributes ---------- adsorption_rate : list of unsigned floats. Length depends on n_comp. Adsorption rate constants. desorption_rate : list of unsigned floats. Length depends on n_comp. Desorption rate constants. characteristic_charge : list of unsigned floats. Length depends on n_comp. The characteristic charge of the protein: The number sites v that protein interacts on the resin surface. steric_factor : list of unsigned floats. Length depends on n_comp. Steric factors of the protein: The number of sites o on the surface that are shileded by the protein and prevented from exchange with salt counterions in solution. capacity : unsigned float. Stationary phase capacity (monovalent salt counterions); The total number of binding sites available on the resin surface. reference_liquid_phase_conc : unsigned float. Reference liquid phase concentration (optional, default value = 1.0). reference_solid_phase_conc : unsigned float. Reference liquid phase concentration (optional, default value = 1.0). """ adsorption_rate = DependentlySizedUnsignedList(dep='n_comp') desorption_rate = DependentlySizedUnsignedList(dep='n_comp', default=1) characteristic_charge = DependentlySizedUnsignedList(dep='n_comp') steric_factor = DependentlySizedUnsignedList(dep='n_comp') capacity = UnsignedFloat() reference_liquid_phase_conc = UnsignedFloat() reference_solid_phase_conc = UnsignedFloat() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._parameter_names += [ 'adsorption_rate', 'desorption_rate', 'characteristic_charge', 'steric_factor', 'capacity', 'reference_liquid_phase_conc', 'reference_solid_phase_conc' ]
class WenoParameters(ParametersGroup): """Class for defining the disrectization_weno_parameters Defines several parameters as UnsignedInteger, UnsignedFloat and save their names into a list named parameters. See also -------- ParametersGroup """ boundary_model = UnsignedInteger(default=0, ub=3) weno_eps = UnsignedFloat(default=1e-10) weno_order = UnsignedInteger(default=3, ub=3) _parameters = ['boundary_model', 'weno_eps', 'weno_order']
class ConsistencySolverParametersGroup(ParametersGroup): """Class for defining the consistency solver parameters for cadet. See also -------- ParametersGroup """ solver_name = Switch( default='LEVMAR', valid=['LEVMAR', 'ATRN_RES', 'ARTN_ERR', 'COMPOSITE'] ) init_damping = UnsignedFloat(default=0.01) min_damping = UnsignedFloat(default=0.0001) max_iterations = UnsignedInteger(default=50) subsolvers = Switch( default='LEVMAR', valid=['LEVMAR', 'ATRN_RES', 'ARTN_ERR'] ) _parameters = [ 'solver_name', 'init_damping', 'min_damping', 'max_iterations', 'subsolvers' ]
class ModelSolverParametersGroup(ParametersGroup): """Class for defining the model_solver_parameters. Defines several parameters as UnsignedInteger with default values and save their names into a list named parameters. See also -------- ParametersGroup """ GS_TYPE = UnsignedInteger(default=1, ub=1) MAX_KRYLOV = UnsignedInteger(default=0) MAX_RESTARTS = UnsignedInteger(default=10) SCHUR_SAFETY = UnsignedFloat(default=1e-8) _parameters = ['GS_TYPE', 'MAX_KRYLOV', 'MAX_RESTARTS', 'SCHUR_SAFETY']
class LRMPDiscretizationFV(DiscretizationParametersBase): ncol = UnsignedInteger(default=100) par_geom = Switch( default='SPHERE', valid=['SPHERE', 'CYLINDER', 'SLAB'] ) use_analytic_jacobian = Bool(default=True) reconstruction = Switch(default='WENO', valid=['WENO']) gs_type = Bool(default=True) max_krylov = UnsignedInteger(default=0) max_restarts = UnsignedInteger(default=10) schur_safety = UnsignedFloat(default=1.0e-8) _parameters = DiscretizationParametersBase._parameters + [ 'ncol', 'par_geom', 'use_analytic_jacobian', 'reconstruction', 'gs_type', 'max_krylov', 'max_restarts', 'schur_safety' ] _dimensionality = ['ncol']
class GRMDiscretizationFV(DiscretizationParametersBase): ncol = UnsignedInteger(default=100) npar = UnsignedInteger(default=5) par_geom = Switch( default='SPHERE', valid=['SPHERE', 'CYLINDER', 'SLAB'] ) par_disc_type = Switch( default='EQUIDISTANT_PAR', valid=['EQUIDISTANT_PAR', 'EQUIVOLUME_PAR', 'USER_DEFINED_PAR'] ) par_disc_vector = DependentlySizedRangedList(lb=0, ub=1, dep='par_disc_vector_length') par_boundary_order = RangedInteger(lb=1, ub=2, default=2) use_analytic_jacobian = Bool(default=True) reconstruction = Switch(default='WENO', valid=['WENO']) gs_type = Bool(default=True) max_krylov = UnsignedInteger(default=0) max_restarts = UnsignedInteger(default=10) schur_safety = UnsignedFloat(default=1.0e-8) fix_zero_surface_diffusion = Bool(default=False) _parameters = DiscretizationParametersBase._parameters + [ 'ncol', 'npar', 'par_geom', 'par_disc_type', 'par_disc_vector', 'par_boundary_order', 'use_analytic_jacobian', 'reconstruction', 'gs_type', 'max_krylov', 'max_restarts', 'schur_safety', 'fix_zero_surface_diffusion', ] _dimensionality = ['ncol', 'npar'] @property def par_disc_vector_length(self): return self.npar + 1
class GeneralRateModel(TubularReactor): """Parameters for the general rate model. Attributes ---------- bed_porosity : UnsignedFloat between 0 and 1. Porosity of the bed particle_porosity : UnsignedFloat between 0 and 1. Porosity of particles. particle_radius : UnsignedFloat Radius of the particles. pore_diffusion : List of unsinged floats. Length depends on n_comp. Diffusion rate for components in pore volume. surface_diffusion : List of unsinged floats. Length depends on n_comp. Diffusion rate for components in adsrobed state. pore_accessibility : List of unsinged floats. Length depends on n_comp. Accessibility of pores for components. cp : List of unsinged floats. Length depends on n_comp Initial concentration of the pores q : List of unsinged floats. Length depends on n_comp Initial concntration of the bound phase. """ supports_bulk_reaction = True supports_particle_reaction = True bed_porosity = UnsignedFloat(ub=1) particle_porosity = UnsignedFloat(ub=1) particle_radius = UnsignedFloat() film_diffusion = DependentlySizedUnsignedList(dep='n_comp') pore_diffusion = DependentlySizedUnsignedList(dep='n_comp') surface_diffusion = DependentlySizedUnsignedList(dep=('n_comp', '_n_bound_states')) pore_accessibility = DependentlySizedUnsignedList(dep='n_comp') _parameters = \ TubularReactor._parameter_names + \ ['bed_porosity', 'particle_porosity', 'particle_radius', 'film_diffusion', 'pore_diffusion', 'surface_diffusion'] _section_dependent_parameters = \ TubularReactor._section_dependent_parameters + \ ['film_diffusion', 'pore_diffusion', 'surface_diffusion'] _cp = DependentlySizedUnsignedList(dep='n_comp') q = DependentlySizedUnsignedList(dep=('n_comp', '_n_bound_states'), default=0) _initial_state = TubularReactor._initial_state + ['cp', 'q'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._discretization = GRMDiscretizationFV() @property def total_porosity(self): """float: Total porosity of the column """ return self.bed_porosity + \ (1 - self.bed_porosity) * self.particle_porosity @property def cross_section_area_interstitial(self): """float: Interstitial area between particles. See also -------- cross_section_area cross_section_area_liquid cross_section_area_solid """ return self.bed_porosity * self.cross_section_area def set_diameter_from_interstitial_velicity(self, Q, u0): """Set diamter from flow rate and interstitial velocity. In literature, often only the interstitial velocity is given. This method, the diameter / cross section area can be inferred from the flow rate, velocity, and bed porosity. Parameters ---------- Q : float Volumetric flow rate. u0 : float Interstitial velocity. Notes ----- Overwrites parent method. """ self.cross_section_area = Q / (u0 * self.bed_porosity) @property def cp(self): if self._cp is None: return self.c @cp.setter def cp(self, cp): self._cp = cp
class EventHandler(CachedPropertiesMixin, metaclass=StructMeta): """Baseclass for handling Events that change a property of an event performer. Attributes ---------- event_performers : dict Dictionary with all objects whose attributes can be modified events : list list of events event_dict : dict Dictionary with the information abaout all added events of a process. durations_dict : dict Dictionary with the information abaout all added durations of a process. See also -------- Events add_event add_event_dependency Duration """ cycle_time = UnsignedFloat(default=10.0) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._events = [] self._durations = [] self._lock = False self._parameters = None @property def events(self): """list: All Events ordered by event time. See Also -------- Event add_event remove_event Durations """ return sorted(self._events, key=lambda evt: evt.time) @property def events_dict(self): """dict: Events and Durations orderd by name. """ evts = {evt.name: evt for evt in self.events} durs = {dur.name: dur for dur in self.durations} return {**evts, **durs} def add_event( self, name, parameter_path, state, time=0.0, entry_index=None, dependencies=None, factors=None, ): """Factory function for creating and adding events. Parameters ---------- name : str Name of the event. parameter_path : str Path of the parameter that is changed in dot notation. state : float Value of the attribute that is changed at Event execution. time : float Time at which the event is executed. Raises ------ CADETProcessError If Event already exists in the event_dict CADETProcessError If EventPerformer is not found in EventHandler See also -------- Event remove_event add_event_dependency """ if name in self.events_dict: raise CADETProcessError("Event already exists") evt = Event(name, self, parameter_path, state, time=time, entry_index=entry_index) self._events.append(evt) super().__setattr__(name, evt) if dependencies is not None: self.add_event_dependency(evt.name, dependencies, factors) return evt def remove_event(self, evt_name): """Remove event from the EventHandler. Parameters ---------- evt_name : str Name of the event to be removed Raises ------ CADETProcessError If Event is not found. Note ---- !!! Check remove_event_dependencies See also -------- add_event remove_event_dependency Event """ try: evt = self.events_dict[evt_name] except KeyError: raise CADETProcessError("Event does not exist") self._events.remove(evt) self.__dict__.pop(evt_name) def add_duration(self, name, time=0.0): """Add duration to the EventHandler. Parameters ---------- name: str Name of the event. time : float Time point for perfoming the event. Raises ------ CADETProcessError If Duration already exists. See also -------- durations remove_duration Duration add_event add_event_dependency """ if name in self.events_dict: raise CADETProcessError("Duration already exists") dur = Duration(name, self, time) self._durations.append(dur) super().__setattr__(name, dur) def remove_duration(self, duration_name): """Remove duration from list of durations. Parameters ---------- duration : str Name of the duration be removed from the EventHandler. Raises ------ CADETProcessError If Duration is not found. See also -------- Duration add_duration remove_event_dependency """ try: dur = self.events_dict[duration_name] except KeyError: raise CADETProcessError("Duration does not exist") self._durations.remove(dur) self.__dict__.pop(duration_name) @property def durations(self): """List of all durations in the process """ return self._durations def add_event_dependency(self, dependent_event, independent_events, factors=None): """Add dependency between two events. First it combines the events in the events_dict and the durations_dict into one local variable combined_evt_dur. It raises a CADETProcessError if the given dependent_event is not in the combined_evt_dur dictionary. Also a CADETProcessErroris raised if the length of factors does not equal the length of given independent_events. Then it adds the dependency for the given dependent event by calling the method add_dependency from the event object. Parameters --------- dependent_event : str Name of the event whose value will depend on other events. independent_events : list List of independent event names. factors : list List of factors used for the relation with the independent events. Factors has to be integers of 1 or -1. The length of this list has to be equal the list of independent. Raises ------ CADETProcessError If dependent_event OR independent_events are not in the combined_evt_dur dictionary. If length of factors does not equal length of independent events. See also -------- Event add_dependency """ try: evt = self.events_dict[dependent_event] except KeyError: raise CADETProcessError("Cannot find dependent Event") if not isinstance(independent_events, list): independent_events = [independent_events] if not all(indep in self.events_dict for indep in independent_events): raise CADETProcessError( "Cannot find one or more independent events") if factors is None: factors = [1] * len(independent_events) if not isinstance(factors, list): factors = [factors] if len(factors) != len(independent_events): raise CADETProcessError( "Length of factors must be equal to length of independent Events" ) for indep, fac in zip(independent_events, factors): indep = self.events_dict[indep] evt.add_dependency(indep, fac) def remove_event_dependency(self, dependent_event, independent_events): """Remove dependency between two events. First it checks if the dependent_event exists in list events and also if one or more independet event doesn't exist in list events and durations and raises a CADETProcessError if it do so. Otherwise the method remove_dependency from the event object is called to remove this dependency. Parameters --------- dependent_event : str Name of the event whose value will depend on other events. independent_events : list List of independent event names. Raises ------ CADETProcessError If dependent_event is not in list events. If one or more independent event is not in list events and durations. See also: --------- remove_dependecy Event """ if dependent_event not in self.events: raise CADETProcessError("Cannot find dependent Event") if not all(evt in self.events_dict for evt in independent_events): raise CADETProcessError( "Cannot find one or more independent events") for indep in independent_events: self.events[dependent_event].remove_dependency(indep) @property def independent_events(self): """list: Independent Events. """ return list(filter(lambda evt: evt.isIndependent, self.events)) @property def dependent_events(self): """list: Events with dependencies. """ return list(filter(lambda evt: evt.isIndependent == False, self.events)) @property def event_parameters(self): """list: Event parameters. """ return list({evt.parameter_path for evt in self.events}) @property def event_performers(self): """list: Event peformers. """ return list({evt.performer for evt in self.events}) @property def event_times(self): """list: Time of events, sorted by Event time. """ event_times = list({evt.time for evt in self.events}) event_times.sort() return event_times @property def section_times(self): """list: Section times. Includes 0 and cycle_time if they do not coincide with event time. """ if len(self.event_times) == 0: return [0, self.cycle_time] section_times = self.event_times if section_times[0] != 0: section_times = [0] + section_times if section_times[-1] != self.cycle_time: section_times = section_times + [self.cycle_time] return section_times @property def n_sections(self): """int: Number of sections. """ return len(self.section_times) - 1 @cached_property_if_locked def section_states(self): """dict: state of event parameters at every section. """ parameter_timelines = self.parameter_timelines section_states = defaultdict(dict) for sec_time in self.section_times[0:-1]: for param, tl in parameter_timelines.items(): section_states[sec_time][param] = tl.coefficients(sec_time) return Dict(section_states) @cached_property_if_locked def parameter_events(self): """dict: list of events for every event parameter. Notes ----- For entry dependent events, a key is added per component. """ parameter_events = defaultdict(list) for evt in self.events: if evt.entry_index is not None: parameter_events[ f'{evt.parameter_path}_{evt.entry_index}'].append(evt) else: parameter_events[evt.parameter_path].append(evt) return Dict(parameter_events) @cached_property_if_locked def parameter_timelines(self): """dict: TimeLine for every event parameter. """ parameter_timelines = { param: TimeLine() for param in self.event_parameters if param not in self.entry_dependent_parameters } parameters = self.parameters multi_timelines = {} for param in self.entry_dependent_parameters: base_state = get_nested_value(parameters, param) multi_timelines[param] = MultiTimeLine(base_state) for evt_parameter, events in self.parameter_events.items(): for index, evt in enumerate(events): section_start = evt.time if index < len(events) - 1: section_end = events[index + 1].time section = Section(section_start, section_end, evt.state, evt.n_entries, evt.degree) self._add_section(evt, section, parameter_timelines, multi_timelines) else: section_end = self.cycle_time section = Section(section_start, section_end, evt.state, evt.n_entries, evt.degree) self._add_section(evt, section, parameter_timelines, multi_timelines) if events[0].time != 0: section = Section(0.0, events[0].time, evt.state, evt.n_entries, evt.degree) self._add_section(evt, section, parameter_timelines, multi_timelines) for param, tl in multi_timelines.items(): parameter_timelines[param] = tl.combined_time_line() return Dict(parameter_timelines) def _add_section(self, evt, section, parameter_timelines, multi_timelines): """Helper function to add sections to timelines.""" if evt.parameter_path in self.entry_dependent_parameters: multi_timelines[evt.parameter_path].add_section( section, evt.entry_index) else: parameter_timelines[evt.parameter_path].add_section(section) @property def performer_events(self): """dict: list of events for every event peformer. """ performer_events = defaultdict(list) for evt in self.events: performer_events[evt.performer].append(evt) return Dict(performer_events) @cached_property_if_locked def performer_timelines(self): """dict: TimeLines for every event parameter of a performer. """ performer_timelines = { performer: {} for performer in self.event_performers } for param, tl in self.parameter_timelines.items(): performer, param = param.rsplit('.', 1) performer_timelines[performer][param] = tl return performer_timelines @property def parameters(self): parameters = Dict() events = {evt.name: evt.parameters for evt in self.independent_events} parameters.update(events) durations = {dur.name: dur.parameters for dur in self.durations} parameters.update(durations) parameters['cycle_time'] = self.cycle_time return parameters @parameters.setter def parameters(self, parameters): try: self.cycle_time = parameters.pop('cycle_time') except KeyError: pass for evt_name, evt_parameters in parameters.items(): try: evt = self.events_dict[evt_name] except AttributeError: raise CADETProcessError('Not a valid event') if evt not in self.independent_events + self.durations: raise CADETProcessError('{} is not a valid event'.format( str(evt))) evt.parameters = evt_parameters @abstractmethod def section_dependent_parameters(self): return @property def entry_dependent_parameters(self): parameters = { evt.parameter_path for evt in self.events if evt.entry_index is not None } return parameters @abstractmethod def polynomial_parameters(self): return def plot_events(self): """Plot parameter state as function of time. """ time = np.linspace(0, self.cycle_time, 1001) for parameter, tl in self.parameter_timelines.items(): y = tl.value(time) fig, ax = plotting.setup_figure() layout = plotting.Layout() layout.title = str(parameter) layout.x_label = '$time~/~min$' layout.y_label = '$state$' ax.plot(time / 60, y) plotting.set_layout(fig, ax, layout)
class HLines(metaclass=StructMeta): y = UnsignedFloat() x_min = UnsignedFloat() x_max = UnsignedFloat()
class SciPyInterface(SolverBase): """Wrapper around scipy's optimization suite. Defines the bounds and all constraints, saved in a constraint_object. Also the jacobian matrix is defined for several solvers. """ finite_diff_rel_step = UnsignedFloat(default=1e-2) tol = UnsignedFloat() jac = '2-point' def run(self, optimization_problem): """Solve the optimization problem using any of the scipy methodss Returns ------- results : OptimizationResults Optimization results including optimization_problem and solver configuration. See also -------- COBYLA TrustConstr NelderMead SLSQP CADETProcess.optimization.OptimizationProblem.evaluate_objectives options scipy.optimize.minimize """ if optimization_problem.n_objectives > 1: raise CADETProcessError("Can only handle single objective.") cache = dict() objective_function = \ lambda x: optimization_problem.evaluate_objectives(x, cache=cache)[0] start = time.time() with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=OptimizeWarning) warnings.filterwarnings('ignore', category=RuntimeWarning) scipy_results = optimize.minimize( objective_function, x0=optimization_problem.x0, method=str(self), tol = self.tol, jac=self.jac, constraints=self.get_constraint_objects(optimization_problem), options=self.options ) elapsed = time.time() - start if not scipy_results.success: raise CADETProcessError('Optimization Failed') x = scipy_results.x eval_object = copy.deepcopy(optimization_problem.evaluation_object) if optimization_problem.evaluator is not None: frac = optimization_problem.evaluator.evaluate(eval_object) performance = frac.performance else: frac = None performance = optimization_problem.evaluate(x, force=True) f = optimization_problem.evaluate_objectives(x) c = optimization_problem.evaluate_nonlinear_constraints(x) results = OptimizationResults( optimization_problem = optimization_problem, evaluation_object = eval_object, solver_name = str(self), solver_parameters = self.options, exit_flag = scipy_results.status, exit_message = scipy_results.message, time_elapsed = elapsed, x = list(x), f = f, c = c, frac = frac, performance = performance.to_dict() ) return results def get_bounds(self, optimization_problem): """Returns the optimized bounds of a given optimization_problem as a Bound object. Optimizes the bounds of the optimization_problem by calling the method optimize.Bounds. Keep_feasible is set to True. Returns ------- bounds : Bounds Returns the optimized bounds as an object called bounds. """ return optimize.Bounds( optimization_problem.lower_bounds, optimization_problem.upper_bounds, keep_feasible=True ) def get_constraint_objects(self, optimization_problem): """Defines the constraints of the optimization_problem and resturns them into a list. First defines the lincon, the linequon and the nonlincon constraints. Returns the constrainst in a list. Returns ------- constraint_objects : list List containing a sorted list of all constraints of an optimization_problem, if they're not None. See also -------- lincon_obj lincon_obj nonlincon_obj """ lincon = self.get_lincon_obj(optimization_problem) lineqcon = self.get_lineqcon_obj(optimization_problem) nonlincon = self.get_nonlincon_obj(optimization_problem) constraints = [lincon, lineqcon, *nonlincon] return [con for con in constraints if con is not None] def get_lincon_obj(self, optimization_problem): """Returns the optimized linear constraint as an object. Sets the lower and upper bounds of the optimization_problem and returns optimized linear constraints. Keep_feasible is set to True. Returns ------- lincon_obj : LinearConstraint Linear Constraint object with optimized upper and lower bounds of b of the optimization_problem. See also -------- constraint_objects A b """ lb = [-np.inf]*len(optimization_problem.b) ub = optimization_problem.b return optimize.LinearConstraint( optimization_problem.A, lb, ub, keep_feasible=True ) def get_lineqcon_obj(self, optimization_problem): """Returns the optimized linear equality constraints as an object. Checks the length of the beq first, before setting the bounds of the constraint. Sets the lower and upper bounds of the optimization_problem and returns optimized linear equality constraints. Keep_feasible is set to True. Returns ------- None: bool If the length of the beq of the optimization_problem is equal zero. lineqcon_obj : LinearConstraint Linear equality Constraint object with optimized upper and lower bounds of beq of the optimization_problem. See also -------- constraint_objects Aeq beq """ if len(optimization_problem.beq) == 0: return None lb = optimization_problem.beq ub = optimization_problem.beq return optimize.LinearConstraint( optimization_problem.Aeq, lb, ub, keep_feasible=True ) def get_nonlincon_obj(self, optimization_problem): """Returns the optimized nonlinear constraints as an object. Checks the length of the nonlinear_constraints first, before setting the bounds of the constraint. Tries to set the bounds from the list nonlinear_constraints from the optimization_problem for the lower bounds and sets the upper bounds for the length of the nonlinear_constraints list. If a TypeError is excepted it sets the lower bound by the first entry of the nonlinear_constraints list and the upper bound to infinity. Then a local variable named finite_diff_rel_step is defined. After setting the bounds it returns the optimized nonlinear constraints as an object with the finite_diff_rel_step and the jacobian matrix. The jacobian matrix is got by calling the method nonlinear_constraint_jacobian from the optimization_problem. Keep_feasible is set to True. Returns ------- None: bool If the length of the nonlinear_constraints of the optimization_problem is equal zero. nonlincon_obj : NonlinearConstraint Linear equality Constraint object with optimized upper and lower bounds of beq of the optimization_problem. See also -------- constraint_objects nonlinear_constraints """ if optimization_problem.nonlinear_constraints is None: return None def makeConstraint(i): constr = optimize.NonlinearConstraint( lambda x: optimization_problem.evaluate_nonlinear_constraints(x)[i], lb=-np.inf, ub=0, finite_diff_rel_step=self.finite_diff_rel_step, keep_feasible=True ) return constr constraints = [] for i, constr in enumerate(optimization_problem.nonlinear_constraints): constraints.append(makeConstraint(i)) return constraints def __str__(self): return self.__class__.__name__
class Cadet(SolverBase): """CADET class for running a simulation for given process objects. Attributes ---------- install_path: str Path to the installation of CADET temp_dir : str Path to directory for temporary files time_out : UnsignedFloat Maximum duration for simulations model_solver_parameters : ModelSolverParametersGroup Container for solver parameters unit_discretization_parameters : UnitDiscretizationParametersGroup Container for unit discretization parameters discretization_weno_parameters : DiscretizationWenoParametersGroup Container for weno discretization parameters in units adsorption_consistency_solver_parameters : ConsistencySolverParametersGroup Container for consistency solver parameters solver_parameters : SolverParametersGroup Container for general solver settings time_integrator_parameters : SolverTimeIntegratorParametersGroup Container for time integrator parameters return_parameters : ReturnParametersGroup Container for return information of the system unit_return_parameters : UnitReturnParametersGroup Container for return information of units Note ---- !!! UnitParametersGroup and AdsorptionParametersGroup should be implemented with global options that are then copied for each unit in get_unit_config !!! Implement method for loading CADET file that have not been generated with CADETProcess and create Process See also -------- ReturnParametersGroup ModelSolverParametersGroup SolverParametersGroup SolverTimeIntegratorParametersGroup cadetInterface """ timeout = UnsignedFloat() def __init__(self, install_path=None, temp_dir=None, *args, **kwargs): self.install_path = install_path self.temp_dir = temp_dir super().__init__(*args, **kwargs) self.model_solver_parameters = ModelSolverParametersGroup() self.solver_parameters = SolverParametersGroup() self.time_integrator_parameters = SolverTimeIntegratorParametersGroup() self.return_parameters = ReturnParametersGroup() self.unit_return_parameters = UnitReturnParametersGroup() @property def install_path(self): """str: Path to the installation of CADET Parameters ---------- install_path : str or None Path to the installation of CADET. If None, the system installation will be used. Raises ------ FileNotFoundError If CADET can not be found. See Also -------- check_cadet() """ return self._install_path @install_path.setter def install_path(self, install_path): if install_path is None: try: if platform.system() == 'Windows': executable_path = Path(shutil.which("cadet-cli.exe")) else: executable_path = Path(shutil.which("cadet-cli")) except TypeError: raise FileNotFoundError( "CADET could not be found. Please set an install path" ) install_path = executable_path.parent.parent install_path = Path(install_path) if platform.system() == 'Windows': cadet_bin_path = install_path / "bin" / "cadet-cli.exe" else: cadet_bin_path = install_path / "bin" / "cadet-cli" if cadet_bin_path.exists(): self._install_path = install_path CadetAPI.cadet_path = cadet_bin_path else: raise FileNotFoundError( "CADET could not be found. Please check the path" ) cadet_lib_path = install_path / "lib" try: if cadet_lib_path.as_posix() not in os.environ['LD_LIBRARY_PATH']: os.environ['LD_LIBRARY_PATH'] = \ cadet_lib_path.as_posix() \ + os.pathsep \ + os.environ['LD_LIBRARY_PATH'] except KeyError: os.environ['LD_LIBRARY_PATH'] = cadet_lib_path.as_posix() def check_cadet(self): """Wrapper around a basic CADET example for testing functionality""" if platform.system() == 'Windows': lwe_path = self.install_path / "bin" / "createLWE.exe" else: lwe_path = self.install_path / "bin" / "createLWE" ret = subprocess.run( [lwe_path.as_posix()], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.temp_dir ) if ret.returncode != 0: if ret.stdout: print('Output', ret.stdout.decode('utf-8')) if ret.stderr: print('Errors', ret.stderr.decode('utf-8')) raise CADETProcessError( "Failure: Creation of test simulation ran into problems" ) lwe_hdf5_path = Path(self.temp_dir) / 'LWE.h5' sim = CadetAPI() sim.filename = lwe_hdf5_path.as_posix() data = sim.run() os.remove(sim.filename) if data.returncode == 0: print("Test simulation completed successfully") else: print(data) raise CADETProcessError( "Simulation failed" ) @property def temp_dir(self): return tempfile.gettempdir() @temp_dir.setter def temp_dir(self, temp_dir): if temp_dir is not None: try: exists = Path(temp_dir).exists() except TypeError: raise CADETProcessError('Not a valid path') if not exists: raise CADETProcessError('Not a valid path') tempfile.tempdir = temp_dir def get_tempfile_name(self): f = next(tempfile._get_candidate_names()) return os.path.join(self.temp_dir, f + '.h5') def run(self, process, file_path=None): """Interface to the solver run function The configuration is extracted from the process object and then saved as a temporary .h5 file. After termination, the same file is processed and the results are returned. Cadet Return information: - 0: pass (everything allright) - 1: Standard Error - 2: IO Error - 3: Solver Error Parameters ---------- process : Process process to be simulated Returns ------- results : SimulationResults Simulation results including process and solver configuration. Raises ------ TypeError If process is not instance of Process See also -------- get_process_config get_simulation_results """ if not isinstance(process, Process): raise TypeError('Expected Process') cadet = CadetAPI() cadet.root = self.get_process_config(process) if file_path is None: cadet.filename = self.get_tempfile_name() else: cadet.filename = file_path cadet.save() try: start = time.time() return_information = cadet.run(timeout=self.timeout) elapsed = time.time() - start except TimeoutExpired: raise CADETProcessError('Simulator timed out') if return_information.returncode != 0: self.logger.error( 'Simulation of {} with parameters {} failed.'.format( process.name, process.config ) ) raise CADETProcessError( 'CADET Error: {}'.format(return_information.stderr) ) try: cadet.load() results = self.get_simulation_results( process, cadet, elapsed, return_information ) except TypeError: raise CADETProcessError('Unexpected error reading SimulationResults.') # Remove files if file_path is None: os.remove(cadet.filename) return results def save_to_h5(self, process, file_path): cadet = CadetAPI() cadet.root = self.get_process_config(process) cadet.filename = file_path cadet.save() def load_from_h5(self, file_path): cadet = CadetAPI() cadet.filename = file_path cadet.load() return cadet def get_process_config(self, process): """Create the CADET config. Returns ------- config : Dict / Note ---- Sensitivities not implemented yet. See also -------- input_model input_solver input_return """ process.lock = True config = Dict() config.input.model = self.get_input_model(process) config.input.solver = self.get_input_solver(process) config.input['return'] = self.get_input_return(process) process.lock = False return config def get_simulation_results( self, process, cadet, time_elapsed, return_information, ): """Saves the simulated results for each unit into the dictionary concentration_record for the complete simulation and splitted into each cycle. For each unit in the flow_sheet of a process the index of the unit is get by calling the method get_unit_index. The process results of the simualtion are saved in concentration_record of the process for each unit for the key complete. For saving the process resulst for each cycle start and end variables are defined an saved under the key cycles in the concentration_record dictionary for each unit. Parameters ---------- process : Process Process that was simulated. cadet : CadetAPI Cadet object with simulation results. time_elapsed : float Time of simulation. return_information: str CADET-cli return information. Returns ------- results : SimulationResults Simulation results including process and solver configuration. Notes ----- !!! Implement method to read .h5 files that have no process associated. """ time = process.time solution = Dict() from collections import defaultdict try: for unit in process.flow_sheet.units: solution[unit.name] = defaultdict(list) unit_index = self.get_unit_index(process, unit) unit_solution = cadet.root.output.solution[unit_index] unit_coordinates = cadet.root.output.coordinates[unit_index].copy() particle_coordinates = \ unit_coordinates.pop('particle_coordinates_000', None) for cycle in range(process._n_cycles): start = cycle * (len(process.time) -1) end = (cycle + 1) * (len(process.time) - 1) + 1 if 'solution_inlet' in unit_solution.keys(): sol_inlet = unit_solution.solution_inlet[start:end,:] solution[unit.name]['inlet'].append( SolutionIO(unit.component_system, time, sol_inlet) ) if 'solution_outlet' in unit_solution.keys(): sol_outlet = unit_solution.solution_outlet[start:end,:] solution[unit.name]['outlet'].append( SolutionIO(unit.component_system, time, sol_outlet) ) if 'solution_bulk' in unit_solution.keys(): sol_bulk = unit_solution.solution_bulk[start:end,:] solution[unit.name]['bulk'].append( SolutionBulk( unit.component_system, time, sol_bulk, **unit_coordinates ) ) if 'solution_particle' in unit_solution.keys(): sol_particle = unit_solution.solution_particle[start:end,:] solution[unit.name]['particle'].append( SolutionParticle( unit.component_system, time, sol_particle, **unit_coordinates, particle_coordinates=particle_coordinates ) ) if 'solution_solid' in unit_solution.keys(): sol_solid = unit_solution.solution_solid[start:end,:] solution[unit.name]['solid'].append( SolutionSolid( unit.component_system, unit.binding_model.n_states, time, sol_solid, **unit_coordinates, particle_coordinates=particle_coordinates ) ) if 'solution_volume' in unit_solution.keys(): sol_volume = unit_solution.solution_volume[start:end,:] solution[unit.name]['volume'].append( SolutionVolume(unit.component_system, time, sol_volume) ) solution = Dict(solution) system_state = { 'state': cadet.root.output.last_state_y, 'state_derivative': cadet.root.output.last_state_ydot } chromatograms = [ Chromatogram( process.time, solution[chrom.name].outlet[-1].solution, process.flow_rate_timelines[chrom.name].total, name=chrom.name ) for chrom in process.flow_sheet.chromatogram_sinks ] except KeyError: raise CADETProcessError('Results don\'t match Process') results = SimulationResults( solver_name = str(self), solver_parameters = dict(), exit_flag = return_information.returncode, exit_message = return_information.stderr.decode(), time_elapsed = time_elapsed, process_name = process.name, process_config = process.config, process_meta = process.process_meta, solution_cycles = solution, system_state = system_state, chromatograms = chromatograms ) return results def get_input_model(self, process): """Config branch /input/model/ Note ---- !!! External functions not implemented yet See also -------- model_connections model_solver model_units input_model_parameters """ input_model = Dict() input_model.connections = self.get_model_connections(process) # input_model.external = self.model_external # !!! not working yet input_model.solver = self.model_solver_parameters.to_dict() input_model.update(self.get_model_units(process)) if process.system_state is not None: input_model['INIT_STATE_Y'] = process.system_state if process.system_state_derivative is not None: input_model['INIT_STATE_YDOT'] = process.system_state_derivative return input_model def get_model_connections(self, process): """Config branch /input/model/connections """ model_connections = Dict() model_connections['CONNECTIONS_INCLUDE_DYNAMIC_FLOW'] = 1 index = 0 section_states = process.flow_rate_section_states for cycle in range(0, process._n_cycles): for flow_rates_state in section_states.values(): switch_index = 'switch' + '_{0:03d}'.format(index) model_connections[switch_index].section = index connections = self.cadet_connections( flow_rates_state, process.flow_sheet ) model_connections[switch_index].connections = connections index += 1 model_connections.nswitches = index return model_connections def cadet_connections(self, flow_rates, flow_sheet): """list: Connections matrix for flow_rates state. Parameters ---------- flow_rates : dict UnitOperations with outgoing flow rates. flow_sheet : FlowSheet Object which hosts units (for getting unit index). Returns ------- ls : list Connections matrix for DESCRIPTION. """ table = Dict() enum = 0 for origin, unit_flow_rates in flow_rates.items(): origin = flow_sheet[origin] origin_index = flow_sheet.get_unit_index(origin) for dest, flow_rate in unit_flow_rates.destinations.items(): destination = flow_sheet[dest] destination_index = flow_sheet.get_unit_index(destination) if np.any(flow_rate): table[enum] = [] table[enum].append(int(origin_index)) table[enum].append(int(destination_index)) table[enum].append(-1) table[enum].append(-1) table[enum] += flow_rate.tolist() enum += 1 ls = [] for connection in table.values(): ls += connection return ls def get_unit_index(self, process, unit): """Helper function for getting unit index in CADET format unit_xxx. Parameters ----------- process : Process process to be simulated unit : UnitOperation Indexed object Returns ------- unit_index : str Return the unit index in CADET format unitXXX """ index = process.flow_sheet.get_unit_index(unit) return 'unit' + '_{0:03d}'.format(index) def get_model_units(self, process): """Config branches for all units /input/model/unit_000 ... unit_xxx. See also -------- get_unit_config get_unit_index """ model_units = Dict() model_units.nunits = len(process.flow_sheet.units) for unit in process.flow_sheet.units: unit_index = self.get_unit_index(process, unit) model_units[unit_index] = self.get_unit_config(unit) self.set_section_dependent_parameters(model_units, process) return model_units def get_unit_config(self, unit): """Config branch /input/model/unit_xxx for individual unit. The parameters from the unit are extracted and converted to CADET format Note ---- For now, only constant values for the concentration in sources are valid. In CADET, the parameter unit_config['discretization'].NBOUND should be moved to binding config or unit config See also -------- get_adsorption_config """ unit_parameters = UnitParametersGroup(unit) unit_config = Dict(unit_parameters.to_dict()) if not isinstance(unit.binding_model, NoBinding): n_bound = [unit.binding_model.n_states] * unit.binding_model.n_comp unit_config['adsorption'] = \ self.get_adsorption_config(unit.binding_model) unit_config['adsorption_model'] = unit_config['adsorption']['ADSORPTION_MODEL'] else: n_bound = unit.n_comp*[0] if not isinstance(unit.discretization, NoDiscretization): unit_config['discretization'] = unit.discretization.parameters if isinstance(unit, Cstr) and not isinstance(unit.binding_model, NoBinding): unit_config['nbound'] = n_bound else: unit_config['discretization']['nbound'] = n_bound if not isinstance(unit.bulk_reaction_model, NoReaction): parameters = self.get_reaction_config(unit.bulk_reaction_model) if isinstance(unit, LumpedRateModelWithoutPores): unit_config['reaction_model'] = parameters['REACTION_MODEL'] unit_config['reaction'] = parameters else: unit_config['reaction_model'] = parameters['REACTION_MODEL'] unit_config['reaction_bulk'] = parameters if not isinstance(unit.particle_reaction_model, NoReaction): parameters = self.get_reaction_config(unit.particle_reaction_model) unit_config['reaction_model_particle'] = parameters['REACTION_MODEL'] unit_config['reaction_particle'].update(parameters) if isinstance(unit, Source): unit_config['sec_000']['const_coeff'] = unit.c[:,0] unit_config['sec_000']['lin_coeff'] = unit.c[:,1] unit_config['sec_000']['quad_coeff']= unit.c[:,2] unit_config['sec_000']['cube_coeff'] = unit.c[:,3] return unit_config def set_section_dependent_parameters(self, model_units, process): """Add time dependent model parameters to units """ section_states = process.section_states.values() section_index = 0 for cycle in range(0, process._n_cycles): for param_states in section_states: for param, state in param_states.items(): param = param.split('.') unit_name = param[1] param_name = param[-1] try: unit = process.flow_sheet[unit_name] except KeyError: if unit_name == 'output_states': continue else: raise CADETProcessError( 'Unexpected section dependent parameter' ) if param_name == 'flow_rate': continue unit_index = process.flow_sheet.get_unit_index(unit) if isinstance(unit, Source) and param_name == 'c': self.add_inlet_section( model_units, section_index, unit_index, state ) else: unit_model = unit.model self.add_parameter_section( model_units, section_index, unit_index, unit_model, param_name, state ) section_index += 1 def add_inlet_section(self, model_units, sec_index, unit_index, coeffs): unit_index = 'unit' + '_{0:03d}'.format(unit_index) section_index = 'sec' + '_{0:03d}'.format(sec_index) model_units[unit_index][section_index]['const_coeff'] = coeffs[:,0] model_units[unit_index][section_index]['lin_coeff'] = coeffs[:,1] model_units[unit_index][section_index]['quad_coeff']= coeffs[:,2] model_units[unit_index][section_index]['cube_coeff'] = coeffs[:,3] def add_parameter_section( self, model_units, sec_index, unit_index, unit_model, parameter, state ): """Add section value to parameter branch. """ unit_index = 'unit' + '_{0:03d}'.format(unit_index) parameter_name = inv_unit_parameters_map[unit_model]['parameters'][parameter] if sec_index == 0: model_units[unit_index][parameter_name] = [] model_units[unit_index][parameter_name] += list(state.ravel()) def get_adsorption_config(self, binding): """Config branch /input/model/unit_xxx/adsorption for individual unit The parameters from the adsorption object are extracted and converted to CADET format See also -------- get_unit_config """ adsorption_config = AdsorptionParametersGroup(binding).to_dict() return adsorption_config def get_reaction_config(self, reaction): """Config branch /input/model/unit_xxx/reaction for individual unit Parameters ---------- reaction : ReactionBaseClass Reaction configuration object See also -------- get_unit_config """ reaction_config = ReactionParametersGroup(reaction).to_dict() return reaction_config def get_input_solver(self, process): """Config branch /input/solver/ See also -------- solver_sections solver_time_integrator """ input_solver = Dict() input_solver.update(self.solver_parameters.to_dict()) input_solver.user_solution_times = process._time_complete input_solver.sections = self.get_solver_sections(process) input_solver.time_integrator = \ self.time_integrator_parameters.to_dict() return input_solver def get_solver_sections(self, process): """Config branch /input/solver/sections """ solver_sections = Dict() solver_sections.nsec = process._n_cycles * process.n_sections solver_sections.section_times = [ round((cycle*process.cycle_time + evt),1) for cycle in range(process._n_cycles) for evt in process.section_times[0:-1] ] solver_sections.section_times.append( round(process._n_cycles * process.cycle_time,1) ) solver_sections.section_continuity = [0] * (solver_sections.nsec - 1) return solver_sections def get_input_return(self, process): """Config branch /input/return """ return_parameters = self.return_parameters.to_dict() unit_return_parameters = self.get_unit_return_parameters(process) return {**return_parameters, **unit_return_parameters} def get_unit_return_parameters(self, process): """Config branches for all units /input/return/unit_000 ... unit_xxx """ unit_return_parameters = Dict() for unit in process.flow_sheet.units: unit_index = self.get_unit_index(process, unit) unit_return_parameters[unit_index] = \ self.unit_return_parameters.to_dict() return unit_return_parameters def __str__(self): return 'CADET'
class PymooInterface(SolverBase): """Wrapper around pymoo. """ seed = UnsignedInteger(default=12345) x_tol = UnsignedFloat(default=1e-8) cv_tol = UnsignedFloat(default=1e-6) f_tol = UnsignedFloat(default=0.0025) pop_size = UnsignedInteger(default=100) nth_gen = UnsignedInteger(default=1) n_last = UnsignedInteger(default=30) n_max_gen = UnsignedInteger(default=100) n_max_evals = UnsignedInteger(default=100000) n_cores = UnsignedInteger(default=0) _options = [ 'x_tol', 'cv_tol', 'f_tol', 'nth_gen', 'n_last', 'n_max_gen', 'n_max_evals', ] def run(self, optimization_problem, use_checkpoint=True): """Solve the optimization problem using the functional pymoo implementation. Returns ------- results : OptimizationResults Optimization results including optimization_problem and solver configuration. See Also -------- evaluate_objectives options """ self.optimization_problem = optimization_problem ieqs = [ lambda x: optimization_problem.evaluate_linear_constraints(x)[0] ] self.problem = PymooProblem(optimization_problem, self.n_cores) if use_checkpoint and os.path.isfile(self.pymoo_checkpoint_path): random.seed(self.seed) algorithm, = np.load(self.pymoo_checkpoint_path, allow_pickle=True).flatten() else: algorithm = self.setup_algorithm() start = time.time() while algorithm.has_next(): algorithm.next() np.save(self.pymoo_checkpoint_path, algorithm) print(algorithm.result().X, algorithm.result().F) elapsed = time.time() - start res = algorithm.result() x = res.X eval_object = optimization_problem.set_variables(x, make_copy=True) if self.optimization_problem.evaluator is not None: frac = optimization_problem.evaluator.simulate_and_fractionate( eval_object, ) performance = frac.performance else: frac = None performance = optimization_problem.evaluate(x, force=True) results = OptimizationResults( optimization_problem=optimization_problem, evaluation_object=eval_object, solver_name=str(self), solver_parameters=self.options, exit_flag=0, exit_message='success', time_elapsed=elapsed, x=res.X.tolist(), f=res.F, c=res.CV, frac=frac, performance=performance.to_dict(), history=res.history, ) return results @property def pymoo_checkpoint_path(self): pymoo_checkpoint_path = os.path.join( settings.project_directory, self.optimization_problem.name + '/pymoo_checkpoint.npy') return pymoo_checkpoint_path @property def population_size(self): if self.pop_size is None: return min(200, max(25 * self.optimization_problem.n_variables, 50)) else: return self.pop_size @property def max_number_of_generations(self): if self.n_max_gen is None: return min(100, max(10 * self.optimization_problem.n_variables, 40)) else: return self.n_max_gen def setup_algorithm(self): algorithm = pymoo.factory.get_algorithm( str(self), ref_dirs=self.ref_dirs, pop_size=self.population_size, sampling=self.optimization_problem.create_initial_values( self.population_size, method='chebyshev', seed=self.seed), repair=RoundIndividuals(self.optimization_problem), ) algorithm.setup(self.problem, termination=self.termination, seed=self.seed, verbose=True) return algorithm @property def termination(self): termination = MultiObjectiveDefaultTermination( x_tol=self.x_tol, cv_tol=self.cv_tol, f_tol=self.f_tol, nth_gen=self.nth_gen, n_last=self.n_last, n_max_gen=self.n_max_gen, n_max_evals=self.n_max_evals) return termination @property def ref_dirs(self): ref_dirs = get_reference_directions( "energy", self.optimization_problem.n_objectives, self.population_size, seed=1) return ref_dirs
class SimulationResults(metaclass=StructMeta): """Class for storing simulation results including the solver configuration Attributes ---------- solver_name : str Name of the solver used to simulate the process solver_parameters : dict Dictionary with parameters used by the solver exit_flag : int Information about the solver termination. exit_message : str Additional information about the solver status time_elapsed : float Execution time of simulation. process_name : str Name of the simulated proces process_config : dict Configuration of the simulated process process_meta : dict Meta information of the process. solution : dict Time signals for all cycles of all Unit Operations. system_state : dict Final state and state_derivative of the system. chromatograms : List of chromatogram Solution of the final cycle of the chromatogram_sinks. n_cycles : int Number of cycles that were simulated. Notes ----- Ideally, the final state for each unit operation should be saved. However, CADET does currently provide this functionality. """ solver_name = String() solver_parameters = Dict() exit_flag = UnsignedInteger() exit_message = String() time_elapsed = UnsignedFloat() process_name = String() process_config = Dict() solution_cycles = Dict() system_state = Dict() chromatograms = List() def __init__(self, solver_name, solver_parameters, exit_flag, exit_message, time_elapsed, process_name, process_config, process_meta, solution_cycles, system_state, chromatograms): self.solver_name = solver_name self.solver_parameters = solver_parameters self.exit_flag = exit_flag self.exit_message = exit_message self.time_elapsed = time_elapsed self.process_name = process_name self.process_config = process_config self.process_meta = process_meta self.solution_cycles = solution_cycles self.system_state = system_state self.chromatograms = chromatograms def update(self, new_results): if self.process_name != new_results.process_name: raise CADETProcessError('Process does not match') self.exit_flag = new_results.exit_flag self.exit_message = new_results.exit_message self.time_elapsed += new_results.time_elapsed self.system_state = new_results.system_state self.chromatograms = new_results.chromatograms for unit, solutions in self.solution_cycles.items(): for sol in solutions: self.solution_cycles[unit][sol].append( new_results.solution[unit][sol]) @property def solution(self): """Construct complete solution from individual cyles. """ cycle_time = self.process_config['parameters']['cycle_time'] time_complete = self.time_cycle for i in range(1, self.n_cycles): time_complete = np.hstack( (time_complete, self.time_cycle[1:] + i * cycle_time)) solution = addict.Dict() for unit, solutions in self.solution_cycles.items(): for sol, cycles in solutions.items(): solution[unit][sol] = copy.deepcopy(cycles[0]) solution_complete = cycles[0].solution for i in range(1, self.n_cycles): solution_complete = np.vstack( (solution_complete, cycles[i].solution[1:])) solution[unit][sol].time = time_complete solution[unit][sol].solution = solution_complete return solution @property def n_cycles(self): return len( self.solution_cycles[self._first_unit][self._first_solution]) @property def _first_unit(self): return next(iter(self.solution_cycles)) @property def _first_solution(self): return next(iter(self.solution_cycles[self._first_unit])) @property def time_cycle(self): """np.array: Solution times vector """ return \ self.solution_cycles[self._first_unit][self._first_solution][0].time def save(self, case_dir, unit=None, start=0, end=None): path = os.path.join(settings.project_directory, case_dir) if unit is None: units = self.solution.keys() else: units = self.solution[unit] for unit in units: self.solution[unit][-1].plot(save_path=path + '/' + unit + '_last.png', start=start, end=end) for unit in units: self.solution_complete[unit].plot(save_path=path + '/' + unit + '_complete.png', start=start, end=end) for unit in units: self.solution[unit][-1].plot( save_path=path + '/' + unit + '_overlay.png', overlay=[cyc.signal for cyc in self.solution[unit][0:-1]], start=start, end=end)
class TubularReactor(UnitBaseClass): """Class for tubular reactors. Class can be used for a regular tubular reactor. Also serves as parent for other tubular models like the GRM by providing methods for calculating geometric properties such as the cross section area and volume, as well as methods for convective and dispersive properties like mean residence time or NTP. Notes ----- For subclassing, check that the total porosity and interstitial cross section area are computed correctly depending on the model porosities! Attributes ---------- length : UnsignedFloat Length of column. diameter : UnsignedFloat Diameter of column. axial_dispersion : UnsignedFloat Dispersion rate of compnents in axial direction. c : List of unsinged floats. Length depends on n_comp Initial concentration of the reactor. """ supports_bulk_reaction = True length = UnsignedFloat(default=0) diameter = UnsignedFloat(default=0) axial_dispersion = UnsignedFloat() total_porosity = 1 flow_direction = Switch(valid=[-1, 1], default=1) _parameter_names = UnitBaseClass._parameter_names + [ 'length', 'diameter', 'axial_dispersion', 'flow_direction' ] _section_dependent_parameters = UnitBaseClass._section_dependent_parameters + [ 'axial_dispersion', 'flow_direction' ] c = DependentlySizedUnsignedList(dep='n_comp', default=0) _initial_state = UnitBaseClass._initial_state + ['c'] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._discretization = LRMDiscretizationFV() @property def cross_section_area(self): """float: Cross section area of a Column. See also -------- volume cross_section_area_interstitial cross_section_area_liquid cross_section_area_solid """ return math.pi / 4 * self.diameter**2 @cross_section_area.setter def cross_section_area(self, cross_section_area): self.diameter = (4 * cross_section_area / math.pi)**0.5 def set_diameter_from_interstitial_velicity(self, Q, u0): """Set diamter from flow rate and interstitial velocity. In literature, often only the interstitial velocity is given. This method, the diameter / cross section area can be inferred from the flow rate, velocity, and porosity. Parameters ---------- Q : float Volumetric flow rate. u0 : float Interstitial velocity. Notes ----- Needs to be overwritten depending on the model porosities! """ self.cross_section_area = Q / (u0 * self.total_porosity) @property def cross_section_area_interstitial(self): """float: Interstitial area between particles. Notes ----- Needs to be overwritten depending on the model porosities! See also -------- cross_section_area cross_section_area_liquid cross_section_area_solid """ return self.total_porosity * self.cross_section_area @property def cross_section_area_liquid(self): """float: Liquid fraction of column cross section area. See also -------- cross_section_area cross_section_area_interstitial cross_section_area_solid volume """ return self.total_porosity * self.cross_section_area @property def cross_section_area_solid(self): """float: Liquid fraction of column cross section area. See also -------- cross_section_area cross_section_area_interstitial cross_section_area_liquid """ return (1 - self.total_porosity) * self.cross_section_area @property def volume(self): """float: Volume of the TubularReactor. See also -------- cross_section_area """ return self.cross_section_area * self.length @property def volume_interstitial(self): """float: Interstitial volume between particles. See also -------- cross_section_area """ return self.cross_section_area_interstitial * self.length @property def volume_liquid(self): """float: Volume of the liquid phase. """ return self.cross_section_area_liquid * self.length @property def volume_solid(self): """float: Volume of the solid phase. """ return self.cross_section_area_solid * self.length def t0(self, flow_rate): """Mean residence time of a (non adsorbing) volume element. Parameters ---------- flow_rate : float volumetric flow rate Returns ------- t0 : float Mean residence time See also -------- u0 """ return self.volume_interstitial / flow_rate def u0(self, flow_rate): """Flow velocity of a (non adsorbint) volume element. Parameters ---------- flow_rate : float volumetric flow rate Returns ------- u0 : float interstitial flow velocity See also -------- t0 NTP """ return self.length / self.t0(flow_rate) def NTP(self, flow_rate): """Number of theoretical plates. Parameters ---------- flow_rate : float volumetric flow rate Calculated using the axial dispersion coefficient: :math: NTP = \frac{u \cdot L_{Column}}{2 \cdot D_a} Returns ------- NTP : float Number of theretical plates """ return self.u0(flow_rate) * self.length / (2 * self.axial_dispersion) def set_axial_dispersion_from_NTP(self, NTP, flow_rate): """ Parameters ---------- NTP : float Number of theroetical plates flow_rate : float volumetric flow rate Calculated using the axial dispersion coefficient: :math: NTP = \frac{u \cdot L_{Column}}{2 \cdot D_a} Returns ------- NTP : float Number of theretical plates See also -------- u0 NTP """ self.axial_dispersion = self.u0(flow_rate) * self.length / (2 * NTP)
class DEAP(SolverBase): """ Adapter for optimization with an Genetic Algorithm called DEAP. Defines the solver options, the statistics, the history, the logbook and the toolbox for recording the optimization progess. It implements the abstract run method for running the optimization with DEAP. Attributes ---------- optimizationProblem: optimizationProblem Given optimization problem to be solved. options : dict Solver options, default set to None, if nothing is given. See also -------- base tools Statistics """ cxpb = UnsignedFloat(default=1) mutpb = UnsignedFloat(default=1) sig_figures = UnsignedInteger(default=3) seed = UnsignedInteger(default=12345) _options = ['cxpb', 'mutpb', 'sig_figures', 'seed'] def run(self, optimization_problem, n_gen=None, population_size=None, use_multicore=True, use_checkpoint=True): self.optimization_problem = optimization_problem # Abbreviations lb = optimization_problem.lower_bounds ub = optimization_problem.upper_bounds n_vars = optimization_problem.n_variables # Settings if population_size is None: population_size = min( 200, max(25 * len(optimization_problem.variables), 50)) if n_gen is None: n_gen = min(100, max(10 * len(optimization_problem.variables), 40)) # NSGA3 Settings n_obj = 1 p = 4 ref_points = tools.uniform_reference_points(n_obj, p) # !!! emo functions breaks if n_obj == 1, this is a temporary fix if n_obj == 1: def sortNDHelperB(best, worst, obj, front): if obj < 0: return sortNDHelperB(best, worst, obj, front) tools.emo.sortNDHelperB = sortNDHelperB # Definition of classes creator.create("FitnessMin", base.Fitness, weights=(-1.0, ) * n_obj) creator.create("Individual", list, fitness=creator.FitnessMin) # Tools toolbox = base.Toolbox() # Map for parallel evaluation manager = multiprocessing.Manager() cache = manager.dict() pool = multiprocessing.Pool() if use_multicore: toolbox.register("map", pool.map) # Functions for creating individuals and population toolbox.register("individual", tools.initIterate, creator.Individual, optimization_problem.create_initial_values) def initIndividual(icls, content): return icls(content) toolbox.register("individual_guess", initIndividual, creator.Individual) def initPopulation(pcls, ind_init, population_size): population = optimization_problem.create_initial_values( population_size) return pcls(ind_init(c) for c in population) toolbox.register( "population", initPopulation, list, toolbox.individual_guess, ) # Functions for evolution toolbox.register("evaluate", self.evaluate, cache=cache) toolbox.register("mate", tools.cxSimulatedBinaryBounded, low=lb, up=ub, eta=30.0) toolbox.register("mutate", tools.mutPolynomialBounded, low=lb, up=ub, eta=20.0, indpb=1.0 / n_vars) toolbox.register("select", tools.selNSGA3, nd="standard", ref_points=ref_points) # Round individuals to prevent reevaluation of similar individuals def round_individuals(): def decorator(func): def wrapper(*args, **kargs): offspring = func(*args, **kargs) for child in offspring: for index, el in enumerate(child): child[index] = round(el, self.sig_figures) return offspring return wrapper return decorator toolbox.decorate("mate", round_individuals()) toolbox.decorate("mutate", round_individuals()) statistics = tools.Statistics(key=lambda ind: ind.fitness.values) statistics.register("min", np.min) statistics.register("max", np.max) statistics.register("avg", np.mean) statistics.register("std", np.std) # Load checkpoint if present checkpoint_path = os.path.join( settings.project_directory, optimization_problem.name + '/checkpoint.pkl') if use_checkpoint and os.path.isfile(checkpoint_path): # A file name has been given, then load the data from the file with open(checkpoint_path, "rb") as cp_file: cp = pickle.load(cp_file) self.population = cp["population"] start_gen = cp["generation"] self.halloffame = cp["halloffame"] self.logbook = cp["logbook"] random.setstate(cp["rndstate"]) else: # Start a new evolution start_gen = 0 self.halloffame = tools.HallOfFame(maxsize=1) self.logbook = tools.Logbook() self.logbook.header = "gen", "evals", "std", "min", "avg", "max" # Initialize random population random.seed(self.seed) self.population = toolbox.population(population_size) # Evaluate the individuals with an invalid fitness invalid_ind = [ ind for ind in self.population if not ind.fitness.valid ] fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) for ind, fit in zip(invalid_ind, fitnesses): ind.fitness.values = fit # Compile statistics about the population record = statistics.compile(self.population) self.logbook.record(gen=0, evals=len(invalid_ind), **record) # Begin the generational process start = time.time() for gen in range(start_gen, n_gen): self.offspring = algorithms.varAnd(self.population, toolbox, self.cxpb, self.mutpb) # Evaluate the individuals with an invalid fitness invalid_ind = [ ind for ind in self.offspring if not ind.fitness.valid ] fitnesses = toolbox.map(toolbox.evaluate, invalid_ind) for ind, fit in zip(invalid_ind, fitnesses): ind.fitness.values = fit # Select the next generation population from parents and offspring self.population = toolbox.select(self.population + self.offspring, population_size) # Compile statistics about the new population record = statistics.compile(self.population) self.logbook.record(gen=gen, evals=len(invalid_ind), **record) self.halloffame.update(self.population) # Create Checkpoint file cp = dict(population=self.population, generation=gen, halloffame=self.halloffame, logbook=self.logbook, rndstate=random.getstate()) with open(checkpoint_path, "wb") as cp_file: pickle.dump(cp, cp_file) best = self.halloffame.items[0] self.logger.info('Generation {}: x: {}, f: {}'.format( str(gen), str(best), str(best.fitness.values[0]))) elapsed = time.time() - start x = self.halloffame.items[0] eval_object = optimization_problem.set_variables(x, make_copy=True) if self.optimization_problem.evaluator is not None: frac = optimization_problem.evaluator.evaluate(eval_object, return_frac=True) performance = frac.performance else: frac = None performance = optimization_problem.evaluate(x, force=True) f = optimization_problem.objective_fun(performance) results = OptimizationResults( optimization_problem=optimization_problem, evaluation_object=eval_object, solver_name=str(self), solver_parameters=self.options, exit_flag=1, exit_message='DEAP terminated successfully', time_elapsed=elapsed, x=list(x), f=f, c=None, frac=frac, performance=performance.to_dict()) return results def evaluate(self, ind, cache=None): results = self.optimization_problem.evaluate_objectives(ind, make_copy=True, cache=cache) return results
class Cstr(UnitBaseClass, SourceMixin, SinkMixin): """Parameters for an ideal mixer. Parameters ---------- c : List of unsinged floats. Length depends on n_comp Initial concentration of the reactor. q : List of unsinged floats. Length depends on n_comp Initial concentration of the bound phase. V : Unsinged float Initial volume of the reactor. total_porosity : UnsignedFloat between 0 and 1. Total porosity of the column. flow_rate_filter: np.Array Flow rate of pure liquid without components to reduce volume. """ supports_bulk_reaction = True supports_particle_reaction = True porosity = UnsignedFloat(ub=1, default=1) flow_rate_filter = Polynomial(dep=('_n_poly_coeffs'), default=0) _parameter_names = \ UnitBaseClass._parameter_names + \ SourceMixin._parameter_names + \ ['porosity', 'flow_rate_filter'] _section_dependent_parameters = \ UnitBaseClass._section_dependent_parameters + \ SourceMixin._section_dependent_parameters + \ ['flow_rate_filter'] _polynomial_parameters = \ UnitBaseClass._polynomial_parameters + \ SourceMixin._polynomial_parameters + \ ['flow_rate_filter'] c = DependentlySizedUnsignedList(dep='n_comp', default=0) q = DependentlySizedUnsignedList(dep=('n_comp', '_n_bound_states'), default=0) V = UnsignedFloat(default=0) volume = V _initial_state = \ UnitBaseClass._initial_state + \ ['c', 'q', 'V'] @property def volume_liquid(self): """float: Volume of the liquid phase. """ return self.porosity * self.V @property def volume_solid(self): """float: Volume of the solid phase. """ return (1 - self.porosity) * self.V def t0(self, flow_rate): """Mean residence time of a (non adsorbing) volume element. Parameters ---------- flow_rate : float volumetric flow rate Returns ------- t0 : float Mean residence time See also -------- u0 """ return self.volume_liquid / flow_rate
class OptimizationResults(metaclass=StructMeta): """Class for storing optimization results including the solver configuration Attributes ---------- optimization_problem : OptimizationProblem Optimization problem evaluation_object : obj Evaluation object in optimized state. solver_name : str Name of the solver used to simulate the process solver_parameters : dict Dictionary with parameters used by the solver exit_flag : int Information about the solver termination. exit_message : str Additional information about the solver status time_elapsed : float Execution time of simulation. x : list Values of optimization variables at optimum. f : np.ndarray Value of objective function at x. c : np.ndarray Values of constraint function at x """ x0 = List() solver_name = String() solver_parameters = Dict() exit_flag = UnsignedInteger() exit_message = String() time_elapsed = UnsignedFloat() x = List() f = NdArray() c = NdArray() performance = Dict() def __init__( self, optimization_problem, evaluation_object, solver_name, solver_parameters, exit_flag, exit_message, time_elapsed, x, f, c, performance, frac=None, history=None ): self.optimization_problem = optimization_problem self.evaluation_object = evaluation_object self.solver_name = solver_name self.solver_parameters = solver_parameters self.exit_flag = exit_flag self.exit_message = exit_message self.time_elapsed = time_elapsed self.x = x self.f = f if c is not None: self.c = c self.performance = performance self.frac = frac self.history = history def to_dict(self): return { 'optimization_problem': self.optimization_problem.name, 'optimization_problem_parameters': self.optimization_problem.parameters, 'evaluation_object_parameters': self.evaluation_object.parameters, 'x0': self.optimization_problem.x0, 'solver_name': self.solver_name, 'solver_parameters': self.solver_parameters, 'exit_flag': self.exit_flag, 'exit_message': self.exit_message, 'time_elapsed': self.time_elapsed, 'x': self.x, 'f': self.f.tolist(), 'c': self.c.tolist(), 'performance': self.performance, 'git': { 'chromapy_branch': str(settings.repo.active_branch), 'chromapy_commit': settings.repo.head.object.hexsha } } def save(self, directory): path = os.path.join(settings.project_directory, directory, 'results.json') with open(path, 'w') as f: json.dump(self.to_dict(), f, indent=4) def plot_solution(self): pass
class CarouselBuilder(metaclass=StructMeta): switch_time = UnsignedFloat() def __init__(self, n_comp, name): self.n_comp = n_comp self.name = name self._flow_sheet = FlowSheet(n_comp, name) self._column = None @property def flow_sheet(self): return self._flow_sheet @property def column(self): return self._column @column.setter def column(self, column): if not isinstance(column, TubularReactor): raise TypeError if not self.n_comp == column.n_comp: raise CADETProcessError('Number of components does not match.') self._column = column def add_unit(self, unit): """Wrapper around auxiliary flow_sheet """ self.flow_sheet.add_unit(unit) def add_connection(self, origin, destination): """Wrapper around auxiliary flow_sheet """ self.flow_sheet.add_connection(origin, destination) def set_output_state(self, unit, state): """Wrapper around auxiliary flow_sheet """ self.flow_sheet.set_output_state(unit, state) @property def zones(self): """list: list of all zones in the carousel system. """ return [ unit for unit in self.flow_sheet.units if isinstance(unit, ZoneBaseClass) ] @property def zones_dict(self): """dict: Zone names and objects. """ return {zone.name: zone for zone in self.zones} @property def n_zones(self): """int: Number of zones in the Carousel System """ return len(self.zones) @property def n_columns(self): """int: Number of columns in the Carousel System """ return sum([zone.n_columns for zone in self.zones]) def build_flow_sheet(self): flow_sheet = FlowSheet(self.n_comp, self.name) self.add_units(flow_sheet) self.add_inter_zone_connections(flow_sheet) self.add_intra_zone_connections(flow_sheet) return flow_sheet def add_units(self, flow_sheet): col_index = 0 for unit in self.flow_sheet.units: if not isinstance(unit, ZoneBaseClass): flow_sheet.add_unit(unit) else: flow_sheet.add_unit(unit.inlet_unit) flow_sheet.add_unit(unit.outlet_unit) for i_col in range(unit.n_columns): col = deepcopy(self.column) col.name = f'column_{col_index}' if unit.initial_state is not None: col.initial_state = unit.initial_state[i_col] flow_sheet.add_unit(col) col_index += 1 def add_inter_zone_connections(self, flow_sheet): for unit, connections in self.flow_sheet.connections.items(): if isinstance(unit, ZoneBaseClass): origin = unit.outlet_unit else: origin = unit for destination in connections.destinations: if isinstance(destination, ZoneBaseClass): destination = destination.inlet_unit flow_sheet.add_connection(origin, destination) flow_rates = self.flow_sheet.get_flow_rates() for zone in self.zones: output_state = self.flow_sheet.output_states[zone] flow_sheet.set_output_state(zone.outlet_unit, output_state) flow_sheet[zone.inlet_unit.name].flow_rate = flow_rates[ zone.name].total flow_sheet[zone.outlet_unit.name].flow_rate = flow_rates[ zone.name].total def add_intra_zone_connections(self, flow_sheet): for zone in self.zones: for col_index in range(self.n_columns): col = flow_sheet[f'column_{col_index}'] flow_sheet.add_connection(zone.inlet_unit, col) col = flow_sheet[f'column_{col_index}'] flow_sheet.add_connection(col, zone.outlet_unit) for col_index in range(self.n_columns): col_orig = flow_sheet[f'column_{col_index}'] if col_index < self.n_columns - 1: col_dest = flow_sheet[f'column_{col_index + 1}'] else: col_dest = flow_sheet[f'column_{0}'] flow_sheet.add_connection(col_orig, col_dest) def build_process(self): flow_sheet = self.build_flow_sheet() process = Process(flow_sheet, self.name) self.add_events(process) return process def add_events(self, process): process.cycle_time = self.n_columns * self.switch_time process.add_duration('switch_time', self.switch_time) for carousel_state in range(self.n_columns): position_counter = 0 for i_zone, zone in enumerate(self.zones): col_indices = np.arange(zone.n_columns) col_indices += position_counter col_indices = self.unit_index(col_indices, carousel_state) if isinstance(zone, SerialZone): evt = process.add_event( f'{zone.name}_{carousel_state}', f'flow_sheet.output_states.{zone.inlet_unit}', col_indices[0]) process.add_event_dependency(evt.name, 'switch_time', [carousel_state]) for i, col in enumerate(col_indices): if i < (zone.n_columns - 1): evt = process.add_event( f'column_{col}_{carousel_state}', f'flow_sheet.output_states.column_{col}', self.n_zones) else: evt = process.add_event( f'column_{col}_{carousel_state}', f'flow_sheet.output_states.column_{col}', i_zone) process.add_event_dependency(evt.name, 'switch_time', [carousel_state]) elif isinstance(zone, ParallelZone): output_state = self.n_columns * [0] for col in col_indices: output_state[col] = 1 / zone.n_columns evt = process.add_event( f'{zone.name}_{carousel_state}', f'flow_sheet.output_states.{zone.inlet_unit}', output_state) process.add_event_dependency(evt.name, 'switch_time', [carousel_state]) for col in col_indices: evt = process.add_event( f'column_{col}_{carousel_state}', f'flow_sheet.output_states.column_{col}', i_zone) process.add_event_dependency(evt.name, 'switch_time', [carousel_state]) for i, col in enumerate(col_indices): evt = process.add_event( f'column_{col}_{carousel_state}_velocity', f'flow_sheet.column_{col}.flow_direction', zone.flow_direction) process.add_event_dependency(evt.name, 'switch_time', [carousel_state]) position_counter += zone.n_columns def unit_index(self, carousel_position, carousel_state): """Return unit index of column at given carousel position and state. Parameters ---------- carousel_position : int Column position index (e.g. wash position, elute position). carousel_state : int Curent state of the carousel system. n_columns : int Total number of columns in system. """ return (carousel_position + carousel_state) % self.n_columns
class StationarityEvaluator(metaclass=StructMeta): """Class for checking two succeding chromatograms for stationarity Attributes ---------- Notes ----- !!! Implement check_skewness and width deviation """ check_concentration = Bool(default=True) max_concentration_deviation = UnsignedFloat(default=0.1) check_area = Bool(default=True) max_area_deviation = UnsignedFloat(default=1) check_height = Bool(default=True) max_height_deviation = UnsignedFloat(default=0.1) def __init__(self): self.logger = log.get_logger('StationarityEvaluator') def assert_stationarity(self, conc_old, conc_new): """Check Wrapper function for checking stationarity of two succeeding cycles. First the module 'stationarity' is imported, then the concentration profiles for the current and the last cycles are defined. After this all checking function from module 'stafs = FlowSheet(n_comp=2, name=flow_sheet_name) tionarity' are called. Parameters ---------- conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle """ if not isinstance(conc_old, TimeSignal): raise TypeError('Expcected TimeSignal') if not isinstance(conc_new, TimeSignal): raise TypeError('Expcected TimeSignal') criteria = {} if self.check_concentration: criterion, value = self.check_concentration_deviation( conc_old, conc_new) criteria['concentration_deviation'] = { 'status': criterion, 'value': value } if self.check_area: criterion, value = self.check_area_deviation(conc_old, conc_new) criteria['area_deviation'] = {'status': criterion, 'value': value} if self.check_height: criterion, value = self.check_height_deviation(conc_old, conc_new) criteria['height_deviation'] = { 'status': criterion, 'value': value } self.logger.debug('Stationrity criteria: {}'.format(criteria)) if all([crit['status'] for crit in criteria.values()]): return True return False def concentration_deviation(self, conc_old, conc_new): """Calculate the concentration profile deviation of two succeeding cycles. Parameters ---------- conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle Returns ------- concentration_deviation : np.array Concentration difference of two succeding cycles. """ return np.max(abs(conc_new.signal - conc_old.signal), axis=0) def check_concentration_deviation(self, conc_old, conc_new): """Check if deviation in concentration profiles is smaller than eps Parameters ---------- conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle Returns ------- bool True, if concentration deviation smaller than eps, False otherwise. False """ conc_dev = self.concentration_deviation(conc_old, conc_new) if np.any(conc_dev > self.max_concentration_deviation): criterion = False else: criterion = True return criterion, np.max(conc_dev) def area_deviation(self, conc_old, conc_new): """Calculate the area deviation of two succeeding cycles. conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle Returns ------- area_deviation : np.array Area deviation of two succeding cycles. """ area_old = simps(conc_old.signal, conc_old.time, axis=0) area_new = simps(conc_new.signal, conc_new.time, axis=0) return abs(area_old - area_new) def check_area_deviation(self, conc_old, conc_new, eps=1): """Check if deviation in concentration profiles is smaller than eps Parameters ---------- conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle Returns ------- bool True, if area deviation is smaller than eps, False otherwise. """ area_dev = self.area_deviation(conc_old, conc_new) if np.any(area_dev > self.max_area_deviation): criterion = False else: criterion = True return criterion, area_dev def height_deviation(self, conc_old, conc_new): """Calculate the height deviation of two succeeding cycles. conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle Returns ------- height_deviation : np.array Height deviation of two succeding cycles. """ height_old = np.amax(conc_old.signal, 0) height_new = np.amax(conc_new.signal, 0) return abs(height_old - height_new) def check_height_deviation(self, conc_old, conc_new): """Check if deviation in peak heigth is smaller than eps. Parameters ---------- conc_old : TimeSignal Concentration profile of previous cycle conc_new : TimeSignal Concentration profile of current cycle Returns ------- bool True, if height deviation is smaller than eps, False otherwise. """ abs_height_deviation = self.height_deviation(conc_old, conc_new) if np.any(abs_height_deviation > self.max_height_deviation): criterion = False else: criterion = True return criterion, abs_height_deviation