"""
|
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()
|