Files
PX4-Autopilot/src/modules/ekf2/EKF/python/ekf_derivation/derivation.py
T
Mathieu Bresciani 0d6c2c8ce9 EKF2: Error-State Kalman Filter (#22262)
* ekf derivation: change to error state formulation
* ekf2: update auto-generated code for error-state
* ekf2: adjust ekf2 code for error state formulation
* ekf2_tests: adjust unit tests for error-state EKF
* update change indicator for error-state EKF
* ekf2_derivation: allow disabling mag and wind states

---------

Co-authored-by: bresch <[brescianimathieu@gmail.com](mailto:brescianimathieu@gmail.com)>
2023-10-31 10:02:18 -04:00

657 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright (c) 2022-2023 PX4 Development Team
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. Neither the name PX4 nor the names of its contributors may be
used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
File: derivation.py
Description:
"""
import argparse
import symforce
symforce.set_epsilon_to_symbol()
import symforce.symbolic as sf
from symforce import typing as T
from symforce import ops
from symforce.values import Values
from derivation_utils import *
# Initialize parser
parser = argparse.ArgumentParser()
parser.add_argument("--disable_mag", action='store_true', help="disable mag")
parser.add_argument("--disable_wind", action='store_true', help="disable wind")
# Read arguments from command line
args = parser.parse_args()
# The state vector is organized in an ordered dictionary
State = Values(
quat_nominal = sf.Rot3(),
vel = sf.V3(),
pos = sf.V3(),
gyro_bias = sf.V3(),
accel_bias = sf.V3(),
mag_I = sf.V3(),
mag_B = sf.V3(),
wind_vel = sf.V2()
)
if args.disable_mag:
del State["mag_I"]
del State["mag_B"]
if args.disable_wind:
del State["wind_vel"]
class IdxDof():
def __init__(self, idx, dof):
self.idx = idx
self.dof = dof
def BuildTangentStateIndex():
# Build a dictionary that can be used to access elements of vectors
# and matrices defined in the state tangent space (e.g.: P, K and H)
tangent_state_index = {}
idx = 0
for key in State.keys_recursive():
dof = State[key].tangent_dim()
tangent_state_index[key] = IdxDof(idx, dof)
idx += dof
return tangent_state_index
tangent_idx = BuildTangentStateIndex()
class VState(sf.Matrix):
SHAPE = (State.storage_dim(), 1)
class VTangent(sf.Matrix):
SHAPE = (State.tangent_dim(), 1)
class MTangent(sf.Matrix):
SHAPE = (State.tangent_dim(), State.tangent_dim())
def vstate_to_state(v: VState):
state = State.from_storage(v)
q_px4 = state["quat_nominal"].to_storage()
state["quat_nominal"] = sf.Rot3(sf.Quaternion(xyz=sf.V3(q_px4[1], q_px4[2], q_px4[3]), w=q_px4[0]))
return state
def predict_covariance(
state: VState,
P: MTangent,
d_vel: sf.V3,
d_vel_dt: sf.Scalar,
d_vel_var: sf.V3,
d_ang: sf.V3,
d_ang_dt: sf.Scalar,
d_ang_var: sf.Scalar
) -> MTangent:
state = vstate_to_state(state)
g = sf.Symbol("g") # does not appear in the jacobians
state_error = Values(
theta = sf.V3.symbolic("delta_theta"),
vel = sf.V3.symbolic("delta_v"),
pos = sf.V3.symbolic("delta_p"),
gyro_bias = sf.V3.symbolic("delta_w_b"),
accel_bias = sf.V3.symbolic("delta_a_b"),
mag_I = sf.V3.symbolic("mag_I"),
mag_B = sf.V3.symbolic("mag_B"),
wind_vel = sf.V2.symbolic("wind_vel")
)
if args.disable_mag:
del state_error["mag_I"]
del state_error["mag_B"]
if args.disable_wind:
del state_error["wind_vel"]
# True state kinematics
state_t = Values()
for key in state.keys():
if key == "quat_nominal":
# Create true quaternion using small angle approximation of the error rotation
state_t["quat_nominal"] = state["quat_nominal"] * sf.Rot3(sf.Quaternion(xyz=(0.5 * state_error["theta"]), w=1))
else:
state_t[key] = state[key] + state_error[key]
noise = Values(
d_vel = sf.V3.symbolic("a_n"),
d_ang = sf.V3.symbolic("w_n"),
)
input_t = Values(
d_vel = d_vel - state_t["accel_bias"] * d_vel_dt - noise["d_vel"],
d_ang = d_ang - state_t["gyro_bias"] * d_ang_dt - noise["d_ang"]
)
R_t = state_t["quat_nominal"]
state_t_pred = state_t.copy()
state_t_pred["quat_nominal"] = state_t["quat_nominal"] * sf.Rot3(sf.Quaternion(xyz=(0.5 * input_t["d_ang"]), w=1))
state_t_pred["vel"] = state_t["vel"] + R_t * input_t["d_vel"] + sf.V3(0, 0, g) * d_vel_dt
state_t_pred["pos"] = state_t["pos"] + state_t["vel"] * d_vel_dt
# Nominal state kinematics
input = Values(
d_vel = d_vel - state["accel_bias"] * d_vel_dt,
d_ang = d_ang - state["gyro_bias"] * d_ang_dt
)
R = state["quat_nominal"]
state_pred = state.copy()
state_pred["quat_nominal"] = state["quat_nominal"] * sf.Rot3(sf.Quaternion(xyz=(0.5 * input["d_ang"]), w=1))
state_pred["vel"] = state["vel"] + R * input["d_vel"] + sf.V3(0, 0, g) * d_vel_dt
state_pred["pos"] = state["pos"] + state["vel"] * d_vel_dt
# Error state kinematics
state_error_pred = Values()
for key in state_error.keys():
if key == "theta":
delta_q = sf.Quaternion.from_storage(state_pred["quat_nominal"].to_storage()).conj() * sf.Quaternion.from_storage(state_t_pred["quat_nominal"].to_storage())
state_error_pred["theta"] = 2 * sf.V3(delta_q.x, delta_q.y, delta_q.z) # Use small angle approximation to obtain a simpler jacobian
else:
state_error_pred[key] = state_t_pred[key] - state_pred[key]
zero_state_error = {state_error[key]: state_error[key].zero() for key in state_error.keys()}
zero_noise = {noise[key]: noise[key].zero() for key in noise.keys()}
# State propagation jacobian
A = VTangent(state_error_pred.to_storage()).jacobian(state_error).subs(zero_state_error).subs(zero_noise)
G = VTangent(state_error_pred.to_storage()).jacobian(noise).subs(zero_state_error).subs(zero_noise)
# Covariance propagation
var_u = sf.Matrix.diag([d_vel_var[0], d_vel_var[1], d_vel_var[2], d_ang_var, d_ang_var, d_ang_var])
P_new = A * P * A.T + G * var_u * G.T
# Generate the equations for the upper triangular matrix and the diagonal only
# Since the matrix is symmetric, the lower triangle does not need to be derived
# and can simply be copied in the implementation
for index in range(state.tangent_dim()):
for j in range(state.tangent_dim()):
if index > j:
P_new[index,j] = 0
return P_new
def compute_airspeed_innov_and_innov_var(
state: VState,
P: MTangent,
airspeed: sf.Scalar,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, sf.Scalar):
state = vstate_to_state(state)
wind = sf.V3(state["wind_vel"][0], state["wind_vel"][1], 0.0)
vel_rel = state["vel"] - wind
airspeed_pred = vel_rel.norm(epsilon=epsilon)
innov = airspeed_pred - airspeed
H = sf.V1(airspeed_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov, innov_var)
def compute_airspeed_h_and_k(
state: VState,
P: MTangent,
innov_var: sf.Scalar,
epsilon: sf.Scalar
) -> (VTangent, VTangent):
state = vstate_to_state(state)
wind = sf.V3(state["wind_vel"][0], state["wind_vel"][1], 0.0)
vel_rel = state["vel"] - wind
airspeed_pred = vel_rel.norm(epsilon=epsilon)
H = sf.V1(airspeed_pred).jacobian(state)
K = P * H.T / sf.Max(innov_var, epsilon)
return (H.T, K)
def compute_wind_init_and_cov_from_airspeed(
v_local: sf.V3,
heading: sf.Scalar,
airspeed: sf.Scalar,
v_var: sf.V3,
heading_var: sf.Scalar,
sideslip_var: sf.Scalar,
airspeed_var: sf.Scalar,
) -> (sf.V2, sf.M22):
# Initialise wind states assuming horizontal flight
sideslip = sf.Symbol("beta")
wind = sf.V2(v_local[0] - airspeed * sf.cos(heading + sideslip), v_local[1] - airspeed * sf.sin(heading + sideslip))
J = wind.jacobian([v_local[0], v_local[1], heading, sideslip, airspeed])
R = sf.M55()
R[0,0] = v_var[0]
R[1,1] = v_var[1]
R[2,2] = heading_var
R[3,3] = sideslip_var
R[4,4] = airspeed_var
P = J * R * J.T
# Assume zero sideslip
P = P.subs({sideslip: 0.0})
wind = wind.subs({sideslip: 0.0})
return (wind, P)
def predict_sideslip(
state: State,
epsilon: sf.Scalar
) -> (sf.Scalar):
wind = sf.V3(state["wind_vel"][0], state["wind_vel"][1], 0.0)
vel_rel = state["vel"] - wind
relative_wind_body = state["quat_nominal"].inverse() * vel_rel
# Small angle approximation of side slip model
# Protect division by zero using epsilon
sideslip_pred = add_epsilon_sign(relative_wind_body[1] / relative_wind_body[0], relative_wind_body[0], epsilon)
return sideslip_pred
def compute_sideslip_innov_and_innov_var(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, sf.Scalar, sf.Scalar):
state = vstate_to_state(state)
sideslip_pred = predict_sideslip(state, epsilon);
innov = sideslip_pred - 0.0
H = sf.V1(sideslip_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov, innov_var)
def compute_sideslip_h_and_k(
state: VState,
P: MTangent,
innov_var: sf.Scalar,
epsilon: sf.Scalar
) -> (VTangent, VTangent):
state = vstate_to_state(state)
sideslip_pred = predict_sideslip(state, epsilon);
H = sf.V1(sideslip_pred).jacobian(state)
K = P * H.T / sf.Max(innov_var, epsilon)
return (H.T, K)
def predict_mag_body(state) -> sf.V3:
mag_field_earth = state["mag_I"]
mag_bias_body = state["mag_B"]
mag_body = state["quat_nominal"].inverse() * mag_field_earth + mag_bias_body
return mag_body
def compute_mag_innov_innov_var_and_hx(
state: VState,
P: MTangent,
meas: sf.V3,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.V3, sf.V3, VTangent):
state = vstate_to_state(state)
meas_pred = predict_mag_body(state);
innov = meas_pred - meas
innov_var = sf.V3()
Hx = sf.V1(meas_pred[0]).jacobian(state)
innov_var[0] = (Hx * P * Hx.T + R)[0,0]
Hy = sf.V1(meas_pred[1]).jacobian(state)
innov_var[1] = (Hy * P * Hy.T + R)[0,0]
Hz = sf.V1(meas_pred[2]).jacobian(state)
innov_var[2] = (Hz * P * Hz.T + R)[0,0]
return (innov, innov_var, Hx.T)
def compute_mag_y_innov_var_and_h(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
meas_pred = predict_mag_body(state);
H = sf.V1(meas_pred[1]).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov_var, H.T)
def compute_mag_z_innov_var_and_h(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
meas_pred = predict_mag_body(state);
H = sf.V1(meas_pred[2]).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov_var, H.T)
def compute_yaw_321_innov_var_and_h(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
R_to_earth = state["quat_nominal"].to_rotation_matrix()
# Fix the singularity at pi/2 by inserting epsilon
meas_pred = sf.atan2(R_to_earth[1,0], R_to_earth[0,0], epsilon=epsilon)
H = sf.V1(meas_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov_var, H.T)
def compute_yaw_321_innov_var_and_h_alternate(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
R_to_earth = state["quat_nominal"].to_rotation_matrix()
# Alternate form that has a singularity at yaw 0 instead of pi/2
meas_pred = sf.pi/2 - sf.atan2(R_to_earth[0,0], R_to_earth[1,0], epsilon=epsilon)
H = sf.V1(meas_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov_var, H.T)
def compute_yaw_312_innov_var_and_h(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
R_to_earth = state["quat_nominal"].to_rotation_matrix()
# Alternate form to be used when close to pitch +-pi/2
meas_pred = sf.atan2(-R_to_earth[0,1], R_to_earth[1,1], epsilon=epsilon)
H = sf.V1(meas_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov_var, H.T)
def compute_yaw_312_innov_var_and_h_alternate(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
R_to_earth = state["quat_nominal"].to_rotation_matrix()
# Alternate form to be used when close to pitch +-pi/2
meas_pred = sf.pi/2 - sf.atan2(-R_to_earth[1,1], R_to_earth[0,1], epsilon=epsilon)
H = sf.V1(meas_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (innov_var, H.T)
def compute_mag_declination_pred_innov_var_and_h(
state: VState,
P: MTangent,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, sf.Scalar, VTangent):
state = vstate_to_state(state)
meas_pred = sf.atan2(state["mag_I"][1], state["mag_I"][0], epsilon=epsilon)
H = sf.V1(meas_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (meas_pred, innov_var, H.T)
def predict_opt_flow(state, distance, epsilon):
R_to_body = state["quat_nominal"].inverse()
# Calculate earth relative velocity in a non-rotating sensor frame
rel_vel_sensor = R_to_body * state["vel"]
# Divide by range to get predicted angular LOS rates relative to X and Y
# axes. Note these are rates in a non-rotating sensor frame
flow_pred = sf.V2()
flow_pred[0] = rel_vel_sensor[1] / distance
flow_pred[1] = -rel_vel_sensor[0] / distance
flow_pred = add_epsilon_sign(flow_pred, distance, epsilon)
return flow_pred
def compute_flow_xy_innov_var_and_hx(
state: VState,
P: MTangent,
distance: sf.Scalar,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.V2, VTangent):
state = vstate_to_state(state)
meas_pred = predict_opt_flow(state, distance, epsilon);
innov_var = sf.V2()
Hx = sf.V1(meas_pred[0]).jacobian(state)
innov_var[0] = (Hx * P * Hx.T + R)[0,0]
Hy = sf.V1(meas_pred[1]).jacobian(state)
innov_var[1] = (Hy * P * Hy.T + R)[0,0]
return (innov_var, Hx.T)
def compute_flow_y_innov_var_and_h(
state: VState,
P: MTangent,
distance: sf.Scalar,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
meas_pred = predict_opt_flow(state, distance, epsilon);
Hy = sf.V1(meas_pred[1]).jacobian(state)
innov_var = (Hy * P * Hy.T + R)[0,0]
return (innov_var, Hy.T)
def compute_gnss_yaw_pred_innov_var_and_h(
state: VState,
P: MTangent,
antenna_yaw_offset: sf.Scalar,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, sf.Scalar, VTangent):
state = vstate_to_state(state)
R_to_earth = state["quat_nominal"]
# define antenna vector in body frame
ant_vec_bf = sf.V3(sf.cos(antenna_yaw_offset), sf.sin(antenna_yaw_offset), 0)
# rotate into earth frame
ant_vec_ef = R_to_earth * ant_vec_bf
# Calculate the yaw angle from the projection
meas_pred = sf.atan2(ant_vec_ef[1], ant_vec_ef[0], epsilon=epsilon)
H = sf.V1(meas_pred).jacobian(state)
innov_var = (H * P * H.T + R)[0,0]
return (meas_pred, innov_var, H.T)
def predict_drag(
state: State,
rho: sf.Scalar,
cd: sf.Scalar,
cm: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar):
R_to_body = state["quat_nominal"].inverse()
wind = sf.V3(state["wind_vel"][0], state["wind_vel"][1], 0.0)
vel_rel = state["vel"] - wind
vel_rel_body = R_to_body * vel_rel
vel_rel_body_xy = sf.V2(vel_rel_body[0], vel_rel_body[1])
bluff_body_drag = -0.5 * rho * cd * vel_rel_body_xy * vel_rel_body.norm(epsilon=epsilon)
momentum_drag = -cm * vel_rel_body_xy
return bluff_body_drag + momentum_drag
def compute_drag_x_innov_var_and_k(
state: VState,
P: MTangent,
rho: sf.Scalar,
cd: sf.Scalar,
cm: sf.Scalar,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
meas_pred = predict_drag(state, rho, cd, cm, epsilon)
Hx = sf.V1(meas_pred[0]).jacobian(state)
innov_var = (Hx * P * Hx.T + R)[0,0]
Ktotal = P * Hx.T / sf.Max(innov_var, epsilon)
K = VTangent()
K[tangent_idx["wind_vel"].idx: tangent_idx["wind_vel"].idx + tangent_idx["wind_vel"].dof] = Ktotal[tangent_idx["wind_vel"].idx: tangent_idx["wind_vel"].idx + tangent_idx["wind_vel"].dof]
return (innov_var, K)
def compute_drag_y_innov_var_and_k(
state: VState,
P: MTangent,
rho: sf.Scalar,
cd: sf.Scalar,
cm: sf.Scalar,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.Scalar, VTangent):
state = vstate_to_state(state)
meas_pred = predict_drag(state, rho, cd, cm, epsilon)
Hy = sf.V1(meas_pred[1]).jacobian(state)
innov_var = (Hy * P * Hy.T + R)[0,0]
Ktotal = P * Hy.T / sf.Max(innov_var, epsilon)
K = VTangent()
K[tangent_idx["wind_vel"].idx: tangent_idx["wind_vel"].idx + tangent_idx["wind_vel"].dof] = Ktotal[tangent_idx["wind_vel"].idx: tangent_idx["wind_vel"].idx + tangent_idx["wind_vel"].dof]
return (innov_var, K)
def compute_gravity_innov_var_and_k_and_h(
state: VState,
P: MTangent,
meas: sf.V3,
R: sf.Scalar,
epsilon: sf.Scalar
) -> (sf.V3, sf.V3, VTangent, VTangent, VTangent):
state = vstate_to_state(state)
# get transform from earth to body frame
R_to_body = state["quat_nominal"].inverse()
# the innovation is the error between measured acceleration
# and predicted (body frame), assuming no body acceleration
meas_pred = R_to_body * sf.Matrix([0,0,-9.80665])
innov = meas_pred - 9.80665 * meas.normalized(epsilon=epsilon)
# initialize outputs
innov_var = sf.V3()
K = [None] * 3
# calculate observation jacobian (H), kalman gain (K), and innovation variance (S)
# for each axis
for i in range(3):
H = sf.V1(meas_pred[i]).jacobian(state)
innov_var[i] = (H * P * H.T + R)[0,0]
K[i] = P * H.T / innov_var[i]
return (innov, innov_var, K[0], K[1], K[2])
print("Derive EKF2 equations...")
generate_px4_function(predict_covariance, output_names=None)
if not args.disable_mag:
generate_px4_function(compute_mag_declination_pred_innov_var_and_h, output_names=["pred", "innov_var", "H"])
generate_px4_function(compute_mag_innov_innov_var_and_hx, output_names=["innov", "innov_var", "Hx"])
generate_px4_function(compute_mag_y_innov_var_and_h, output_names=["innov_var", "H"])
generate_px4_function(compute_mag_z_innov_var_and_h, output_names=["innov_var", "H"])
if not args.disable_wind:
generate_px4_function(compute_airspeed_h_and_k, output_names=["H", "K"])
generate_px4_function(compute_airspeed_innov_and_innov_var, output_names=["innov", "innov_var"])
generate_px4_function(compute_drag_x_innov_var_and_k, output_names=["innov_var", "K"])
generate_px4_function(compute_drag_y_innov_var_and_k, output_names=["innov_var", "K"])
generate_px4_function(compute_sideslip_h_and_k, output_names=["H", "K"])
generate_px4_function(compute_sideslip_innov_and_innov_var, output_names=["innov", "innov_var"])
generate_px4_function(compute_wind_init_and_cov_from_airspeed, output_names=["wind", "P_wind"])
generate_px4_function(compute_yaw_312_innov_var_and_h, output_names=["innov_var", "H"])
generate_px4_function(compute_yaw_312_innov_var_and_h_alternate, output_names=["innov_var", "H"])
generate_px4_function(compute_yaw_321_innov_var_and_h, output_names=["innov_var", "H"])
generate_px4_function(compute_yaw_321_innov_var_and_h_alternate, output_names=["innov_var", "H"])
generate_px4_function(compute_flow_xy_innov_var_and_hx, output_names=["innov_var", "H"])
generate_px4_function(compute_flow_y_innov_var_and_h, output_names=["innov_var", "H"])
generate_px4_function(compute_gnss_yaw_pred_innov_var_and_h, output_names=["meas_pred", "innov_var", "H"])
generate_px4_function(compute_gravity_innov_var_and_k_and_h, output_names=["innov", "innov_var", "Kx", "Ky", "Kz"])
generate_px4_state(State, tangent_idx)