Пример #1
0
    def terminal_cost(self, function, unit):
        r"""Sets the terminal cost of the OCP.

        Examples
        ========

        >>> from beluga.problem import OCP
        >>> sigma = OCP()
        >>> sigma.terminal_cost('x^2', 'm^2')
        terminal_cost: {'function': x**2, 'unit': m**2}

        .. seealso::
            initial_cost, path_cost, get_terminal_cost
        """
        if isinstance(function, str):
            function = sympify(function)
        if isinstance(unit, str):
            unit = sympify(unit)

        if not isinstance(function, Expr):
            raise ValueError
        if not isinstance(unit, Expr):
            raise ValueError

        temp = {'function': function, 'unit': unit}
        self._properties['terminal_cost'] = temp
        return self
Пример #2
0
def make_boundary_conditions(
    constraints,
    states,
    costates,
    cost,
    derivative_fn,
    location,
    prefix_map=(('initial', (r'([\w\d\_]+)_0', r"_x0['\1']", sympify('-1'))),
                ('terminal', (r'([\w\d\_]+)_f', r"_xf['\1']", sympify('1'))))):
    """
    Creates boundary conditions for initial and terminal constraints.

    :param constraints: List of boundary constraints.
    :param states: List of state variables.
    :param costates: List of costate variables.
    :param cost: Cost function.
    :param derivative_fn: Total derivative function.
    :param location: Location of each boundary constraint.
    :param prefix_map: Prefix mapping.
    :return: List of boundary conditions.
    """
    prefix_map = dict(prefix_map)
    bc_list = [
        sanitize_constraint_expr(x, states, location, prefix_map)
        for x in constraints[location]
    ]
    *_, sign = dict(prefix_map)[location]
    cost_expr = sign * cost
    bc_list += [
        str(costate - derivative_fn(cost_expr, state))
        for state, costate in zip(states, costates)
    ]

    return bc_list
    def make_ctrl_dae(self, problem):
        """!
        \brief     Compute EOMs for controls.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.1
        \date      04/10/16
        """
        if len(self.equality_constraints) > 0:
            self.mu_vars = [sympify('mu'+str(i+1)) for i in range(len(self.equality_constraints))]
            self.mu_lhs = [sympify(c.expr) for c in self.equality_constraints]
        else:
            self.mu_vars = self.mu_lhs = []

        g = self.ham_ctrl_partial + self.mu_lhs
        X = [state.sym for state in problem.states()] + [Symbol(costate) for costate in self.costates]
        U = [c.sym for c in problem.controls()] + self.mu_vars

        xdot = Matrix([sympify(state.process_eqn) for state in problem.states()] + self.costate_rates)
        # Compute Jacobian
        dgdX = Matrix([[self.derivative(g_i, x_i, self.quantity_vars) for x_i in X] for g_i in g])
        dgdU = Matrix([[self.derivative(g_i, u_i, self.quantity_vars) for u_i in U] for g_i in g])

        udot = dgdU.LUsolve(-dgdX*xdot); # dgdU * udot + dgdX * xdot = 0

        self.dae_states = U

        self.dae_equations = list(udot)
        self.dae_bc = g
    def make_state_dep_ham(self, state, problem):
        """!
        \brief     Symbolically create the Hamiltonian.
        \author    Michael Grant
        \author    Sean Nolan
        \version   0.1
        \date      06/22/16
        """

        H = sympify(0)

        new_terms = sympify2(problem.cost['path'].expr)
        atoms = new_terms.atoms()
        if state in atoms:
            H += new_terms


        for i in range(len(problem.states())):
            new_terms = sympify2(self.costates[i]) * (sympify2(problem.states()[i].process_eqn))
            atoms = new_terms.atoms()
            if state in atoms:
                H += new_terms

        # Adjoin equality constraints
        for i in range(len(self.equality_constraints)):
            new_terms = sympify2('mu' + str(i + 1)) * (sympify2(self.equality_constraints[i].expr))
            atoms = new_terms.atoms()
            if state in atoms:
                H += new_terms

        raw_terms = H.as_terms()
        H_parts = [raw_terms[0][k][0] for k in range(len(raw_terms[0]))]
        new_H = sympify(0)
        for part in H_parts:
            store = sympify(0)
            tries = 0
            while (part != store) & (tries <= 5):
                store = part
                part = part.expand()
                tries += 1
            raw_terms = part.as_terms()
            sub_parts = [raw_terms[0][k][0] for k in range(len(raw_terms[0]))]
            new_part = sympify(0)
            for sub_part in sub_parts:
                atoms = sub_part.atoms()
                if state in atoms:
                    # sub_part = sub_part.factor()
                    new_part += sub_part
            new_part = new_part.factor()
            if len(str(new_part)) < 50:
                 new_part = new_part.simplify(ratio=1.0)
            new_H += new_part

        # print('New:' + str(len(str(new_H))))
        # print('Old:' + str(len(str(H))))

        return new_H
    def make_ctrl_analytic(self, controls):
        """!
        \brief     Symbolically compute the solutions for the control along control-unconstrained arcs.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.1
        \date      06/30/15
        """
        # Solve all controls simultaneously
        logging.info("Finding optimal control law ...")

        # If equality constraints are present
        # We need to solve for 'mu's as well
        lhs = self.ham_ctrl_partial
        vars = [c.sym for c in controls]
        self.mu_vars = []
        self.mu_lhs = []

        if len(self.equality_constraints) > 0:
            self.mu_vars = [sympify('mu'+str(i+1)) for i in range(len(self.equality_constraints))]

            self.mu_lhs = [sympify(c.expr) for c in self.equality_constraints]
        try:
            var_list = list(vars + self.mu_vars)
            # var_list = vars
            eqn_list = []
            for eqn in list(lhs + self.mu_lhs ):
                eqn_list.append(self.selective_expand(eqn, controls, self.quantity_vars))
            logging.info("Attempting using SymPy ...")
            logging.debug("dHdu = "+str(eqn_list))

            ctrl_sol = solve(eqn_list, var_list, dict=True)
            #TODO: ^ uncomment once done with the aircraft noise problem
            #ctrl_sol = CtrlSols() #I'm going to hell for this
            #logging.info("Grabbed the control laws for the aircraft noise!")

            # raise ValueError() # Force mathematica
        except ValueError as e:  # FIXME: Use right exception name here
            logging.debug(e)
            logging.info("No control law found")
            from beluga.utils.pythematica import mathematica_solve
            logging.info("Attempting using Mathematica ...")
            #var_sol = mathematica_solve(lhs+self.mu_lhs,vars+self.mu_vars)
            # TODO: Extend numerical control laws to mu's
            ctrl_sol = var_sol
            if ctrl_sol == []:
                logging.info("No analytic control law found, switching to numerical method")

        #logging.info("Done")
        # solve() returns answer in the form
        # [ {ctrl1: expr11, ctrl2:expr22},
        #   {ctrl1: expr21, ctrl2:expr22}]
        # Convert this to format required by template
        self.control_options = [ [{'name':str(ctrl), 'expr':str(expr)}
                                    for (ctrl,expr) in option.items()]
                                for option in ctrl_sol]
Пример #6
0
    def terminal(self, function, unit):
        if not isinstance(function, str):
            raise ValueError
        if not isinstance(unit, str):
            raise ValueError

        temp = self.get('terminal', [])
        temp.append({'function': sympify(function), 'unit': sympify(unit)})
        self['terminal'] = temp
        return self
Пример #7
0
    def __init__(self, sym_ocp):

        self.sym_ocp = sym_ocp
        self.name = None

        # Compile the cost functions from the original OCP
        independent = [sym_ocp.get_independent()['symbol']]
        states = [s['symbol'] for s in sym_ocp.states()]
        controls = [s['symbol'] for s in sym_ocp.controls()]
        params = [s['symbol'] for s in sym_ocp.parameters()]
        consts = [s['symbol'] for s in sym_ocp.get_constants()]

        if sym_ocp.get_initial_cost() is not None:
            initial_compiled = lambdify_(
                [independent, states, controls, params, consts],
                sym_ocp.get_initial_cost()['function'])
        else:
            initial_compiled = lambdify_(
                [independent, states, controls, params, consts], sympify('0'))

        if sym_ocp.get_path_cost() is not None:
            path_compiled = lambdify_(
                [independent, states, controls, params, consts],
                sym_ocp.get_path_cost()['function'])
        else:
            path_compiled = lambdify_(
                [independent, states, controls, params, consts], sympify('0'))

        if sym_ocp.get_terminal_cost() is not None:
            terminal_compiled = lambdify_(
                [independent, states, controls, params, consts],
                sym_ocp.get_terminal_cost()['function'])
        else:
            terminal_compiled = lambdify_(
                [independent, states, controls, params, consts], sympify('0'))

        self.initial_cost = initial_compiled
        self.path_cost = path_compiled
        self.terminal_cost = terminal_compiled

        x_dot = [s['eom'] for s in self.sym_ocp.states()]
        self.deriv_func = lambdify_(
            [independent, states, controls, params, consts], x_dot)

        bc0 = [s['function'] for s in self.sym_ocp.constraints()['initial']]
        bcf = [s['function'] for s in self.sym_ocp.constraints()['terminal']]

        self.bc_initial = lambdify_(
            [independent, states, controls, params, consts], bc0)
        self.bc_terminal = lambdify_(
            [independent, states, controls, params, consts], bcf)
Пример #8
0
    def path_constraint(self, function, unit, lower, upper, activator, method):
        r"""Defines a path constraint for the OCP.

        Examples
        ========

        >>> from beluga.problem import OCP
        >>> sigma = OCP()
        >>> sigma.path_constraint('x^2', 'm^2', lower='x_min', upper='x_max', activator='eps', method='utm')
        path_constraints: [{'function': x**2, 'unit': m**2, 'lower': x_min, 'upper': x_max, 'activator': eps, 'method': 'utm'}]

        .. seealso::
            get_path_constraints

        """
        if isinstance(function, str):
            function = sympify(function)
        if isinstance(unit, str):
            unit = sympify(unit)
        if isinstance(lower, str):
            lower = sympify(lower)
        if isinstance(upper, str):
            upper = sympify(upper)
        if isinstance(activator, str):
            activator = sympify(activator)

        if not isinstance(function, Expr):
            raise ValueError
        if not isinstance(unit, Expr):
            raise ValueError
        if not isinstance(lower, Expr):
            raise ValueError
        if not isinstance(upper, Expr):
            raise ValueError
        if not isinstance(activator, Expr):
            raise ValueError
        if not isinstance(method, str):
            raise ValueError

        temp = self._properties.get('path_constraints', [])
        temp.append({
            'function': function,
            'unit': unit,
            'lower': lower,
            'upper': upper,
            'activator': activator,
            'method': method
        })
        self._properties['path_constraints'] = temp
        return self
Пример #9
0
    def compute_base_scaling(self, sol, scale_expr):
        if isinstance(scale_expr, num.Number):
            # If scaling factor is a number, use it
            return scale_expr
        else:
            variables = [
                (const_name, const_val)
                for (const_name,
                     const_val) in zip(self.problem_data['constants'],
                                       self.problem_data['constants_values'])
            ]
            # Have to do in this order to override state values with arrays
            variables += [
                (state, max(abs(sol.y[:, idx])))
                for idx, state in enumerate(self.problem_data['states'])
            ]
            variables += [
                (quad, max(abs(sol.q[:, idx])))
                for idx, quad in enumerate(self.problem_data['quads'])
            ]

            var_dict = dict(variables)

            # Evaluate expression to get scaling factor
            return float(
                sympify(scale_expr).subs(var_dict, dtype=float).evalf())
Пример #10
0
def mathematica_solve(expr, vars):
    if isinstance(expr, list):
        m_expr = mcode(
            ['Exp[dummyfoobar]*(' + mcode(e) + ') == 0' for e in expr])
    else:
        m_expr = 'Exp[dummyfoobar]*(' + mcode(expr) + ') == 0'

    cmd = 'Quiet[Simplify[Solve[%s,%s]]]' % (m_expr, mcode(vars)
                                             )  # Suppress warnings for now
    sol_str = mathematica_run(cmd).strip()
    # print(sol_str)
    if sol_str == '{}' or "Solve[" in sol_str:  # No solution found
        return []
    else:
        # Convert solution to  dictionary
        out = [
            dict([varsol.split(' -> ') for varsol in s.split(', ')])
            for s in sol_str[2:-2].split('}, {')
        ]
        # Convert solution strings to sympy expressions
        out = [
            dict([(sympify(var), mathematica_parse(expr))
                  for (var, expr) in sol.items()]) for sol in out
        ]
        # print(out)
        return out
Пример #11
0
    def independent(self, symbol, unit):
        r"""Sets the independent variable.

        Examples
        ========

        >>> from beluga.problem import OCP
        >>> sigma = OCP()
        >>> sigma.independent('t', 's')
        independent: {'symbol': t, 'unit': s}

        .. seealso::
            get_independent
        """
        if isinstance(symbol, str):
            symbol = Symbol(symbol)
        if isinstance(unit, str):
            unit = sympify(unit)
        if not isinstance(symbol, Symbol):
            raise ValueError
        if not isinstance(unit, Expr):
            raise ValueError

        temp = {'symbol': symbol, 'unit': unit}
        self._properties['independent'] = temp
        return self
    def make_ham(self, problem):
        """!
        \brief     Symbolically create the Hamiltonian.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.1
        \date      06/30/15
        """
        #TODO: Make symbolic
        self.ham = sympify(problem.cost['path'].expr)
        for i in range(len(problem.states())):
            self.ham += sympify(self.costates[i]) * (sympify(problem.states()[i].process_eqn))

        # Adjoin equality constraints
        for i in range(len(self.equality_constraints)):
            self.ham += sympify('mu'+str(i+1)) * (sympify(self.equality_constraints[i].expr))
    def make_costate_rate(self, states):
        """!
        \brief     Creates the symbolic differential equations for the costates.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.1
        \date      06/30/15
        """

        # TODO: Automate partial derivatives of numerical functions
        # for state in states:
        #     rate = diff(sympify('-1*(' + self.ham + ')'),state)
        #     # numerical_diff = rate.atoms(Derivative)
        #     self.costate_rates.append(str(rate))
        self.costate_rates = [self.derivative(-1*(self.ham),state, self.quantity_vars) for state in states]
        # self.costate_rates.append(str(diff(sympify(
        # '-1*(' + self.ham + ')'),state)))

        for state in states:
            corate=self.derivative(-1*(self.ham),state,self.quantity_vars)
            custom_diff=corate.atoms(Derivative)
            repl = [] #Replacements for custom functions
            for d in custom_diff:
                for f in d.atoms(AppliedUndef): #Just extracts (never more than 1 f)
                    for v in f.atoms(Symbol):
                        if str(v)==str(state): #This should only happen once!
                            replv=sympify('im('+str(f.subs(v,v+_h))+')/1e-30')
                            repl.append((d,replv))
            self.costate_rates.append(corate.subs(repl))
        self.costate_rates=sympify2(self.costate_rates)
    def __init__(self, problem, cached=True):
        """!
        \brief     Initializes all of the relevant necessary conditions of opimality.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.2
        \date      05/15/16
        """

        self.problem = problem
        self.get_actions()

        self.aug_cost = {}
        self.costates = []
        self.costate_rates = []
        self.ham = sympify('0')
        self.ham_ctrl_partial = []
        self.ctrl_free = []
        self.parameter_list = []
        self.bc_initial = []
        self.bc_terminal = []
        # from .. import Beluga # helps prevent cyclic imports
        self.compile_list = ['deriv_func','bc_func','compute_control']
        # self.template_prefix = Beluga.config.getroot()+'/beluga/bvpsol/templates/'
        self.template_suffix = '.py.mu'
        self.dae_states = []
        self.dae_equations = []
Пример #15
0
 def __init__(self, param_dict, sym_key='name', excluded=()):
     self.__dict__ = {k: sympify(v) if k not in excluded else v
                      for k, v in param_dict.items()}
     self.param_list = list(param_dict.keys())
     if sym_key is not None:
         self._sym = self.__dict__[sym_key]
     else:
         self._sym = None
Пример #16
0
def make_costate_names(states):
    r"""
    Makes a list of variables representing each costate.

    :param states: List of state variables, :math:`x`.
    :return: List of costate variables, :math:`\lambda_x`.
    """

    return [sympify('lam' + str(s.name).upper()) for s in states]
    def make_costate_bc(self, states, location):
        """!
        \brief     Symbolically create the boundary conditions at initial and final locations.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.1
        \date      06/30/15
        """

        if location is 'initial':
            sign = sympify('-1')
        elif location is 'terminal':
            sign = sympify('1')

        cost_expr = sign * (self.aug_cost[location])

        #TODO: Fix hardcoded if conditions
        #TODO: Change to symbolic
        if location == 'initial':
            # Using list comprehension instead of loops
            # lagrange_ changed to l. Removed hardcoded prefix
            self.bc_initial += [str(sympify(state.make_costate()) - self.derivative(sympify(cost_expr),state.sym, self.quantity_vars))
                                    for state in states]
        else:
            # Using list comprehension instead of loops
            self.bc_terminal += [str(sympify(state.make_costate()) - self.derivative(sympify(cost_expr),state.sym,self.quantity_vars))
                                    for state in states]
Пример #18
0
def mathematica_parse(expr):
    rules = (
        (r'(\w+)\[', lambda m: m.group(0).lower()
         ),  # Convert all function calls to lowercase
        (r'\[', '('),  # Replace square brackets with parenthesis
        (r'\]', ')'),  # Replace square brackets with parenthesis
        (r'arc(\w+)', 'a\\1'),  # Replace inverse trig functions
        (r'sec\((\w+)\)', '(1/cos(\\1))')  # Change sec to 1/cos
    )
    for rule, replacement in rules:
        expr, n = re.subn(rule, replacement, expr)

    return sympify(expr)
    def make_ctrl_partial(self, controls):
        """!
        \brief     Symbolically compute dH/du where H is the Hamiltonian and u is the control.
        \author    Michael Grant
        \author    Thomas Antony
        \version   0.1
        \date      06/30/15
        """

        self.ham_ctrl_partial = []
        _h = sympify2('1j*(1e-30)')

        for ctrl in controls:
            dHdu = self.derivative(sympify(self.ham), ctrl, self.quantity_vars)
            custom_diff = dHdu.atoms(Derivative)
            for d in custom_diff:
                for f in d.atoms(AppliedUndef):
                    for v in f.atoms(Symbol):
                        if str(v)==str(ctrl):
                            replv=sympify('im('+str(f.subs(v,v+_h))+')/1e-30')
                            repl.append((d,replv))

            self.ham_ctrl_partial.append(dHdu.subs(repl))
Пример #20
0
def make_constrained_arc_fns(states, costates, controls, parameters, constants,
                             quantity_vars, hamiltonian):
    """
    Creates constrained arc control functions.

    :param workspace:
    :return:
    """
    tf_var = sympify('tf')
    costate_eoms = [{
        'eom': [str(_.eom * tf_var) for _ in costates],
        'arctype': 0
    }]
    bc_list = []  # Unconstrained arc placeholder

    return costate_eoms, bc_list
Пример #21
0
    def compute_base_scaling(self, sol, scale_expr):
        if isinstance(scale_expr, num.Number):
            # If scaling factor is a number, use it
            return scale_expr
        else:
            variables = [(aux_name, aux_val) for aux_type in sol.aux
                         if isinstance(sol.aux[aux_type], dict)
                         for (aux_name, aux_val) in sol.aux[aux_type].items()]
            # Have to do in this order to override state values with arrays
            variables += [
                (state, max(abs(sol.y[:, idx])))
                for idx, state in enumerate(self.problem_data['state_list'])
            ]

            var_dict = dict(variables)

            # Evaluate expression to get scaling factor
            return float(
                sympify(scale_expr).subs(var_dict, dtype=float).evalf())
Пример #22
0
    def quantity(self, symbol, function):
        r"""

        :param symbol:
        :param function:
        :return:
        """
        if isinstance(symbol, str):
            symbol = Symbol(symbol)
        if isinstance(function, str):
            function = sympify(function)

        if not isinstance(symbol, Symbol):
            raise ValueError
        if not isinstance(function, Expr):
            raise ValueError

        temp = self._properties.get('switches', [])
        temp.append({'symbol': symbol, 'function': function})
        self._properties['switches'] = temp
        return self
    def make_costate_rate_numeric(self, states):
        """!
        \brief     Creates the symbolic differential equations for the costates.
        \author    Michael Grant
        \author    Sean Nolan
        \version   0.1
        \date      06/22/16
        """

        h = 1e-100
        j = symbols('1j')

        self.costate_rates = []

        for state in states:
            state = sympify(state)
            H = self.make_state_dep_ham(state, self.problem)
            if H != 0:
                self.costate_rates.append('-np.imag(' + str((H.subs(state, (state + h * j)))) + ')/' + str(h))
            else:
                self.costate_rates.append('0')
Пример #24
0
    def control(self, symbol, unit):
        r"""Defines a control variable.

        Examples
        ========

        >>> from beluga.problem import OCP
        >>> sigma = OCP()
        >>> sigma.control('u', 'rad')
        controls: [{'symbol': u, 'unit': rad}]

        .. seealso::
            get_controls
        """
        if not isinstance(symbol, str):
            raise ValueError
        if not isinstance(unit, str):
            raise ValueError

        temp = self._properties.get('controls', [])
        temp.append({'symbol': Symbol(symbol), 'unit': sympify(unit)})
        self._properties['controls'] = temp
        return self
Пример #25
0
 def make_lagrange_mult(c, ind=1):
     return sympify('lagrange_' + location + '_' + str(ind))
Пример #26
0
 def create_scale_fn(self, unit_expr):
     return lambdify(self.units_sym, sympify(unit_expr))
    def get_bvp(self,problem,mode='dae'):
        """Perform variational calculus calculations on optimal control problem
           and returns an object describing the boundary value problem to be solved

        Returns: bvpsol.BVP object
        """
        # Process intermediate variables
        self.process_quantities(problem)

        # Regularize path constraints using saturation functions
        self.process_path_constraints(problem)

        # self.state_subs = [(state.sym, sympify(state.process_eqn)) for state in problem.states()]
        ## Create costate list
        # self.costates = [state.make_costate() for state in problem.states()]
        self.make_costates(problem)

        # for i in range(len(self.problem.states())):
        #     self.costates.append(self.problem.states()[i].make_costate())

        # Build augmented cost strings
        # aug_cost_init = sympify(problem.cost['initial'].expr)
        self.make_aug_cost('initial')
        # self.make_aug_cost(aug_cost_init, problem.constraints(), 'initial')

        # aug_cost_term = sympify(problem.cost['terminal'].expr)
        self.make_aug_cost('terminal')
        # self.make_aug_cost(aug_cost_term, problem.constraints(), 'terminal')

        # Add state boundary conditions
        self.bc_initial = [self.sanitize_constraint(x,problem).expr
                            for x in problem.constraints().get('initial')]
        self.bc_terminal = [self.sanitize_constraint(x,problem).expr
                    for x in problem.constraints().get('terminal')]

        self.equality_constraints = problem.constraints().get('equality')

        ## Unconstrained arc calculations
        # Construct Hamiltonian
        self.make_ham(problem)
        logging.debug('Hamiltonian : '+str(self.ham))
        # Get list of all custom functions in the problem
        # TODO: Check in places other than the Hamiltonian?
        # TODO: Move to separate method?
        func_list = sympify(self.ham).atoms(AppliedUndef)

        # TODO: Change this to not be necessary
        self.problem = problem

        # Load required functions from the input file
        new_functions = {(str(f.func),getattr(problem.input_module,str(f.func)))
                            for f in func_list
                            if hasattr(problem.input_module,str(f.func)) and
                                inspect.isfunction(getattr(problem.input_module,str(f.func)))}

        problem.functions.update(new_functions)

        undefined_func = [f.func for f in func_list if str(f.func) not in problem.functions]

        if not all(x is None for x in undefined_func):
            raise ValueError('Invalid function(s) specified: '+str(undefined_func))

        # Compute costate conditions
        self.make_costate_bc(problem.states(),'initial')
        self.make_costate_bc(problem.states(),'terminal')

        # TODO: Make this more generalized free final time condition
        # HARDCODED tf variable
        time_constraints = problem.constraints().get('independent')
        if len(time_constraints) > 0:
            self.bc_terminal.append('tf - 1')
        else:
            # Add free final time boundary condition
            self.bc_terminal.append('_H - 0')

        # Compute costate process equations
        if mode == 'numerical':
            self.make_costate_rate_numeric(problem.states())
        else:
            self.make_costate_rate(problem.states())
        self.make_ctrl_partial(problem.controls())

        # # Add support for state and control constraints
        # problem.state('xi11','xi12','m')
        # problem.state('xi12','ue1','m')
        # self.costates += ['eta11','eta12']   # Costates for xi
        #
        # # Add new states to hamiltonian
        # h1_3 = '(psi12*xi12^2 + psi11*ue1)';  # xi12dot = ue1
        # c1_2 = 'u'
        # self.ham += sympify('eta11*xi12 + eta12*ue1')  #
        # self.ham += sympify('mu1 * ('+c1_2+' - '+h1_3')')
        #
        # # TODO: Compute these automatically
        # self.costate_rates += ['mu1*(xi12**2*psi1_3 + psi12*ue1)',
        #                        'mu1*(2*psi12*xi12)  - eta1']
        #
        # Compute unconstrained control law
        # (need to add singular arc and bang/bang smoothing, numerical solutions)
        self.make_ctrl(problem, mode)

        # Create problem dictionary
        # NEED TO ADD BOUNDARY CONDITIONS

        # bc1 = [self.sanitize_constraint(x) for x in initial_bc]

        self.problem_data = {
        'aux_list': [
                {
                'type' : 'const',
                'vars': [const.var for const in problem.constants()]
                },
                {
                'type' : 'constraint',
                'vars': []
                },
                {
                'type' : 'function',
                'vars' : [func_name for func_name in problem.functions]
                }
         ],
         # TODO: Generalize 'tf' to independent variable for current arc
         'state_list':
             [str(state) for state in problem.states()] +
             [str(costate) for costate in self.costates] +
             ['tf']
         ,
         'parameter_list': [str(param) for param in self.parameter_list],
         'deriv_list':
             ['(tf)*(' + str(sympify(state.process_eqn)) + ')' for state in problem.states()] +
             ['(tf)*(' + str(costate_rate) + ')' for costate_rate in self.costate_rates] +
            #  ['(tf)*((' + str(costate_rate) + ').imag)' for costate_rate in self.costate_rates] +
             ['tf*0']   # TODO: Hardcoded 'tf'
         ,
         'state_rate_list':
            ['(tf)*(' + str(sympify2(state.process_eqn)) + ')' for state in problem.states()]
         ,
         'dae_var_list':
             [str(dae_state) for dae_state in self.dae_states],
         'dae_eom_list':
             ['(tf)*('+str(dae_eom)+')' for dae_eom in self.dae_equations],
         'dae_var_num': len(self.dae_states),
         'num_states': 2*len(problem.states()) + 1,
         'dHdu': [str(dHdu) for dHdu in self.ham_ctrl_partial] + self.mu_lhs,
         'left_bc_list': self.bc_initial + (self.dae_bc if (mode == 'dae') else []),
         'right_bc_list': self.bc_terminal,
         'control_options': [] if (mode == 'dae') else self.control_options,
         'control_list': [str(u) for u in problem.controls()] + [str(mu) for mu in self.mu_vars],
         'num_controls': len(problem.controls()) + len(self.mu_vars),  # Count mu multipliers
         'ham_expr':self.ham,
         # 'contr_dep_ham': [],
         'quantity_list': self.quantity_list,
        #  'dae_mode': mode == 'dae',
        }
    #    problem.constraints[i].expr for i in range(len(problem.constraints))

        # Create problem functions by importing from templates
        self.compiled = imp.new_module('_probobj_'+problem.name)

        if mode == 'dae':
            # self.template_suffix = '_dae' + self.template_suffix
            self.template_suffix = '_dae_num' + self.template_suffix
        if mode == 'num':
            self.template_suffix = '_num' + self.template_suffix

        compile_result = [self.compile_function(self.template_prefix+func+self.template_suffix, verbose=True)
                                        for func in self.compile_list]

        dhdu_fn = self.compiled.get_dhdu_func
        dae_num = len(problem.controls()) + len(self.mu_vars)

        self.bvp = BVP(self.compiled.deriv_func,self.compiled.bc_func,dae_func_gen=dhdu_fn,dae_num_states=dae_num)
        self.bvp.solution.aux['const'] = dict((const.var,const.val) for const in problem.constants())
        self.bvp.solution.aux['parameters'] = self.problem_data['parameter_list']
        self.bvp.solution.aux['function']  = problem.functions

        # TODO: Fix hardcoding of function handle name (may be needed for multivehicle/phases)?
        self.bvp.control_func = self.compiled.compute_control
        self.bvp.problem_data = self.problem_data
        # TODO: ^^ Do same for constraint values
        return self.bvp
    def process_path_constraints(self, problem):
        constraints = problem.constraints().get('path')
        quantity_subs = self.quantity_vars.items()

        path_cost_expr = sympify(problem.cost['path'].expr)
        path_cost_unit = sympify(problem.cost['path'].unit)
        if path_cost_expr == 0:
            logging.debug('No path cost specified, using unit from terminal cost function')
            problem.cost['path'].unit = problem.cost['terminal'].unit
            path_cost_unit = sympify(problem.cost['terminal'].unit)

        logging.debug('Path cost is of unit: '+str(path_cost_unit))
        time_unit = Symbol('s')

        for (ind,c) in enumerate(constraints):
            # Determine order of constraint
            logging.debug('Processing path constraint: '+c.label)
            order = 0
            cq = [sympify(c.expr)]
            dxdt = [sympify(state.process_eqn) for state in problem.states()]

            # Zeroth order constraints have no 'xi' state
            xi_vars = []
            h = []
            while True:
                control_found = False

                for u in problem.controls():
                    if u.sym in cq[-1].subs(quantity_subs).atoms():
                        logging.info('Constraint is of order '+str(order))
                        control_found = True
                        break
                if control_found:
                    break

                dcdx = [self.derivative(cq[-1], state.sym, self.quantity_vars) for state in problem.states()]

                # Chain rule (Assume there is no explciit time-dependence) to find dcdt
                cq.append(sum(d1*d2 for d1,d2 in zip(dcdx, dxdt)))
                order = order + 1

                # Create the auxiliary state variables
                xi_vars.append(Symbol('xi'+str(ind+1)+str(order)))

            # Create the smoothing control variable
            xi_vars.append(Symbol('ue'+str(ind+1)))

            # TODO: Fix constraint object to accept two limits
            c_limit = sympify(c.limit)
            if c_limit.is_Number:
                # TODO: Allow continuation on constraints
                # Define new hidden constant
                c_limit = sympify('_'+c.label)
                print(c.limit)
                problem.constant(str(c_limit),float(c.limit),c.unit)
                logging.debug('Added constant '+str(c_limit))

            if c.direction == '>':
                c.lbound = c_limit
                c.ubound = -c_limit
            elif c.direction == '<':
                c.ubound = c_limit
                c.lbound = -c_limit
            else:
                raise ValueError('Invalid direction specified for constraint')

            psi = self.get_satfn(xi_vars[0], ubound=c.ubound, lbound=c.lbound, slopeAtZero=1)
            psi_vars = [(Symbol('psi'+str(ind+1)), psi)]

            # Add to quantity list
            self.quantity_vars[Symbol('psi'+str(ind+1))] = psi
            self.quantity_list.append({'name':('psi'+str(ind+1)), 'expr':str(psi)})

            # m-th order constraint needs up to m-th derivative of psi to be defined
            psi_i = psi
            for i in range(order):
                psi_i = diff(psi_i, xi_vars[0])
                # psi_vars.append((Symbol('psi'+str(ind+1)+str(i+1)+'('+str(xi_vars[0])+')'), psi_i))
                psi_vars.append((Symbol('psi'+str(ind+1)+str(i+1)), psi_i))
                self.quantity_vars[Symbol('psi'+str(ind+1)+str(i+1))] =  psi_i
                self.quantity_list.append({'name':('psi'+str(ind+1)+str(i+1)), 'expr':str(psi_i)})

            # psi_vars = psi_vars + []
            psi_var_sub = [(v,k) for k,v in psi_vars]

            # FIXME: Hardcoded h derivatives for now
            # h = [psi_vars[0][0]]
            # h.append(psi_vars[1][0]*xi_vars[1]) # psi'*xi12
            # h.append(psi_vars[2][0]*xi_vars[1] + psi_vars[1][0]*xi_vars[2]) # psi''*xi12 + psi'*xi13
            # psi'''*xi12 + xi13*psi12'' + psi12*xi13 + psi11*ue1
            # h.append(psi_vars[3][0]*xi_vars[1] + 2 * psi_vars[2][0]*xi_vars[2] + psi_vars[1][0]*xi_vars[3] )

            #TODO: Hardcoded 't' as independent variable with unit of 's'
            # c_vals = [80e3, -5000, 9.539074102210087] # third number is vdot at zero approx
            c_vals = np.ones(order)*0.1
            h = [psi_vars[0][1]]
            for i in range(order):
                # Add 'xi' state
                problem.state(str(xi_vars[i]), str(xi_vars[i+1]),'('+c.unit+')/s^('+str(i)+')')
                # Constraint all cq at initial point (forms constraints for xi_ij)
                problem.constraints().initial(str(cq[i] - h[i]),'('+c.unit+')/s^('+str(i)+')')
                # Add to initial guess vector
                problem.guess.start.append(c_vals[i])

                dhdxi = [diff(h[i], xi_v) for xi_v in xi_vars[:-1]]
                dhdt  = sum(d1*d2 for d1,d2 in zip(dhdxi,xi_vars[1:])) # xi11dot = xi12 etc.
                dhdt = dhdt.subs(psi_var_sub)
                h.append(dhdt)

            # Add the smoothing control with the right unit
            ue_unit = sympify('('+c.unit+')/(s^('+str(order)+'))')
            problem.control(str(xi_vars[-1]), str(ue_unit))
            logging.debug('Adding control '+str(xi_vars[-1])+' with unit '+str(ue_unit))

            # Add equality constraint
            cqi_unit = ue_unit*time_unit
            problem.constraints().equality(str(cq[-1] - h[-1]),str(cqi_unit))

            # Add smoothing factor
            eps_const = Symbol('eps_'+c.label)
            eps_unit = (path_cost_unit/ue_unit**2)/time_unit #Unit of integrand
            problem.constant(str(eps_const), 1e-6, str(eps_unit))
            logging.debug('Adding smoothing factor '+str(eps_const)+' with unit '+str(eps_unit))

            # Append new control to path cost
            path_cost_expr = path_cost_expr + eps_const*xi_vars[-1]**2

        logging.debug('Updated path cost is: '+str(path_cost_expr))
        problem.cost['path'].expr = str(path_cost_expr)

        u_constraints = problem.constraints().get('control')

        for (ind,c) in enumerate(u_constraints):
            w_i = sympify('uw'+str(ind+1))
            psi = self.get_satfn(w_i, ubound=sympify(c.ubound), lbound = sympify(c.lbound))

            # Add the smoothing control
            problem.control(str(w_i), c.unit)

            # Add equality constraint
            csym = sympify(c.expr)
            problem.constraints().equality(str(csym - psi),c.unit)

            uw_unit = symipfy2(c.unit)
            eps_const = Symbol('eps_'+str(ind+1))
            eps_unit = (path_cost_unit/uw_unit**2)/time_unit #Unit of integrand
            problem.constant(str(eps_const), 1, str(eps_unit))
Пример #29
0
    def __init__(self, problem_data):

        self.raw = problem_data

        # Unpack and sympify problem data
        # self.t = sympify(problem_data['independent'])
        self.x = sympify(problem_data['states'])
        self.u = sympify(problem_data['controls'])
        self.p_d = sympify(problem_data['dynamical_parameters'])
        self.k = sympify(problem_data['constants'])
        self.q = sympify(problem_data['quads'])
        self.p_n = sympify(problem_data['nondynamical_parameters'])
        self.p = self.p_d + self.p_n  # TODO: The parameter usage is inconsistent. Should clean-up down the line

        self.x_dot = sympify(problem_data['states_rates'])
        self.q_dot = sympify(problem_data['quads_rates'])
        self.ham = sympify(problem_data['hamiltonian'])
        self.bc_0 = sympify(problem_data['bc_initial'])
        self.bc_f = sympify(problem_data['bc_terminal'])

        self.dhdu = sympify(problem_data['dHdu'])
        self.initial_cost = sympify(problem_data['initial_cost'])
        self.path_cost = sympify(problem_data['path_cost'])
        self.terminal_cost = sympify(problem_data['terminal_cost'])

        self.df_dy = sympify(problem_data['states_jac'][0])
        self.df_dp = sympify(problem_data['states_jac'][1])

        self.dbc_0_dy = sympify(problem_data['bc_initial_jac'])
        self.dbc_f_dy = sympify(problem_data['bc_terminal_jac'])
        self.dbc_0_dp = sympify(problem_data['bc_initial_parameter_jac'])
        self.dbc_f_dp = sympify(problem_data['bc_terminal_parameter_jac'])

        self.path_constraints = problem_data['path_constraints']

        control_options = problem_data['control_options']
        if control_options is None:
            self.algebraic_control_options = []
        else:
            self.algebraic_control_options = \
                [[sympify(option[str(u_i)]) for u_i in self.u] for option in control_options]

        # self.name = problem_data['problem_name']
        self.name = None
Пример #30
0
def ocp_to_bvp(ocp, guess):
    ws = init_workspace(ocp, guess)
    problem_name = ws['problem_name']
    independent_variable = ws['indep_var']
    states = ws['states']
    controls = ws['controls']
    constants = ws['constants']
    constants_of_motion = ws['constants_of_motion']
    constraints = ws['constraints']
    quantities = ws['quantities']
    initial_cost = ws['initial_cost']
    terminal_cost = ws['terminal_cost']
    path_cost = ws['path_cost']
    quantity_vars, quantity_list, derivative_fn = process_quantities(
        quantities)
    augmented_initial_cost = make_augmented_cost(initial_cost,
                                                 constraints,
                                                 location='initial')
    initial_lm_params = make_augmented_params(constraints, location='initial')
    augmented_terminal_cost = make_augmented_cost(terminal_cost,
                                                  constraints,
                                                  location='terminal')
    terminal_lm_params = make_augmented_params(constraints,
                                               location='terminal')
    hamiltonian, costates = make_ham_lamdot(states, path_cost, derivative_fn)
    bc_initial = make_boundary_conditions(constraints,
                                          states,
                                          costates,
                                          augmented_initial_cost,
                                          derivative_fn,
                                          location='initial')
    bc_terminal = make_boundary_conditions(constraints,
                                           states,
                                           costates,
                                           augmented_terminal_cost,
                                           derivative_fn,
                                           location='terminal')
    bc_terminal = make_time_bc(constraints, bc_terminal)
    dHdu = make_dhdu(hamiltonian, controls, derivative_fn)
    nond_parameters = initial_lm_params + terminal_lm_params
    control_law = make_control_law(dHdu, controls)
    # Generate the problem data
    tf_var = sympify('tf')
    out = ws
    out['costates'] = costates
    out['initial_lm_params'] = initial_lm_params
    out['terminal_lm_params'] = terminal_lm_params
    out['problem_data'] = {
        'method':
        'brysonho',
        'problem_name':
        problem_name,
        'aux_list': [{
            'type': 'const',
            'vars': [str(k) for k in constants]
        }],
        'state_list': [str(x) for x in it.chain(states, costates)],
        'deriv_list':
        [tf_var * state.eom
         for state in states] + [tf_var * costate.eom for costate in costates],
        'states':
        states,
        'costates':
        costates,
        'constants':
        constants,
        'constants_of_motion':
        constants_of_motion,
        'dynamical_parameters': [tf_var],
        'nondynamical_parameters':
        nond_parameters,
        'control_list': [str(x) for x in it.chain(controls)],
        'controls':
        controls,
        'quantity_vars':
        quantity_vars,
        'hamiltonian':
        hamiltonian,
        'num_states':
        2 * len(states),
        'num_params':
        len(nond_parameters) + 1,
        'dHdu':
        dHdu,
        'bc_initial': [_ for _ in bc_initial],
        'bc_terminal': [_ for _ in bc_terminal],
        'control_options':
        control_law,
        'num_controls':
        len(controls),
        'quantity_list':
        quantity_list,
        'nOdes':
        2 * len(states)
    }
    return out