处理 Dymos 运动方程中的奇点的最佳方法?

Best way to handle singularaties in Dymos equations of motion?

大家好,我正在使用 Dymos 进行火箭弹道优化。我模型的运动方程适用于球形非旋转地球

其中 v 是火箭的速度,gamma 是与当地水平面的飞行路径角度,h 是表面上方的高度,theta 是沿轨迹大圆的角度,m 是质量,epsilon 是攻角,T是信任,D 是阻力,L 是升力。这些与 Dymos 水火箭示例中的运动方程非常相似。我使用了 check_partials,我的解析导数似乎是正确的。

如您所见,伽马的变化率除以 v,因此在轨迹开始处有一个奇点,其中 v=0。您可以通过以某个较小的数字开始 v 来解决这个问题,该数字是预期最终速度的一小部分 - 例如轨道火箭的 1 到 m/s。

但是,我设置的初始速度越低,我就越难看到优化器和网格细化需要工作才能获得收敛的解决方案。例如,

设置 h=200 km,v= 7668 m/s 和 gamma=0(圆形轨道)的最终条件,v initial = 10 m/s,我必须从 20 段开始6阶才能达到收敛,最终的网格是这样的

段数 = 25 段结束 = [-1.0, -0.9833, -0.9667, -0.95, -0.9333, -0.9, -0.85, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0 , 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] 段顺序 = [9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6]

此聚合网格的优化时间为 45 秒

如果我使用 v_initial = 1000 m/s,相同的最终条件,以及 20 个 3 阶段的起始网格,最终网格是

段结束 = [-1.0, -0.95, -0.9, -0.8, -0.7, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0] 段顺序 = [6, 6, 6, 6, 6, 3, 3, 3, 3, 3, 3, 3, 3]

收敛网格的优化时间为 3.18 秒。

考虑到网格点集中到轨迹起点的方式,我认为由 v 引起的 gamma_dot 中的奇异性是问题所在。

我可以做些什么来提高性能?

import matplotlib.pyplot as plt

import openmdao.api as om
from openmdao.utils.assert_utils import assert_near_equal

import dymos as dm
import numpy as np

from dymos.examples.plotting import plot_results

import sys

from rocket_ode import rocketODE

#
# Instantiate the problem and configure the optimization driver
#
p = om.Problem(model=om.Group())

p.driver = om.pyOptSparseDriver()
p.driver.options['optimizer'] = 'IPOPT'
p.driver.declare_coloring()

h_target = 200E3
v_circ = (3.986004418E14/(h_target + 6378E3))**0.5

#
# Instantiate the trajectory and phase
#
traj = dm.Trajectory()


seg_ends = [-1.0, -0.9833, -0.9667, -0.95, -0.9333, -0.9, -0.85, -0.8, -0.7, -0.6, -0.5, -0.4,  -0.3, -0.2, -0.1, 0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]
seg_order = [9, 9, 9, 9, 9, 9, 9, 9, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6]
phase = dm.Phase(ode_class=rocketODE,
                   transcription=dm.Radau(num_segments=25, order=seg_order,
                   segment_ends=seg_ends, compressed=False))



traj.add_phase('phase0', phase)

p.model.add_subsystem('traj', traj)

#
# Set the options on the optimization variables
# Note the use of explicit state units here since much of the ODE uses imperial units
# and we prefer to solve this problem using metric units.
# #
phase.set_time_options(fix_initial=True, duration_bounds=(50, 1000))

# phase.set_time_options(fix_initial=True, fix_duration=True, duration_ref=100.0)

phase.add_state('v', fix_initial=True, lower=10.0, units='m/s',
                ref=1.0E3, targets=['eom.v'], rate_source='eom.v_dot')

phase.add_state('h', fix_initial=True, lower=0, upper=1.0E6, units='m',
                ref=1.0E5, targets=['eom.h'], rate_source='eom.h_dot')

phase.add_state('gamma', fix_initial=True, lower=0, upper=np.pi, units='rad',
                ref=1.0, targets=['eom.gamma'], rate_source='eom.gamma_dot')

phase.add_state('lambda', fix_initial=True, lower=0., upper=np.pi, units='rad',
                ref=0.01, targets=['eom.lambda'], rate_source='eom.lambda_dot')

phase.add_state('alpha', fix_initial=True, lower=-np.pi / 2, upper=np.pi / 2, units='rad',
                ref=1, defect_ref=1, targets=['eom.alpha'],
                rate_source='eom.alpha_dot')


phase.add_state('m', fix_initial=True, lower=100.0, upper=1.0E5, units='kg',
                ref=1.0E4, targets=['eom.m'], rate_source='eom.m_dot')


phase.add_control('alpha_rate', fix_initial=True, units='rad/s', lower=-0.1, upper=0.1, scaler=1.0, targets=['eom.alpha_rate'])

# phase.add_control('alpha', fix_initial=True, units='deg', lower=-89.0, upper=89.0, scaler=1.0, targets=['eom.alpha'])
# phase.add_control('thrust', units='N', lower=0, upper=1.35E5, ref=1E5, targets=['eom.thrust'])


phase.add_parameter('Isp', val=500.0, units='s', opt=False, targets=['eom.Isp'])
phase.add_parameter('thrust', val=1.3E5, opt=False, targets=['eom.thrust'], units='N')

phase.add_boundary_constraint('h', loc='final', ref=1.0E5, equals=h_target)
phase.add_boundary_constraint('v', loc='final', ref=1.0E3, equals=v_circ)
phase.add_boundary_constraint('gamma', loc='final', equals=0.0, units='rad')


# Minimize time at the end of the phase
phase.add_objective('time', loc='final', scaler=1.0)
p.model.linear_solver = om.DirectSolver()

#
# Setup the problem and set the initial guess
#
p.setup(check=True)

p['traj.phase0.t_initial'] = 0.0
p['traj.phase0.t_duration'] = 300
p['traj.phase0.states:v'] = phase.interpolate(ys=[10.0, v_circ], nodes='state_input')
p['traj.phase0.states:h'] = phase.interpolate(ys=[0.0, h_target], nodes='state_input')
p['traj.phase0.states:gamma'] = phase.interpolate(ys=[np.pi/2, 0.0], nodes='state_input')
p['traj.phase0.states:lambda'] = phase.interpolate(ys=[0.0, 0.02], nodes='state_input')
p['traj.phase0.states:m'] = phase.interpolate(ys=[10000.0, 1000.0], nodes='state_input')
p['traj.phase0.states:alpha'] = phase.interpolate(ys=[0.0, 0.0], nodes='state_input')

p['traj.phase0.controls:alpha_rate'] = phase.interpolate(ys=[0.0, 0.0], nodes='control_input')


# Solve for the optimal trajectory
#
# dm.run_problem(p,refine_method='hp',refine_iteration_limit=20)
dm.run_problem(p)
#
# Get the explicitly simulated solution and plot the results
#
exp_out = traj.simulate(times_per_seg=100,method='RK45')

这是 EOM 代码

import numpy as np

import openmdao.api as om


class rocketEOM(om.ExplicitComponent):

    def initialize(self):
        self.options.declare('num_nodes', types=int)

        self.options.declare('CD', types=float, default=0.5,
                             desc='coefficient of drag')

        self.options.declare('S', types=float, default=7.069,
                             desc='aerodynamic reference area (m**2)')

        self.options.declare('r0', types=float, default=6.3781E6,
                             desc='Earth radius m')

        self.options.declare('g0', types=float, default=9.80665,
                             desc='acceleration at earth surface')

        self.options.declare('omega', types=float, default=7.292115E-5,
                             desc='rate of Earth rotation, rad/s')

    def setup(self):
        nn = self.options['num_nodes']

        # Inputs
        self.add_input('v',
                       val=np.zeros(nn),
                       desc='relative velocity',
                       units='m/s')

        self.add_input('h',
                       val=np.zeros(nn),
                       desc='altitude',
                       units='m')

        self.add_input('gamma',
                       val=np.zeros(nn),
                       desc='flight path angle',
                       units='rad')

        self.add_input('lambda',
                       val=np.zeros(nn),
                       desc='longitude',
                       units='rad')

        self.add_input('alpha',
                       val=np.zeros(nn),
                       desc='angle of attack',
                       units='rad')

        self.add_input('alpha_rate',
                       val=np.zeros(nn),
                       desc='angle of attack rate',
                       units='rad/s')

        self.add_input('m',
                       val=np.zeros(nn),
                       desc='mass',
                       units='kg')

        self.add_input('rho',
                       val=np.zeros(nn),
                       desc='density',
                       units='kg/m**3')

        self.add_input('thrust',
                       val=1.4E5 * np.ones(nn),
                       desc='thrust',
                       units='N')

        self.add_input('Isp',
                       val=400.0 * np.ones(nn),
                       desc='specific impulse',
                       units='s')

        # Outputs
        self.add_output('v_dot',
                        val=np.zeros(nn),
                        desc='acceleraton',
                        units='m/s**2')

        self.add_output('gamma_dot',
                        val=np.zeros(nn),
                        desc='flight path angle rate of change',
                        units='rad/s')

        self.add_output('h_dot',
                        val=np.zeros(nn),
                        desc='height rate of change',
                        units='m/s')

        self.add_output('lambda_dot',
                        val=np.zeros(nn),
                        desc='longitude rate of change',
                        units='rad/s')

        self.add_output('alpha_dot',
                        val=np.zeros(nn),
                        desc='angle of attack rate of change',
                        units='rad/s')

        self.add_output('m_dot',
                        val=np.zeros(nn),
                        desc='mass rate of change',
                        units='kg/s')

        # Setup partials
        ar = np.arange(self.options['num_nodes'])

        self.declare_partials(of='v_dot', wrt='h', rows=ar, cols=ar)
        self.declare_partials(of='v_dot', wrt='gamma', rows=ar, cols=ar)
        self.declare_partials(of='v_dot', wrt='m', rows=ar, cols=ar)
        self.declare_partials(of='v_dot', wrt='alpha', rows=ar, cols=ar)
        self.declare_partials(of='v_dot', wrt='thrust', rows=ar, cols=ar)

        self.declare_partials(of='gamma_dot', wrt='v', rows=ar, cols=ar)
        self.declare_partials(of='gamma_dot', wrt='h', rows=ar, cols=ar)
        self.declare_partials(of='gamma_dot', wrt='gamma', rows=ar, cols=ar)
        self.declare_partials(of='gamma_dot', wrt='m', rows=ar, cols=ar)
        self.declare_partials(of='gamma_dot', wrt='alpha', rows=ar, cols=ar)
        self.declare_partials(of='gamma_dot', wrt='thrust', rows=ar, cols=ar)

        self.declare_partials(of='h_dot', wrt='v', rows=ar, cols=ar)
        self.declare_partials(of='h_dot', wrt='gamma', rows=ar, cols=ar)

        self.declare_partials(of='lambda_dot', wrt='v', rows=ar, cols=ar)
        self.declare_partials(of='lambda_dot', wrt='h', rows=ar, cols=ar)
        self.declare_partials(of='lambda_dot', wrt='gamma', rows=ar, cols=ar)

        self.declare_partials(of='m_dot', wrt='Isp', rows=ar, cols=ar)
        self.declare_partials(of='m_dot', wrt='thrust', rows=ar, cols=ar)

        self.declare_partials(of='alpha_dot', wrt='alpha_rate', rows=ar, cols=ar, val=1.0)

    def compute(self, inputs, outputs):

        alpha = inputs['alpha']
        v = inputs['v']
        h = inputs['h']
        gamma = inputs['gamma']
        m = inputs['m']
        T = inputs['thrust']
        ISP = inputs['Isp']

        g = self.g_func(h)
        r0 = self.options['r0']

        D = 0  # calc drag here
        L = 0  # calc lift here
        g = self.g_func(h)
        r = r0 + h

        sin_gamma = np.sin(gamma)
        cos_gamma = np.cos(gamma)

        v_dot = (T * np.cos(alpha) - D) / m - g * sin_gamma

        gamma_dot = ((T * np.sin(alpha) + L) / m - (g - v**2 / r) * cos_gamma) / v

        h_dot = v * sin_gamma

        lambda_dot = v * cos_gamma / r

        m_dot = -T / (9.80665 * ISP)

        outputs['v_dot'] = v_dot
        outputs['gamma_dot'] = gamma_dot
        outputs['h_dot'] = h_dot
        outputs['lambda_dot'] = lambda_dot
        outputs['m_dot'] = m_dot
        outputs['alpha_dot'] = inputs['alpha_rate']

    def g_func(self, h):
        r0 = self.options['r0']
        g0 = self.options['g0']
        return g0 * (r0 / (r0 + h))**2

    def compute_partials(self, inputs, jacobian):

        r0 = self.options['r0']
        g0 = self.options['g0']

        alpha = inputs['alpha']
        v = inputs['v']
        h = inputs['h']
        gamma = inputs['gamma']
        m = inputs['m']
        T = inputs['thrust']
        ISP = inputs['Isp']

        g = self.g_func(h)
        r0 = self.options['r0']

        D = 0  # calc drag here
        L = 0  # calc lift here
        # g = self.g_func(h)
        r = r0 + h

        sin_gamma = np.sin(gamma)
        cos_gamma = np.cos(gamma)
        sin_alpha = np.sin(alpha)
        cos_alpha = np.cos(alpha)

        jacobian['v_dot', 'h'] = 2 * g0 * r0**2 * sin_gamma / r**3

        jacobian['v_dot', 'gamma'] = -g0 * r0**2 * cos_gamma / r**2

        jacobian['v_dot', 'm'] = (D - T * cos_alpha) / m**2

        jacobian['v_dot', 'alpha'] = -T * sin_alpha / m

        jacobian['v_dot', 'thrust'] = cos_alpha / m

        jacobian['gamma_dot', 'v'] = cos_gamma * (g / v**2 + 1 / r) + (L - T * sin_alpha) / (m * v**2)

        jacobian['gamma_dot', 'h'] = (2 * g / (r * v) - v / r**2) * cos_gamma

        jacobian['gamma_dot', 'gamma'] = ((g0 * r0**2 - v**2 * r) * sin_gamma) / (r**2 * v)

        jacobian['gamma_dot', 'm'] = -(-L + T * sin_alpha) / (m**2 * v)

        jacobian['gamma_dot', 'alpha'] = (T * cos_alpha) / (m * v)

        jacobian['gamma_dot', 'thrust'] = sin_alpha / (m * v)

        jacobian['h_dot', 'v'] = sin_gamma

        jacobian['h_dot', 'gamma'] = v * cos_gamma

        jacobian['lambda_dot', 'v'] = cos_gamma / r

        jacobian['lambda_dot', 'h'] = -v * cos_gamma / r**2

        jacobian['lambda_dot', 'gamma'] = -v * sin_gamma / r

        jacobian['m_dot', 'Isp'] = T / (9.80665 * ISP**2)

        jacobian['m_dot', 'thrust'] = - 1 / (9.80665 * ISP)

我同意这里的奇异性是个问题,您将其设置为一些小的初始值的方法通常是一个很好的方法。

虽然它们通常非常合理,但当 EOM 开始变得奇异时,当前在 Dymos 中的网格细化算法可能会出现问题,我想你在这里看到了。

你还能尝试什么?

-您可以将问题分解为多个阶段。第一阶段将涵盖俯仰开始前的垂直上升。通过为轨迹的这个阶段使用一组非奇异的 EOM,您可以让车辆在过渡到第二阶段之前建立一些速度,使用您当前的 EOM。

-您可以重新表述问题以使用在您的操作条件附近没有奇点的 EOM。飞行路径 EOM 通常良好且良性,但它们在 v=0 时存在奇点(在 3D 情况下 gamma=+/-90 度)。

EOM 的笛卡尔公式,其中车辆惯性位置和速度为 [x, y, vx, vy],将提供仅在行星中心奇异的 EOM。参数化初始状态有点棘手(尤其是在 3D 情况下),因为初始速度是固定在旋转地球上的发射台的初始速度。

您也可以尝试将飞行路径角度作为第一阶段的固定参数,将其固定为 90 度,直到建立足够的速度。在这种情况下,为了获得更好的行为,您将接受动态中的一些小错误。