You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Dymos中施加周期性约束时优化失败的问题排查

波能转换器周期性最优控制建模问题

问题描述

我用Dymos给简化波能转换器做最优控制建模。这个线性模型包含质量、阻尼、刚度项,还有正弦波激励。控制量是每个时间步的任意力,目标是最小化能量(也就是最大化捕获能量),计算方式是控制力乘速度。系统本身应该是周期性的——固定位移x和速度v的初始值与终值时,优化能成功完成。

但参考赛车绕圈示例用traj.link_phases()加周期性约束时,优化失败。我需要加周期性约束的原因是不确定x和v的初始值,系统得根据参数和激励有特定相位偏移。想问下是不是在加周期性约束时漏了什么,或者有没有过约束的问题?

代码实现

import numpy as np
import openmdao.api as om
import dymos as dm
import matplotlib.pyplot as plt

class OscillatorODE(om.ExplicitComponent):
    """
    A Dymos ODE for a damped harmonic oscillator with wave excitation and optimal 
    controls.
    """

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

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

        # Inputs
        self.add_input('x', shape=(nn,), desc='displacement', units='m')
        self.add_input('v', shape=(nn,), desc='velocity', units='m/s')
        self.add_input('k', desc='spring constant', units='N/m')
        self.add_input('c', desc='damping coefficient', units='N*s/m')
        self.add_input('m', desc='mass', units='kg') 
        self.add_input('exc_amp', desc='exc force amplitude', units='N')  
        self.add_input('time',shape=(nn,),desc='time',units='s') # New input for time
        self.add_input("u", shape=(nn,), units="N", desc="control force applied") # control force

        # frequency inputs
        self.add_input('wave_freq', desc='fundamental frequency', units='Hz') 
        #self.add_input('phi', desc='excitation phase offset', units='rad') # add phase offset to optimize

        self.add_output('v_dot', val=np.zeros(nn), desc='rate of change of velocity', units='m/s**2')
        self.add_output("power", shape=(nn,), units="W", desc="power to be integrated")
        self.add_output("x_dot", shape=(nn,), desc="rate of change of displacement", units="m/s") # for integration

        # v_dot wrt vector inputs (diagonal)
        self.declare_partials('v_dot', 'x', rows=ar, cols=ar)
        self.declare_partials('v_dot', 'v', rows=ar, cols=ar)
        self.declare_partials('v_dot', 'u', rows=ar, cols=ar)
        self.declare_partials('v_dot', 'time', rows=ar, cols=ar)

        # power wrt vector inputs (diagonal)
        self.declare_partials('power', 'v', rows=ar, cols=ar)
        self.declare_partials('power', 'u', rows=ar, cols=ar)

        # v_dot wrt scalar-ish inputs (dense (nn x 1) column)
        self.declare_partials('v_dot', 'k')
        self.declare_partials('v_dot', 'c')
        self.declare_partials('v_dot', 'm')
        self.declare_partials('v_dot', 'exc_amp')
        self.declare_partials('v_dot', 'wave_freq')

        self.declare_partials('x_dot', 'v', rows=ar, cols=ar)

        #self.declare_partials('v_dot', 'phi')

    def compute(self, inputs, outputs):
        x = inputs['x']
        v = inputs['v']
        k = inputs['k']
        c = inputs['c']
        m = inputs['m']
        exc_amp = inputs['exc_amp']
        wave_freq = inputs['wave_freq']
        t = inputs['time']
        u = inputs['u']

        f_spring = -k * x
        f_damper = -c * v
        f_control = u

        #phi = inputs['phi']
        w = 2*np.pi*wave_freq
        f_ext = exc_amp*np.cos(w*t)   # or sin, as you intend
        
        #outputs['f_dot'] = -wave_freq*2*np.pi*exc_amp*np.sin(wave_freq*2*np.pi*t) # force is steadily increasing
        outputs['v_dot'] = (f_spring + f_damper + f_ext + f_control) / m # this is the residual
        outputs['power'] = f_control*v # power is control force*velocity
        outputs['x_dot'] = v # for integration
        #outputs['t_check'] = 1
 
    def compute_partials(self, inputs, partials):
        nn = self.options['num_nodes']

        x = inputs['x']
        v = inputs['v']
        k = inputs['k']
        c = inputs['c']
        m = inputs['m']
        exc_amp = inputs['exc_amp']
        wave_freq = inputs['wave_freq']
        t = inputs['time']
        u = inputs['u']

        partials['v_dot', 'x'] = (-k / m) * np.ones(nn)
        partials['v_dot', 'v'] = (-c / m) * np.ones(nn)
        partials['v_dot', 'k'] = -x / m
        partials['v_dot', 'c'] = -v / m
        
        #phi = inputs['phi']
        w = 2*np.pi*wave_freq
        partials['v_dot', 'm'] = -( -k * x - c * v + exc_amp*np.cos(w*t) + u ) / m**2
        partials['v_dot', 'exc_amp'] = np.cos(w*t) / m
        partials['v_dot', 'wave_freq'] = -exc_amp*2*np.pi*t*np.sin(w*t) / m
        partials['v_dot', 'time'] = -exc_amp*w*np.sin(w*t) / m
        partials['v_dot', 'u'] = (1 / m) * np.ones(nn)
        
        partials['power', 'v'] = u
        partials['power', 'u'] = v

        partials['x_dot', 'v'] = np.ones(nn)

wavefreq = 0.1 # Hz
f1 = wavefreq
nfreq = 10
t_vec = np.linspace(0, 1/f1, 2*nfreq, endpoint=True)

# Instantiate an OpenMDAO Problem instance.
prob = om.Problem()

# We need an optimization driver.  To solve this simple problem ScipyOptimizerDriver will work.
prob.driver = om.ScipyOptimizeDriver(maxiter=500)
prob.driver.options['optimizer'] = 'SLSQP'
prob.driver.declare_coloring()

# Instantiate a Dymos Trajectory and add it to the Problem model.
traj = dm.Trajectory()
prob.model.add_subsystem('traj', traj)

# Instantiate a Phase and add it to the Trajectory.
phase = dm.Phase(ode_class=OscillatorODE, transcription=dm.GaussLobatto(num_segments=24,order=3))
traj.add_phase('phase0', phase)

# Tell Dymos that the duration of the phase is bounded.
phase.set_time_options(fix_initial=True, fix_duration=True, targets=['time'])

# Tell Dymos the states to be propagated using the given ODE.
phase.add_state('x', fix_initial=False, fix_final=False, rate_source='x_dot', scaler=1, targets=['x'], units='m')
phase.add_state('v', fix_initial=False, fix_final=False, rate_source='v_dot', targets=['v'], units='m/s')
phase.add_state('energy', fix_initial=True, rate_source='power', ref=10, defect_ref=1, units='J')  # integration of power

phase.add_control('u', fix_initial=False, rate_continuity=False, opt=True, continuity=True, targets=['u'], val=0, lower=-30, upper=30, scaler=1e-1, units='N')

# Enforce periodic states: final = initial
traj.link_phases(phases=['phase0', 'phase0'], vars=['x','v'], locs=('final', 'initial'))

# The spring constant, damping coefficient, and mass are inputs to the system that are
# constant throughout the phase.
phase.add_parameter('k', units='N/m', targets=['k'], static_target=True)
phase.add_parameter('c', units='N*s/m', targets=['c'], static_target=True)
phase.add_parameter('m', units='kg', targets=['m'], static_target=True)
phase.add_parameter('exc_amp', units='N', targets=['exc_amp'], static_target=True)
phase.add_parameter('wave_freq', units='Hz', targets=['wave_freq'], static_target=True)

phase.add_timeseries_output('time', output_name='time')

# Since we're using an optimization driver, an objective is required.  We'll minimize
# the final time in this case.
phase.add_objective('energy', loc='final',scaler=1e-2)
prob.model.linear_solver = om.DirectSolver()

# Setup the OpenMDAO problem
prob.setup()

# Assign values to the times and states
t_final = t_vec[-1]
phase.set_time_val(0.0, t_final) # 1 period
phase.set_state_val('x', vals=[1.0, 1.0], time_vals=[0.0, t_final])
phase.set_state_val('v', vals=[0.0,0.0], time_vals=[0.0, t_final])
phase.set_control_val('u', vals=[10, 10], time_vals=[0.0, t_final])
phase.set_state_val('energy', vals=[0.0,-1000], time_vals=[0.0, t_final]) # the final value is a guess

phase.set_parameter_val('k', 24452)
phase.set_parameter_val('c', 71)
phase.set_parameter_val('m', 876 + 1492)
phase.set_parameter_val('exc_amp', np.abs(23518+45*1j))
phase.set_parameter_val('wave_freq', 0.1)
#phase.set_parameter_val('u', 1.0)
#phase.set_parameter_val('f', 1.0)

# Now we're using the optimization driver to iteratively run the model and vary the
dm.run_problem(prob)

# Perform an explicit simulation of our ODE from the initial conditions.
sim_out = traj.simulate(times_per_seg=10)

# Plot the state values obtained from the phase timeseries objects in the simulation output.
t_sol = prob.get_val('traj.phase0.timeseries.time')
t_sim = sim_out.get_val('traj.phase0.timeseries.time')

问题分析与解决建议

1. 修正周期性约束的添加方式

traj.link_phases()设计用于连接不同相位,单相位的周期性约束更适合用add_boundary_constraint直接绑定同一相位的初终值:

# 替换原有的traj.link_phases代码
phase.add_boundary_constraint('x', loc='final', equals='x', loc_ref='initial')
phase.add_boundary_constraint('v', loc='final', equals='v', loc_ref='initial')

如果需要更灵活的约束,也可以直接给OpenMDAO模型加等式约束:

prob.model.add_constraint('traj.phase0.timeseries.x', equals='traj.phase0.timeseries.x', indices=[-1, 0])
prob.model.add_constraint('traj.phase0.timeseries.v', equals='traj.phase0.timeseries.v', indices=[-1, 0])

2. 优化初始猜测

你当前给x和v的初终值猜测都是相同值,虽然符合周期性,但可能限制优化器的搜索范围。建议先计算无控制时系统的稳态响应,用这个结果作为初始猜测,帮助优化器找到合理的起点。

3. 补充控制量的周期性约束

对于周期性系统,控制量也应该满足周期性(终值等于初值),否则系统无法持续循环运行。添加约束:

phase.add_boundary_constraint('u', loc='final', equals='u', loc_ref='initial')

4. 更换更稳健的优化器

SLSQP对初始猜测和约束条件比较敏感,若条件允许,换用IPOPT优化器会更适合这类带周期性约束的问题:

# 替换原有的driver设置
prob.driver = om.pyOptSparseDriver()
prob.driver.options['optimizer'] = 'IPOPT'
prob.driver.options['print_results'] = True
prob.driver.opt_settings['max_iter'] = 1000
prob.driver.opt_settings['tol'] = 1e-6

5. 检查过约束情况

当前的状态设置中,energy初始值固定、终值作为目标,x和v无固定值,加上周期性约束后自由度是合理的,不存在过约束。但要确保所有约束的缩放系数(scaler/ref)设置合理,避免优化器数值问题。


内容的提问来源于stack exchange,提问作者jtgrasb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.11 12:15:53