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,105 @@
|
|
|
1
|
+
# GNC Design Report - Pitch Attitude Hold Autopilot
|
|
2
|
+
|
|
3
|
+
**Vehicle:** Utility UAV (example cruise condition)
|
|
4
|
+
**Condition:** Cruise, 60 m/s, altitude 3000 m, level flight
|
|
5
|
+
**Mission / item:** Pitch attitude hold autopilot design
|
|
6
|
+
**Status:** draft-for-review
|
|
7
|
+
|
|
8
|
+
## 1. Architecture and plant
|
|
9
|
+
|
|
10
|
+
- Control architecture: cascade pitch attitude hold - inner pitch rate (q) loop with PID; outer pitch attitude (theta) loop (proportional). Guidance and navigation layers analyzed in sections 2 and 6.
|
|
11
|
+
- Frames: body-fixed axes (x forward, z down) for the short-period model; stability axes for the longitudinal modes; inertial NED for the guidance geometry.
|
|
12
|
+
- Plant (reduced-order short-period, elevator to pitch rate, at the stated cruise condition):
|
|
13
|
+
|
|
14
|
+
G_q(s) = 4.0 / (s^2 + 6.6 s + 36.0)
|
|
15
|
+
|
|
16
|
+
with short-period natural frequency 6.0 rad/s, damping 0.55 and elevator effectiveness 4.0 1/s (example trim data; verify against the vehicle aerodynamic database).
|
|
17
|
+
- States: pitch rate q (plant state; the short-period model implicitly includes angle of attack). Input: elevator deflection. Output: pitch rate q. Pitch attitude theta = q/s for the outer loop.
|
|
18
|
+
- Assumptions: rigid airframe (no aeroelastic modes in this band), actuator modeled at >= 40 rad/s in the follow-up stage.
|
|
19
|
+
|
|
20
|
+
## 2. Navigation
|
|
21
|
+
|
|
22
|
+
- Sensor suite: rate gyro (pitch axis) and accelerometer-derived pitch measurement; scalar attitude Kalman filter at 100 Hz.
|
|
23
|
+
- Noise models: process noise q = 0.0009 deg^2 per step (integrated gyro drift), measurement noise r = 1.00 deg^2 (accel-derived pitch, non-accelerated-flight assumption).
|
|
24
|
+
- Filter: constant model f = 1, h = 1. Steady-state predicted covariance P = 0.0305 deg^2; steady-state gain K = 0.0296; post-update covariance P+ = 0.0296 deg^2.
|
|
25
|
+
- Attitude (pitch) error budget: 1-sigma = 0.172 deg against a budget of 0.5 deg - MET.
|
|
26
|
+
- Innovation monitor: 1-sigma innovation = 1.015 deg.
|
|
27
|
+
- GNSS/RAIM: not part of this attitude channel; the gnss-pseudorange-positioning / gnss-raim-fde / dilution-of-precision leaves apply when a full position navigation solution is in scope.
|
|
28
|
+
|
|
29
|
+
## 3. Control design
|
|
30
|
+
|
|
31
|
+
Inner pitch-rate loop (PID by pole placement on the short-period plant, matched to (s^2 + 2 z_i wn_i s + wn_i^2)(s + p3_i)):
|
|
32
|
+
|
|
33
|
+
| Loop | Gains | Phase margin | Gain margin | Requirement | Verdict |
|
|
34
|
+
|---|---|---|---|---|---|
|
|
35
|
+
| inner q (PID) | kp=156.60, ki=864.00, kd=9.75 | 76.7 deg | inf dB | PM >= 45 deg, GM >= 6 dB | PASS |
|
|
36
|
+
| outer theta (P) | kp_theta=5.400 1/s | 65.5 deg | inf dB | PM >= 45 deg, GM >= 6 dB | PASS |
|
|
37
|
+
|
|
38
|
+
Inner loop design: wn_i = 12.0 rad/s (>= 3x the attitude bandwidth requirement of 3.0 rad/s), zeta_i = 0.90, non-dominant pole p3_i = 24.0 rad/s. Inner open loop L(s) = C(s) G_q(s): gain crossover 40.4 rad/s, phase crossover none, phase margin 76.7 deg, gain margin inf dB (type-1 loop, phase recovers above -180 deg so the gain margin is infinite).
|
|
39
|
+
|
|
40
|
+
Outer loop: the closed inner loop is reduced to the lag a/(s + a) with a = zeta_i * wn_i = 10.8 rad/s, so the outer attitude plant is a/(s(s + a)) (type-1). The proportional gain kp_theta is sized for damping zeta = 0.71 (f(z) = 1 at z = 0.707, so bandwidth = natural frequency):
|
|
41
|
+
|
|
42
|
+
- Natural frequency from the bandwidth requirement: wn,req = w_bw / f(zeta) = 3.0 / 1.000 = 3.0 rad/s. The natural frequency implied by the damping target (wn = a / (2 zeta)) is 7.64 rad/s and exceeds the requirement.
|
|
43
|
+
- Achieved: wn_theta = 7.64 rad/s, zeta_theta = 0.71, bandwidth w_bw = 7.64 rad/s >= 3.0 rad/s requirement (PASS), phase margin 65.5 deg >= 45 deg (PASS), gain margin inf dB >= 6 dB (PASS).
|
|
44
|
+
|
|
45
|
+
## 4. Digital implementation
|
|
46
|
+
|
|
47
|
+
- Sample period T = 0.010 s (100 Hz); closed-loop characteristic frequency wn_i = 12.0 rad/s.
|
|
48
|
+
- Sample-rate rule (10-20 samples per closed-loop cycle): w_s,min = 10 * wn_i = 120.0 rad/s, T_max = 0.0524 s; verdict: ok (T = 0.010 s <= T_max).
|
|
49
|
+
- Inner PID velocity-form coefficients at T = 0.010 s (u(k) = u(k-1) + b0 e(k) + b1 e(k-1) + b2 e(k-2)): b0 = 1140.2400, b1 = -2106.6000, b2 = 975.0000, a1 = -1.
|
|
50
|
+
|
|
51
|
+
## 5. State estimation (observer)
|
|
52
|
+
|
|
53
|
+
- Full-order observer (Luenberger/Ackermann) for the comparison state-feedback design; plant theta_ddot = -2 q + u (normalized elevator pitch acceleration), measured state theta (C = [1, 0]).
|
|
54
|
+
- Observer poles: -30 rad/s (double) - about 10x faster than the controller poles (separation principle), settling time 4/sigma = 0.133 s.
|
|
55
|
+
- Observer gain L = [58.0, 784.0].
|
|
56
|
+
|
|
57
|
+
## 6. Guidance and optimal control
|
|
58
|
+
|
|
59
|
+
Guidance (terminal approach example, proportional navigation comparison; relative state from the nav estimate):
|
|
60
|
+
|
|
61
|
+
- Geometry: range r = 2061.6 m, line-of-sight 14.04 deg; heading error 2.58 deg; closing speed Vc = 181.90 m/s; line-of-sight rate 0.00706 rad/s.
|
|
62
|
+
- Commanded lateral acceleration a_c = N * Vc * lam_dot = 4.0 * 181.9 * 0.00706 = 5.14 m/s^2 (0.52 g) with N = 4.0; pure pursuit capture possible (V_i = 182.5 m/s > V_t = 0), tail-chase intercept time ~11.3 s.
|
|
63
|
+
|
|
64
|
+
Optimal control (LQR comparison on the normalized pitch-axis model x = [theta, q], x_dot = A x + B u with A = [[0,1],[0,-2]], B = [0,1], u = normalized elevator pitch acceleration):
|
|
65
|
+
|
|
66
|
+
- Weights: Q = diag(10, 1) (state error), R = 0.1 (control effort).
|
|
67
|
+
- Riccati solution P = [[5.831, 1.000],[1.000, 0.383]]; gain K = R^-1 B' P = [10.00, 3.83].
|
|
68
|
+
- Closed loop A - B K stable: yes (poles at -2.92 +/- j1.22 rad/s, wn = 3.16 rad/s, zeta = 0.32). The LQR result is a comparison design on the same axis; the PID cascade of section 3 is the baseline.
|
|
69
|
+
|
|
70
|
+
## 7. Space GNC
|
|
71
|
+
|
|
72
|
+
- Not applicable for this aircraft example. The space leaves (attitude-dynamics, orbit-dynamics, orbit-determination, rendezvous-phasing) load when the vehicle is a spacecraft.
|
|
73
|
+
|
|
74
|
+
## 8. Monte Carlo / robustness
|
|
75
|
+
|
|
76
|
+
- Monte Carlo on the outer loop (400 draws, seed 7): lag a and gain kp_theta varied uniformly over +/- 15% (a, inner-loop uncertainty) and +/- 10% (kp_theta, implementation tolerance).
|
|
77
|
+
- Result: phase margin min 60.9 deg / mean 65.4 deg / max 69.6 deg; all 400 draws >= 45 deg (PASS); zeta_theta range 0.63-0.79; minimum bandwidth 6.83 rad/s (still >= 3.0 rad/s requirement).
|
|
78
|
+
- Disturbance response: the rate-loop integral gives zero steady-state attitude error to constant pitch disturbances (type-1 loop); transient response and gust loads verified in the full 6-DOF simulation (open item).
|
|
79
|
+
|
|
80
|
+
## 9. Conclusions
|
|
81
|
+
|
|
82
|
+
| Gate | Result |
|
|
83
|
+
|---|---|
|
|
84
|
+
| Inner loop margins vs requirement | PASS |
|
|
85
|
+
| Outer loop margins vs requirement | PASS |
|
|
86
|
+
| Attitude bandwidth >= 3.0 rad/s | PASS |
|
|
87
|
+
| Nav error budget (1-sigma <= 0.5 deg) | PASS |
|
|
88
|
+
| Monte Carlo robustness (PM >= 45 deg) | PASS |
|
|
89
|
+
|
|
90
|
+
- Margins are design predictions from the stated reduced-order model and sensor assumptions.
|
|
91
|
+
- Open items for flight test / formal verification: full 6-DOF nonlinear simulation with actuator and aeroelastic models, sensor-in-the-loop tests, gain-schedule coverage of the flight envelope, and handling-qualities evaluation per MIL-STD-1797A where applicable.
|
|
92
|
+
|
|
93
|
+
## Appendix A. Calculation traceability
|
|
94
|
+
|
|
95
|
+
- PID gains: pole placement matching (s^2 + 2 z wn s + wn^2)(s + p3) (pid-control-design).
|
|
96
|
+
- Bandwidth/natural frequency: w_bw = f(z) wn with f(z) = sqrt(1 - 2z^2 + sqrt(2 - 4z^2 + 4z^4)); f(0.707) = 1.
|
|
97
|
+
- Margins: type-1 loop L = K/(s(s + a)) closed form; general loops evaluated on jw with root-sum unwrapped phase (frequency-response-design).
|
|
98
|
+
- LQR: K = R^-1 B' P, ARE closed form (lqr-design).
|
|
99
|
+
- Kalman: steady-state covariance = scalar Riccati root; K = h P/(h^2 P + r) (kalman-filter-design).
|
|
100
|
+
- Observer: Ackermann L = phi(A) O^-1 e_n (observer-design).
|
|
101
|
+
- Guidance: a_c = N Vc lam_dot (pursuit-guidance).
|
|
102
|
+
- Digital: 10-20 samples per closed-loop cycle; velocity-form discrete PID (digital-control-design).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
*Generated by Aero Agent Roles gnc-engineer core (2026-09-05). DRAFT - for human GNC lead review. Design predictions only; not flight software release, not a certification finding, not an approval document.*
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test gnc_core: the executable engine of the GNC Engineer role.
|
|
3
|
+
|
|
4
|
+
Proves the role can DO its job standalone (no AeroSkills needed):
|
|
5
|
+
PID pole placement, bandwidth/natural-frequency rules, margin
|
|
6
|
+
computation, LQR Riccati solution, Kalman steady state, observer
|
|
7
|
+
gains, guidance commands, digital design checks, Monte Carlo
|
|
8
|
+
robustness, report generation, and evidence-gate checks. Anchor
|
|
9
|
+
numbers match the bound AeroSkills leaves (pid-control-design,
|
|
10
|
+
frequency-response-design, lqr-design, kalman-filter-design,
|
|
11
|
+
observer-design, pursuit-guidance, digital-control-design).
|
|
12
|
+
"""
|
|
13
|
+
import math
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
import unittest
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
19
|
+
"..", "core"))
|
|
20
|
+
import gnc_core as gnc # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestDomainRules(unittest.TestCase):
|
|
24
|
+
|
|
25
|
+
def test_bandwidth_factor_anchors(self):
|
|
26
|
+
# f(0.7071) == 1 exactly (bandwidth = natural frequency at the
|
|
27
|
+
# classic 0.707 damping); f(0.5) = 1.27201965...
|
|
28
|
+
self.assertAlmostEqual(gnc.second_order_bandwidth_factor(
|
|
29
|
+
math.sqrt(2) / 2.0), 1.0, delta=1e-12)
|
|
30
|
+
self.assertAlmostEqual(gnc.second_order_bandwidth_factor(0.5),
|
|
31
|
+
1.272019649514069, delta=1e-12)
|
|
32
|
+
|
|
33
|
+
def test_natural_frequency_from_bandwidth(self):
|
|
34
|
+
# at zeta = 0.707 the required wn equals the bandwidth
|
|
35
|
+
self.assertAlmostEqual(
|
|
36
|
+
gnc.natural_frequency_from_bandwidth(3.0, math.sqrt(2) / 2.0),
|
|
37
|
+
3.0, delta=1e-9)
|
|
38
|
+
# at zeta = 0.5, wn < bandwidth
|
|
39
|
+
self.assertLess(gnc.natural_frequency_from_bandwidth(3.0, 0.5), 3.0)
|
|
40
|
+
|
|
41
|
+
def test_pid_pole_placement_anchors(self):
|
|
42
|
+
# leaf anchors (pid-control-design): a=2,b=1,wn=3,zeta=0.7
|
|
43
|
+
kp, ki = gnc.pid_gains_first_order(2.0, 1.0, 3.0, 0.7)
|
|
44
|
+
self.assertAlmostEqual(kp, 2.2, delta=1e-9)
|
|
45
|
+
self.assertAlmostEqual(ki, 9.0, delta=1e-9)
|
|
46
|
+
# a1=2,a0=1,b=1,wn=3,zeta=0.7,p3=4 -> kp=24.8 ki=36 kd=6.2
|
|
47
|
+
kp2, ki2, kd2 = gnc.pid_gains_second_order(2.0, 1.0, 1.0, 3.0,
|
|
48
|
+
0.7, 4.0)
|
|
49
|
+
self.assertAlmostEqual(kp2, 24.8, delta=1e-6)
|
|
50
|
+
self.assertAlmostEqual(ki2, 36.0, delta=1e-6)
|
|
51
|
+
self.assertAlmostEqual(kd2, 6.2, delta=1e-6)
|
|
52
|
+
|
|
53
|
+
def test_type1_margins_anchor(self):
|
|
54
|
+
# leaf anchor: L = 2/(s(s+2)) -> PM 65.5305 deg, GM inf
|
|
55
|
+
m = gnc.type1_margins(2.0, 2.0)
|
|
56
|
+
self.assertAlmostEqual(m["crossover_rad_s"], 0.9101797, delta=1e-4)
|
|
57
|
+
self.assertAlmostEqual(m["phase_margin_deg"], 65.5305, delta=1e-2)
|
|
58
|
+
self.assertEqual(m["gain_margin"], float("inf"))
|
|
59
|
+
|
|
60
|
+
def test_loop_margins_canonical_anchor(self):
|
|
61
|
+
# frequency-response-design anchor: G = 2/(s(s+1)(s+2)) ->
|
|
62
|
+
# GM 3.0 (9.5424 dB) at w_pc = sqrt(2), PM ~ 32.61 deg at
|
|
63
|
+
# w_gc ~ 0.74935 rad/s
|
|
64
|
+
m = gnc.loop_margins([2.0], [1.0, 3.0, 2.0, 0.0])
|
|
65
|
+
self.assertAlmostEqual(m["gain_margin"], 3.0, delta=1e-3)
|
|
66
|
+
self.assertAlmostEqual(m["gain_margin_db"], 9.5424, delta=1e-2)
|
|
67
|
+
self.assertAlmostEqual(m["phase_crossover_rad_s"],
|
|
68
|
+
math.sqrt(2.0), delta=1e-2)
|
|
69
|
+
self.assertAlmostEqual(m["gain_crossover_rad_s"], 0.74935,
|
|
70
|
+
delta=1e-2)
|
|
71
|
+
self.assertAlmostEqual(m["phase_margin_deg"], 32.61, delta=1e-1)
|
|
72
|
+
|
|
73
|
+
def test_riccati_anchor(self):
|
|
74
|
+
# q1=10, q2=1, r=0.1, a=2 -> P and K closed form
|
|
75
|
+
r = gnc.riccati_solution([[0, 1], [0, -2]], [0, 1],
|
|
76
|
+
[[10, 0], [0, 1]], 0.1)
|
|
77
|
+
self.assertAlmostEqual(r["P"][0][0], 5.8310, delta=1e-3)
|
|
78
|
+
self.assertAlmostEqual(r["P"][1][1], 0.3831, delta=1e-3)
|
|
79
|
+
self.assertAlmostEqual(r["K"][0], 10.0, delta=1e-9)
|
|
80
|
+
self.assertAlmostEqual(r["K"][1], 3.8310, delta=1e-3)
|
|
81
|
+
self.assertTrue(r["stable"])
|
|
82
|
+
|
|
83
|
+
def test_kalman_steady_state_anchor(self):
|
|
84
|
+
# q=1, r=1, f=h=1 -> P_inf = (1+sqrt(5))/2 (golden ratio),
|
|
85
|
+
# K = P/(P+1) ~ 0.61803
|
|
86
|
+
k = gnc.kalman_steady_state(1.0, 1.0, 1.0, 1.0)
|
|
87
|
+
self.assertAlmostEqual(k["p_pred"], (1.0 + math.sqrt(5)) / 2.0,
|
|
88
|
+
delta=1e-9)
|
|
89
|
+
self.assertAlmostEqual(k["gain"], 0.61803398875, delta=1e-9)
|
|
90
|
+
self.assertAlmostEqual(k["p_apost"],
|
|
91
|
+
(1.0 - k["gain"]) * k["p_pred"], delta=1e-12)
|
|
92
|
+
|
|
93
|
+
def test_observer_gain_2state_anchor(self):
|
|
94
|
+
# A = [[0,1],[0,-2]], C = [1,0], poles -30,-30 -> L = [58, 784]
|
|
95
|
+
o = gnc.observer_gain_2state([[0, 1], [0, -2]], [[1, 0]],
|
|
96
|
+
[-30.0, -30.0])
|
|
97
|
+
self.assertAlmostEqual(o["L"][0], 58.0, delta=1e-6)
|
|
98
|
+
self.assertAlmostEqual(o["L"][1], 784.0, delta=1e-6)
|
|
99
|
+
self.assertAlmostEqual(o["settling_time_s"], 4.0 / 30.0, delta=1e-9)
|
|
100
|
+
|
|
101
|
+
def test_pn_acceleration(self):
|
|
102
|
+
# rx=1000, ry=0, vx=-100, vy=10 -> Vc=100, lam_dot=0.01,
|
|
103
|
+
# a_c = N * 1.0 for N
|
|
104
|
+
a = gnc.pn_acceleration(1000.0, 0.0, -100.0, 10.0, 4.0)
|
|
105
|
+
self.assertAlmostEqual(a["closing_speed_m_s"], 100.0, delta=1e-9)
|
|
106
|
+
self.assertAlmostEqual(a["los_rate_rad_s"], 0.01, delta=1e-12)
|
|
107
|
+
self.assertAlmostEqual(a["accel_cmd_m_s2"], 4.0, delta=1e-9)
|
|
108
|
+
|
|
109
|
+
def test_discrete_pid_velocity(self):
|
|
110
|
+
d = gnc.discrete_pid_velocity(2.0, 3.0, 4.0, 0.1)
|
|
111
|
+
self.assertAlmostEqual(d["b0"], 2.0 + 3.0 * 0.1 + 4.0 / 0.1,
|
|
112
|
+
delta=1e-12)
|
|
113
|
+
self.assertAlmostEqual(d["b1"], -2.0 - 2.0 * 4.0 / 0.1, delta=1e-12)
|
|
114
|
+
self.assertAlmostEqual(d["b2"], 4.0 / 0.1, delta=1e-12)
|
|
115
|
+
self.assertEqual(d["a1"], -1.0)
|
|
116
|
+
# b0 + b1 + b2 == Ki*T (constant-error injection per step)
|
|
117
|
+
self.assertAlmostEqual(d["b0"] + d["b1"] + d["b2"], 3.0 * 0.1,
|
|
118
|
+
delta=1e-12)
|
|
119
|
+
|
|
120
|
+
def test_sample_rate_rule(self):
|
|
121
|
+
r = gnc.sample_rate_rule(10.0, 0.02) # 50 Hz vs 10 rad/s loop
|
|
122
|
+
self.assertAlmostEqual(r["w_s_min_rad_s"], 100.0, delta=1e-9)
|
|
123
|
+
self.assertEqual(r["verdict"], "ok")
|
|
124
|
+
self.assertEqual(gnc.sample_rate_rule(10.0, 0.1)["verdict"],
|
|
125
|
+
"too-slow")
|
|
126
|
+
|
|
127
|
+
def test_monte_carlo_deterministic(self):
|
|
128
|
+
a1 = gnc.monte_carlo_outer_margins(10.0, 5.0, 100, seed=7)
|
|
129
|
+
a2 = gnc.monte_carlo_outer_margins(10.0, 5.0, 100, seed=7)
|
|
130
|
+
self.assertEqual(a1["pm_min_deg"], a2["pm_min_deg"])
|
|
131
|
+
self.assertEqual(a1["pm_max_deg"], a2["pm_max_deg"])
|
|
132
|
+
self.assertEqual(a1["pm_mean_deg"], a2["pm_mean_deg"])
|
|
133
|
+
|
|
134
|
+
def test_invalid_inputs_raise(self):
|
|
135
|
+
with self.assertRaises(ValueError):
|
|
136
|
+
gnc.pid_gains_second_order(2, 1, 0.0, 3, 0.7, 4)
|
|
137
|
+
with self.assertRaises(ValueError):
|
|
138
|
+
gnc.type1_margins(0.0, 2.0)
|
|
139
|
+
with self.assertRaises(ValueError):
|
|
140
|
+
gnc.kalman_steady_state(1.0, 1.0, 1.0, 0.0)
|
|
141
|
+
with self.assertRaises(ValueError):
|
|
142
|
+
gnc.riccati_solution([[0, 1], [0, -2]], [0, 1],
|
|
143
|
+
[[10, 0], [0, 1]], 0.0)
|
|
144
|
+
with self.assertRaises(ValueError):
|
|
145
|
+
gnc.observer_gain_2state([[0, 1], [0, -2]], [[1, 0]],
|
|
146
|
+
[-30.0, 5.0]) # unstable pole
|
|
147
|
+
with self.assertRaises(ValueError):
|
|
148
|
+
gnc.observer_gain_2state([[0, 1], [0, -2]], [[0, 0]],
|
|
149
|
+
[-30.0, -30.0]) # not observable
|
|
150
|
+
with self.assertRaises(ValueError):
|
|
151
|
+
gnc.pn_acceleration(0.0, 0.0, -100.0, 10.0, 4.0) # zero range
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class TestReportBuilder(unittest.TestCase):
|
|
155
|
+
|
|
156
|
+
def test_example_model_values(self):
|
|
157
|
+
m = gnc.build_gnc_report(gnc.example_project())
|
|
158
|
+
# inner loop gains from the documented example facts
|
|
159
|
+
self.assertAlmostEqual(m["inner"]["kp"], 156.6, delta=1e-6)
|
|
160
|
+
self.assertAlmostEqual(m["inner"]["ki"], 864.0, delta=1e-6)
|
|
161
|
+
self.assertAlmostEqual(m["inner"]["kd"], 9.75, delta=1e-6)
|
|
162
|
+
# inner margins: comfortable PM, infinite GM (type-1 loop with
|
|
163
|
+
# numerator lead)
|
|
164
|
+
self.assertGreater(m["inner"]["margins"]["pm_deg"], 45.0)
|
|
165
|
+
self.assertEqual(m["inner"]["margins"]["gm_db"], float("inf"))
|
|
166
|
+
# outer loop numbers
|
|
167
|
+
self.assertAlmostEqual(m["outer"]["kp_theta"], 5.4, delta=1e-9)
|
|
168
|
+
self.assertAlmostEqual(m["outer"]["zeta_theta"],
|
|
169
|
+
math.sqrt(2) / 2.0, delta=1e-9)
|
|
170
|
+
self.assertGreater(m["outer"]["bw_rad_s"], 3.0)
|
|
171
|
+
# nav budget
|
|
172
|
+
self.assertLess(m["nav"]["sigma_deg"], m["nav"]["budget_deg"])
|
|
173
|
+
self.assertAlmostEqual(m["nav"]["gain"], 0.02956, delta=1e-3)
|
|
174
|
+
# lqr + observer + guidance real
|
|
175
|
+
self.assertTrue(m["lqr"]["stable"])
|
|
176
|
+
self.assertAlmostEqual(m["observer"]["L"][0], 58.0, delta=1e-6)
|
|
177
|
+
self.assertAlmostEqual(m["observer"]["L"][1], 784.0, delta=1e-6)
|
|
178
|
+
self.assertGreater(m["guidance"]["accel_cmd_m_s2"], 0.0)
|
|
179
|
+
|
|
180
|
+
def test_requirements_recorded(self):
|
|
181
|
+
m = gnc.build_gnc_report(gnc.example_project())
|
|
182
|
+
self.assertEqual(m["requirements"]["phase_margin_deg"], 45.0)
|
|
183
|
+
self.assertEqual(m["requirements"]["gain_margin_db"], 6.0)
|
|
184
|
+
self.assertEqual(m["requirements"]["bandwidth_rad_s"], 3.0)
|
|
185
|
+
self.assertEqual(m["verdicts"]["inner_margins"], "PASS")
|
|
186
|
+
self.assertEqual(m["verdicts"]["outer_margins"], "PASS")
|
|
187
|
+
self.assertEqual(m["verdicts"]["outer_bandwidth"], "PASS")
|
|
188
|
+
self.assertEqual(m["verdicts"]["nav_budget"], "PASS")
|
|
189
|
+
self.assertEqual(m["verdicts"]["mc_robustness"], "PASS")
|
|
190
|
+
|
|
191
|
+
def test_report_markdown_sections(self):
|
|
192
|
+
md = gnc.example_report_markdown()
|
|
193
|
+
for sec in ["## 1. Architecture", "## 2. Navigation",
|
|
194
|
+
"## 3. Control design", "## 4. Digital",
|
|
195
|
+
"## 5. State estimation", "## 6. Guidance",
|
|
196
|
+
"## 7. Space GNC", "## 8. Monte Carlo",
|
|
197
|
+
"## 9. Conclusions"]:
|
|
198
|
+
self.assertIn(sec, md, "missing section %s" % sec)
|
|
199
|
+
low = md.lower()
|
|
200
|
+
self.assertIn("not an approval", low)
|
|
201
|
+
self.assertIn("not flight software release", low)
|
|
202
|
+
self.assertNotIn("___", md)
|
|
203
|
+
|
|
204
|
+
def test_model_gates_all_pass(self):
|
|
205
|
+
m = gnc.build_gnc_report(gnc.example_project())
|
|
206
|
+
gates = gnc.check_gnc_report(m)
|
|
207
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
208
|
+
|
|
209
|
+
def test_markdown_gates_all_pass(self):
|
|
210
|
+
md = gnc.example_report_markdown()
|
|
211
|
+
gates = gnc.check_gnc_report_markdown(md)
|
|
212
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
213
|
+
|
|
214
|
+
def test_gates_fail_on_bad_input(self):
|
|
215
|
+
# tamper with the model: margins below requirement must fail
|
|
216
|
+
m = gnc.build_gnc_report(gnc.example_project())
|
|
217
|
+
m["inner"]["margins"]["pm_deg"] = 20.0
|
|
218
|
+
gates = gnc.check_gnc_report(m)
|
|
219
|
+
self.assertFalse(gates["all_pass"])
|
|
220
|
+
self.assertFalse(gates["margins_meet_requirement"])
|
|
221
|
+
# tampered markdown: no draft marker must fail
|
|
222
|
+
md = (gnc.example_report_markdown().replace("draft", "REVIEW")
|
|
223
|
+
.replace("DRAFT", "REVIEW"))
|
|
224
|
+
g = gnc.check_gnc_report_markdown(md)
|
|
225
|
+
self.assertFalse(g["all_pass"])
|
|
226
|
+
self.assertFalse(g["has_draft_marker"])
|
|
227
|
+
|
|
228
|
+
def test_standalone_no_skills_repo(self):
|
|
229
|
+
# Core must work with no AeroSkills present.
|
|
230
|
+
os.environ["AEROSKILLS_DEV"] = "/nonexistent"
|
|
231
|
+
model = gnc.build_gnc_report(gnc.example_project())
|
|
232
|
+
self.assertTrue(model["inner"]["req_ok"])
|
|
233
|
+
md = gnc.render_gnc_report_markdown(model)
|
|
234
|
+
self.assertGreater(len(md), 3000)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
unittest.main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Role test: gnc-engineer."""
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
ROLES_REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(
|
|
9
|
+
os.path.abspath(__file__)))))
|
|
10
|
+
AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
|
|
11
|
+
HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
|
|
12
|
+
ROLE_DIR = os.path.join(ROLES_REPO, "roles", "gnc-engineer")
|
|
13
|
+
TEMPLATE = os.path.join(ROLE_DIR, "templates", "gnc-report-template.md")
|
|
14
|
+
CORE_DIR = os.path.join(ROLE_DIR, "core")
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, CORE_DIR)
|
|
17
|
+
import gnc_core # noqa: E402
|
|
18
|
+
|
|
19
|
+
EXPECTED_BOUND = [
|
|
20
|
+
"gnc-autonomy/control/pid-control-design",
|
|
21
|
+
"gnc-autonomy/control/lead-lag-compensation",
|
|
22
|
+
"gnc-autonomy/control/frequency-response-design",
|
|
23
|
+
"gnc-autonomy/control/digital-control-design",
|
|
24
|
+
"gnc-autonomy/control/observer-design",
|
|
25
|
+
"gnc-autonomy/control/control-allocation",
|
|
26
|
+
"gnc-autonomy/control/gain-scheduling",
|
|
27
|
+
"gnc-autonomy/control/adaptive-control",
|
|
28
|
+
"gnc-autonomy/navigation/navigation-frames",
|
|
29
|
+
"gnc-autonomy/navigation/inertial-navigation",
|
|
30
|
+
"gnc-autonomy/navigation/kalman-filter-design",
|
|
31
|
+
"gnc-autonomy/navigation/gnss-pseudorange-positioning",
|
|
32
|
+
"gnc-autonomy/navigation/gnss-raim-fde",
|
|
33
|
+
"gnc-autonomy/navigation/dilution-of-precision",
|
|
34
|
+
"gnc-autonomy/guidance/pursuit-guidance",
|
|
35
|
+
"gnc-autonomy/optimal-control/lqr-design",
|
|
36
|
+
"gnc-autonomy/optimal-control/model-predictive-control",
|
|
37
|
+
"gnc-autonomy/optimal-control/bang-bang-control",
|
|
38
|
+
"gnc-autonomy/optimal-control/dymos-trajectory",
|
|
39
|
+
"gnc-autonomy/space/attitude-dynamics",
|
|
40
|
+
"gnc-autonomy/space/orbit-dynamics",
|
|
41
|
+
"gnc-autonomy/space/orbit-determination",
|
|
42
|
+
"gnc-autonomy/space/rendezvous-phasing"
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
STAGES = [
|
|
46
|
+
"1. Frames + plant",
|
|
47
|
+
"2. Navigation filter",
|
|
48
|
+
"3. Inner loop",
|
|
49
|
+
"4. Digital implementation",
|
|
50
|
+
"5. State estimation",
|
|
51
|
+
"6. Allocation/scheduling",
|
|
52
|
+
"7. Guidance",
|
|
53
|
+
"8. Optimal control",
|
|
54
|
+
"9. Space GNC",
|
|
55
|
+
"10. Report"
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def role_text():
|
|
60
|
+
p = os.path.join(ROLE_DIR, "ROLE.md")
|
|
61
|
+
if not os.path.exists(p):
|
|
62
|
+
return None
|
|
63
|
+
with open(p) as f:
|
|
64
|
+
return f.read()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestGncEngineerRole(unittest.TestCase):
|
|
68
|
+
|
|
69
|
+
def test_bound_skills_resolve(self):
|
|
70
|
+
if not HAS_SKILLS:
|
|
71
|
+
self.skipTest("skills checkout absent (cross-repo dev check)")
|
|
72
|
+
for leaf in EXPECTED_BOUND:
|
|
73
|
+
self.assertTrue(
|
|
74
|
+
os.path.exists(os.path.join(AEROSKILLS, "skills", leaf, "SKILL.md")),
|
|
75
|
+
f"bound skill missing: {leaf}")
|
|
76
|
+
|
|
77
|
+
def test_stages_ordered(self):
|
|
78
|
+
role = role_text()
|
|
79
|
+
self.assertIsNotNone(role)
|
|
80
|
+
wf = role[role.find("## Workflow"):role.find("## Evidence gates")]
|
|
81
|
+
idx = [wf.find(s) for s in STAGES]
|
|
82
|
+
self.assertTrue(all(i >= 0 for i in idx), "stage missing in workflow")
|
|
83
|
+
self.assertEqual(idx, sorted(idx), "stage order not deterministic")
|
|
84
|
+
|
|
85
|
+
def test_template_present(self):
|
|
86
|
+
self.assertTrue(os.path.exists(TEMPLATE), "deliverable template missing")
|
|
87
|
+
|
|
88
|
+
def test_template_is_filled_deliverable(self):
|
|
89
|
+
"""100% standard: template is the FILLED worked example - all 9
|
|
90
|
+
numbered sections + appendix, zero blank fields, draft markers."""
|
|
91
|
+
self.assertTrue(os.path.exists(TEMPLATE))
|
|
92
|
+
with open(TEMPLATE) as f:
|
|
93
|
+
text = f.read()
|
|
94
|
+
for n in range(1, 10):
|
|
95
|
+
self.assertTrue(
|
|
96
|
+
re.search(rf"^## {n}\. ", text, re.M),
|
|
97
|
+
f"report section {n} missing")
|
|
98
|
+
self.assertIn("Appendix A", text)
|
|
99
|
+
self.assertNotIn("___", text)
|
|
100
|
+
low = text.lower()
|
|
101
|
+
self.assertIn("draft", low)
|
|
102
|
+
self.assertIn("not an approval", low)
|
|
103
|
+
self.assertIn("not flight software release", low)
|
|
104
|
+
|
|
105
|
+
def test_template_passes_core_gates(self):
|
|
106
|
+
"""The shipped template must pass the engine's own markdown
|
|
107
|
+
evidence gates (standalone core, no skills repo needed)."""
|
|
108
|
+
with open(TEMPLATE) as f:
|
|
109
|
+
text = f.read()
|
|
110
|
+
gates = gnc_core.check_gnc_report_markdown(text)
|
|
111
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
112
|
+
|
|
113
|
+
def test_core_and_cli_present(self):
|
|
114
|
+
self.assertTrue(os.path.exists(os.path.join(CORE_DIR,
|
|
115
|
+
"gnc_core.py")),
|
|
116
|
+
"core/gnc_core.py missing (100% standard)")
|
|
117
|
+
self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, "cli.py")),
|
|
118
|
+
"cli.py missing (100% standard)")
|
|
119
|
+
|
|
120
|
+
def test_core_standalone_example_gates(self):
|
|
121
|
+
"""Core produces a correct, gate-passing report with no skills."""
|
|
122
|
+
os.environ["AEROSKILLS_DEV"] = "/nonexistent"
|
|
123
|
+
model = gnc_core.build_gnc_report(gnc_core.example_project())
|
|
124
|
+
gates = gnc_core.check_gnc_report(model)
|
|
125
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
126
|
+
md = gnc_core.example_report_markdown()
|
|
127
|
+
mgates = gnc_core.check_gnc_report_markdown(md)
|
|
128
|
+
self.assertTrue(mgates["all_pass"], mgates)
|
|
129
|
+
|
|
130
|
+
def test_boundaries(self):
|
|
131
|
+
role = role_text()
|
|
132
|
+
self.assertIsNotNone(role)
|
|
133
|
+
for phrase in ["sign_off_required: true", "forbidden"]:
|
|
134
|
+
self.assertIn(phrase, role)
|
|
135
|
+
|
|
136
|
+
def test_sources(self):
|
|
137
|
+
self.assertTrue(os.path.exists(os.path.join(ROLE_DIR, "SOURCES.md")),
|
|
138
|
+
"SOURCES.md missing")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
unittest.main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: role
|
|
3
|
+
name: guidance-engineer
|
|
4
|
+
title: "Guidance Engineer"
|
|
5
|
+
status: draft
|
|
6
|
+
domain: gnc-autonomy
|
|
7
|
+
deliverable_type: "guidance law design and assessment report"
|
|
8
|
+
standards_bound:
|
|
9
|
+
- id: arp4754a
|
|
10
|
+
tier: TIER-2
|
|
11
|
+
reference-only: true
|
|
12
|
+
- id: far-25
|
|
13
|
+
tier: TIER-1
|
|
14
|
+
reference-only: false
|
|
15
|
+
- id: cs-25
|
|
16
|
+
tier: TIER-1
|
|
17
|
+
reference-only: false
|
|
18
|
+
skills_bound:
|
|
19
|
+
- gnc-autonomy/guidance/proportional-navigation
|
|
20
|
+
- gnc-autonomy/guidance/augmented-proportional-navigation
|
|
21
|
+
- gnc-autonomy/guidance/pursuit-guidance
|
|
22
|
+
- gnc-autonomy/guidance/collision-course-guidance
|
|
23
|
+
- gnc-autonomy/guidance/command-to-line-of-sight
|
|
24
|
+
- gnc-autonomy/guidance/midcourse-guidance
|
|
25
|
+
- gnc-autonomy/guidance/impact-point-prediction
|
|
26
|
+
- gnc-autonomy/guidance/dubins-path-planning
|
|
27
|
+
- gnc-autonomy/guidance/coverage-path-planning
|
|
28
|
+
tools_allowed: [stdlib, offline-file-processing]
|
|
29
|
+
forbidden:
|
|
30
|
+
- "declare the weapon or vehicle ready for guided release"
|
|
31
|
+
- "approve flight of the guidance software"
|
|
32
|
+
- "claim certification approval"
|
|
33
|
+
- "assert intercept capability without engagement analysis"
|
|
34
|
+
sign_off_required: true
|
|
35
|
+
license: Apache-2.0
|
|
36
|
+
compatibility: "agentskills.io ROLE.md; any SKILL.md host"
|
|
37
|
+
metadata:
|
|
38
|
+
version: 0.1.0
|
|
39
|
+
author: ashfordeOU
|
|
40
|
+
skills_release: "v1.3.0+"
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
# Guidance Engineer
|
|
44
|
+
|
|
45
|
+
## Role identity
|
|
46
|
+
|
|
47
|
+
Owns the guidance-law design and assessment for a guided vehicle
|
|
48
|
+
(missile/UAV homing loop, interceptor, guided munition): selecting and
|
|
49
|
+
sizing the terminal law (proportional navigation and its augmented
|
|
50
|
+
variant, pursuit, command-to-line-of-sight), sizing the guidance gains
|
|
51
|
+
(navigation constant), checking the commanded acceleration against the
|
|
52
|
+
vehicle's lateral-acceleration limit, estimating the miss distance
|
|
53
|
+
(zero-effort miss), and checking midcourse waypoint steering before
|
|
54
|
+
terminal handover. Use when a guided engagement needs a quantified
|
|
55
|
+
guidance-law assessment with real command numbers. Do NOT use for
|
|
56
|
+
navigation state estimation or control-law design (see gnc-engineer),
|
|
57
|
+
for trajectory optimization (optimal-control roles), or for
|
|
58
|
+
certification of the guidance software (see do178c-cert-engineer).
|
|
59
|
+
|
|
60
|
+
## Deliverable contract
|
|
61
|
+
|
|
62
|
+
The role produces the **Guidance Law Design and Assessment Report** per
|
|
63
|
+
templates/guidance-report-template.md:
|
|
64
|
+
|
|
65
|
+
1. **Scope and engagement definition** — item, vehicle, stated project
|
|
66
|
+
facts (miss requirement, acceleration limit, target-maneuver
|
|
67
|
+
estimate).
|
|
68
|
+
2. **Engagement geometry** — range, closing velocity, LOS rate and LOS
|
|
69
|
+
angle at terminal handover.
|
|
70
|
+
3. **Law candidates and commands** — PN and augmented-PN commands,
|
|
71
|
+
pursuit-family comparison (heading error, lead angle, capture
|
|
72
|
+
condition, tail-chase intercept time).
|
|
73
|
+
4. **Guidance gain selection** — navigation constant N within the
|
|
74
|
+
3-to-5 band (4 baseline) and the APN effective ratio.
|
|
75
|
+
5. **Acceleration limit check** — command utilization vs the vehicle
|
|
76
|
+
lateral-acceleration limit.
|
|
77
|
+
6. **Miss-distance estimate** — zero-effort miss at the handover state
|
|
78
|
+
against the guided-intercept requirement.
|
|
79
|
+
7. **Midcourse waypoint steering check** — turn-rate-limited desired
|
|
80
|
+
course, commanded turn, velocity to be gained.
|
|
81
|
+
8. **Findings and open items** — what is settled and what remains for
|
|
82
|
+
the human guidance lead.
|
|
83
|
+
9. **Compliance and sign-off boundary** — recommendation, law sources,
|
|
84
|
+
DRAFT marker.
|
|
85
|
+
|
|
86
|
+
## Workflow (stages → bound skills)
|
|
87
|
+
|
|
88
|
+
| Stage | Loads skill | Produces |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| 1. Engagement definition | guidance/collision-course-guidance, guidance/impact-point-prediction | engagement geometry, required miss distance, stated sizing facts |
|
|
91
|
+
| 2. Law trade space | guidance/proportional-navigation, guidance/pursuit-guidance, guidance/augmented-proportional-navigation, guidance/command-to-line-of-sight | candidate law set with command levels at the design point |
|
|
92
|
+
| 3. Gain sizing | guidance/proportional-navigation, guidance/augmented-proportional-navigation | navigation constant N (3-to-5 band, 4 baseline), APN effective ratio |
|
|
93
|
+
| 4. Acceleration limit check | guidance/proportional-navigation, guidance/augmented-proportional-navigation | commanded-acceleration utilization vs vehicle limit |
|
|
94
|
+
| 5. Miss-distance estimate | guidance/midcourse-guidance | zero-effort miss and time to go vs the requirement |
|
|
95
|
+
| 6. Midcourse steering | guidance/midcourse-guidance, guidance/dubins-path-planning, guidance/coverage-path-planning | waypoint steering check, path feasibility context |
|
|
96
|
+
| 7. Report and gates | guidance (all bound leaves) | assessment report, evidence bundle, gate verdicts |
|
|
97
|
+
|
|
98
|
+
## Evidence gates
|
|
99
|
+
|
|
100
|
+
- Stage 2 done = every candidate law produces a real command number at
|
|
101
|
+
the design point (PN, APN, pursuit geometry).
|
|
102
|
+
- Stage 4 done = command utilization computed against the vehicle
|
|
103
|
+
limit; saturation flagged.
|
|
104
|
+
- Stage 5 done = zero-effort miss and time to go estimated and compared
|
|
105
|
+
with the miss requirement.
|
|
106
|
+
- FINAL = every computed number in the report is backed by a guidance
|
|
107
|
+
law encoded from the bound leaves (gnc-autonomy/guidance *_logic.py);
|
|
108
|
+
nothing asserted without a checkable basis. The rendered report
|
|
109
|
+
carries DRAFT + human-review + not-an-approval markers (code-enforced
|
|
110
|
+
by check_report / check_report_markdown).
|
|
111
|
+
|
|
112
|
+
## Boundary / forbidden
|
|
113
|
+
|
|
114
|
+
- NEVER declare the weapon or vehicle ready for guided release, and
|
|
115
|
+
NEVER approve flight of the guidance software — the human guidance
|
|
116
|
+
lead and the program's release authority sign.
|
|
117
|
+
- NEVER assert intercept capability beyond what the engagement analysis
|
|
118
|
+
supports (seeker noise, autopilot lag and out-of-sizing target
|
|
119
|
+
maneuvers stay open items).
|
|
120
|
+
- NEVER claim certification approval; ARP4754A and FAR/CS-25 are
|
|
121
|
+
reference-only context and no proprietary standard text is
|
|
122
|
+
reproduced.
|
|
123
|
+
- Output is a DRAFT for human review — mark it draft.
|
|
124
|
+
|
|
125
|
+
## Verification
|
|
126
|
+
|
|
127
|
+
The role runs STANDALONE: `core/guidance_core.py` is an executable
|
|
128
|
+
engine that computes PN/APN commands, pursuit geometry, turn-rate-
|
|
129
|
+
limited waypoint steering, acceleration utilization, zero-effort miss,
|
|
130
|
+
BUILDS the report, and gate-checks deliverables. No AeroSkills checkout
|
|
131
|
+
required. When the library IS present, the bound leaf logic files are
|
|
132
|
+
dispatched on the same inputs and their agreement (delta ~ 0) is
|
|
133
|
+
recorded in provenance.json.
|
|
134
|
+
|
|
135
|
+
Run the role:
|
|
136
|
+
```bash
|
|
137
|
+
python3 cli.py build --out report.md --bundle # example item
|
|
138
|
+
python3 cli.py build --n-nav 5 --miss-requirement 5
|
|
139
|
+
python3 cli.py check --file report.md # gate-check a report
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Tests:
|
|
143
|
+
- tests/test_guidance_engineer_core.py: domain rules with the real
|
|
144
|
+
leaf anchors (PN a_c = 15.762965 m/s^2, ZEM 150 m at t_go 20 s, vgo
|
|
145
|
+
65.08 m/s, waypoint 26.565 deg) + builder + gates + standalone (no
|
|
146
|
+
skills repo) + evidence-bundle smoke.
|
|
147
|
+
- tests/test_role_guidance_engineer.py: bound-skill resolution (skips
|
|
148
|
+
if the skills repo is absent) + workflow order + filled template +
|
|
149
|
+
boundaries.
|
|
150
|
+
|
|
151
|
+
## Compliance
|
|
152
|
+
|
|
153
|
+
- arp4754a TIER-2 reference-only and far-25/cs-25 TIER-1 reference-only
|
|
154
|
+
per the skills standards map; the laws are common knowledge encoded
|
|
155
|
+
by the leaves, paraphrased, never copied verbatim.
|
|
156
|
+
- SOURCES.md records the sources and the encoded domain rules.
|