aero-agent-roles 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +18 -0
- package/README.md +49 -0
- package/bin/aero-agent-roles.js +127 -0
- package/lib/catalog.js +97 -0
- package/lib/install.js +48 -0
- package/lib/mcp.js +199 -0
- package/manifest.json +516 -0
- package/package.json +44 -0
- package/roles/aerodynamics-engineer/ROLE.md +153 -0
- package/roles/aerodynamics-engineer/SOURCES.md +12 -0
- package/roles/aerodynamics-engineer/cli.py +143 -0
- package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
- package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
- package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
- package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
- package/roles/aircraft-design-engineer/ROLE.md +138 -0
- package/roles/aircraft-design-engineer/SOURCES.md +12 -0
- package/roles/aircraft-design-engineer/cli.py +152 -0
- package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
- package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
- package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
- package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
- package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
- package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
- package/roles/airworthiness-compliance-engineer/cli.py +146 -0
- package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
- package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
- package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
- package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
- package/roles/as9100-quality-auditor/ROLE.md +128 -0
- package/roles/as9100-quality-auditor/SOURCES.md +16 -0
- package/roles/as9100-quality-auditor/cli.py +136 -0
- package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
- package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
- package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
- package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
- package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
- package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
- package/roles/data-bus-avionics-engineer/cli.py +311 -0
- package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
- package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
- package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
- package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
- package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
- package/roles/do160-environmental-engineer/ROLE.md +136 -0
- package/roles/do160-environmental-engineer/SOURCES.md +30 -0
- package/roles/do160-environmental-engineer/cli.py +147 -0
- package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
- package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
- package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
- package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
- package/roles/do178c-cert-engineer/ROLE.md +120 -0
- package/roles/do178c-cert-engineer/SOURCES.md +16 -0
- package/roles/do178c-cert-engineer/cli.py +129 -0
- package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
- package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
- package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
- package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
- package/roles/do254-hardware-engineer/ROLE.md +127 -0
- package/roles/do254-hardware-engineer/SOURCES.md +19 -0
- package/roles/do254-hardware-engineer/cli.py +308 -0
- package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
- package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
- package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
- package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
- package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
- package/roles/engineering-analysis-engineer/ROLE.md +154 -0
- package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
- package/roles/engineering-analysis-engineer/cli.py +151 -0
- package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
- package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
- package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
- package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
- package/roles/flight-management-engineer/ROLE.md +160 -0
- package/roles/flight-management-engineer/SOURCES.md +30 -0
- package/roles/flight-management-engineer/cli.py +258 -0
- package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
- package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
- package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
- package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
- package/roles/flight-mechanics-engineer/ROLE.md +162 -0
- package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
- package/roles/flight-mechanics-engineer/cli.py +149 -0
- package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
- package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
- package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
- package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
- package/roles/flight-test-engineer/ROLE.md +137 -0
- package/roles/flight-test-engineer/SOURCES.md +24 -0
- package/roles/flight-test-engineer/cli.py +135 -0
- package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
- package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
- package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
- package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
- package/roles/flight-test-performance-engineer/ROLE.md +165 -0
- package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
- package/roles/flight-test-performance-engineer/cli.py +319 -0
- package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
- package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
- package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
- package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
- package/roles/gnc-engineer/ROLE.md +141 -0
- package/roles/gnc-engineer/SOURCES.md +35 -0
- package/roles/gnc-engineer/cli.py +141 -0
- package/roles/gnc-engineer/core/gnc_core.py +1074 -0
- package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
- package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
- package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
- package/roles/guidance-engineer/ROLE.md +156 -0
- package/roles/guidance-engineer/SOURCES.md +43 -0
- package/roles/guidance-engineer/cli.py +280 -0
- package/roles/guidance-engineer/core/guidance_core.py +820 -0
- package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
- package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
- package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
- package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
- package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
- package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
- package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
- package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
- package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
- package/roles/ndt-engineer/ROLE.md +157 -0
- package/roles/ndt-engineer/SOURCES.md +18 -0
- package/roles/ndt-engineer/cli.py +316 -0
- package/roles/ndt-engineer/core/ndt_core.py +1590 -0
- package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
- package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
- package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
- package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
- package/roles/numerical-analysis-engineer/ROLE.md +158 -0
- package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
- package/roles/numerical-analysis-engineer/cli.py +414 -0
- package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
- package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
- package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
- package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
- package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
- package/roles/propulsion-engineer/ROLE.md +163 -0
- package/roles/propulsion-engineer/SOURCES.md +15 -0
- package/roles/propulsion-engineer/cli.py +168 -0
- package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
- package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
- package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
- package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
- package/roles/space-systems-engineer/ROLE.md +161 -0
- package/roles/space-systems-engineer/SOURCES.md +49 -0
- package/roles/space-systems-engineer/cli.py +173 -0
- package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
- package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
- package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
- package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
- package/roles/state-estimation-engineer/ROLE.md +154 -0
- package/roles/state-estimation-engineer/SOURCES.md +27 -0
- package/roles/state-estimation-engineer/cli.py +326 -0
- package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
- package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
- package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
- package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
- package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
- package/roles/structures-loads-engineer/ROLE.md +171 -0
- package/roles/structures-loads-engineer/SOURCES.md +44 -0
- package/roles/structures-loads-engineer/cli.py +219 -0
- package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
- package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
- package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
- package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
- package/roles/structures-loads-engineer/tests/test_structures_loads_core.py +261 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""state_estimation_core.py - State Estimation Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given a navigation state estimation item's
|
|
5
|
+
project facts (vehicle, sensor set, sample rates, noise-model inputs) it
|
|
6
|
+
computes the estimator design numbers and BUILDS the Navigation State
|
|
7
|
+
Estimator Design Report content:
|
|
8
|
+
|
|
9
|
+
- complementary-filter attitude channel: Mahony-style explicit
|
|
10
|
+
complementary filter gains (kp, ki), gyro-bias drift anchor |b|*t,
|
|
11
|
+
discrete-integration lag |omega|*dt, innovation tolerance
|
|
12
|
+
(bound leaf: gnc-autonomy/estimation-filtering/complementary-filter)
|
|
13
|
+
- navigation (position/velocity) channel: discrete constant-velocity
|
|
14
|
+
Kalman recursion with the standard discretized process-noise
|
|
15
|
+
covariance Q = q*[[dt^4/4, dt^3/2], [dt^3/2, dt^2]] (bound leaves:
|
|
16
|
+
rts-smoother forward_kalman model, interacting-multiple-model-filter
|
|
17
|
+
kalman recursion)
|
|
18
|
+
- alpha-beta design companion: Benedict-Bordner critical-damping beta
|
|
19
|
+
and Kalata gains from the maneuverability/tracking index
|
|
20
|
+
lambda = sigma_w*dt^2/sigma_v (bound leaf: alpha-beta-filter)
|
|
21
|
+
- nonlinear update design: EKF linearization by central finite
|
|
22
|
+
differences of a bearing/range measurement, innovation S, gain K,
|
|
23
|
+
corrected state, and the NEES consistency metric (bound leaves:
|
|
24
|
+
extended-kalman-filter, unscented-kalman-filter)
|
|
25
|
+
- fallback/verification methods: IMM two-mode bank, SIR particle
|
|
26
|
+
filter effective-sample-size rule ESS < n/2, fixed-interval RTS
|
|
27
|
+
smoothing with covariance-reduction numbers (bound leaves:
|
|
28
|
+
interacting-multiple-model-filter, particle-filter, rts-smoother)
|
|
29
|
+
- observability analysis: discrete observability matrix rank/determinant
|
|
30
|
+
for the measured position channel
|
|
31
|
+
- evidence gates check_* that verify the deliverable
|
|
32
|
+
|
|
33
|
+
Standalone: stdlib only, deterministic, no AeroSkills checkout required.
|
|
34
|
+
Every formula below is grounded in the equations shipped by the bound
|
|
35
|
+
AeroSkills leaf logic files (their *_logic.py) or in the leaf SKILL.md
|
|
36
|
+
documentation; see SOURCES.md. No number is invented.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import math
|
|
41
|
+
import re
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from datetime import date
|
|
44
|
+
|
|
45
|
+
def _today() -> str:
|
|
46
|
+
import os
|
|
47
|
+
from datetime import date
|
|
48
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Domain anchors (from the bound estimation-filtering leaves)
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
# Complementary-filter module defaults documented by the leaf (SKILL.md):
|
|
55
|
+
# proportional gain k_p in 1/s, integral gain k_i in 1/s^2.
|
|
56
|
+
CF_KP_DEFAULT = 2.0 # 1/s
|
|
57
|
+
CF_KI_DEFAULT = 0.4 # 1/s^2
|
|
58
|
+
# Steady-state innovation-norm tolerance (rad) used by the leaf's
|
|
59
|
+
# steady_state_verdict (trailing <=10 steps below tolerance => converged).
|
|
60
|
+
CF_INNOVATION_TOL = 1e-3 # rad
|
|
61
|
+
|
|
62
|
+
# SIR particle-filter degeneracy rule documented by the leaf: resample
|
|
63
|
+
# when the effective sample size drops below half the particle count.
|
|
64
|
+
PARTICLE_ESS_FRACTION = 0.5
|
|
65
|
+
|
|
66
|
+
# Fixed-interval smoothing semantics (leaf rts-smoother): the smoothed
|
|
67
|
+
# covariance must sit at or below the filtered covariance at every step
|
|
68
|
+
# and match it at the final (boundary) step.
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Small list-matrix helpers (stdlib; 2x2/4x4 sized use)
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _vec(n, v):
|
|
76
|
+
return [float(v[i]) for i in range(n)]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def mat_mul(a, b):
|
|
80
|
+
bt = list(zip(*b))
|
|
81
|
+
return [[sum(ra[j] * cb[j] for j in range(len(ra))) for cb in bt]
|
|
82
|
+
for ra in a]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def mat_vec(a, v):
|
|
86
|
+
return [sum(a[i][j] * v[j] for j in range(len(v))) for i in range(len(a))]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def mat_transpose(a):
|
|
90
|
+
return [list(r) for r in zip(*a)]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def mat_add(a, b):
|
|
94
|
+
return [[a[i][j] + b[i][j] for j in range(len(a[0]))]
|
|
95
|
+
for i in range(len(a))]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def mat_sub(a, b):
|
|
99
|
+
return [[a[i][j] - b[i][j] for j in range(len(a[0]))]
|
|
100
|
+
for i in range(len(a))]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def mat_scale(a, s):
|
|
104
|
+
return [[a[i][j] * s for j in range(len(a[0]))] for i in range(len(a))]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def mat_identity(n):
|
|
108
|
+
return [[1.0 if i == j else 0.0 for j in range(n)] for i in range(n)]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def vec_sub(a, b):
|
|
112
|
+
return [a[i] - b[i] for i in range(len(a))]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def vec_dot(a, b):
|
|
116
|
+
return sum(a[i] * b[i] for i in range(len(a)))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def mat_inverse_2x2(a):
|
|
120
|
+
"""Inverse of a 2x2 matrix (RTS-smoother leaf inv_2x2 formula)."""
|
|
121
|
+
det = a[0][0] * a[1][1] - a[0][1] * a[1][0]
|
|
122
|
+
if abs(det) < 1e-300:
|
|
123
|
+
raise ValueError("singular 2x2 matrix")
|
|
124
|
+
return [[a[1][1] / det, -a[0][1] / det],
|
|
125
|
+
[-a[1][0] / det, a[0][0] / det]]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def mat_inverse(m):
|
|
129
|
+
"""Gauss-Jordan inverse (extended-kalman-filter leaf mat_inverse)."""
|
|
130
|
+
n = len(m)
|
|
131
|
+
aug = [row[:] + [1.0 if i == j else 0.0 for j in range(n)]
|
|
132
|
+
for i, row in enumerate(m)]
|
|
133
|
+
for col in range(n):
|
|
134
|
+
pivot = max(range(col, n), key=lambda r: abs(aug[r][col]))
|
|
135
|
+
if abs(aug[pivot][col]) < 1e-300:
|
|
136
|
+
raise ValueError("singular matrix")
|
|
137
|
+
aug[col], aug[pivot] = aug[pivot], aug[col]
|
|
138
|
+
pv = aug[col][col]
|
|
139
|
+
aug[col] = [v / pv for v in aug[col]]
|
|
140
|
+
for r in range(n):
|
|
141
|
+
if r == col:
|
|
142
|
+
continue
|
|
143
|
+
f = aug[r][col]
|
|
144
|
+
if f != 0.0:
|
|
145
|
+
aug[r] = [a - f * b for a, b in zip(aug[r], aug[col])]
|
|
146
|
+
return [row[n:] for row in aug]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Domain rules: alpha-beta gains (alpha-beta-filter leaf formulas)
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def steady_state_beta(alpha: float) -> float:
|
|
154
|
+
"""Benedict-Bordner critical-damping beta = alpha^2/(2 - alpha).
|
|
155
|
+
|
|
156
|
+
Grounded in alpha-beta-filter leaf steady_state_gains() (valid for
|
|
157
|
+
alpha in (0, 2)).
|
|
158
|
+
"""
|
|
159
|
+
aa = float(alpha)
|
|
160
|
+
if not 0.0 < aa < 2.0:
|
|
161
|
+
raise ValueError("smoothing factor alpha must be in (0, 2)")
|
|
162
|
+
return aa * aa / (2.0 - aa)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def kalata_gains(tracking_index: float) -> dict:
|
|
166
|
+
"""Kalata critical-damping (alpha, beta) from the tracking index.
|
|
167
|
+
|
|
168
|
+
lambda = sigma_w*dt^2/sigma_v (ratio of position uncertainty
|
|
169
|
+
accumulated by the process noise over one step to the measurement
|
|
170
|
+
standard deviation). Grounded in the alpha-beta-filter leaf
|
|
171
|
+
gains_from_tracking_index() closed forms.
|
|
172
|
+
"""
|
|
173
|
+
lam = float(tracking_index)
|
|
174
|
+
if lam < 0.0:
|
|
175
|
+
raise ValueError("tracking index must be >= 0")
|
|
176
|
+
if lam == 0.0:
|
|
177
|
+
return {"alpha": 0.0, "beta": 0.0}
|
|
178
|
+
s = math.sqrt(lam * lam + 8.0 * lam)
|
|
179
|
+
alpha = -(lam * lam + 8.0 * lam - (lam + 4.0) * s) / 8.0
|
|
180
|
+
beta = (lam * lam + 4.0 * lam - lam * s) / 4.0
|
|
181
|
+
return {"alpha": alpha, "beta": beta}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def tracking_index_from_noise(sigma_w: float, dt: float,
|
|
185
|
+
sigma_v: float) -> float:
|
|
186
|
+
"""lambda = sigma_w*dt^2/sigma_v (alpha-beta leaf definition)."""
|
|
187
|
+
if sigma_v <= 0.0:
|
|
188
|
+
raise ValueError("measurement sigma must be > 0")
|
|
189
|
+
if dt <= 0.0:
|
|
190
|
+
raise ValueError("dt must be > 0")
|
|
191
|
+
return sigma_w * dt * dt / sigma_v
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
# Domain rules: discrete constant-velocity Kalman recursion
|
|
196
|
+
# (rts-smoother forward_kalman + IMM kalman_predict/kalman_update models)
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def cv_matrices(dt: float, q: float):
|
|
200
|
+
"""(F, Q) for the discrete constant-velocity model.
|
|
201
|
+
|
|
202
|
+
F = [[1, dt], [0, 1]] and the discretized process-noise covariance
|
|
203
|
+
Q = q*[[dt^4/4, dt^3/2], [dt^3/2, dt^2]] - identical to the
|
|
204
|
+
rts-smoother leaf _model_matrices().
|
|
205
|
+
"""
|
|
206
|
+
return ([[1.0, dt], [0.0, 1.0]],
|
|
207
|
+
[[q * dt ** 4 / 4.0, q * dt ** 3 / 2.0],
|
|
208
|
+
[q * dt ** 3 / 2.0, q * dt ** 2]])
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def run_cv_kalman(measurements, dt: float, q: float, r: float,
|
|
212
|
+
x0=None, p0=None) -> list:
|
|
213
|
+
"""Run the linear constant-velocity Kalman recursion (per axis).
|
|
214
|
+
|
|
215
|
+
Every step: predict x_pred = F x, P_pred = F P F^T + Q; update with
|
|
216
|
+
innovation y = z - H x_pred (H = [1, 0]), innovation variance
|
|
217
|
+
S = H P_pred H^T + r, gain K = P_pred H^T / S, and the filtered
|
|
218
|
+
state/covariance (Joseph-form, as the rts-smoother leaf). Returns
|
|
219
|
+
one record per measurement with x_pred, P_pred, innovation, S, gain,
|
|
220
|
+
x_filt, P_filt. Deterministic: the covariance/gain recursion does
|
|
221
|
+
not depend on the measurement values.
|
|
222
|
+
"""
|
|
223
|
+
if dt <= 0.0:
|
|
224
|
+
raise ValueError("dt must be > 0")
|
|
225
|
+
if q < 0.0:
|
|
226
|
+
raise ValueError("process noise intensity q must be >= 0")
|
|
227
|
+
if r <= 0.0:
|
|
228
|
+
raise ValueError("measurement variance r must be > 0")
|
|
229
|
+
if not measurements:
|
|
230
|
+
raise ValueError("measurements must be non-empty")
|
|
231
|
+
f, q_disc = cv_matrices(dt, q)
|
|
232
|
+
x = _vec(2, x0 if x0 is not None else [0.0, 0.0])
|
|
233
|
+
p = [[float(v) for v in row] for row in
|
|
234
|
+
(p0 if p0 is not None else [[100.0, 0.0], [0.0, 25.0]])]
|
|
235
|
+
records = []
|
|
236
|
+
for z in measurements:
|
|
237
|
+
x_pred = mat_vec(f, x)
|
|
238
|
+
p_pred = mat_add(mat_mul(mat_mul(f, p), mat_transpose(f)), q_disc)
|
|
239
|
+
innov = float(z) - x_pred[0]
|
|
240
|
+
s = p_pred[0][0] + r
|
|
241
|
+
gain = [p_pred[0][0] / s, p_pred[1][0] / s]
|
|
242
|
+
x_filt = [x_pred[0] + gain[0] * innov,
|
|
243
|
+
x_pred[1] + gain[1] * innov]
|
|
244
|
+
kh = [[gain[0], 0.0], [gain[1], 0.0]]
|
|
245
|
+
eye_kh = [[1.0 - kh[0][0], -kh[0][1]],
|
|
246
|
+
[-kh[1][0], 1.0 - kh[1][1]]]
|
|
247
|
+
p_filt = mat_add(
|
|
248
|
+
mat_mul(mat_mul(eye_kh, p_pred), mat_transpose(eye_kh)),
|
|
249
|
+
mat_scale(mat_mul([[gain[0]], [gain[1]]],
|
|
250
|
+
[[gain[0], gain[1]]]), r))
|
|
251
|
+
records.append({
|
|
252
|
+
"x_pred": x_pred, "P_pred": p_pred,
|
|
253
|
+
"innovation": innov, "S": s,
|
|
254
|
+
"gain": gain, "x_filt": x_filt, "P_filt": p_filt,
|
|
255
|
+
})
|
|
256
|
+
x = x_filt
|
|
257
|
+
p = p_filt
|
|
258
|
+
return records
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Domain rules: fixed-interval RTS smoothing over the forward records
|
|
263
|
+
# (rts-smoother leaf rts_smooth + smoother_reduction semantics)
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def rts_smooth(records: list) -> dict:
|
|
267
|
+
"""Backward Rauch-Tung-Striebel pass over CV forward records.
|
|
268
|
+
|
|
269
|
+
Smoother gain G_k = P_filt[k] F^T (P_pred[k+1])^-1; smoothed state
|
|
270
|
+
x_s[k] = x_filt[k] + G_k (x_s[k+1] - x_pred[k+1]); smoothed
|
|
271
|
+
covariance P_s[k] = P_filt[k] + G_k (P_s[k+1] - P_pred[k+1]) G_k^T.
|
|
272
|
+
Returns smoothed states/covariances plus the leaf's reduction
|
|
273
|
+
verdict: max_reduction (largest relative drop of the position
|
|
274
|
+
variance), all_reduced, boundary_matches.
|
|
275
|
+
"""
|
|
276
|
+
n = len(records)
|
|
277
|
+
if n < 2:
|
|
278
|
+
raise ValueError("at least two forward records required")
|
|
279
|
+
dt = records[0].get("dt", None)
|
|
280
|
+
f = [[1.0, dt], [0.0, 1.0]] if dt else None
|
|
281
|
+
if f is None:
|
|
282
|
+
raise ValueError("forward records carry no dt")
|
|
283
|
+
f_t = mat_transpose(f)
|
|
284
|
+
x_s = [None] * n
|
|
285
|
+
p_s = [None] * n
|
|
286
|
+
gains = [None] * n
|
|
287
|
+
x_s[n - 1] = list(records[n - 1]["x_filt"])
|
|
288
|
+
p_s[n - 1] = [row[:] for row in records[n - 1]["P_filt"]]
|
|
289
|
+
gains[n - 1] = [[0.0, 0.0], [0.0, 0.0]]
|
|
290
|
+
for k in range(n - 2, -1, -1):
|
|
291
|
+
fwd, nxt = records[k], records[k + 1]
|
|
292
|
+
g = mat_mul(mat_mul(fwd["P_filt"], f_t),
|
|
293
|
+
mat_inverse_2x2(nxt["P_pred"]))
|
|
294
|
+
diff = mat_vec(g, vec_sub(x_s[k + 1], nxt["x_pred"]))
|
|
295
|
+
x_s[k] = vec_add_list(fwd["x_filt"], diff)
|
|
296
|
+
p_corr = mat_sub(p_s[k + 1], nxt["P_pred"])
|
|
297
|
+
p_s[k] = mat_add(fwd["P_filt"],
|
|
298
|
+
mat_mul(mat_mul(g, p_corr), mat_transpose(g)))
|
|
299
|
+
gains[k] = g
|
|
300
|
+
reductions = []
|
|
301
|
+
all_reduced = True
|
|
302
|
+
for k in range(n):
|
|
303
|
+
p_f = records[k]["P_filt"][0][0]
|
|
304
|
+
p_sk = p_s[k][0][0]
|
|
305
|
+
reductions.append((p_f - p_sk) / p_f)
|
|
306
|
+
if p_sk > p_f + 1e-12:
|
|
307
|
+
all_reduced = False
|
|
308
|
+
p_f_last = records[-1]["P_filt"]
|
|
309
|
+
p_s_last = p_s[-1]
|
|
310
|
+
boundary_matches = all(
|
|
311
|
+
abs(p_s_last[i][j] - p_f_last[i][j]) <= 1e-12
|
|
312
|
+
for i in range(2) for j in range(2))
|
|
313
|
+
return {
|
|
314
|
+
"smoothed_states": x_s,
|
|
315
|
+
"smoothed_covs": p_s,
|
|
316
|
+
"gains": gains,
|
|
317
|
+
"max_reduction": max(reductions) if reductions else 0.0,
|
|
318
|
+
"all_reduced": all_reduced,
|
|
319
|
+
"boundary_matches": boundary_matches,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def vec_add_list(a, b):
|
|
324
|
+
return [a[i] + b[i] for i in range(len(a))]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# ---------------------------------------------------------------------------
|
|
328
|
+
# Domain rules: EKF linearization and one nonlinear update
|
|
329
|
+
# (extended-kalman-filter leaf: central-difference Jacobians)
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
|
|
332
|
+
def bearing_range_measurement(x):
|
|
333
|
+
"""Nonlinear measurement [range, bearing] of a 4-state CV target.
|
|
334
|
+
|
|
335
|
+
Same model as the extended-kalman-filter leaf
|
|
336
|
+
bearing_range_measurement(): range = hypot(px, py), bearing =
|
|
337
|
+
atan2(py, px) (rad).
|
|
338
|
+
"""
|
|
339
|
+
px, py = x[0], x[1]
|
|
340
|
+
return [math.hypot(px, py), math.atan2(py, px)]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def numeric_jacobian(f, x, eps: float = 1e-6):
|
|
344
|
+
"""Central-finite-difference Jacobian (leaf jacobian())."""
|
|
345
|
+
n = len(x)
|
|
346
|
+
fx = f(x)
|
|
347
|
+
scalar_out = isinstance(fx, (int, float))
|
|
348
|
+
m = 1 if scalar_out else len(fx)
|
|
349
|
+
j = [[0.0] * n for _ in range(m)]
|
|
350
|
+
for col in range(n):
|
|
351
|
+
xp = list(x)
|
|
352
|
+
xm = list(x)
|
|
353
|
+
xp[col] += eps
|
|
354
|
+
xm[col] -= eps
|
|
355
|
+
fp = f(xp)
|
|
356
|
+
fm = f(xm)
|
|
357
|
+
if scalar_out:
|
|
358
|
+
j[0][col] = (fp - fm) / (2.0 * eps)
|
|
359
|
+
else:
|
|
360
|
+
for row in range(m):
|
|
361
|
+
j[row][col] = (fp[row] - fm[row]) / (2.0 * eps)
|
|
362
|
+
return j
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def ekf_correct(x_pred, p_pred, z, h, r_mat, eps: float = 1e-6) -> dict:
|
|
366
|
+
"""One EKF correction at the predicted state (leaf ekf_update)."""
|
|
367
|
+
h_jac = numeric_jacobian(h, x_pred, eps)
|
|
368
|
+
hx = h(x_pred)
|
|
369
|
+
z = [float(v) for v in z]
|
|
370
|
+
innov = vec_sub(z, hx)
|
|
371
|
+
s = mat_add(mat_mul(mat_mul(h_jac, p_pred), mat_transpose(h_jac)),
|
|
372
|
+
r_mat)
|
|
373
|
+
s_inv = mat_inverse(s)
|
|
374
|
+
k = mat_mul(mat_mul(p_pred, mat_transpose(h_jac)), s_inv)
|
|
375
|
+
x_new = vec_add_list(x_pred, mat_vec(k, innov))
|
|
376
|
+
p_new = mat_mul(mat_sub(mat_identity(len(x_pred)),
|
|
377
|
+
mat_mul(k, h_jac)), p_pred)
|
|
378
|
+
return {"x": x_new, "P": p_new, "y": innov, "S": s, "K": k, "H": h_jac}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
# ---------------------------------------------------------------------------
|
|
382
|
+
# Domain rules: consistency metrics (UKF + particle-filter leaves)
|
|
383
|
+
# ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def nees(x_est, p_est, x_true) -> float:
|
|
386
|
+
"""Normalized estimation error squared (UKF leaf nees()).
|
|
387
|
+
|
|
388
|
+
NEES = (x_est - x_true)^T P^-1 (x_est - x_true); for a consistent
|
|
389
|
+
filter the expected value is the state dimension n.
|
|
390
|
+
"""
|
|
391
|
+
d = vec_sub([float(v) for v in x_est], [float(v) for v in x_true])
|
|
392
|
+
return vec_dot(d, mat_vec(mat_inverse(p_est), d))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def effective_sample_size(weights) -> float:
|
|
396
|
+
"""ESS = (sum w)^2 / sum(w^2) (particle-filter leaf formula)."""
|
|
397
|
+
if not weights:
|
|
398
|
+
raise ValueError("weights must be non-empty")
|
|
399
|
+
total = sum(float(w) for w in weights)
|
|
400
|
+
if total <= 0.0:
|
|
401
|
+
raise ValueError("total weight must be positive")
|
|
402
|
+
sq = sum(w * w for w in weights)
|
|
403
|
+
if sq <= 0.0:
|
|
404
|
+
raise ValueError("sum of squared weights must be positive")
|
|
405
|
+
return total * total / sq
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def steady_state_verdict(innovation_norms, tolerance) -> bool:
|
|
409
|
+
"""Complementary-filter steady-state acceptance verdict.
|
|
410
|
+
|
|
411
|
+
Mirrors the complementary-filter leaf steady_state_verdict(): True
|
|
412
|
+
when the mean of the last min(10, n) per-step innovation norms is
|
|
413
|
+
below the tolerance (rad). Raises ValueError on an empty sequence,
|
|
414
|
+
non-finite norms, or a non-positive tolerance.
|
|
415
|
+
"""
|
|
416
|
+
if not innovation_norms:
|
|
417
|
+
raise ValueError("innovation_norms must be non-empty")
|
|
418
|
+
norms = [float(x) for x in innovation_norms]
|
|
419
|
+
for x in norms:
|
|
420
|
+
if not math.isfinite(x):
|
|
421
|
+
raise ValueError("innovation norms must be finite")
|
|
422
|
+
tol = float(tolerance)
|
|
423
|
+
if tol <= 0.0:
|
|
424
|
+
raise ValueError("tolerance must be > 0")
|
|
425
|
+
window = norms[-min(10, len(norms)):]
|
|
426
|
+
return sum(window) / len(window) < tol
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# ---------------------------------------------------------------------------
|
|
430
|
+
# Domain rules: observability (discrete linear systems practice)
|
|
431
|
+
# ---------------------------------------------------------------------------
|
|
432
|
+
|
|
433
|
+
def observability_rank(dt: float) -> dict:
|
|
434
|
+
"""Discrete observability of the CV pair [x, vx] with H = [1, 0].
|
|
435
|
+
|
|
436
|
+
Observability matrix rows [H; H F] = [[1, 0], [1, dt]]; the
|
|
437
|
+
determinant is dt, so the pair is fully observable (rank 2) for any
|
|
438
|
+
dt > 0: velocity is observable through the dynamics between
|
|
439
|
+
position samples. Returns the determinant, the rank, and the
|
|
440
|
+
verdict.
|
|
441
|
+
"""
|
|
442
|
+
if dt <= 0.0:
|
|
443
|
+
raise ValueError("dt must be > 0")
|
|
444
|
+
det = dt
|
|
445
|
+
rank = 2 if abs(det) > 1e-12 else 1
|
|
446
|
+
return {"det": det, "rank": rank, "observable": rank == 2}
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ---------------------------------------------------------------------------
|
|
450
|
+
# Item facts (inputs; noise-model values are design inputs from the
|
|
451
|
+
# sensor datasheets, never invented rules)
|
|
452
|
+
# ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
@dataclass
|
|
455
|
+
class NavEstimatorItem:
|
|
456
|
+
"""Project facts the role needs to build the design report."""
|
|
457
|
+
item_name: str
|
|
458
|
+
description: str = ""
|
|
459
|
+
estimator_class: str = "attitude (complementary filter) + loosely-coupled GNSS/INS"
|
|
460
|
+
vehicle: str = ""
|
|
461
|
+
intended_use: str = ""
|
|
462
|
+
dt: float = 1.0 # GNSS/nav update interval, s
|
|
463
|
+
horizon_steps: int = 60 # design evaluation horizon
|
|
464
|
+
process_noise_intensity: float = 0.01 # q, m^2/s^3 (accel PSD input)
|
|
465
|
+
meas_variance: float = 4.0 # r, m^2 (GNSS position variance input)
|
|
466
|
+
initial_pos_var: float = 100.0 # P0[0][0], m^2
|
|
467
|
+
initial_vel_var: float = 25.0 # P0[1][1], (m/s)^2
|
|
468
|
+
alpha: float = 0.5 # alpha-beta smoothing factor
|
|
469
|
+
gyro_bias_vec: list = field(default_factory=lambda:
|
|
470
|
+
[0.001, -0.0008, 0.0006]) # rad/s, per axis
|
|
471
|
+
gyro_rate_mag: float = 0.0229 # rad/s (example body-rate magnitude)
|
|
472
|
+
imu_dt: float = 0.01 # IMU/attitude update interval, s
|
|
473
|
+
attitude_horizon_s: float = 100.0 # drift evaluation horizon, s
|
|
474
|
+
cf_kp: float = CF_KP_DEFAULT
|
|
475
|
+
cf_ki: float = CF_KI_DEFAULT
|
|
476
|
+
ekf_op_point: list = field(default_factory=lambda:
|
|
477
|
+
[300.0, 40.0, 12.0, 0.0]) # px,py,vx,vy
|
|
478
|
+
ekf_range_r: float = 9.0 # m^2 (range-aid measurement variance)
|
|
479
|
+
ekf_range_error: float = 3.0 # m (deterministic range error input)
|
|
480
|
+
particle_count: int = 1000
|
|
481
|
+
measurement_drift: float = 0.5 # m per step (example target motion)
|
|
482
|
+
|
|
483
|
+
@property
|
|
484
|
+
def sigma_w(self) -> float:
|
|
485
|
+
return math.sqrt(self.process_noise_intensity)
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def sigma_v(self) -> float:
|
|
489
|
+
return math.sqrt(self.meas_variance)
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def bias_norm(self) -> float:
|
|
493
|
+
return math.sqrt(sum(b * b for b in self.gyro_bias_vec))
|
|
494
|
+
|
|
495
|
+
def tracking_index(self) -> float:
|
|
496
|
+
return tracking_index_from_noise(self.sigma_w, self.dt,
|
|
497
|
+
self.sigma_v)
|
|
498
|
+
|
|
499
|
+
def measurement_sequence(self):
|
|
500
|
+
"""Deterministic ramp measurements (m) for the design run."""
|
|
501
|
+
return [self.measurement_drift * k for k in range(1,
|
|
502
|
+
self.horizon_steps + 1)]
|
|
503
|
+
|
|
504
|
+
def imu_measurement_sequence(self):
|
|
505
|
+
"""Gyro samples: constant rate about the body z axis."""
|
|
506
|
+
return [[0.0, 0.0, self.gyro_rate_mag] for _ in range(
|
|
507
|
+
int(self.attitude_horizon_s / self.imu_dt))]
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
# ---------------------------------------------------------------------------
|
|
511
|
+
# Report builder
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
def build_report(item: NavEstimatorItem) -> dict:
|
|
515
|
+
"""Build the complete Navigation State Estimator Design Report model."""
|
|
516
|
+
dt = item.dt
|
|
517
|
+
# --- navigation channel: linear CV Kalman recursion ---------------
|
|
518
|
+
x0 = [0.0, 0.0]
|
|
519
|
+
p0 = [[item.initial_pos_var, 0.0], [0.0, item.initial_vel_var]]
|
|
520
|
+
records = run_cv_kalman(item.measurement_sequence(), dt,
|
|
521
|
+
item.process_noise_intensity,
|
|
522
|
+
item.meas_variance, x0, p0)
|
|
523
|
+
kf = [{
|
|
524
|
+
"step": k + 1,
|
|
525
|
+
"gain": records[k]["gain"],
|
|
526
|
+
"S": records[k]["S"],
|
|
527
|
+
"innovation": records[k]["innovation"],
|
|
528
|
+
"P_filt": records[k]["P_filt"],
|
|
529
|
+
"x_filt": records[k]["x_filt"],
|
|
530
|
+
} for k in range(len(records))]
|
|
531
|
+
|
|
532
|
+
# --- RTS smoothing over the same run ------------------------------
|
|
533
|
+
recs_dt = [dict(r, dt=dt) for r in records]
|
|
534
|
+
smooth = rts_smooth(recs_dt)
|
|
535
|
+
|
|
536
|
+
# --- alpha-beta design companion ----------------------------------
|
|
537
|
+
beta_bb = steady_state_beta(item.alpha)
|
|
538
|
+
lam = item.tracking_index()
|
|
539
|
+
kal = kalata_gains(lam)
|
|
540
|
+
|
|
541
|
+
# --- EKF nonlinear (range/bearing) update design ------------------
|
|
542
|
+
op = [float(v) for v in item.ekf_op_point]
|
|
543
|
+
h_model = bearing_range_measurement(op) # true measurement
|
|
544
|
+
z = [h_model[0] + item.ekf_range_error, h_model[1]] # deterministic meas
|
|
545
|
+
p_op = [[item.initial_pos_var, 0.0, 0.0, 0.0],
|
|
546
|
+
[0.0, item.initial_pos_var, 0.0, 0.0],
|
|
547
|
+
[0.0, 0.0, item.initial_vel_var, 0.0],
|
|
548
|
+
[0.0, 0.0, 0.0, item.initial_vel_var]]
|
|
549
|
+
ekf = ekf_correct(op, p_op, z, bearing_range_measurement,
|
|
550
|
+
[[item.ekf_range_r, 0.0], [0.0, (math.pi / 180.0) ** 2]])
|
|
551
|
+
ekf_nees = nees(ekf["x"][:2], [[ekf["P"][0][0], ekf["P"][0][1]],
|
|
552
|
+
[ekf["P"][1][0], ekf["P"][1][1]]],
|
|
553
|
+
op[:2])
|
|
554
|
+
|
|
555
|
+
# --- attitude channel (complementary filter design numbers) -------
|
|
556
|
+
bias_norm = item.bias_norm
|
|
557
|
+
drift_100 = bias_norm * item.attitude_horizon_s
|
|
558
|
+
drift_10 = bias_norm * 10.0
|
|
559
|
+
lag = item.gyro_rate_mag * item.imu_dt
|
|
560
|
+
|
|
561
|
+
# --- consistency metrics (SIR rule + NEES) ------------------------
|
|
562
|
+
ess = effective_sample_size([1.0 / item.particle_count]
|
|
563
|
+
* item.particle_count)
|
|
564
|
+
|
|
565
|
+
# --- observability -------------------------------------------------
|
|
566
|
+
obs = observability_rank(dt)
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
"document_type": "Navigation State Estimator Design Report",
|
|
570
|
+
"status": "draft-for-review",
|
|
571
|
+
"item": item.item_name,
|
|
572
|
+
"item_description": item.description,
|
|
573
|
+
"estimator_class": item.estimator_class,
|
|
574
|
+
"vehicle": item.vehicle,
|
|
575
|
+
"intended_use": item.intended_use,
|
|
576
|
+
"design_inputs": {
|
|
577
|
+
"dt_s": dt,
|
|
578
|
+
"horizon_steps": item.horizon_steps,
|
|
579
|
+
"process_noise_intensity": item.process_noise_intensity,
|
|
580
|
+
"meas_variance": item.meas_variance,
|
|
581
|
+
"sigma_w_m_per_s2": round(item.sigma_w, 6),
|
|
582
|
+
"sigma_v_m": round(item.sigma_v, 6),
|
|
583
|
+
"initial_pos_var": item.initial_pos_var,
|
|
584
|
+
"initial_vel_var": item.initial_vel_var,
|
|
585
|
+
"imu_dt_s": item.imu_dt,
|
|
586
|
+
"attitude_horizon_s": item.attitude_horizon_s,
|
|
587
|
+
"particle_count": item.particle_count,
|
|
588
|
+
},
|
|
589
|
+
"attitude": {
|
|
590
|
+
"filter": "Mahony-style explicit complementary filter (SO(3))",
|
|
591
|
+
"kp_1_per_s": item.cf_kp,
|
|
592
|
+
"ki_1_per_s2": item.cf_ki,
|
|
593
|
+
"innovation_tolerance_rad": CF_INNOVATION_TOL,
|
|
594
|
+
"gyro_bias_vec": item.gyro_bias_vec,
|
|
595
|
+
"bias_norm": round(bias_norm, 6),
|
|
596
|
+
"drift_10s_rad": round(drift_10, 6),
|
|
597
|
+
"drift_100s_rad": round(drift_100, 6),
|
|
598
|
+
"drift_100s_deg": round(math.degrees(drift_100), 3),
|
|
599
|
+
"integration_lag_rad_per_step": round(lag, 9),
|
|
600
|
+
"integration_lag_deg_per_step": round(math.degrees(lag), 6),
|
|
601
|
+
"bias_observability_note": (
|
|
602
|
+
"gyro bias is observable when at least two non-parallel "
|
|
603
|
+
"reference vectors (accelerometer + magnetometer) are "
|
|
604
|
+
"fused; a single reference leaves the bias along its "
|
|
605
|
+
"axis unobservable (Mahony et al. 2008 nonlinear "
|
|
606
|
+
"complementary filter on SO(3))"),
|
|
607
|
+
},
|
|
608
|
+
"alpha_beta": {
|
|
609
|
+
"alpha": item.alpha,
|
|
610
|
+
"beta_benedict_bordner": round(beta_bb, 6),
|
|
611
|
+
"tracking_index_lambda": round(lam, 6),
|
|
612
|
+
"kalata_alpha": round(kal["alpha"], 6),
|
|
613
|
+
"kalata_beta": round(kal["beta"], 6),
|
|
614
|
+
},
|
|
615
|
+
"kalman": {
|
|
616
|
+
"model": "discrete constant-velocity, H = [1 0], per axis",
|
|
617
|
+
"F": [[1.0, dt], [0.0, 1.0]],
|
|
618
|
+
"Q_discrete": [[round(item.process_noise_intensity * dt ** 4 / 4.0, 9),
|
|
619
|
+
round(item.process_noise_intensity * dt ** 3 / 2.0, 9)],
|
|
620
|
+
[round(item.process_noise_intensity * dt ** 3 / 2.0, 9),
|
|
621
|
+
round(item.process_noise_intensity * dt ** 2, 9)]],
|
|
622
|
+
"first": {
|
|
623
|
+
"gain": [round(kf[0]["gain"][0], 9), round(kf[0]["gain"][1], 9)],
|
|
624
|
+
"S": round(kf[0]["S"], 9),
|
|
625
|
+
"pos_var_after": round(kf[0]["P_filt"][0][0], 9),
|
|
626
|
+
},
|
|
627
|
+
"mid": {
|
|
628
|
+
"gain": [round(kf[len(kf) // 2]["gain"][0], 9),
|
|
629
|
+
round(kf[len(kf) // 2]["gain"][1], 9)],
|
|
630
|
+
"S": round(kf[len(kf) // 2]["S"], 9),
|
|
631
|
+
"pos_var_after": round(kf[len(kf) // 2]["P_filt"][0][0], 9),
|
|
632
|
+
},
|
|
633
|
+
"final": {
|
|
634
|
+
"step": len(kf),
|
|
635
|
+
"gain": [round(kf[-1]["gain"][0], 9), round(kf[-1]["gain"][1], 9)],
|
|
636
|
+
"S": round(kf[-1]["S"], 9),
|
|
637
|
+
"pos_var_after": round(kf[-1]["P_filt"][0][0], 9),
|
|
638
|
+
"vel_var_after": round(kf[-1]["P_filt"][1][1], 9),
|
|
639
|
+
},
|
|
640
|
+
"residual_rms": round(math.sqrt(
|
|
641
|
+
sum(r["innovation"] ** 2 for r in records) / len(records)), 6),
|
|
642
|
+
},
|
|
643
|
+
"ekf": {
|
|
644
|
+
"measurement_model": "range + bearing (nonlinear)",
|
|
645
|
+
"operating_point": [round(v, 4) for v in op],
|
|
646
|
+
"true_measurement": [round(h_model[0], 4), round(h_model[1], 6)],
|
|
647
|
+
"measurement_z": [round(z[0], 4), round(z[1], 6)],
|
|
648
|
+
"H": [[round(v, 9) for v in row] for row in ekf["H"]],
|
|
649
|
+
"innovation": [round(v, 6) for v in ekf["y"]],
|
|
650
|
+
"S_diag": [round(ekf["S"][0][0], 9), round(ekf["S"][1][1], 9)],
|
|
651
|
+
"gain_row0": [round(v, 9) for v in ekf["K"][0]],
|
|
652
|
+
"x_corrected": [round(v, 6) for v in ekf["x"]],
|
|
653
|
+
"nees_2d": round(ekf_nees, 4),
|
|
654
|
+
"nees_dimension": 2,
|
|
655
|
+
},
|
|
656
|
+
"verification_metrics": {
|
|
657
|
+
"nees_expected_dim": 2,
|
|
658
|
+
"nees_2d": round(ekf_nees, 4),
|
|
659
|
+
"ess_uniform": round(ess, 4),
|
|
660
|
+
"ess_rule": "resample when ESS < n/2 (SIR, particle-filter leaf)",
|
|
661
|
+
"ess_threshold": item.particle_count * PARTICLE_ESS_FRACTION,
|
|
662
|
+
},
|
|
663
|
+
"smoothing": {
|
|
664
|
+
"method": "fixed-interval RTS smoother over stored forward run",
|
|
665
|
+
"max_reduction": round(smooth["max_reduction"], 9),
|
|
666
|
+
"max_reduction_pct": round(100.0 * smooth["max_reduction"], 3),
|
|
667
|
+
"all_reduced": smooth["all_reduced"],
|
|
668
|
+
"boundary_matches": smooth["boundary_matches"],
|
|
669
|
+
"smoothed_final_pos_var": round(
|
|
670
|
+
smooth["smoothed_covs"][-1][0][0], 9),
|
|
671
|
+
},
|
|
672
|
+
"observability": {
|
|
673
|
+
"matrix_rows": [[1.0, 0.0], [1.0, dt]],
|
|
674
|
+
"det": round(obs["det"], 9),
|
|
675
|
+
"rank": obs["rank"],
|
|
676
|
+
"observable": obs["observable"],
|
|
677
|
+
},
|
|
678
|
+
"fallback_methods": {
|
|
679
|
+
"imm": "two-mode CV/CA IMM bank with Markov mode mixing for "
|
|
680
|
+
"maneuvering flight",
|
|
681
|
+
"particle": "SIR bootstrap filter (non-Gaussian fallback), "
|
|
682
|
+
"systematic resampling when ESS < n/2",
|
|
683
|
+
"ukf": "scaled-unscented-transform alternative to EKF "
|
|
684
|
+
"linearization for strongly nonlinear updates",
|
|
685
|
+
"rts": "fixed-interval smoothing for offline trajectory "
|
|
686
|
+
"reconstruction",
|
|
687
|
+
},
|
|
688
|
+
"generated": _today(),
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def render_report_markdown(model: dict) -> str:
|
|
693
|
+
"""Render the report content model as the deliverable markdown."""
|
|
694
|
+
di = model["design_inputs"]
|
|
695
|
+
at = model["attitude"]
|
|
696
|
+
ab = model["alpha_beta"]
|
|
697
|
+
km = model["kalman"]
|
|
698
|
+
ek = model["ekf"]
|
|
699
|
+
vm = model["verification_metrics"]
|
|
700
|
+
sm = model["smoothing"]
|
|
701
|
+
ob = model["observability"]
|
|
702
|
+
lines = [
|
|
703
|
+
"# Navigation State Estimator Design Report",
|
|
704
|
+
"",
|
|
705
|
+
f"**Item:** {model['item']}",
|
|
706
|
+
f"**Estimator class:** {model['estimator_class']}",
|
|
707
|
+
f"**Vehicle / platform:** {model['vehicle']}",
|
|
708
|
+
f"**Intended use:** {model['intended_use']}",
|
|
709
|
+
f"**Status:** {model['status']}",
|
|
710
|
+
"",
|
|
711
|
+
f"## 1. Scope",
|
|
712
|
+
"",
|
|
713
|
+
f"This report produces the navigation state estimator design for "
|
|
714
|
+
f"{model['item']}." +
|
|
715
|
+
(f" {model['item_description']}" if model["item_description"] else "") +
|
|
716
|
+
" The estimator architecture is " + model["estimator_class"] +
|
|
717
|
+
": an attitude channel (complementary filter) plus a "
|
|
718
|
+
"loosely-coupled GNSS position/velocity channel (linear "
|
|
719
|
+
"constant-velocity Kalman recursion with an EKF nonlinear-update "
|
|
720
|
+
"option). Outputs are DRAFT design numbers for a human navigation "
|
|
721
|
+
"engineer to review before implementation on the target computer.",
|
|
722
|
+
"",
|
|
723
|
+
"## 2. Design inputs (noise model)",
|
|
724
|
+
"",
|
|
725
|
+
f"- Navigation update interval dt = {di['dt_s']:g} s over a "
|
|
726
|
+
f"{di['horizon_steps']}-step design horizon.",
|
|
727
|
+
f"- Process noise intensity q = {di['process_noise_intensity']:g} "
|
|
728
|
+
f"m^2/s^3 -> sigma_w = {di['sigma_w_m_per_s2']} m/s^2.",
|
|
729
|
+
f"- Measurement variance r = {di['meas_variance']:g} m^2 (GNSS "
|
|
730
|
+
f"position, sigma_v = {di['sigma_v_m']} m).",
|
|
731
|
+
f"- Initial covariance P0 = diag({di['initial_pos_var']:g} m^2, "
|
|
732
|
+
f"{di['initial_vel_var']:g} (m/s)^2).",
|
|
733
|
+
f"- IMU/attitude rate dt = {di['imu_dt_s']:g} s; attitude design "
|
|
734
|
+
f"horizon {di['attitude_horizon_s']:g} s.",
|
|
735
|
+
f"- Particle fallback count n = {di['particle_count']:g}.",
|
|
736
|
+
"",
|
|
737
|
+
"## 3. Attitude channel (complementary filter)",
|
|
738
|
+
"",
|
|
739
|
+
f"- Filter: {at['filter']}.",
|
|
740
|
+
f"- Gains: k_p = {at['kp_1_per_s']:g} 1/s, k_i = "
|
|
741
|
+
f"{at['ki_1_per_s2']:g} 1/s^2 (leaf module defaults); corrected "
|
|
742
|
+
"rate omega_c = omega_m - b + k_p*e with bias step "
|
|
743
|
+
"b <- b - k_i*e*dt.",
|
|
744
|
+
f"- Gyro bias input b = {at['gyro_bias_vec']} rad/s per axis, "
|
|
745
|
+
f"|b| = {at['bias_norm']} rad/s.",
|
|
746
|
+
f"- Drift anchor: uncompensated bias integrates to |b|*t = "
|
|
747
|
+
f"{at['drift_10s_rad']} rad after 10 s and "
|
|
748
|
+
f"{at['drift_100s_rad']} rad ({at['drift_100s_deg']} deg) after "
|
|
749
|
+
f"{di['attitude_horizon_s']:g} s - the error the bias estimate "
|
|
750
|
+
"removes.",
|
|
751
|
+
f"- Discrete-integration lag bound |omega|*dt = "
|
|
752
|
+
f"{at['integration_lag_rad_per_step']} rad/step "
|
|
753
|
+
f"({at['integration_lag_deg_per_step']} deg/step) at the example "
|
|
754
|
+
"body rate.",
|
|
755
|
+
f"- Steady-state acceptance: trailing-window innovation norm "
|
|
756
|
+
f"below {at['innovation_tolerance_rad']:g} rad "
|
|
757
|
+
"(leaf steady_state_verdict).",
|
|
758
|
+
f"- Observability note: {at['bias_observability_note']}.",
|
|
759
|
+
"",
|
|
760
|
+
"## 4. Navigation channel (Kalman recursion)",
|
|
761
|
+
"",
|
|
762
|
+
f"- Model: {km['model']}.",
|
|
763
|
+
f"- Transition F = {km['F'][0]} / {km['F'][1]}; discretized "
|
|
764
|
+
f"process covariance Q = {km['Q_discrete'][0]} / "
|
|
765
|
+
f"{km['Q_discrete'][1]}.",
|
|
766
|
+
f"- Alpha-beta design companion (fixed-gain alternative for the "
|
|
767
|
+
f"smoothing stage): alpha = {ab['alpha']:g}, "
|
|
768
|
+
f"Benedict-Bordner critical-damping beta = alpha^2/(2-alpha) = "
|
|
769
|
+
f"{ab['beta_benedict_bordner']}; tracking index "
|
|
770
|
+
f"lambda = sigma_w*dt^2/sigma_v = {ab['tracking_index_lambda']} "
|
|
771
|
+
f"gives Kalata gains alpha = {ab['kalata_alpha']}, "
|
|
772
|
+
f"beta = {ab['kalata_beta']}.",
|
|
773
|
+
f"- First update: gain K = {km['first']['gain']}, innovation "
|
|
774
|
+
f"variance S = {km['first']['S']}, position variance after "
|
|
775
|
+
f"{km['first']['pos_var_after']} m^2.",
|
|
776
|
+
f"- Mid-horizon (step {di['horizon_steps'] // 2}): "
|
|
777
|
+
f"gain K = {km['mid']['gain']}, S = {km['mid']['S']}, position "
|
|
778
|
+
f"variance {km['mid']['pos_var_after']} m^2.",
|
|
779
|
+
f"- Final (step {km['final']['step']}): gain K = "
|
|
780
|
+
f"{km['final']['gain']}, S = {km['final']['S']}, position "
|
|
781
|
+
f"variance {km['final']['pos_var_after']} m^2, velocity variance "
|
|
782
|
+
f"variance {km['final']['vel_var_after']} (m/s)^2 - the steady-state "
|
|
783
|
+
"covariance the design reports. The converged gain equals the "
|
|
784
|
+
"Kalata alpha-beta pair above within the recursion tolerance, "
|
|
785
|
+
"confirming the fixed-gain design is a faithful steady-state "
|
|
786
|
+
"approximation of the full recursion.",
|
|
787
|
+
f"- Residual RMS over the deterministic design run: "
|
|
788
|
+
f"{km['residual_rms']} m.",
|
|
789
|
+
"",
|
|
790
|
+
"## 5. Nonlinear update design (EKF linearization)",
|
|
791
|
+
"",
|
|
792
|
+
f"- Measurement model: {ek['measurement_model']} "
|
|
793
|
+
f"h(x) = [hypot(px,py), atan2(py,px)].",
|
|
794
|
+
f"- Operating point x = {ek['operating_point']} "
|
|
795
|
+
f"(true measurement {ek['true_measurement']}).",
|
|
796
|
+
f"- Linearized measurement Jacobian (central differences) "
|
|
797
|
+
f"H = {ek['H'][0]} / {ek['H'][1]}.",
|
|
798
|
+
f"- Deterministic measurement z = {ek['measurement_z']} "
|
|
799
|
+
"(range biased +3 m input).",
|
|
800
|
+
f"- Innovation y = {ek['innovation']}; innovation covariance "
|
|
801
|
+
f"diag(S) = {ek['S_diag']}.",
|
|
802
|
+
f"- Gain first row K = {ek['gain_row0']}.",
|
|
803
|
+
f"- Corrected state x+ = {ek['x_corrected']}.",
|
|
804
|
+
"",
|
|
805
|
+
"## 6. Consistency and fallback verification",
|
|
806
|
+
"",
|
|
807
|
+
f"- NEES = {ek['nees_2d']} against the {ek['nees_dimension']}-state "
|
|
808
|
+
"position subspace (expected value n = "
|
|
809
|
+
f"{vm['nees_expected_dim']} for a consistent filter).",
|
|
810
|
+
f"- SIR particle fallback: uniform ensemble of "
|
|
811
|
+
f"{di['particle_count']:g} particles gives ESS = "
|
|
812
|
+
f"{vm['ess_uniform']:g}; {vm['ess_rule']} (threshold "
|
|
813
|
+
f"{vm['ess_threshold']:g}).",
|
|
814
|
+
f"- IMM two-mode bank ({model['fallback_methods']['imm']}).",
|
|
815
|
+
f"- UKF option ({model['fallback_methods']['ukf']}).",
|
|
816
|
+
"",
|
|
817
|
+
"## 7. Offline post-processing (RTS smoothing)",
|
|
818
|
+
"",
|
|
819
|
+
f"- Method: {sm['method']}.",
|
|
820
|
+
f"- Smoothed position variance at the final boundary equals the "
|
|
821
|
+
f"filtered value ({sm['boundary_matches']}); every interior "
|
|
822
|
+
f"smoothed variance at or below the filtered "
|
|
823
|
+
f"({sm['all_reduced']}).",
|
|
824
|
+
f"- Maximum covariance reduction: {sm['max_reduction_pct']}% "
|
|
825
|
+
f"(smoothed final position variance "
|
|
826
|
+
f"{sm['smoothed_final_pos_var']} m^2).",
|
|
827
|
+
"",
|
|
828
|
+
"## 8. Observability analysis",
|
|
829
|
+
"",
|
|
830
|
+
f"- Discrete observability matrix rows {ob['matrix_rows'][0]} / "
|
|
831
|
+
f"{ob['matrix_rows'][1]} with determinant {ob['det']}.",
|
|
832
|
+
f"- Verdict: rank {ob['rank']}, "
|
|
833
|
+
f"{'fully observable' if ob['observable'] else 'NOT fully observable'} "
|
|
834
|
+
"for dt > 0: the velocity state is observable through the "
|
|
835
|
+
"dynamics between position samples.",
|
|
836
|
+
"",
|
|
837
|
+
"## 9. Verification evidence and open items",
|
|
838
|
+
"",
|
|
839
|
+
"- Evidence gates (see cli.py check): architecture stated, "
|
|
840
|
+
"attitude + navigation channels designated, alpha-beta and "
|
|
841
|
+
"Kalman/EKF numbers present, observability checked, consistency "
|
|
842
|
+
"metrics present.",
|
|
843
|
+
"- Open items for the human navigation engineer: final sensor "
|
|
844
|
+
"datasheet noise values, lever-arm and time-tagging alignment, "
|
|
845
|
+
"flight-computer numerical format, and the in-flight "
|
|
846
|
+
"consistency monitoring policy.",
|
|
847
|
+
"",
|
|
848
|
+
"---",
|
|
849
|
+
f"*Generated by Aero Agent Roles state-estimation-engineer core "
|
|
850
|
+
f"({model['generated']}). DRAFT for human navigation engineer "
|
|
851
|
+
"review. Not an approval document. Not flight software release.*",
|
|
852
|
+
]
|
|
853
|
+
return "\n".join(lines)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def example_item() -> NavEstimatorItem:
|
|
857
|
+
return NavEstimatorItem(
|
|
858
|
+
item_name="UAV navigation state estimator (INS/GNSS + AHRS)",
|
|
859
|
+
description="Loosely-coupled GNSS/INS navigation filter with a "
|
|
860
|
+
"complementary-filter attitude channel for a small "
|
|
861
|
+
"fixed-wing UAV autopilot.",
|
|
862
|
+
estimator_class="attitude (complementary filter) + loosely-coupled GNSS/INS",
|
|
863
|
+
vehicle="small fixed-wing UAV (autopilot reference design)",
|
|
864
|
+
intended_use="design analysis for the navigation state estimator "
|
|
865
|
+
"before implementation on the flight computer",
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def example_report_markdown() -> str:
|
|
870
|
+
return render_report_markdown(build_report(example_item()))
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
# ---------------------------------------------------------------------------
|
|
874
|
+
# Evidence gate checkers
|
|
875
|
+
# ---------------------------------------------------------------------------
|
|
876
|
+
|
|
877
|
+
GATE_CHECKS = {
|
|
878
|
+
"architecture_stated": "estimator class / architecture is named",
|
|
879
|
+
"channels_designated": "attitude and navigation filter designs are present",
|
|
880
|
+
"gains_computed": "alpha-beta and complementary-filter gains are numbers",
|
|
881
|
+
"kalman_numbers_present": "Kalman gain + covariance at the final step",
|
|
882
|
+
"linearization_present": "EKF Jacobian H is a numeric matrix",
|
|
883
|
+
"observability_checked": "observability rank/determinant computed",
|
|
884
|
+
"consistency_metrics_present": "NEES + ESS figures computed",
|
|
885
|
+
"sign_off_honest": "report is marked draft-for-review, not approval",
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def check_report(model: dict) -> dict:
|
|
890
|
+
"""Run the evidence gates against a report content model."""
|
|
891
|
+
ab = model.get("alpha_beta", {})
|
|
892
|
+
km = model.get("kalman", {})
|
|
893
|
+
ek = model.get("ekf", {})
|
|
894
|
+
ob = model.get("observability", {})
|
|
895
|
+
results = {
|
|
896
|
+
"architecture_stated": bool(model.get("estimator_class")),
|
|
897
|
+
"channels_designated": bool(model.get("attitude"))
|
|
898
|
+
and bool(model.get("kalman")),
|
|
899
|
+
"gains_computed": isinstance(ab.get("kalata_beta"), (int, float))
|
|
900
|
+
and isinstance(ab.get("beta_benedict_bordner"), (int, float)),
|
|
901
|
+
"kalman_numbers_present": isinstance(
|
|
902
|
+
km.get("final", {}).get("gain"), list)
|
|
903
|
+
and isinstance(km.get("final", {}).get("pos_var_after"),
|
|
904
|
+
(int, float)),
|
|
905
|
+
"linearization_present": isinstance(ek.get("H"), list)
|
|
906
|
+
and bool(ek.get("H")),
|
|
907
|
+
"observability_checked": ob.get("observable") is not None,
|
|
908
|
+
"consistency_metrics_present": isinstance(
|
|
909
|
+
model.get("verification_metrics", {}).get("nees_2d"),
|
|
910
|
+
(int, float)),
|
|
911
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
912
|
+
}
|
|
913
|
+
results["all_pass"] = all(results.values())
|
|
914
|
+
return results
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
def check_report_markdown(md_text: str) -> dict:
|
|
918
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
919
|
+
low = md_text.lower()
|
|
920
|
+
checks = {
|
|
921
|
+
"has_title": "navigation state estimator design report" in low,
|
|
922
|
+
"has_architecture": "estimator class" in low,
|
|
923
|
+
"has_attitude_channel": "complementary filter" in low
|
|
924
|
+
and "bias" in low,
|
|
925
|
+
"has_nav_channel": "kalman" in low and "innovation" in low,
|
|
926
|
+
"has_alpha_beta": "kalata" in low and "benedict" in low,
|
|
927
|
+
"has_numbers": "gain" in low and "variance" in low,
|
|
928
|
+
"has_observability": "observability" in low and "rank" in low,
|
|
929
|
+
"has_draft_marker": "draft" in low,
|
|
930
|
+
"has_not_approval": "not an approval" in low,
|
|
931
|
+
}
|
|
932
|
+
checks["all_pass"] = all(checks.values())
|
|
933
|
+
return checks
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
937
|
+
"""Public entry point used by gate tooling: check a report document."""
|
|
938
|
+
return check_report_markdown(md_text)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
if __name__ == "__main__":
|
|
942
|
+
model = build_report(example_item())
|
|
943
|
+
md = render_report_markdown(model)
|
|
944
|
+
print("ESTIMATOR CLASS:", model["estimator_class"])
|
|
945
|
+
print("KALATA GAINS (lambda=%.4f): alpha=%.6f beta=%.6f"
|
|
946
|
+
% (model["alpha_beta"]["tracking_index_lambda"],
|
|
947
|
+
model["alpha_beta"]["kalata_alpha"],
|
|
948
|
+
model["alpha_beta"]["kalata_beta"]))
|
|
949
|
+
print("KF FINAL: gain=%s pos_var=%.6f"
|
|
950
|
+
% (model["kalman"]["final"]["gain"],
|
|
951
|
+
model["kalman"]["final"]["pos_var_after"]))
|
|
952
|
+
print("EKF NEES: %.4f OBSERVABLE: %s"
|
|
953
|
+
% (model["ekf"]["nees_2d"],
|
|
954
|
+
model["observability"]["observable"]))
|
|
955
|
+
print("SMOOTH MAX REDUCTION: %.4f%%"
|
|
956
|
+
% model["smoothing"]["max_reduction_pct"])
|
|
957
|
+
print("GATES:", check_report(model))
|
|
958
|
+
print("RENDERED: %d chars" % len(md))
|