yincheng.zhong
8 天以前 b9aca884f88abdda860d5c62be841a9e21ce68a1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
"""
Offline simulation and path visualization
- Run complete trajectory with approach path
- Plot final path and control signals
- Save detailed logs for analysis
"""
import json
import time
from math import cos, sin, pi, hypot, atan2
import numpy as np
import matplotlib
# use non-interactive backend to avoid blocking GUI (run headless)
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mower_controller import MowerController, wrap_angle
from path_planner import plan_approach_path, compute_path_heading, combine_paths
 
 
def simulate_full_path(work_path, mower_start_pos, mower_start_heading, params=None, 
                       approach_method='smooth'):
    """
    Run complete simulation with approach path and return state history.
    
    Args:
        work_path: Planned work path [(x, y), ...]
        mower_start_pos: (x, y) mower starting position
        mower_start_heading: mower starting heading in radians
        params: Controller parameters dict
        approach_method: 'line' or 'smooth'
    """
    # Generate approach path from mower start to work path start
    work_path_heading = compute_path_heading(work_path, 0)
    approach_path = plan_approach_path(mower_start_pos, mower_start_heading,
                                       work_path[0], work_path_heading, 
                                       method=approach_method)
    
    # Combine paths
    combined_path, approach_end_idx = combine_paths(approach_path, work_path)
    
    print(f"Generated approach path: {len(approach_path)} points")
    print(f"Work path: {len(work_path)} points")
    print(f"Combined path: {len(combined_path)} points (approach ends at idx {approach_end_idx})")
    
    # Initialize controller with combined path
    ctrl = MowerController(combined_path, params=params)
    
    # Initial state
    x, y = mower_start_pos
    heading = mower_start_heading
    t = 0.0
    dt = 1.0/74.0  # control period
    
    # History for plotting
    history = {
        't': [0.0],
        'x': [x],
        'y': [y],
        'heading': [heading],
        'forward': [0],
        'turn': [0],
        'approach_end_idx': approach_end_idx,  # Store for visualization
        'approach_path': approach_path,
        'work_path': work_path,
    }
    
    finished = False
    while not finished and t < 300.0:  # timeout after 300s
        # Update controller
        ctrl.update_gps((x, y), heading)
        ctrl.update_imu((0,0,0), (0,0,0))
        
        # Get control signals
        out = ctrl.compute_control(t)
        f_sig = out['forward']
        t_sig = out['turn']
        
        # Convert to physical quantities
        f_mps = (f_sig / 100.0) * ctrl.max_forward_mps
        yawrate = (t_sig / 100.0) * ctrl.max_yawrate
        
        # Update state
        heading = wrap_angle(heading + yawrate * dt)
        x = x + f_mps * cos(heading) * dt
        y = y + f_mps * sin(heading) * dt
        t += dt
        
        # Record
        history['t'].append(t)
        history['x'].append(x)
        history['y'].append(y)
        history['heading'].append(heading)
        history['forward'].append(f_sig)
        history['turn'].append(t_sig)
        
        # Check completion
        if out['info'].get('status') == 'finished':
            finished = True
            print(f'Reached goal in {t:.1f}s')
    
    if not finished:
        print('Timeout!')
    
    return history
 
 
def plot_results(history):
    """Plot path following results with approach and work paths."""
    approach_path = history['approach_path']
    work_path = history['work_path']
    approach_end_idx = history['approach_end_idx']
    
    fig = plt.figure(figsize=(15, 10))
    gs = fig.add_gridspec(3, 2)
    
    # Path plot (larger, spans two rows)
    ax_path = fig.add_subplot(gs[0:2, 0])
    
    # Plot approach path (dashed orange)
    apx = [p[0] for p in approach_path]
    apy = [p[1] for p in approach_path]
    ax_path.plot(apx, apy, '--', color='orange', lw=2, label='Approach Path', alpha=0.7)
    ax_path.scatter(apx, apy, c='orange', s=15, alpha=0.5)
    
    # Plot work path (solid black)
    wpx = [p[0] for p in work_path]
    wpy = [p[1] for p in work_path]
    ax_path.plot(wpx, wpy, '-k', lw=2, label='Work Path')
    ax_path.scatter(wpx, wpy, c='k', s=20)
    
    # Plot actual trajectory
    ax_path.plot(history['x'], history['y'], '-r', lw=1.5, label='Actual', alpha=0.8)
    
    # Add markers
    ax_path.plot(history['x'][0], history['y'][0], 'go', markersize=12, 
                label='Mower Start', zorder=10)
    ax_path.plot(work_path[0][0], work_path[0][1], 'bs', markersize=10,
                label='Work Path Start', zorder=10)
    ax_path.plot(history['x'][-1], history['y'][-1], 'ro', markersize=10,
                label='End', zorder=10)
    
    ax_path.set_aspect('equal')
    ax_path.grid(True, alpha=0.3)
    ax_path.set_xlabel('X (m)')
    ax_path.set_ylabel('Y (m)')
    ax_path.set_title('Path Following Results (Approach + Work)')
    ax_path.legend(loc='best')
    
    # XY position vs time
    ax_xy = fig.add_subplot(gs[0:2, 1])
    ax_xy.plot(history['t'], history['x'], '-b', label='X')
    ax_xy.plot(history['t'], history['y'], '-r', label='Y')
    ax_xy.set_xlabel('Time (s)')
    ax_xy.set_ylabel('Position (m)')
    ax_xy.grid(True)
    ax_xy.legend()
    
    # Control signals
    ax_ctrl = fig.add_subplot(gs[2, :])
    ax_ctrl.plot(history['t'], history['forward'], '-b', label='Forward')
    ax_ctrl.plot(history['t'], history['turn'], '-r', label='Turn')
    ax_ctrl.set_xlabel('Time (s)')
    ax_ctrl.set_ylabel('Control Signal')
    ax_ctrl.grid(True)
    ax_ctrl.legend()
    
    plt.tight_layout()
    plt.savefig('simulation_results.png', dpi=300, bbox_inches='tight')
    plt.show()
 
 
def main():
    # Load planned work path
    try:
        with open('example_path.json', 'r') as f:
            work_path = json.load(f)
    except Exception as e:
        print(f'Failed to load path: {e}')
        return
    
    # Define mower starting position (offset from work path start)
    mower_start_pos = (work_path[0][0] + 3.5, work_path[0][1] + 1.0)
    mower_start_heading = pi / 4  # 45 degrees
    
    print(f"Mower starts at: {mower_start_pos}, heading: {mower_start_heading:.2f} rad")
    print(f"Work path starts at: {work_path[0]}")
    
    # Run simulation with stable high-precision parameters
    params = {
        # === 核心控制增益(修正后) ===
        'k_heading': 2.0,            # 航向增益
        'k_xte': 0.8,                # 横向误差增益:2.0 → 0.8 (大幅降低以防止过度修正)
        'k_heading_d': 0.1,          # 航向微分增益
        'k_heading_i': 0.015,        # 航向积分增益
        'k_xte_i': 0.02,             # 横向积分增益
        
        # === 速度控制 ===
        'base_speed': 0.8,           # 基础速度:0.6 → 0.8
        'max_forward_mps': 1.2,      # 最大速度:0.95 → 1.2
        'min_follow_speed': 0.40,    # 最小速度:0.30 → 0.40
        'max_reverse_mps': 0.25,
        'max_yawrate': pi/2.5,       # 转向速率:pi/3 → pi/2.5 (更快转向)
        
        # === 前视距离 ===
        'lookahead_min': 0.35,       # 最小前视
        'lookahead_max': 1.2,        # 最大前视
        'lookahead_time': 2.0,       # 前视时间系数
        
        # === 误差容忍度 ===
        'max_xte':1.5,              # 最大横向误差:0.8 → 1.0 (增大以减少归一化放大)
        'goal_tolerance': 0.1,       # 目标容忍
        'large_err_threshold': 0.5,  # 大误差阈值
        
        # === 滤波参数 ===
        'yawrate_alpha': 0.85,       # 转向滤波
        
        # === 速度缩放增益 ===
        'curv_gain_scale': 2.0,      # 曲率缩放
        'heading_err_gain_scale': 1.5,  # 航向误差缩放
        'xte_gain_scale': 1.0,       # 横向误差缩放:1.2 → 1.0 (移除额外放大)
        
        # === 终点减速区域 ===
        'slow_zone': 1.5,
        'final_approach_dist': 0.8,
        
        # === 起点阶段 ===
        'start_min_speed': 0.28,
        'start_slow_speed': 0.38,
        
        # === 恢复模式 ===
        'recovery_dist': 2.5,
        'recovery_exit_dist': 0.6,
        'recovery_speed': 0.6,
    }
    
    history = simulate_full_path(work_path, mower_start_pos, mower_start_heading, 
                                 params, approach_method='smooth')
    plot_results(history)
 
 
if __name__ == '__main__':
    main()