Beispiel #1
0
    def test_get_results_this_sample(self):
        """
        Test that get_results_this_sample returns the optimization result for
        this optimization.
        """
        op = transfer_to_casadi_interface(
            "CSTR.CSTR_MPC",
            self.cstr_file_path,
            compiler_options={"state_initial_equations": True})
        op.set('_start_c', float(self.c_0_A))
        op.set('_start_T', float(self.T_0_A))

        # Set options collocation
        n_e = 50
        opt_opts = op.optimize_options()
        opt_opts['n_e'] = n_e
        opt_opts['IPOPT_options']['print_level'] = 0

        # Define some MPC-options
        sample_period = 3
        horizon = 50
        seed = 7
        cvc = {'T': 1e6}

        # Define blocking factors
        bl_list = [1] * horizon
        factors = {'Tc': bl_list}
        bf = BlockingFactors(factors=factors)
        opt_opts['blocking_factors'] = bf

        # Create MPC-object
        MPC_object = MPC(op,
                         opt_opts,
                         sample_period,
                         horizon,
                         constr_viol_costs=cvc,
                         noise_seed=seed)

        MPC_object.update_state()
        u_k1 = MPC_object.sample()
        result1 = MPC_object.get_results_this_sample()
        N.testing.assert_equal(0, result1['time'][0])
        N.testing.assert_equal(sample_period * horizon, result1['time'][-1])

        MPC_object.update_state()
        u_k2 = MPC_object.sample()
        result2 = MPC_object.get_results_this_sample()
        N.testing.assert_equal(sample_period, result2['time'][0])
        N.testing.assert_equal(sample_period * (horizon + 1),
                               result2['time'][-1])
Beispiel #2
0
    def test_du_bounds(self):
        """
        Test with blocking_factor du_bounds.
        """
        op = transfer_to_casadi_interface(
            "CSTR.CSTR_MPC",
            self.cstr_file_path,
            compiler_options={"state_initial_equations": True})
        op.set('_start_c', float(self.c_0_A))
        op.set('_start_T', float(self.T_0_A))

        # Set options collocation
        n_e = 50
        opt_opts = op.optimize_options()
        opt_opts['n_e'] = n_e
        opt_opts['IPOPT_options']['print_level'] = 0

        # Define some MPC-options
        sample_period = 3
        horizon = 50
        seed = 7

        # Define blocking factors
        bl_list = [1] * horizon
        factors = {'Tc': bl_list}
        du_bounds = {'Tc': 5}
        bf = BlockingFactors(factors=factors, du_bounds=du_bounds)
        opt_opts['blocking_factors'] = bf

        # Create MPC-object
        MPC_object = MPC(op,
                         opt_opts,
                         sample_period,
                         horizon,
                         noise_seed=seed,
                         initial_guess='trajectory')

        MPC_object.update_state()
        u_k1 = MPC_object.sample()

        res = MPC_object.get_results_this_sample()

        Tc = res['Tc']

        prev_value = Tc[0]
        largest_delta = 0

        for value in Tc:
            delta = value - prev_value
            if delta > largest_delta:
                largest_delta = delta
            prev_value = value

        assert largest_delta < 5, "Value {} is not less than {}".format(
            largest_delta, 5)
Beispiel #3
0
    def test_auto_bl_factors(self):
        """
        Test blocking factors generated in the mpc-class.
        """
        op = transfer_to_casadi_interface(
            "CSTR.CSTR_MPC",
            self.cstr_file_path,
            compiler_options={"state_initial_equations": True})

        # Set options collocation
        n_e = 50
        opt_opts = op.optimize_options()
        opt_opts['n_e'] = n_e
        opt_opts['IPOPT_options']['print_level'] = 0

        # Define some MPC-options
        sample_period = 3
        horizon = 50

        # Create MPC-object
        MPC_object_auto = MPC(op, opt_opts, sample_period, horizon)
        MPC_object_auto.update_state()
        MPC_object_auto.sample()
        res_auto = MPC_object_auto.get_results_this_sample()

        opt_opts_auto = op.optimize_options()
        opt_opts_auto['n_e'] = n_e
        opt_opts_auto['IPOPT_options']['print_level'] = 0
        bf_list = [1] * horizon
        factors = {'Tc': bf_list}
        bf = BlockingFactors(factors)
        opt_opts_auto['blocking_factors'] = bf

        MPC_object = MPC(op, opt_opts_auto, sample_period, horizon)
        MPC_object.update_state()
        MPC_object.sample()
        res = MPC_object.get_results_this_sample()

        # Assert that res_auto['Tc'] and res['Tc'] are equal
        N.testing.assert_array_equal(res_auto['Tc'], res['Tc'])
Beispiel #4
0
    def test_infeasible_return_input(self):
        """
        Test that the input returned from an unsuccessful optimization is the 
        next input in the last successful optimization.
        """
        op = transfer_to_casadi_interface(
            "CSTR.CSTR_MPC",
            self.cstr_file_path,
            compiler_options={"state_initial_equations": True})

        # Set options collocation
        n_e = 50
        opt_opts = op.optimize_options()
        opt_opts['n_e'] = n_e
        opt_opts['IPOPT_options']['print_level'] = 0

        # Define some MPC-options
        sample_period = 3
        horizon = 50
        seed = 7
        cvc = {'T': 1e6}

        # Define blocking factors
        bl_list = [1] * horizon
        factors = {'Tc': bl_list}
        bf = BlockingFactors(factors=factors)
        opt_opts['blocking_factors'] = bf

        # Create MPC-object
        MPC_object = MPC(op,
                         opt_opts,
                         sample_period,
                         horizon,
                         constr_viol_costs=cvc,
                         noise_seed=seed,
                         create_comp_result=False,
                         initial_guess='trajectory')
        # NOTE: THIS NOT WORKING WITH initial_guess='shift'!!

        MPC_object.update_state({
            '_start_c': 587.47543496,
            '_start_T': 345.64619542
        })
        u_k1 = MPC_object.sample()
        result1 = MPC_object.get_results_this_sample()

        # Optimize with infeasible problem
        MPC_object.update_state({'_start_c': 900, '_start_T': 400})
        u_k2 = MPC_object.sample()

        # Assert that problem was infeasible and that the returned input is
        # the next input from the last succesful optimization
        N.testing.assert_(
            'Infeasible_Problem_Detected',
            MPC_object.collocator.solver_object.getStat('return_status'))

        N.testing.assert_almost_equal(u_k2[1](0)[0],
                                      result1['Tc'][4],
                                      decimal=10)

        # Assert that the returned resultfile is that of the last succesful
        # optimization
        result2 = MPC_object.get_results_this_sample()

        assert result1 == result2, "UNEQUAL VALUES. result1={}\nresult2={}".format(
            result1, result2)

        # Assert that problem was infeasible yet again and that the returned
        # input is the next (third) input from the last succesful optimization
        MPC_object.update_state({'_start_c': 900, '_start_T': 400})
        u_k3 = MPC_object.sample()

        N.testing.assert_(
            'Infeasible_Problem_Detected',
            MPC_object.collocator.solver_object.getStat('return_status'))

        N.testing.assert_almost_equal(u_k3[1](0)[0],
                                      result1['Tc'][7],
                                      decimal=10)
Beispiel #5
0
class RealTimeMPCBase(RealTimeBase):
    """
    A base class for performing real time MPC on a process. Note that
    this is an abstract class; to use it, you need to extend it with
    the functions send_control_signal and get_values.
    """
    def __init__(self,
                 file_path,
                 opt_name,
                 dt,
                 t_hor,
                 t_final,
                 start_values,
                 par_values,
                 output_names,
                 input_names,
                 par_changes=ParameterChanges(),
                 mpc_options={},
                 constr_viol_costs={},
                 noise=0):
        """
        Create a real time MPC object.
        
        Parameters::
        
            file_path --
                The path of the .mop file containing the model to be used for
                the MPC solver.
                
            opt_name --
                The name of the optimization in the file specified by file_path
                to be used by the MPC solver.
                
            dt --
                The time to wait in between each sample.
                
            t_hor --
                The horizon time for the MPC solver. Must be an even multiple
                of dt.
                
            t_final --
                The total time to run the real time MPC.
                
            start_values --
                A dictionary containing the initial state values for the process.
                
            par_values --
                A dictionary containing parameter values to be set in the model.
                
            output_names --
                A list of the names of all of the output variables used in the
                model.
                
            input_names --
                A list of the names of all of the input variables used in the
                model.
                
            par_changes --
                A ParameterChanges object containing parameter changes and the
                times they should be applied.
                Default: An empty ParameterChanges object
                
            mpc_options --
                A dictionary of options to be used for the MPC solver.
                
            constr_viol_costs --
                Constraint violation costs used by the MPC solver. See the
                documentation of the MPC class for more information.
                
            noise --
                Standard deviation of the noise to add to the input signals.
                Default: 0
        """

        super(RealTimeMPCBase,
              self).__init__(dt, t_final, start_values, output_names,
                             input_names, par_changes, noise)
        horizon = int(t_hor / dt)
        n_e = horizon

        self._setup_MPC_solver(file_path, opt_name, dt, horizon, n_e,
                               par_values, constr_viol_costs, mpc_options)

        self.range_ = []
        for i, name in enumerate(self.inputs):
            var = self.solver.op.getVariable(name)
            self.range_.append(
                (var.getMin().getValue(), var.getMax().getValue()))

    def _setup_MPC_solver(self,
                          file_path,
                          opt_name,
                          dt,
                          horizon,
                          n_e,
                          par_values,
                          constr_viol_costs={},
                          mpc_options={}):

        op = transfer_optimization_problem(
            opt_name,
            file_path,
            compiler_options={'state_initial_equations': True})
        op.set(par_values.keys(), par_values.values())

        opt_opts = op.optimize_options()
        opt_opts['n_e'] = n_e
        opt_opts['n_cp'] = 2
        opt_opts['IPOPT_options']['tol'] = 1e-10
        opt_opts['IPOPT_options']['print_time'] = False

        if 'IPOPT_options' in mpc_options:
            opt_opts['IPOPT_options'].update(mpc_options['IPOPT_options'])
        for key in mpc_options:
            if key != 'IPOPT_options':
                opt_opts[key] = mpc_options[key]

        self.solver = MPC(op,
                          opt_opts,
                          dt,
                          horizon,
                          constr_viol_costs=constr_viol_costs)

    def enable_codegen(self, name=None):
        """
        Enables use of generated C code for the MPC solver.
        
        Generates and compiles code for the NLP, gradient of f, Jacobian of g,
        and Hessian of the Lagrangian of g Function objects, and then replaces
        the solver object in the solver's collocator with a new one that makes
        use of the compiled functions as ExternalFunction objects.
        
        Parameters::
                
            name --
                A string that if it is not None, loads existing files
                nlp_[name].so, grad_f_[name].so, jac_g_[name].so and
                hess_lag_[name].so as ExternalFunction objects to be used
                by the solver rather than generating new code.
                Default: None
        """
        self.solver.collocator.enable_codegen(name)

    def enable_integral_action(self, mu, M, error_names=None, u_e=None):
        """
        Enables integral action for the inputs.
        
        If integral action is enabled, in each step the input error is
        calculated and used to update the estimation of the error. By
        default, the input error is calculated as the matrix M times the
        difference between the current state vector as predicted by the
        solver in the last time step and as measured from the process
        in the current time step; however, this can be changed by
        overriding the estimate_input_error method.
        
        The low-pass filter used to update the estimate is
        [new estimate] = mu*[old estimate] + (1-mu)*[current estimate]
        
        Parameters::
        
            mu --
                Controls the convergence rate of the error estimate. 
                See above.
                
            M --
                A matrix used for calculating the input error from the state
                error. See above.
                
            error_names --
                A list containing the names of the model variables for
                the input errors. If set to None, it is assumed be the
                same as the list of input variables with the prefix '_e'
                appended to each one.
                Default: None
            
            u_e --
                A list containing a set of values to be applied as
                stationary errors to the input signals. Used for
                simulating a stationary error where there otherwise wouldn't
                be one. If set to None, no stationary error is applied.
                Default: None.
        """
        self._ia = True
        self.mu = mu
        self.M = M
        if error_names == None:
            self.errors = [name + '_e' for name in self.inputs]
        else:
            self.errors = error_names
        if u_e == None:
            self.u_e = N.zeros(len(self.inputs))
        else:
            self.u_e = N.array(u_e)
        self.u_e_e = N.zeros(len(self.inputs))

    def run(self, save=False):
        """
        Run the real time MPC controller defined by the object.
        
        Parameters::
        
            save --
                Determines whether or not to save data after running. If set
                to False, the same data can still be saved manually by using
                the save_results function.
                Default: False
                
        Returns::
        
            The results and statistics from the run.
        """
        if self._already_run:
            raise RuntimeError('run can only be called once')

        self.e_e = []

        n_outputs = len(self.outputs)
        n_inputs = len(self.inputs)

        x_k = self.start_values.copy()
        x_k_last = x_k.copy()

        time1 = time.clock()
        time2 = time.time()
        time3 = 0

        for k in range(self.n_steps):
            new_pars = self.par_changes.get_new_pars(k * self.dt)
            if new_pars != None:
                self.solver.op.set(new_pars.keys(), new_pars.values())

            self.solver.update_state(x_k)
            u_k = self.solver.sample()
            u_k = self._apply_noise(u_k, std_dev=self.noise)
            if self._ia:
                u_k_e = self._apply_error(u_k)

            if time3 != 0:
                solve_time = time.time() - time3
                self.solve_times.append(solve_time)
                if solve_time > self.dt * 0.2:
                    print 'WARNING: Control signal late by', solve_time, 's'
            if self._ia:
                self.send_control_signal(u_k_e)
            else:
                self.send_control_signal(u_k)
            if k == 0:
                next_time = time.time() + self.dt
            m_k = self.wait_and_get_measurements(next_time)
            next_time = time.time() + self.dt
            time3 = time.time()
            x_k = self.estimate_states(m_k, x_k_last)
            x_k_last = x_k.copy()
            if self._ia:
                self._update_error_estimate(x_k)

            for i in range(n_outputs):
                self.results[self.outputs[i]].append(x_k['_start_' +
                                                         self.outputs[i]])
            for i in range(n_inputs):
                self.results[self.inputs[i]].append(u_k[1](0)[i])
            self.stats.append(self.solver.collocator.solver_object.getStats())

        self.ptime = time.clock() - time1
        self.rtime = time.time() - time2
        print 'Processor time:', self.ptime, 's'
        print 'Real time:', self.rtime, 's'

        if save:
            self.save_results()

        self._already_run = True

        return self.results, self.stats

    def _apply_noise(self, u_k, std_dev=0.0):
        if std_dev == 0:
            return u_k

        inputs = u_k[1](0)
        for n in range(len(inputs)):
            noise = N.random.normal(0.0, std_dev)
            inputs[n] += noise
            inputs[n] = max(self.range_[n][0], min(self.range_[n][1],
                                                   inputs[n]))
        return (u_k[0], lambda t: inputs)

    def _apply_error(self, u_k):
        u_k_e = []
        for i in range(len(self.errors)):
            u_k_e.append(
                max(self.range_[i][0],
                    min(self.range_[i][1], u_k[1](0)[i] + self.u_e[i])))
        return (u_k[0], lambda t: N.array(u_k_e))

    def _update_error_estimate(self, x_k):
        e_k = self._calculate_error(x_k)
        u_e_e_next = self.estimate_input_error(e_k)
        self.u_e_e = (1 - self.mu) * self.u_e_e + self.mu * (u_e_e_next +
                                                             self.u_e_e)
        for i in range(len(self.errors)):
            self.solver.set(self.errors[i], self.u_e_e[i])
        self.e_e.append(self.u_e_e)

    def estimate_input_error(self, e_k):
        """
        Estimates the input error given the state error.
        
        Parameters::
        
            e_k --
                The state error vector.
                
        Returns::
        
            The input error vector.
        """
        return self.M.dot(e_k)

    def _calculate_error(self, x_k):
        x_k_a = N.array([x_k['_start_' + name] for name in self.outputs])
        res = self.solver.get_results_this_sample()
        x_k_e = N.array(
            [res[name][self.solver.options['n_cp']] for name in self.outputs])
        return x_k_a - x_k_e

    def print_stats(self):
        """
        Print statistics from the run.
        
        The times printed are the sums of the corresponding
        statistics from the NLP solver over all time steps.
        """

        if not self._already_run:
            raise RuntimeError(
                'Stats can only be printed after run() has been called')
        stat_names = [
            't_callback_fun', 't_callback_prepare', 't_eval_f', 't_eval_g',
            't_eval_grad_f', 't_eval_h', 't_eval_jac_g', 't_mainloop'
        ]

        total_times = {}
        for name in stat_names:
            total_times[name] = 0.

        for stat in self.stats:
            for name in stat_names:
                total_times[name] += stat[name]

        t_total = total_times['t_mainloop']
        print 'Total times:'
        for name in stat_names:
            print("%19s: %6.4f s (%7.3f%%)" %
                  (name, total_times[name], total_times[name] / t_total * 100))

    def save_results(self, filename=None):
        """
        Pickle and save data from the run to a file name either passed
        as an argument or entered by the user. If an empty string is
        entered as the file name, no data is saved.
        
        Parameters::
        
            filename --
                The file name to save as. If it is not provided, the
                user will be prompted to input it.
                Default: None
        """

        if not self._already_run:
            raise RuntimeError(
                'Results can only be saved after run() has been called')
        result_dict = {}
        result_dict['results'] = self.results
        result_dict['stats'] = self.stats
        result_dict['ptime'] = self.ptime
        result_dict['rtime'] = self.rtime
        result_dict['noise'] = self.noise
        result_dict['late_times'] = self.late_times
        result_dict['wait_times'] = self.wait_times
        result_dict['solve_times'] = self.solve_times
        save_to_file(result_dict, filename)
Beispiel #6
0
    def test_du_quad_pen(self):
        """
        Test with blocking_factor du_quad_pen.
        """
        op = transfer_to_casadi_interface(
            "CSTR.CSTR_MPC",
            self.cstr_file_path,
            compiler_options={"state_initial_equations": True})
        op.set('_start_c', float(self.c_0_A))
        op.set('_start_T', float(self.T_0_A))

        # Set options collocation
        n_e = 50
        opt_opts = op.optimize_options()
        opt_opts['n_e'] = n_e
        opt_opts['IPOPT_options']['print_level'] = 0

        # Define some MPC-options
        sample_period = 3
        horizon = 50
        seed = 7

        # Define blocking factors
        bl_list = [1] * horizon
        factors = {'Tc': bl_list}
        bf = BlockingFactors(factors=factors)
        opt_opts['blocking_factors'] = bf

        # Create MPC-object without du_quad_pen
        MPC_object = MPC(op,
                         opt_opts,
                         sample_period,
                         horizon,
                         noise_seed=seed,
                         initial_guess='trajectory')

        MPC_object.update_state()
        MPC_object.sample()
        res = MPC_object.get_results_this_sample()

        # Create MPC-object with du_quad_pen
        opt_opts_quad = op.optimize_options()
        opt_opts_quad['n_e'] = n_e
        opt_opts_quad['IPOPT_options']['print_level'] = 0
        bf_list = [1] * horizon
        factors = {'Tc': bf_list}
        du_quad_pen = {'Tc': 100}
        bf = BlockingFactors(factors, du_quad_pen=du_quad_pen)
        opt_opts_quad['blocking_factors'] = bf

        MPC_object_quad = MPC(op,
                              opt_opts_quad,
                              sample_period,
                              horizon,
                              noise_seed=seed,
                              initial_guess='trajectory')
        MPC_object_quad.update_state()
        MPC_object_quad.sample()
        res_quad = MPC_object_quad.get_results_this_sample()

        Tc = res['Tc']
        prev_value = Tc[0]
        largest_delta = 0

        for value in Tc:
            delta = value - prev_value
            if delta > largest_delta:
                largest_delta = delta
            prev_value = value

        Tc = res_quad['Tc']
        prev_value = Tc[0]
        largest_delta_quad = 0

        for value in Tc:
            delta = value - prev_value
            if delta > largest_delta_quad:
                largest_delta_quad = delta
            prev_value = value

        N.testing.assert_(largest_delta_quad < largest_delta)