为什么我在卫星位置上的扩展卡尔曼滤波实现不适合我的测量数据而不是太匹配我的预测

问题描述 投票:0回答:1

我在Python上实现了扩展卡尔曼滤波,以过滤国际空间站在地球上的纬度和经度投影的噪声测量数据。然而,滤波器几乎完全符合数学模型,并且根本没有尝试很好地拟合我的噪声测量数据。尽管改变了我的协方差和噪声矩阵的 P、Q 和 R 值,但在滤波器开始表现不稳定之前,几乎没有差异。

我以相当准确的方式对数学模型进行了建模。与 NASA 的 TLE 数据相比,它产生了不错的结果。因此我认为这不是问题所在。我相信问题在于我的扩展卡尔曼滤波器的实现。我认为我编码的雅可比矩阵也是准确的。我的代码有 7 个状态向量。 3 个坐标中的位置、3 个坐标中的速度和半长轴。在过滤器的每个循环中,我更新这些值并将它们输入到observations_model中,该模型将值更改为纬度和经度坐标。我得到的残差是纬度和经度的,并且不超过(-20, 20)的范围。但无论我如何更改 P、Q 和 R 值,我在附加到问题的最终图表中都看不到很大的差异。图像是测量、模型和滤波器输出中经度与纬度的绘制图我的Python代码:

import numpy as np
import matplotlib.pyplot as plt
from scipy.constants import G, pi
from skyfield.api import load
from datetime import timedelta

#Constants
mu = 3.986e14  # Gravitational parameter (m^3/s^2)
Re = 6378100  # Earth radius in m
Cd = 2.7  # Drag coefficient
A = 1996.91  # Cross-sectional area in m^2 
mass = 460460  # Mass of the ISS in kg
earth_rotation_rate = 7.2921159e-5  # rad/s 

# Orbital Elements
semi_major_axis = 6780000  # in m
eccentricity = 0.001
inclination = np.radians(51.64)
raan = np.radians(60)
true_anomaly = np.radians(0)  # starting point (initial anomaly)

# Dynamic atmospheric density function based on altitude
def atmospheric_density(altitude):
    # Approximate atmospheric density model (kg/m^3)
    altitude = altitude / 1000
    return 1.02e07 * altitude**-7.172

# Calculate drag and update semi-major axis
def calculate_drag(semi_major_axis, velocity, t):
    altitude = semi_major_axis - Re
    rho = atmospheric_density(401900)
    # print(rho)
    drag_acc = -0.5 * Cd * (A / mass) * rho * velocity**2
    # Calculate the change in the semi-major axis using the drag formula
    delta_a_dot = - (2 * (semi_major_axis)**2 / mu) * drag_acc * velocity * t
    
    semi_major_axis_updated = semi_major_axis - delta_a_dot
    return semi_major_axis_updated

def gravitational_acceleration(x, y, z):
    r = np.sqrt(x**2 + y**2 + z**2)
    g_x = -mu * x / r**3
    g_y = -mu * y / r**3
    g_z = -mu * z / r**3
    return np.array([g_x, g_y, g_z])

def state_transition(state, delta_t):
    x, y, z, v_x, v_y, v_z, a = state

    # Define velocity and acceleration at current state
    def derivatives(s):
        x, y, z, v_x, v_y, v_z, _ = s
        accel_gravity = gravitational_acceleration(x, y, z)
        return np.array([
            v_x, v_y, v_z,
            accel_gravity[0], accel_gravity[1], accel_gravity[2],
            0
        ])

    # RK4 calculations
    k1 = derivatives(state)
    k2 = derivatives(state + 0.5 * delta_t * k1)
    k3 = derivatives(state + 0.5 * delta_t * k2)
    k4 = derivatives(state + delta_t * k3)

    # Update state
    new_state = state + (delta_t / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)

    # Update semi-major axis with drag
    velocity = np.sqrt(new_state[3]**2 + new_state[4]**2 + new_state[5]**2)
    new_state[6] = calculate_drag(new_state[6], velocity, delta_t)
    r = np.sqrt(new_state[0]**2 + new_state[1]**2 + new_state[2]**2)
    return new_state

def compute_F_jacobian(state, delta_t):
    x, y, z, v_x, v_y, v_z, a = state  
    rho = atmospheric_density(401900)
    # Initialize the Jacobian F
    F = np.zeros((7, 7))
    
    # Position derivatives
    F[0, 0] = 1
    F[0, 3] = (-mu*delta_t*(-2*x**2 + y**2 + z**2))/(np.sqrt((x**2+y**2+z**2)**5)) 
    F[0, 4] = (3*mu*delta_t*x*y)/(np.sqrt((x**2+y**2+z**2)**5))
    F[0, 5] = (3*mu*delta_t*x*z)/(np.sqrt((x**2+y**2+z**2)**5))
    F[1, 1] = 1
    F[1, 3] = (3*mu*delta_t*x*y)/(np.sqrt((x**2+y**2+z**2)**5))
    F[1, 4] = (-mu*delta_t*(-2*y**2 + x**2 + z**2))/(np.sqrt((x**2+y**2+z**2)**5))  
    F[1, 5] = (3*mu*delta_t*y*z)/(np.sqrt((x**2+y**2+z**2)**5))
    F[2, 2] = 1
    F[2, 3] = (3*mu*delta_t*x*z)/(np.sqrt((x**2+y**2+z**2)**5))
    F[2, 4] = (3*mu*delta_t*y*z)/(np.sqrt((x**2+y**2+z**2)**5))
    F[2, 5] = (-mu*delta_t*(-2*z**2 + x**2 + y**2))/(np.sqrt((x**2+y**2+z**2)**5)) 

    # Velocity derivatives
    F[3, 0] = delta_t
    F[3, 3] = 1 - Cd*(A/mass)*rho*v_x*delta_t
    F[3, 6] = -3*(a**2/mu)*Cd*(A/mass)*rho*(v_x**2+v_y**2+v_z**2)**0.5*v_x*delta_t
    F[4, 1] = delta_t
    F[4, 4] = 1 - Cd*(A/mass)*rho*v_y*delta_t
    F[4, 6] = -3*(a**2/mu)*Cd*(A/mass)*rho*(v_x**2+v_y**2+v_z**2)**0.5*v_y*delta_t
    F[5, 2] = delta_t
    F[5, 5] = 1 - Cd*(A/mass)*rho*v_z*delta_t
    F[5, 6] = -3*(a**2/mu)*Cd*(A/mass)*rho*(v_x**2+v_y**2+v_z**2)**0.5*v_z*delta_t
    
    # Semi-major axis update derivative
    F[6, 6] = 1 - 2*(a/mu)*Cd*(A/mass)*rho*(v_x**2+v_y**2+v_z**2)**1.5*delta_t
    
    return F

def observation_model(state, t):
    x, y, z = state[0], state[1], state[2]
    
    # Calculate radial distance from Earth's center
    # Latitude (in degrees)
    x_geo = (x * np.cos(raan) - y * np.sin(raan)) * np.cos(inclination)
    y_geo = x * np.sin(raan) + y * np.cos(raan)
    z_geo = y * np.sin(inclination)

    lat = np.arcsin(z_geo / np.sqrt(x_geo**2 + y_geo**2 + z_geo**2)) * 180 / np.pi
    lon = np.arctan2(y_geo, x_geo) * 180 / np.pi
    lon -= earth_rotation_rate * t * 180 / np.pi  # convert rad to degrees
    return np.array([lat, lon])

def calculate_H_jacobian(state):
    x, y, z = state[0:3]
    H = np.zeros((2, 7))
    
    H[0, 0] = -z * x / ((x**2 + y**2 + z**2) * np.sqrt(x**2 + y**2))
    H[0, 1] = -z * y / ((x**2 + y**2 + z**2) * np.sqrt(x**2 + y**2))
    H[0, 2] = np.sqrt(x**2 + y**2) / (x**2 + y**2 + z**2)

    H[1, 0] = -y / (x**2 + y**2)
    H[1, 1] = x / (x**2 + y**2)

    return H

# Load TLE data for ISS from CelesTrak
stations_url = 'https://celestrak.com/NORAD/elements/stations.txt'
satellites = load.tle_file(stations_url)
by_name = {sat.name: sat for sat in satellites}

iss = by_name['ISS (ZARYA)']

# Define the timescale and custom time
ts = load.timescale()
t_custom = ts.utc(2024, 11, 9, 12, 54, 0)

times = []
latitudes_meas = []
longitudes_meas = []

# Measure the ISS's position every minute for the next 90 minutes
for i in range(90):
    t2 = t_custom + timedelta(minutes=i)
    geocentric = iss.at(t2)
    subpoint = geocentric.subpoint()

    latitude = subpoint.latitude.degrees
    longitude = subpoint.longitude.degrees

    # Store the data
    times.append(t2.utc_strftime('%Y-%m-%d %H:%M:%S'))
    latitudes_meas.append(latitude)
    longitudes_meas.append(longitude)


noise_std = 2.5  # Noise level (standard deviation
noisy_latitudes = latitudes_meas + np.random.normal(0, noise_std, len(latitudes_meas))
noisy_longitudes = longitudes_meas + np.random.normal(0, noise_std, len(longitudes_meas))

# Initialize a state vector with semi-major axis
initial_state = np.array([
    5429.128332332650 * 1000,  # Initial x (m)
    -975.311960132481 * 1000,            # Initial y (m)
    3957.346410376780 * 1000,            # Initial z (m)
    -1.80166995818100 * 1000,            # Initial x-velocity (m/s)
    6.28473745611627 * 1000,  # Initial y-velocity for circular orbit (m/s)
    3.99918311510884 * 1000,            # Initial z-velocity (m/s)
    6780 * 1000   # Semi-major axis (m)
])
delta_t = 60
time_steps = 90 * 60 // delta_t
results = np.zeros((time_steps, 7))
ttime = 0
ttimeobs = 0
observed_values = []
P = np.diag([2e3, 2e3, 2e3, 1e0, 1e0, 1e0, 2e0])
Q = np.diag([2e3, 3e3, 3e3, 2e0, 3e0, 2e0, 2e0])
R = np.diag([noise_std, noise_std])
current_state = initial_state

for i in range(time_steps) :
    ttimeobs += i
    current_state = state_transition(current_state, delta_t)  # Update state with refined delta_t
    # print(current_state)
    true_measurements = observation_model(current_state, ttimeobs)
    observed_values.append(true_measurements)

lat_modeled = []
long_modeled = []

for i in observed_values:
    lat_modeled.append(i[0])
    long_modeled.append(i[1])

for i in range(time_steps):
    ttime += i
    results[i] = current_state
    # print(results[i])
    # Prediction Stage 
    current_state = state_transition(current_state, delta_t)  # Update state with refined delta_t
    F_jacobian = compute_F_jacobian(current_state, delta_t)
    P = F_jacobian @ P @ np.transpose(F_jacobian) + Q
    # print(P[0])

    # Update Stage
    observation = observation_model(current_state, ttime)
    
    z = np.array([noisy_latitudes[i], noisy_longitudes[i]])
    residual = z - observation
    print(i, residual, observation[1], z[1])
    H_jacobian = calculate_H_jacobian(current_state)
    S = H_jacobian @ P @ np.transpose(H_jacobian) + R
    K = P @ np.transpose(H_jacobian) @ np.linalg.inv(S)
    # print(K)
    current_state = current_state + K @ residual  # Updated state estimate
    P = (np.eye(len(current_state)) - K @ H_jacobian) @ P  # Updated covariance estimate


lat_filtered = []
long_filtered = []
ttime = 0

for i in range(len(results)):
    ttime += i
    observation = observation_model(results[i], ttime)
    lat_filtered.append(observation[0])
    long_filtered.append(observation[1])

#Plotting longitude vs latitude
plt.figure(figsize=(10, 5))
plt.plot(noisy_longitudes, noisy_latitudes, 'o-', color='blue', markersize=5)
plt.plot(long_filtered, lat_filtered, 'o-', color='green', markersize=5)
plt.plot(long_modeled, lat_modeled, 'o-', label='Mathematical Model (with perturbations)', color='red', markersize=5)
plt.title('Longitude vs Latitude')
plt.xlabel('Longitude (degrees)')
plt.ylabel('Latitude (degrees)')
plt.grid()
plt.tight_layout()
plt.show()
python kalman-filter
1个回答
0
投票

在您共享的代码中,初始位置不确定性的标准偏差约为 45 米 (=sqrt(2*10^3))。每个步骤的过程噪声都是相同的数量级。因此,仅从模型来看,滤波器位置的不确定性约为数十或数百米。

您添加的测量噪声在纬度和经度上的标准偏差为 2.5 度。让我们快速计算一下。国际空间站轨道半径约为6778000米。我们可以使用弧长公式求出以米为单位的不确定度:s = r * theta = (6778000) * (2.5 * pi / 180) = 295746。因此,每次测量的不确定度约为数十万米。

当滤波器知道航天器的位置精确到几十米,然后得到的测量结果只能精确到几十万米时,它会做什么?正确的是,它几乎完全忽略测量结果。如果您希望滤波器关注测量结果,那么我建议将测量噪声“大幅”降低。例如,如果您希望测量结果具有 100 米的不确定度,则 0.0008 度的噪声标准偏差大约就是您想要的。

© www.soinside.com 2019 - 2024. All rights reserved.