Source code for CADETProcess.fractionation.fractionationOptimizer

import warnings

import numpy as np

from CADETProcess import CADETProcessError

from CADETProcess import SimulationResults
from CADETProcess.fractionation import Fractionator
from CADETProcess.optimization import OptimizerBase, OptimizationProblem
from CADETProcess.optimization import COBYLA
from CADETProcess.performance import Mass, Purity


__all__ = ['FractionationOptimizer']


class FractionationEvaluator():
    """Dummy Evaluator to enable caching."""

    def evaluate(self, fractionator):
        """Evaluate the fractionator.

        Parameters
        ----------
        fractionator: Fractionator
            The Fractionator object to be evaluated.

        Returns
        -------
        object
            The evaluation result.
        """
        return fractionator.performance

    __call__ = evaluate

    def __str__(self):
        return __class__.__name__


[docs] class FractionationOptimizer(): """Configuration for fractionating Chromatograms.""" def __init__(self, optimizer=None, log_level='WARNING'): """Initialize the FractionationOptimizer. Parameters ---------- optimizer: OptimizerBase, optional Optimizer for optimizing the fractionation times. If no value is specified, a default COBYLA optimizer will be used. log_level: {'WARNING', 'INFO', 'DEBUG', 'ERROR'} Log level for the fractionation optimization process. The default is 'WARNING'. """ if optimizer is None: optimizer = COBYLA() optimizer.tol = 1e-3 optimizer.catol = 5e-3 optimizer.rhobeg = 1e-4 self.optimizer = optimizer self.log_level = log_level @property def optimizer(self): """OptimizerBase: Optimizer for optimizing the fractionation times.""" return self._optimizer @optimizer.setter def optimizer(self, optimizer): """Set the optimizer. Parameters ---------- optimizer: OptimizerBase The optimizer to be set. Raises ------ TypeError If the optimizer is not an instance of OptimizerBase. """ if not isinstance(optimizer, OptimizerBase): raise TypeError('Expected OptimizerBase') self._optimizer = optimizer def _setup_fractionator( self, simulation_results, purity_required, components=None, use_total_concentration_components=True, allow_empty_fractions=True): """Set up the Fractionator for optimizing the fractionation times of Chromatograms. Parameters ---------- simulation_results: object Simulation results to be used for setting up the Fractionator object. purity_required : list of floats Minimum purity required for the components in the fractionation. components: list, optional List of components to consider in the fractionation process. use_total_concentration_components: bool, optional If True, use the total concentration of the components. The default is True. allow_empty_fractions: bool, optional If True, allow empty fractions. The default is True. Returns ------- Fractionator The Fractionator object that has been set up using the provided arguments. Raises ------ CADETProcessError If no areas with sufficient purity were found and `ignore_failed` is False. """ frac = Fractionator( simulation_results, components=components, use_total_concentration_components=use_total_concentration_components, ) frac.initial_values(purity_required) if not np.any(frac.n_fractions_per_pool[:-1]): raise CADETProcessError("No areas found with sufficient purity.") if not allow_empty_fractions : empty_fractions = [] for i, comp in enumerate(purity_required): if comp > 0: if frac.fraction_pools[i].n_fractions == 0: empty_fractions.append(i) if len(empty_fractions) != 0: raise CADETProcessError( "No areas found with sufficient purity for component(s) " f"{[str(frac.component_system[i]) for i in empty_fractions]}." ) return frac def _setup_optimization_problem( self, frac, purity_required, allow_empty_fractions=True, ranking=1, obj_fun=None, minimize=True, bad_metrics=None, n_objectives=1): """Set up the OptimizationProblem for optimizing the fractionation times. Parameters ---------- frac : Fractionator The Fractionator object. purity_required : list Minimum purity required for the components in the fractionation. allow_empty_fractions: bool, optional If True, allow empty fractions. The default is True. ranking : list, 1 or None, optional Weighting factors for individual components. If 1, the same value is assumed for all components. If None, no ranking is used and the problem is solved as multi-objective. The default is 1. obj_fun : callable, optional Alternative objective function. If no function is provided, the fraction mass is maximized. The default is None. bad_metrics : float or list of floats, optional Values to be returned if evaluation of objective function failes. The default is 0. minimize : bool, optional If True, the obj_fun is assumed to return a value that is to be minimized. The default it True. n_objectives : int Number of objectives. The default is 1. Raises ------ CADETProcessError If the optimization problem setup fails. Returns ------- OptimizationProblem The configured OptimizationProblem object. list The initial values for the optimization variables. """ # Handle empty fractions n_fractions = np.array([pool.n_fractions for pool in frac.fraction_pools]) empty_fractions = np.where(n_fractions[0:-1] == 0)[0] if len(empty_fractions) > 0 and allow_empty_fractions: for empty_fraction in empty_fractions: purity_required[empty_fraction] = 0 # Setup Optimization Problem opt = OptimizationProblem( 'FractionationOptimization', log_level=self.log_level, use_diskcache=False, ) opt.add_evaluation_object(frac) frac_evaluator = FractionationEvaluator() opt.add_evaluator(frac_evaluator) if obj_fun is None: obj_fun = Mass(ranking=ranking) minimize = False bad_metrics = 0 opt.add_objective( obj_fun, requires=frac_evaluator, n_objectives=n_objectives, minimize=minimize, bad_metrics=bad_metrics, ) purity = Purity() purity.n_metrics = frac.component_system.n_comp opt.add_nonlinear_constraint( purity, n_nonlinear_constraints=len(purity_required), bounds=purity_required, comparison_operator='ge', requires=frac_evaluator, ) for evt in frac.events: opt.add_variable( evt.name, parameter_path=evt.name + '.time', lb=-frac.cycle_time, ub=2*frac.cycle_time, transform='linear' ) for chrom_index, chrom in enumerate(frac.chromatograms): chrom_events = frac.chromatogram_events[chrom] evt_names = [evt.name for evt in chrom_events] for evt_index, evt in enumerate(chrom_events): if evt_index < len(chrom_events) - 1: opt.add_linear_constraint( [evt_names[evt_index], evt_names[evt_index+1]], [1, -1] ) else: opt.add_linear_constraint( [evt_names[0], evt_names[-1]], [-1, 1], frac.cycle_time ) x0 = [evt.time for evt in frac.events] if not opt.check_nonlinear_constraints(x0): raise CADETProcessError("No areas found with sufficient purity.") return opt, x0
[docs] def optimize_fractionation( self, simulation_results, purity_required, components=None, use_total_concentration_components=True, ranking=1, obj_fun=None, n_objectives=1, bad_metrics=0, minimize=True, allow_empty_fractions=True, ignore_failed=False, return_optimization_results=False, save_results=False): """Optimize the fractionation times with respect to purity constraints. Parameters ---------- simulation_results : SimulationResults Results containing the chromatograms for fractionation. purity_required : float or array_like Minimum required purity for components. If is float, the same value is assumed for all components. components : list List of components to consider in the fractionation process. ranking : list, 1 or None, optional Weighting factors for individual components. If 1, the same value is assumed for all components. If None, no ranking is used and the problem is solved as multi-objective. The default is 1. obj_fun : function, optional Objective function used for OptimizationProblem. If COBYLA is used, must return single objective. If is None, the mass of all components is maximized. n_objectives : int, optional Number of objectives returned by obj_fun. The default is 1. bad_metrics : float or list of floats, optional Values to be returned if evaluation of objective function failes. The default is 0. minimize : bool, optional If True, the obj_fun is assumed to return a value that is to be minimized. The default it True. allow_empty_fractions: bool, optional If True, allow empty fractions. The default is True. ignore_failed : bool, optional Ignore failed optimization and use initial values. The default is False. return_optimization_results : bool, optional If True, return optimization results. Otherwise, return fractionation object. The default is False. save_results : bool, optional If True, save optimization results. The default is False. Returns ------- Fractionator or OptimizationResults The Fractionator object with optimized cut times or the OptimizationResults object. Raises ------ TypeError If simulation_results is not an instance of SimulationResults. CADETProcessError If simulation_results do not contain chromatograms. Warning If purity requirements cannot be fulfilled. See Also -------- _setup_fractionator _setup_optimization_problem Fractionator CADETProcess.solution.SolutionIO CADETProcess.optimization.OptimizationProblem CADETProcess.optimization.OptimizerBase """ if not isinstance(simulation_results, SimulationResults): raise TypeError('Expected SimulationResults.') if len(simulation_results.chromatograms) == 0: raise CADETProcessError( 'Simulation results do not contain chromatogram.' ) if isinstance(purity_required, float): n_comp = simulation_results.component_system.n_comp purity_required = n_comp * [purity_required] # Store previous lock state, unlock to ensure consistent values lock_state = simulation_results.process.lock simulation_results.process.lock = False frac = self._setup_fractionator( simulation_results, purity_required, components=components, use_total_concentration_components=use_total_concentration_components, allow_empty_fractions=allow_empty_fractions ) opt, x0 = self._setup_optimization_problem( frac, purity_required, allow_empty_fractions, ranking, obj_fun, n_objectives, bad_metrics, minimize, ) # Lock to enable caching simulation_results.process.lock = True try: results = self.optimizer.optimize( opt, x0, save_results=save_results, log_level=self.log_level, delete_cache=True, ) opt.set_variables(results.x[0]) frac.reset() except CADETProcessError as e: if ignore_failed: warnings.warn('Optimization failed. Returning initial values') frac.initial_values(purity_required) else: raise CADETProcessError(str(e)) finally: # Restore previous lock state simulation_results.process.lock = lock_state if return_optimization_results: return results else: return frac
evaluate = optimize_fractionation __call__ = evaluate def __str__(self): return self.__class__.__name__