예제 #1
0
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'
    ]
예제 #2
0
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)
예제 #8
0
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'
    ]    
예제 #13
0
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
예제 #16
0
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)
예제 #18
0
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__
예제 #20
0
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'
예제 #21
0
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
예제 #22
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)
예제 #23
0
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
예제 #25
0
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
예제 #28
0
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