Exemple #1
0
class Layout(Structure):
    style = String()
    title = String()
    x_label = String()
    x_ticks = List()
    y_label = String()
    y_ticks = List()
    xlim = Tuple()
    ylim = Tuple()
Exemple #2
0
class FillRegion(Structure):
    color_index = Integer()
    start = UnsignedFloat()
    end = UnsignedFloat()

    y_max = UnsignedFloat()

    text = String()
Exemple #3
0
class ComponentSystem(metaclass=StructMeta):
    name = String()

    def __init__(self, name=None, n_comp=None):
        self.name = name
        self._components = []

        if n_comp is not None:
            for comp in range(n_comp):
                self.add_component()

    @property
    def components(self):
        return self._components

    @property
    def n_components(self):
        return len(self.components)

    @property
    def n_comp(self):
        return sum([comp.n_species for comp in self.components])

    def add_component(self, *args, **kwargs):
        component = Component(*args, **kwargs)
        self._components.append(component)

    @property
    def labels(self):
        labels = []
        index = 0
        for comp in self.components:
            for label in comp.labels:
                if label is None:
                    labels.append(str(index))
                else:
                   labels.append(label)
               
                index += 1

        return labels

    @property
    def charges(self):
        charges = []
        for comp in self.components:
            charges += comp.charges

        return charges
Exemple #4
0
class Component(Structure):
    name = String()
    species = List()
    charges = DependentlySizedList(dep='n_species')
    molecular_weight = DependentlySizedList(dep='n_species')

    @property
    def n_species(self):
        if self.species is None:
            return 1
        else:
            return len(self.species)

    @property
    def labels(self):
        if self.species is None:
            return [self.name]
        else:
            return self.species
Exemple #5
0
class OptimizationProblem(metaclass=StructMeta):
    """Class for configuring optimization problems

    Defines lists, dictionaries and variables for creating an
    OptimizationProblem. If no name is set it tries to set the name by the
    evaluation_object name. An excepted AttributeError is ignored.

    Attributes
    ----------
    name : str
        Name of the optimization problem
    evaluation_object :  obj
        Object containing parameters to be optimized.
    evaluator : obj
        Object used to evaluate evaluation_object. Returns performance.
    variables : list
        List of optimization variables
    objectives: list of callables
        Functions that return value of objective function for performance.
    nonlinear_constraints: list of callables
        Functions that return value of nonlinear constraints for performance.
    linear_constraints : list
        List of all linear constraints of an OptimizationProblem.
    linear_equality_constraints : list
        List with all linear equality constrains of an OptimizationProblem.
    eval_dict : dict
        Database for storing evaluated individuals
    """
    name = String()

    def __init__(self,
                 evaluation_object,
                 evaluator=None,
                 name=None,
                 save_log=False):
        if evaluator is not None:
            self.evaluator = evaluator
        else:
            self._evaluator = None

        self.evaluation_object = evaluation_object

        if name is None:
            try:
                name = evaluation_object.name + '_optimization'
            except AttributeError:
                msg = '__init__() missing 1 required positional argument: \'name\''
                raise TypeError(msg)
        self.name = name

        if save_log:
            self.logger = log.get_logger(self.name, log_directory=self.name)
        else:
            self.logger = log.get_logger(self.name)

        self._variables = []
        self._objectives = []
        self._nonlinear_constraints = []
        self._linear_constraints = []
        self._linear_equality_constraints = []
        self._x0 = None

    @property
    def evaluation_object(self):
        """obj : Object that contains all parameters that are optimized.

        Returns
        -------
        evaluation_object : obj
            Object to be evaluated during optimization.

        See also
        --------
        OptimizatonVariable
        Evaluator
        evaluate
        Performance
        objectives
        nonlinear_constraints
        """
        return self._evaluation_object

    @evaluation_object.setter
    def evaluation_object(self, evaluation_object):
        self._evaluation_object = evaluation_object

    @property
    def evaluator(self):
        """Object that performs evaluation object during optimization.

        The evaluator has to implement an evaluate method that returns a
        Performance object when called with the evaluation_object. The
        performance Object is required for calculating the objective function
        and nonlinear constraint function.

        Parameters
        ----------
        evaluator : obj
            Object to be evaluated during optimization.

        Raises
        ------
        CADETProcessError
            If evaluation object does not implement evaluation method.

        See also
        --------
        evaluation_object
        evaluate
        Performance
        objectives
        nonlinear_constraints
        """
        return self._evaluator

    @evaluator.setter
    def evaluator(self, evaluator):
        try:
            getattr(evaluator, 'evaluate')
        except TypeError:
            raise TypeError('Evaluator has to implement evaluate method')

        self._evaluator = evaluator

    @property
    def variables(self):
        """list: List of all optimization variables.
        """
        return self._variables

    @property
    def variables_dict(self):
        """Returns a dictionary with all events in a process.

        Returns
        -------
        events_dict : dict
            Dictionary with all events and durations, indexed by Event.name.
        """
        return {var.name: var for var in self._variables}

    @property
    def n_variables(self):
        """int: Number of optimization variables.
        """
        return len(self.variables)

    def add_variable(self,
                     parameter_path,
                     name=None,
                     lb=-math.inf,
                     ub=math.inf,
                     component_index=None):
        """Add optimization variable to the OptimizationProblem.

        The function encapsulates the creation of OoptimizationVariable objects
        in order to prevent invalid OptimizationVariables.

        Parameters
        ----------
        attr : str
            Attribute of the variable to be added.
        name : str
            Name of the variable to be added, default value is None.
        lb : float
            Lower bound of the variable value.
        ub : float
            Upper bound of the variable value.
        component_index : int
            Index for compnent specific variables

        Raises
        ------
        CADETProcessError
            If the Variable already exists in the dictionary.

        See also
        --------
        evaluation_object
        OptimizationVariable
        remove_variable
        """
        if name is None:
            name = parameter_path

        if name in self.variables_dict:
            raise CADETProcessError("Variable already exists")

        var = OptimizationVariable(name,
                                   self.evaluation_object,
                                   parameter_path,
                                   lb=lb,
                                   ub=ub,
                                   component_index=component_index)

        self._variables.append(var)
        super().__setattr__(name, var)

    def remove_variable(self, var_name):
        """Removes variables from the OptimizationProblem.

        Parameters
        ----------
        var_name : str
            Name of the variable to be removed from list of variables.

        Raises
        ------
        CADETProcessError
            If required variable does not exist.

        See also
        --------
        add_variable
        """
        try:
            var = self.variables_dict[var_name]
        except KeyError:
            raise CADETProcessError("Variable does not exist")

        self._variables.remove(var)
        self.__dict__.pop(var_name)

    def set_variables(self, x, make_copy=False):
        """Sets the values from the x-vector to the OptimizationVariables.

        Parameters
        ----------
         x : array_like
            Value of the optimization variables

        make_copy : Bool
            If True, a copy of the evaluation_object attribute is made on which
            the values are set. Otherwise, the values are set on the attribute.

        Returns
        -------
        evaluation_object : object
            Returns copy of evaluation object if make_copy is True, else returns
            the attribute evaluation_object with the values set.

        Raises
        ------
        ValueError
            If value of variable exceeds bounds

        See also
        --------
        OptimizationVariable
        evaluate
        """
        if len(x) != self.n_variables:
            raise CADETProcessError('Expected {} variables'.format(
                self.n_variables))
        if make_copy:
            evaluation_object = copy.deepcopy(self.evaluation_object)
        else:
            evaluation_object = self.evaluation_object

        for variable, value in zip(self.variables, x):
            if value < variable.lb:
                raise ValueError("Exceeds lower bound")
            if value > variable.ub:
                raise ValueError("Exceeds upper bound")

            if variable.component_index is not None:
                value_list = get_nested_value(evaluation_object.parameters,
                                              variable.parameter_path)
                value_list[variable.component_index] = value
                parameters = generate_nested_dict(variable.parameter_path,
                                                  value_list)
            else:
                parameters = generate_nested_dict(variable.parameter_path,
                                                  value)
            evaluation_object.parameters = parameters

        return evaluation_object

    def evaluate(self, x, make_copy=False, cache=None, force=False):
        """Evaluates the evaluation object at x.

        For performance reasons, the function first checks if the function has
        already been evaluated at x and returns the results. If force is True,
        the lookup is ignored and the function is evaluated regularly.

        Parameters
        ----------
         x : array_like
            Value of the optimization variables
        force : bool
            If True, reevaluation of previously evaluated values is enforced.

        Returns
        -------
        performance : Performance
            Performance object from fractionation.
        """
        x = np.array(x)

        # Try to get cached results
        if cache is not None and not force:
            try:
                performance = cache[tuple(x.tolist())]
                return performance
            except KeyError:
                pass

        # Get evaluation_object
        evaluation_object = self.set_variables(x, make_copy)

        # Set bad results if constraints are not met
        if not self.check_linear_constraints(x):
            self.logger.warn(
                f'Linear constraints not met at {x}. Returning bad performance.\
                cycle time: {evaluation_object.process_meta.cycle_time}')
            performance = get_bad_performance(evaluation_object.n_comp)
        # Pass evaluation_object to evaluator and evaluate
        elif self.evaluator is not None:
            try:
                performance = self.evaluator.evaluate(evaluation_object)
            except CADETProcessError:
                self.logger.warn(
                    'Evaluation failed. Returning bad performance')
                performance = get_bad_performance(evaluation_object.n_comp)
        else:
            performance = evaluation_object.performance

        if cache is not None:
            cache[tuple(x.tolist())] = performance

        self.logger.info('{} evaluated at x={} yielded {}'.format(
            self.evaluation_object.name, tuple(x.tolist()),
            performance.to_dict()))

        return performance

    def evaluate_population(self, population, n_cores=0):
        manager = multiprocess.Manager()
        cache = manager.dict()

        eval_fun = lambda ind: self.evaluate(ind, make_copy=True, cache=cache)

        if n_cores == 1:
            for ind in population:
                try:
                    eval_fun(ind)
                except CADETProcessError:
                    print(ind)
        else:
            if n_cores == 0:
                n_cores = None
            with pathos.multiprocessing.ProcessPool(ncpus=n_cores) as pool:
                pool.map(eval_fun, population)

        return cache

    @property
    def objectives(self):
        return self._objectives

    @property
    def n_objectives(self):
        return len(self.objectives)

    def add_objective(self, objective_fun):
        """Add objective function to optimization problem.

        Parameters
        ----------
        objective_fun : function
            Objective function. Funtion should take a Performance object as
            argument and return a scalar value.

        Raises
        ------
        TypeError
            If objective_fun is not callable.
        """
        if not callable(objective_fun):
            raise TypeError("Expected callable object")

        self._objectives.append(objective_fun)

    def evaluate_objectives(self, x, *args, **kwargs):
        """Function that evaluates at x and computes objective function.

        This function is usually presented to the optimization solver. The
        actual evaluation of the evaluation object is peformed by the evaluate
        function which also logs the results.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        f : list
            Values of the objective functions at point x.

        See Also
        --------
        optimization.SolverBase
        add_objective
        evaluate
        evaluate_nonlinear_constraints
        """
        performance = self.evaluate(x, *args, **kwargs)
        f = np.array([obj(performance) for obj in self.objectives])
        return f

    def objective_gradient(self, x, dx=0.1):
        """Calculate the gradient of the objective functions at point x.

        Gradient is approximated using finite differences.

        Parameters
        ----------
        x : ndarray
            Value of the optimization variables.
        dx : float
            Increment to x to use for determining the function gradient.

        Returns
        -------
        grad: list
            The partial derivatives of the objective functions at point x.

        See Also
        --------
        OptimizationProblem
        objectives
        """
        dx = [dx] * len(x)
        grad = [optimize.approx_fprime(x, obj, dx) for obj in self.objectives]

        return grad

    @property
    def nonlinear_constraints(self):
        return self._nonlinear_constraints

    @property
    def n_nonlinear_constraints(self):
        return len(self.nonlinear_constraints)

    def add_nonlinear_constraint(self, nonlinear_constraint_fun):
        """Add nonlinear constraint function to optimization problem.

        Parameters
        ----------
        nonlinear_constraint_fun: function
            Nonlinear constraint function. Funtion should take a Performance 
            object as argument and return a scalar value or an array.

        Raises
        ------
        TypeError
            If nonlinear_constraint_fun is not callable.
        """
        if not callable(nonlinear_constraint_fun):
            raise TypeError("Expected callable object")

        self._nonlinear_constraints.append(nonlinear_constraint_fun)

    def evaluate_nonlinear_constraints(self, x, *args, **kwargs):
        """Function that evaluates at x and computes nonlinear constraitns.

        This function is usually presented to the optimization solver. The
        actual evaluation of the evaluation object is peformed by the evaluate
        function which also logs the results.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        c : list
            Value(s) of the constraint functions at point x.

        See also
        --------
        nonlinear_constraints
        evaluate
        evaluate_objectives
        """
        performance = self.evaluate(x, *args, **kwargs)
        c = np.array(
            [constr(performance) for constr in self.nonlinear_constraints])
        return c

    def check_nonlinear_constraints(self, x):
        """Checks if all nonlinear constraints are kept.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        flag : bool
            True if all nonlinear constraints are smaller or equal to zero,
            False otherwise.
        """
        c = self.evaluate_nonlinear_constraints(x)
        for constr in c:
            if np.any(constr > 0):
                return False
        return True

    def nonlinear_constraint_jacobian(self, x, dx=1e-3):
        """Return the jacobian matrix of the nonlinear constraints at point x.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables
        dx : float
            Increment to x to use for determining the function gradient.

        Returns
        -------
        jacobian: list
            Value of the partial derivatives at point x.

        See also
        --------
        nonlinear_constraint_fun
        approximate_jac
        """
        jacobian = [
            approximate_jac(x, constr, dx)
            for constr in self.nonlinear_constraints
        ]
        return jacobian

    @property
    def lower_bounds(self):
        """list : List of the lower bounds of all OptimizationVariables.

        See also
        --------
        upper_bounds
        """
        return [var.lb for var in self.variables]

    @property
    def upper_bounds(self):
        """list : List of the upper bounds of all OptimizationVariables.

        See also
        --------
        upper_bounds
        """
        return [var.ub for var in self.variables]

    def check_bounds(self, x):
        """Checks if all bound constraints are kept.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables

        First sets a local variable named flag to True. Then checks if the
        values of the list x are exceeding the lower and upper bounds and sets
        the value of the flag to False. For this the list x is converted into
        an np.array.

        Returns
        -------
        flag : Bool
            Returns True, if the values of the list x are in the defined bounds.
            Returns False if the values of the list x are violating the bound
            constraints.
        """
        flag = True

        if np.any(np.less(x, self.lower_bounds)):
            flag = False
        if np.any(np.greater(x, self.upper_bounds)):
            flag = False

        return flag

    @property
    def linear_constraints(self):
        """list : linear inequality constraints of OptimizationProblem
        
        See Also
        --------
        add_linear_constraint
        remove_linear_constraint
        linear_equality_constraints
        """
        return self._linear_constraints

    @property
    def n_linear_constraints(self):
        """int: number of linear inequality constraints
        """
        return len(self.linear_constraints)

    def add_linear_constraint(self, opt_vars, factors, b=0):
        """Add linear inequality constraints.

        Parameters
        -----------
        opt_vars : list of strings
            Names of the OptimizationVariable to be added.
        factors : list of  integers
            Factors for OptimizationVariables.
        b : float, optional
            Constraint of inequality constraint; default set to zero.

        Raises
        -------
        CADETProcessError
            If optimization variables do not exist.
            If length of factors does not match length of optimization variables.

        See also
        ---------
        linear_constraints
        remove_linear_constraint
        linear_equality_constraints
        """
        if not all(var in self.variables_dict for var in opt_vars):
            raise CADETProcessError('Variable not in variables')

        if len(factors) != len(opt_vars):
            raise CADETProcessError('Factors length does not match variables')

        lincon = dict()
        lincon['opt_vars'] = opt_vars
        lincon['factors'] = factors
        lincon['b'] = b

        self._linear_constraints.append(lincon)

    def remove_linear_constraint(self, index):
        """Removes linear inequality constraint.

        Parameters
        ----------
        index : int
            Index of the linear inequality constraint to be removed.
        
        See also
        --------
        add_linear_equality_constraint
        linear_equality_constraint
        """
        del (self._linear_constraints[index])

    @property
    def A(self):
        """np.ndarray: Matrix form of linear inequality constraints.

        See Also
        --------
        b
        add_linear_constraint
        remove_linear_constraint
        """
        A = np.zeros((len(self.linear_constraints), len(self.variables)))

        for lincon_index, lincon in enumerate(self.linear_constraints):
            for var_index, var in enumerate(lincon['opt_vars']):
                index = self.variables.index(self.variables_dict[var])
                A[lincon_index, index] = lincon['factors'][var_index]

        return A

    @property
    def b(self):
        """list: Vector form of linear constraints.

        See Also
        --------
        A
        add_linear_constraint
        remove_linear_constraint
        """
        b = [lincon['b'] for lincon in self.linear_constraints]

        return np.array(b)

    def evaluate_linear_constraints(self, x):
        """Calculate value of linear inequality constraints at point x.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        constraints: np.array
            Value of the linear constraints at point x

        See Also
        --------
        A
        b
        linear_constraints
        """
        x = np.array(x)
        return self.A.dot(x) - self.b

    def check_linear_constraints(self, x):
        """Check if linear inequality constraints are met at point x.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        flag : bool
            Returns True if linear inequality constraints are met. False otherwise.

        See also:
        ---------
        linear_constraints
        evaluate_linear_constraints
        A
        b
        """
        flag = True

        if np.any(self.evaluate_linear_constraints(x) > 0):
            flag = False

        return flag

    @property
    def linear_equality_constraints(self):
        """list: linear equality constraints of OptimizationProblem

        See Also
        --------
        add_linear_equality_constraint
        remove_linear_equality_constraint
        linear_constraints
        """
        return self._linear_equality_constraints

    @property
    def n_linear_equality_constraints(self):
        """int: number of linear equality constraints
        """
        return len(self.linear_constraints)

    def add_linear_equality_constraint(self, opt_vars, factors, beq=0):
        """Add linear equality constraints.

        Parameters
        -----------
        opt_vars : list of strings
            Names of the OptimizationVariable to be added.
        factors : list of  integers
            Factors for OptimizationVariables.
        b_eq : float, optional
            Constraint of equality constraint; default set to zero.

        Raises
        -------
        CADETProcessError
            If optimization variables do not exist.
            If length of factors does not match length of optimization variables.

        See also
        --------
        linear_equality_constraints
        remove_linear_equality_constraint
        linear_constraints
        """
        if not all(var in self.variables for var in opt_vars):
            return CADETProcessError('Variables not in variables')

        if len(factors) != len(opt_vars):
            return CADETProcessError('Factors length does not match variables')

        lineqcon = dict()
        lineqcon['opt_vars'] = opt_vars
        lineqcon['factors'] = factors
        lineqcon['beq'] = beq

        self._linear_equality_constraints.append(lineqcon)

    def remove_linear_equality_constraint(self, index):
        """Removes at given index the added linear equality conctraint.

        Parameters
        ----------
        index : int
            Index of the linear equality constraint to be removed.

        See also
        --------
        add_linear_equality_constraint
        linear_equality_constraint
        """
        del (self._linear_equality_constraints[index])

    @property
    def Aeq(self):
        """np.ndarray: Matrix form of linear equality constraints.

        See Also
        --------
        beq
        add_linear_equality_constraint
        remove_linear_equality_constraint
        """
        Aeq = np.zeros(
            (len(self.linear_equality_constraints), len(self.variables)))

        for lineqcon_index, lineqcon in enumerate(
                self.linear_equality_constraints):
            for var_index, var in enumerate(lineqcon.opt_vars):
                index = self.variables.index(var)
                Aeq[lineqcon_index, index] = lineqcon.factors[var_index]

        return Aeq

    @property
    def beq(self):
        """list: Vector form of linear equality constraints.

        See Also
        --------
        Aeq
        add_linear_equality_constraint
        remove_linear_equality_constraint
        """
        beq = np.zeros((len(self.linear_equality_constraints), ))
        beq = [lineqcon.beq for lineqcon in self.linear_equality_constraints]

        return beq

    def evaluate_linear_equality_constraints(self, x):
        """Calculate value of linear equality constraints at point x.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        constraints: np.array 
            Value of the linear euqlity constraints at point x

        See Also
        --------
        Aeq
        beq
        linear_equality_constraints
        """
        x = np.array(x)
        return self.Aeq.dot(x) - self.beq

    def check_linear_equality_constraints(self, x):
        """Check if linear equality constraints are met at point x.

        Parameters
        ----------
        x : array_like
            Value of the optimization variables.

        Returns
        -------
        flag : bool
            Returns True if linear equality constraints are met. False otherwise.
        """
        flag = True

        if np.any(self.evaluate_linear_equality_constraints(x) != 0):
            flag = False

        return flag

    @property
    def x0(self):
        """Initival values for optimization.

        Parameters
        ----------
        x0 : array_like
            Initival values for optimization.

        Raises
        ------
        CADETProcessError
            If the initial value does not match length of optimization variables
        """
        return self._x0

    @x0.setter
    def x0(self, x0):
        if not len(x0) == len(self.variables):
            raise CADETProcessError(
                "Starting value must be given for all variables")
        self._x0 = x0

    def create_initial_values(self, n_samples=1, method='random', seed=None):
        """Create initial value within parameter space.
        
        Uses hopsy (Highly Optimized toolbox for Polytope Sampling) to retrieve
        uniformly distributed samples from the parameter space.
        
        Parameters
        ----------
        n_samples : int
            Number of initial values to be drawn
        method : str, optional
            chebyshev: Return center of the minimal-radius ball enclosing the 
                entire set .
            random: Any random valid point in the parameter space.
        seed : int, optional
            Seed to initialize random numbers. Only used if method == 'random'

        Returns
        -------
        init : ndarray
            Initial values for starting the optimization.
        """
        model = hopsy.UniformModel()

        problem = hopsy.Problem(self.A, self.b, model)
        problem = hopsy.add_box_constraints(problem, self.lower_bounds,
                                            self.upper_bounds)

        with warnings.catch_warnings():
            warnings.simplefilter("ignore")

            starting_points = [hopsy.compute_chebyshev_center(problem)]
            run = hopsy.Run(problem, starting_points=starting_points)
            if seed is None:
                seed = random.randint(0, 255)
            run.random_seed = seed
            run.sample(n_samples)

        states = np.array(run.data.states[0])

        if n_samples == 1:
            if method == 'chebyshev':
                states = hopsy.compute_chebyshev_center(problem)
            elif method == 'random':
                states = states[0]
            else:
                raise CADETProcessError("Unexpected method.")

        return states

    @property
    def parameters(self):
        parameters = Dict()

        parameters.variables = {
            opt.name: opt.parameters
            for opt in self.variables
        }
        parameters.linear_constraints = self.linear_constraints

        return parameters

    def __str__(self):
        return self.name
Exemple #6
0
class ReactionBaseClass(metaclass=StructMeta):
    """Abstract base class for parameters of binding models.

    Attributes
    ----------
    n_comp : UnsignedInteger
        number of components.
    parameters : dict
        dict with parameter values.
    name : String
        name of the binding model.
    """
    name = String()
    n_comp = UnsignedInteger()
    
    _parameter_names = []

    def __init__(self, component_system, name=None):
        self.component_system = component_system
        self.name = name
        
        self._parameters = {
            param: getattr(self, param)
            for param in self._parameter_names
        }
        
    @property
    def model(self):
        return self.__class__.__name__
    
    @property
    def component_system(self):
        return self._component_system

    @component_system.setter
    def component_system(self, component_system):
        if not isinstance(component_system, ComponentSystem):
            raise TypeError('Expected ComponentSystem')
        self._component_system = component_system
    
    @property
    def n_comp(self):
        return self.component_system.n_comp
    
    @property
    def parameters(self):
        """dict: Dictionary with parameter values.
        """
        return {param: getattr(self, param) for param in self._parameter_names}

    @parameters.setter
    def parameters(self, parameters):
        for param, value in parameters.items():
            if param not in self._parameter_names:
                raise CADETProcessError('Not a valid parameter')
            setattr(self, param, value)


    def __repr__(self):
        return '{}(n_comp={}, name=\'{}\')'.format(self.__class__.__name__,
            self.n_comp, self.name)

    def __str__(self):
        return self.name
Exemple #7
0
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)
Exemple #8
0
class UnitBaseClass(metaclass=StructMeta):
    """Base class for all UnitOperation classes.

    A UnitOperation object stores model parameters and states of a unit. 
    Every unit operation can be assotiated with a binding behavior and a 
    reaction model.
    UnitOperations can be connected in a FlowSheet. 

    Attributes
    ----------
    n_comp : UnsignedInteger
        Number of components in a system.
    parameters : list
        list of parameter names.
    name : String
        name of the unit_operation.
    binding_model : BindingBaseClass
        binding behavior of the unit. Defaults to NoBinding.
    
    See also
    --------
    FlowSheet
    CADETProcess.binding
    CADETProcess.reaction
    """
    name = String()

    _parameter_names = []
    _section_dependent_parameters = []
    _polynomial_parameters = []
    _initial_state = []

    supports_bulk_reaction = False
    supports_particle_reaction = False

    def __init__(self, component_system, name):
        self.name = name
        self.component_system = component_system

        self._binding_model = NoBinding()

        self._bulk_reaction_model = NoReaction()
        self._particle_reaction_model = NoReaction()

        self._discretization = NoDiscretization()

        self._parameters = {
            param: getattr(self, param)
            for param in self._parameter_names
        }

    @property
    def model(self):
        return self.__class__.__name__

    @property
    def component_system(self):
        return self._component_system

    @component_system.setter
    def component_system(self, component_system):
        if not isinstance(component_system, ComponentSystem):
            raise TypeError('Expected ComponentSystem')
        self._component_system = component_system

    @property
    def n_comp(self):
        return self.component_system.n_comp

    @property
    def parameters(self):
        """dict: Dictionary with parameter values.
        """
        parameters = self._parameters

        if not isinstance(self.binding_model, NoBinding):
            parameters['binding_model'] = self.binding_model.parameters
        if not isinstance(self.bulk_reaction_model, NoReaction):
            parameters[
                'bulk_reaction_model'] = self.bulk_reaction_model.parameters
        if not isinstance(self.particle_reaction_model, NoReaction):
            parameters['particle_reaction_model'] = \
                self.particle_reaction_model.parameters
        if not isinstance(self.discretization, NoDiscretization):
            parameters['discretization'] = \
                self.discretization.parameters

        return parameters

    @parameters.setter
    def parameters(self, parameters):
        try:
            self.binding_model.parameters = parameters.pop('binding_model')
        except KeyError:
            pass
        try:
            self.bulk_reaction_model.parameters = parameters.pop(
                'bulk_reaction_model')
        except KeyError:
            pass
        try:
            self.particle_reaction_model.parameters = parameters.pop(
                'particle_reaction_model')
        except KeyError:
            pass
        try:
            self.discretization.parameters = parameters.pop('discretization')
        except KeyError:
            pass

        for param, value in parameters.items():
            if param not in self._parameters:
                raise CADETProcessError('Not a valid parameter')
            if value is not None:
                setattr(self, param, value)

    @property
    def section_dependent_parameters(self):
        parameters = {
            key: value
            for key, value in self.parameters.items()
            if key in self._section_dependent_parameters
        }

        return parameters

    @property
    def polynomial_parameters(self):
        parameters = {
            key: value
            for key, value in self.parameters.items()
            if key in self._polynomial_parameters
        }
        return parameters

    @property
    def initial_state(self):
        """dict: Dictionary with initial states.
        """
        initial_state = {st: getattr(self, st) for st in self._initial_state}

        return initial_state

    @initial_state.setter
    def initial_state(self, initial_state):
        for st, value in initial_state.items():
            if st not in self._initial_state:
                raise CADETProcessError('Not a valid parameter')
            if value is not None:
                setattr(self, st, value)

    @property
    def binding_model(self):
        """binding_model: BindingModel of the unit_operation.

        Raises
        ------
        TypeError
            If binding_model object is not an instance of BindingBaseClass.
        CADETProcessError
            If number of components do not match.
        """
        return self._binding_model

    @binding_model.setter
    def binding_model(self, binding_model):
        if not isinstance(binding_model, BindingBaseClass):
            raise TypeError('Expected BindingBaseClass')

        if binding_model.component_system is not self.component_system:
            raise CADETProcessError('Component systems do not match.')

        self._binding_model = binding_model

    @property
    def _n_bound_states(self):
        return self.binding_model.n_states

    @property
    def bulk_reaction_model(self):
        """bulk_reaction_model: Reaction model in the bulk phase

        Raises
        ------
        TypeError
            If bulk_reaction_model object is not an instance of ReactionBaseClass.
        CADETProcessError
            If unit does not support bulk reaction model.
            If number of components do not match.
        """
        return self._bulk_reaction_model

    @bulk_reaction_model.setter
    def bulk_reaction_model(self, bulk_reaction_model):
        if not isinstance(bulk_reaction_model, ReactionBaseClass):
            raise TypeError('Expected ReactionBaseClass')
        if not self.supports_bulk_reaction:
            raise CADETProcessError('Unit does not support bulk reactions.')

        if bulk_reaction_model.component_system is not self.component_system \
                and not isinstance(bulk_reaction_model, NoReaction):
            raise CADETProcessError('Component systems do not match.')

        self._bulk_reaction_model = bulk_reaction_model

    @property
    def particle_reaction_model(self):
        """particle_liquid_reaction_model: Reaction model in the particle liquid phase

        Raises
        ------
        TypeError
            If particle_reaction_model object is not an instance of ReactionBaseClass.
        CADETProcessError
            If unit does not support particle reaction model.
            If number of components do not match.
        """
        return self._particle_reaction_model

    @particle_reaction_model.setter
    def particle_reaction_model(self, particle_reaction_model):
        if not isinstance(particle_reaction_model, ReactionBaseClass):
            raise TypeError('Expected ReactionBaseClass')
        if not self.supports_bulk_reaction:
            raise CADETProcessError(
                'Unit does not support particle reactions.')

        if particle_reaction_model.component_system is not self.component_system \
                and not isinstance(particle_reaction_model, NoReaction):
            raise CADETProcessError('Component systems do not match.')

        self._particle_reaction_model = particle_reaction_model

    @property
    def discretization(self):
        """discretization: Discretization Parameters
        """
        return self._discretization

    def __repr__(self):
        """String-depiction of the object, can be changed into an object by
        calling the method eval.

        Returns
        -------
        class.name(parameters with values) : str
            Information about the class's name of an object and its parameters
            like number of components and object name, depicted as a string.
        """
        return '{}(n_comp={}, name=\'{}\')'.format(self.__class__.__name__,
                                                   self.n_comp, self.name)

    def __str__(self):
        """Returns the information von __repr__ as a string object.

        Returns
        -------
        name : String
            Information about the class's name of an object and its paremeters
            like number of components and object name
        """
        return self.name
class FlowSheet(metaclass=StructMeta):
    """Class to design process flow sheet.

    In this class, UnitOperation models are added and connected in a flow
    sheet.

    Attributes
    ----------
    n_comp : UnsignedInteger
        Number of components of the units in the flow sheet.
    name : String
        Name of the FlowSheet.
    units : list
        UnitOperations in the FlowSheet.
    connections : dict
        Connections of UnitOperations.
    output_states : dict
        Split ratios of outgoing streams of UnitOperations.
    """

    name = String()
    n_comp = UnsignedInteger()
    
    def __init__(self, component_system, name=None):
        self.component_system = component_system
        self.name = name
        self._units = []
        self._feed_sources = []
        self._eluent_sources = []
        self._chromatogram_sinks = []
        self._connections = Dict()
        self._output_states = Dict()
        self._flow_rates = Dict()
        self._parameters = Dict()
        self._section_dependent_parameters = Dict()
        self._polynomial_parameters = Dict()

    @property
    def component_system(self):
        return self._component_system

    @component_system.setter
    def component_system(self, component_system):
        if not isinstance(component_system, ComponentSystem):
            raise TypeError('Expected ComponentSystem')
        self._component_system = component_system
        
    @property
    def n_comp(self):
        return self.component_system.n_comp
            
    def _unit_name_decorator(func):
        def wrapper(self, unit, *args, **kwargs):
            """Enable calling functions with unit object or unit name.
            """
            if isinstance(unit, str):
                try:
                    unit = self.units_dict[unit]
                except KeyError:
                    raise CADETProcessError('Not a valid unit')
            return func(self, unit, *args, **kwargs)

        return wrapper
    
    def update_parameters(self):
        for unit in self.units:
            self._parameters[unit.name] = unit.parameters
            self._section_dependent_parameters[unit.name] = \
                unit.section_dependent_parameters
            self._polynomial_parameters[unit.name] = unit.polynomial_parameters
        
        self._parameters['output_states'] = {
            unit.name: self.output_states[unit] for unit in self.units
        }
        
        self._section_dependent_parameters['output_states'] = {
            unit.name: self.output_states[unit] 
            for unit in self.units
        }
        
    def update_parameters_decorator(func):
        def wrapper(self, *args, **kwargs):
            """Update parameters dict to save time.
            """
            results = func(self, *args, **kwargs)
            self.update_parameters()
            
            return results
        return wrapper

    @property
    def units(self):
        """list: list of all unit_operations in the flow sheet.
        """
        return self._units

    @property
    def units_dict(self):
        """dict: Unit names and objects.
        """
        return {unit.name: unit for unit in self.units}
    
    @property
    def unit_names(self):
        """list: Unit names
        """
        return [unit.name for unit in self.units]
    
    @property
    def number_of_units(self):
        """int: Number of unit operations in the FlowSheet.
        """
        return len(self._units)

    @_unit_name_decorator
    def get_unit_index(self, unit):
        """Return the unit index of the unit.

        Parameters
        ----------
        unit : UnitBaseClass
            UnitBaseClass object of which the index is to be returned.

        Raises
        ------
        CADETProcessError
            If unit does not exist in the current flow sheet.

        Returns
        -------
        unit_index : int
            Returns the unit index of the unit_operation.
        """
        if unit not in self.units:
            raise CADETProcessError('Unit not in flow sheet')

        return self.units.index(unit)

    @property
    def sources(self):
        """list: All UnitOperations implementing the SourceMixin interface.
        """
        return [unit for unit in self._units if isinstance(unit, SourceMixin)]

    @property
    def sinks(self):
        """list: All UnitOperations implementing the SinkMixin interface.
        """
        return [unit for unit in self._units if isinstance(unit, SinkMixin)]

    @property
    def units_with_binding(self):
        """list: UnitOperations with binding models.
        """
        return [unit for unit in self._units
                if not isinstance(unit.binding_model, NoBinding)]

    @update_parameters_decorator
    def add_unit(
            self, unit, 
            feed_source=False, eluent_source=False, chromatogram_sink=False
        ):
        """Add unit to the flow sheet.

        Parameters
        ----------
        unit : UnitBaseClass
            UnitBaseClass object to be added to the flow sheet.
        feed_source : bool
            If True, add unit to feed sources.
        eluent_source : bool
            If True, add unit to eluent sources.
        chromatogram_sink : bool
            If True, add unit to chromatogram sinks.

        Raises
        ------
        TypeError
            If unit is no instance of UnitBaseClass.
        CADETProcessError
            If unit already exists in flow sheet.
            If n_comp does not match with FlowSheet.

        See Also
        --------
        remove_unit
        """
        if not isinstance(unit, UnitBaseClass):
            raise TypeError('Expected UnitOperation')

        if unit in self._units:
            raise CADETProcessError('Unit already part of System')

        if unit.component_system is not self.component_system:
            raise CADETProcessError('Component systems do not match.')

        self._units.append(unit)
        self._connections[unit] = Dict({
            'origins': [], 
            'destinations': [],
        })
        self._output_states[unit] = []
        self._flow_rates[unit] = []
        
        super().__setattr__(unit.name, unit)

        if feed_source:
            self.add_feed_source(unit)
        if eluent_source:
            self.add_eluent_source(unit)
        if chromatogram_sink:
            self.add_chromatogram_sink(unit)


    @update_parameters_decorator
    def remove_unit(self, unit):
        """Remove unit from flow sheet.

        Removes unit from the list. Tries to remove units which are twice
        located as desinations. For this the origins and destinations are
        deleted for the unit. Raises a CADETProcessError if an ValueError is
        excepted. If the unit is specified as feed_source, eluent_source
        or chromatogram_sink, the corresponding attributes are deleted.

        Parameters
        ----------
        unit : UnitBaseClass
            UnitBaseClass object to be removed to the flow sheet.

        Raises
        ------
        CADETProcessError
            If unit does not exist in the flow sheet.

        See Also
        --------
        add_unit
        feed_source
        eluent_source
        chromatogram_sink
        """
        if unit not in self.units:
            raise CADETProcessError('Unit not in flow sheet')

        if unit is self.feed_sources:
            self.remove_feed_source(unit)
        if unit is self.eluent_sources:
            self.remove_eluent_source(unit)
        if unit is self.chromatogram_sinks:
            self.remove_chromatogram_sink(unit)

        origins = self.connections[unit].origins.copy()
        for origin in origins:
            self.remove_connection(origin, unit)

        destinations = self.connections[unit].destinations.copy()
        for destination in destinations:
            self.remove_connection(unit, destination)

        self._units.remove(unit)
        self._connections.pop(unit)
        self._output_states.pop(unit)
        self.__dict__.pop(unit.name)

    @property
    def connections(self):
        """dict: In- and outgoing connections for each unit.

        See Also
        --------
        add_connection
        remove_connection
        """
        return self._connections
    
    @update_parameters_decorator
    def add_connection(self, origin, destination):
        """Add connection between units 'origin' and 'destination'.

        Parameters
        ----------
        origin : UnitBaseClass
            UnitBaseClass from which the connection originates.
        destination : UnitBaseClass
            UnitBaseClass where the connection terminates.

        Raises
        ------
        CADETProcessError
            If origin OR destination do not exist in the current flow sheet.
            If connection already exists in the current flow sheet.

        See Also
        --------
        connections
        remove_connection
        output_state
        """
        if origin not in self._units:
            raise CADETProcessError('Origin not in flow sheet')
        if destination not in self._units:
            raise CADETProcessError('Destination not in flow sheet')
        
        if destination in self.connections[origin].destinations:
            raise CADETProcessError('Connection already exists')

        self._connections[origin].destinations.append(destination)
        self._connections[destination].origins.append(origin)        
        
        self.set_output_state(origin, 0)

    @update_parameters_decorator
    def remove_connection(self, origin, destination):
        """Remove connection between units 'origin' and 'destination'.

        Parameters
        ----------
        origin : UnitBaseClass
            UnitBaseClass from which the connection originates.
        destination : UnitBaseClass
            UnitBaseClass where the connection terminates.

        Raises
        ------
        CADETProcessError
            If origin OR destination do not exist in the current flow sheet.
            If connection does not exists in the current flow sheet.

        See Also
        --------
        connections
        add_connection
        """
        if origin not in self._units:
            raise CADETProcessError('Origin not in flow sheet')
        if destination not in self._units:
            raise CADETProcessError('Destination not in flow sheet')

        try:
            self._connections[origin].destinations.remove(destination)
            self._connections[destination].origins.remove(origin)
        except KeyError:
            raise CADETProcessError('Connection does not exist.')

    @property
    def output_states(self):
        return self._output_states
    
    @_unit_name_decorator
    @update_parameters_decorator
    def set_output_state(self, unit, state):
        """Set split ratio of outgoing streams for UnitOperation.
        
        Parameters
        ----------
        unit : UnitBaseClass
            UnitOperation of flowsheet.
        state : int or list of floats
            new output state of the unit. 

        Raises
        ------
        CADETProcessError
            If unit not in flowSheet
            If state is integer and the state >= the state_length.
            If the length of the states is unequal the state_length.
            If the sum of the states is not equal to 1.
        """
        if unit not in self._units:
            raise CADETProcessError('Unit not in flow sheet')
            
        state_length = len(self.connections[unit].destinations)

        if state_length == 0:
            output_state = []

        if isinstance(state, (int, np.int64)):
            if state >= state_length:
                raise CADETProcessError('Index exceeds destinations')

            output_state = [0] * state_length
            output_state[state] = 1

        else:
            if len(state) != state_length:
                raise CADETProcessError(
                    'Expected length {}.'.format(state_length))

            elif sum(state) != 1:
                raise CADETProcessError('Sum of fractions must be 1')

            output_state = state

        self._output_states[unit] = output_state                    

    
    def get_flow_rates(self, state=None):
        """Calculate flow rate for all connections.unit operation flow rates.
        
        If an additional state is passed, it will b
        
        Parameters
        ----------
        state : Dict, optional
            Output states

        Returns
        -------
        flow_rates : Dict
            Volumetric flow rate of each unit operation.

        """
        flow_rates = {
            unit.name: unit.flow_rate for unit in self.sources
        }
        output_states = self.output_states

        if state is not None:
            for param, value in state.items():
                param = param.split('.')
                unit_name = param[1]
                param_name = param[-1]
                if param_name == 'flow_rate':
                    flow_rates[unit_name] = value[0]
                elif unit_name == 'output_states':
                    unit = self.units_dict[param_name]
                    output_states[unit] = list(value.ravel())
        
        def list_factory():
            return [0,0,0,0]
        
        total_flow_rates = {unit.name: list_factory() for unit in self.units}
        destination_flow_rates = {
            unit.name: defaultdict(list_factory) for unit in self.units
        }
        
        for i in range(4):
            solution = self.solve_flow_rates(flow_rates, output_states, i)
            if solution is not None:
                for unit_index, unit in enumerate(self.units):
                    total_flow_rates[unit.name][i] = \
                        float(solution['Q_total_{}'.format(unit_index)])
                    
                    for destination in self.connections[unit].destinations:
                        destination_index = self.get_unit_index(destination)
                        destination_flow_rates[unit.name][destination.name][i] = \
                            float(solution['Q_{}_{}'.format(unit_index, destination_index)])
        
        flow_rates = Dict()
        for unit in self.units:
            flow_rates[unit.name].total = np.array(total_flow_rates[unit.name])
            for destination, flow_rate in destination_flow_rates[unit.name].items():
                flow_rates[unit.name].destinations[destination] = np.array(flow_rate)
        
        return flow_rates                

    def solve_flow_rates(self, source_flow_rates, output_states, coeff=0):
        """Solve flow rates of system using sympy.
        
        Because a simple 'push' algorithm cannot be used when closed loops are
        present in a FlowSheet (e.g. SMBs), sympy is used to set up and solve 
        the system of equations.
    
        Parameters
        ----------
        source_flow_rates: dict
            Flow rates of Source UnitOperations.
        output_states: dict
            Output states of all UnitOperations.
        coeff: int
            Polynomial coefficient of flow rates to be solved.
    
        Returns
        -------
        solution : dict
            Solution of the flow rates in the system
            
        Note
        ----
        Since dynamic flow rates can be described as cubic polynomials, the
        flow rates are solved individually for all coefficients.
        """
        coeffs = np.array(
            [source_flow_rates[unit.name][coeff] for unit in self.sources]
        )
        if not np.any(coeffs):
            return None
        
        # Setup lists for symbols
        unit_total_flow_symbols = sym.symbols(
            'Q_total_0:{}'.format(self.number_of_units)
        )
        unit_inflow_symbols = []
        unit_outflow_symbols = []
        
        unit_total_flow_eq = []
        unit_outflow_eq = []
        
        # Setup symbolic equations
        for unit_index, unit in enumerate(self.units):
            if isinstance(unit, SourceMixin):
                unit_total_flow_eq.append(
                    sym.Add(
                        unit_total_flow_symbols[unit_index], 
                        - float(source_flow_rates[unit.name][coeff])
                    )
                )
            else:
                unit_i_inflow_symbols = []
                
                for origin in self.connections[unit].origins:
                    origin_index = self.get_unit_index(origin)
                    unit_i_inflow_symbols.append(
                        sym.symbols('Q_{}_{}'.format(origin_index, unit_index))
                    )
                    
                unit_i_total_flow_eq = sym.Add(
                    *unit_i_inflow_symbols, -unit_total_flow_symbols[unit_index]
                )
                
                unit_inflow_symbols += unit_i_inflow_symbols
                unit_total_flow_eq.append(unit_i_total_flow_eq)
                
            if not isinstance(unit, Sink):
                output_state = output_states[unit]
                unit_i_outflow_symbols = []
                
                for destination in self.connections[unit].destinations:
                    destination_index = self.get_unit_index(destination)
                    unit_i_outflow_symbols.append(
                        sym.symbols('Q_{}_{}'.format(unit_index, destination_index))
                    )
                    
                unit_i_outflow_eq = [
                    sym.Add(
                        unit_i_outflow_symbols[dest],
                        -unit_total_flow_symbols[unit_index]*output_state[dest]
                    )
                    for dest in range(len(self.connections[unit].destinations))
                ]
                                     
                unit_outflow_symbols += unit_i_outflow_symbols
                unit_outflow_eq += unit_i_outflow_eq
                
        # Solve system of equations
        solution = sym.solve(
            unit_total_flow_eq + unit_outflow_eq, 
            (*unit_total_flow_symbols, *unit_inflow_symbols, *unit_outflow_symbols)
        )
        solution = {str(key): value for key, value in solution.items()}
        
        return solution
    
    @property
    def feed_sources(self):
        """list: List of sources considered for calculating recovery yield.
        """
        return self._feed_sources

    @_unit_name_decorator
    def add_feed_source(self, feed_source):
        """Add source to list of units to be considered for recovery.

        Parameters
        ----------
        feed_source : SourceMixin
            Unit to be added to list of feed sources

        Raises
        ------
        CADETProcessError
            If unit is not in a source object
            If unit is already marked as feed source
        """
        if feed_source not in self.sources:
            raise CADETProcessError('Expected Source')
        if feed_source in self._feed_sources:
            raise CADETProcessError(
                '{} is already eluent source'.format(feed_source)
            )
        self._feed_sources.append(feed_source)

    @_unit_name_decorator
    def remove_feed_source(self, feed_source):
        """Remove source from list of units to be considered for recovery.

        Parameters
        ----------
        feed_source : SourceMixin
            Unit to be removed from list of feed sources.
        """
        if feed_source not in self._feed_sources:
            raise CADETProcessError('Unit \'{}\' is not a feed source.'.format(
                    feed_source))
        self._feed_sources.remove(feed_source)

    @property
    def eluent_sources(self):
        """list: List of sources to be considered for eluent consumption.
        """
        return self._eluent_sources

    @_unit_name_decorator
    def add_eluent_source(self, eluent_source):
        """Add source to list of units to be considered for eluent consumption.

        Parameters
        ----------
        eluent_source : SourceMixin
            Unit to be added to list of eluent sources.

        Raises
        ------
        CADETProcessError
            If unit is not in a source object
            If unit is already marked as eluent source
        """
        if eluent_source not in self.sources:
            raise CADETProcessError('Expected Source')
        if eluent_source in self._eluent_sources:
            raise CADETProcessError('{} is already eluent source'.format(
                    eluent_source))
        self._eluent_sources.append(eluent_source)

    @_unit_name_decorator
    def remove_eluent_source(self, eluent_source):
        """Remove source from list of units to be considered eluent consumption.

        Parameters
        ----------
        eluent_source : SourceMixin
            Unit to be added to list of eluent sources.
        
        Raises
        ------
        CADETProcessError
            If unit is not in eluent sources
        """
        if eluent_source not in self._eluent_sources:
            raise CADETProcessError('Unit \'{}\' is not an eluent source.'.format(
                    eluent_source))
        self._eluent_sources.remove(eluent_source)

    @property
    def chromatogram_sinks(self):
        """list: List of sinks to be considered for fractionation.
        """
        return self._chromatogram_sinks

    @_unit_name_decorator
    def add_chromatogram_sink(self, chromatogram_sink):
        """Add sink to list of units to be considered for fractionation.

        Parameters
        ----------
        chromatogram_sink : SinkMixin
            Unit to be added to list of chromatogram sinks.

        Raises
        ------
        CADETProcessError
            If unit is not a sink object.
            If unit is already marked as chromatogram sink.
        """
        if chromatogram_sink not in self.sinks:
            raise CADETProcessError('Expected Sink')
        if chromatogram_sink in self._chromatogram_sinks:
            raise CADETProcessError(
                '{} is already chomatogram sink'.format(chromatogram_sink)
            )
        self._chromatogram_sinks.append(chromatogram_sink)

    @_unit_name_decorator
    def remove_chromatogram_sink(self, chromatogram_sink):
        """Remove sink from list of units to be considered for fractionation.

        Parameters
        ----------
        chromatogram_sink : SinkMixin
            Unit to be added to list of chromatogram sinks.

        Raises
        ------
        CADETProcessError
            If unit is not a chromatogram sink.
        """
        if chromatogram_sink not in self._chromatogram_sinks:
            raise CADETProcessError(
                'Unit \'{}\' is not a chromatogram sink.'.format(chromatogram_sink)
            )
        self._chromatogram_sinks.remove(chromatogram_sink)


    @property
    def parameters(self):
        return self._parameters

    @parameters.setter
    def parameters(self, parameters):
        try:
            output_states = parameters.pop('output_states')
            for unit, state in output_states.items():
                unit = self.units_dict[unit]
                self.set_output_state(unit, state)
        except KeyError:
            pass
        
        for unit, params in parameters.items():
            if unit not in self.units_dict:
                raise CADETProcessError('Not a valid unit')
            self.units_dict[unit].parameters = params
            
        self.update_parameters()

    @property
    def section_dependent_parameters(self):
        return self._section_dependent_parameters

    @property
    def polynomial_parameters(self):
        return self._polynomial_parameters

    @property
    def initial_state(self):
        initial_state = {unit.name: unit.initial_state for unit in self.units}

        return initial_state

    @initial_state.setter
    def initial_state(self, initial_state):
        for unit, st in initial_state.items():
            if unit not in self.units_dict:
                raise CADETProcessError('Not a valid unit')
            self.units_dict[unit].initial_state = st
            
            
    def __getitem__(self, unit_name):
        """Make FlowSheet substriptable s.t. units can be used as keys.

        Parameters
        ----------
        unit_name : str
            Name of the unit.

        Returns
        -------
        unit : UnitBaseClass
            UnitOperation of flowsheet.

        Raises
        ------
        KeyError
            If unit not in flowSheet
        """
        try:
            return self.units_dict[unit_name]
        except KeyError:
            raise KeyError('Not a valid unit')


    def __contains__(self, item):
        """Check if an item is part of units.

        Parameters
        ----------
        item : UnitBaseClass
            item to be searched

        Returns
        -------
        Bool : True if item is in units, otherwise False.

        Note
        ----
        maybe deficient in documentation.
        """
        if (item in self._units) or (item in self.unit_names):
            return True
        else:
            return False
Exemple #10
0
class TimeSignal(metaclass=StructMeta):
    """Class for storing concentration profiles after simulation.

    Attributes
    ----------
    time : NdArray
        NdArray object for the time of a chromatogram.
    signal : NdArray
        NdArray of the concentration of a chromatogram.
    cycle_time : float
            Maximum value of time vector.
    """
    time = NdArray()
    signal = NdArray()
    cycle_time = Float()
    name = String()

    def __init__(self, time, signal, name=''):
        self.time = time
        self.signal = signal
        self.name = name
        self.cycle_time = float(max(self.time))

    @plotting.save_fig
    def plot(self, start=0, end=None):
        """Plots the whole time_signal for each component.

        Parameters
        ----------
        start : float
            start time for plotting
        end : float
            end time for plotting

        See also
        --------
        plotlib
        plot_purity
        """
        x = self.time / 60
        y = self.signal

        fig, ax = plotting.setup_figure()
        ax.plot(x,y)

        layout = plotting.Layout()
        layout.x_label = '$time~/~min$'
        layout.y_label = '$c~/~mol \cdot L^{-1}$'
        layout.xlim = (start, end)
        layout.ylim = (0, 1.1*np.max(y))
        
        plotting.set_layout(fig, ax, layout)        

        return ax

    @property
    def local_purity(self):
        """Returns the local purity of the signal.

        Creates an array with the size of the signal with zero values. An array
        with the sum of the signal is created. Every value for signal_sum under
        a defined value is set to NaN. The other values are set for each
        component. Errorstatehandling of floating point error by the division:
        ignores the divide and invalid. Returns a NaN, for zero and infinity
        with large finite numbers.

        Returns
        -------
        local_purity : NdArray
            Returns the local purity for each component as an array.
        """
        purity = np.zeros(self.signal.shape)
        signal_sum = self.signal.sum(1)
        signal_sum[signal_sum < 1e-6] = np.nan
        for comp in range(self.n_comp):
            signal = self.signal[:,comp]
            with np.errstate(divide='ignore', invalid='ignore'):
                purity[:,comp] = np.divide(signal, signal_sum)
        purity = np.nan_to_num(purity)

        return np.nan_to_num(purity)
    
    @plotting.save_fig
    def plot_purity(self, start=0, end=None):
        """Plots the local purity for each component of the concentration
        profile.

        Parameters
        ----------
        start : float
            start time for plotting
        end : float
            ent time for plotting

        See also
        --------
        plotlib
        plot
        """
        x = self.time / 60
        y = self.local_purity * 100

        fig, ax = plotting.setup_figure()
        ax.plot(x,y)

        layout = plotting.Layout()
        layout.x_label = '$time~/~min$'
        layout.y_label = '$c~/~mol \cdot L^{-1}$'
        layout.xlim = (start, end)
        layout.ylim = (0, 1.1*np.max(y))
        
        plotting.set_layout(fig, ax, layout)        
        
        return ax

    @property
    def n_comp(self):
        """Number of components of the signal

        Returns
        --------
        n_comp : int
            Returns the number of the components for the signal.
        """
        return self.signal.shape[1]

    def __str__(self):
        return self.__class__.__name__
Exemple #11
0
class SecondaryAxis(Structure):
    component_indices = List()
    y_label = String()
    transform = Callable()
Exemple #12
0
class Annotation(Structure):
    text = String()
    xy = Tuple()
    xytext = Tuple()
    arrowstyle = '-|>'
Exemple #13
0
class Tick(Structure):
    location: Tuple()
    label: String()
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