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,820 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""guidance_core.py - Guidance Engineer executable core.
|
|
3
|
+
|
|
4
|
+
This is the role's ENGINE: given a guided-vehicle engagement's project
|
|
5
|
+
facts it computes the proportional navigation (PN) command, the
|
|
6
|
+
augmented PN comparison, pursuit-family geometry, turn-rate-limited
|
|
7
|
+
waypoint (midcourse) steering, the acceleration-limit utilization, and
|
|
8
|
+
the zero-effort-miss distance estimate, then BUILDS the Guidance Law
|
|
9
|
+
Design and Assessment Report content. It also gate-checks deliverables.
|
|
10
|
+
Standalone: no external repo needed.
|
|
11
|
+
|
|
12
|
+
Every formula below is a paraphrase of the classical guidance law as
|
|
13
|
+
encoded by the bound Aero Agent Skills leaves in
|
|
14
|
+
gnc-autonomy/guidance/ (each leaf is a summary of common guidance
|
|
15
|
+
knowledge, framed by ARP4754A / FAR-25 / CS-25 reference-only
|
|
16
|
+
context). The signatures mirror the leaf logic files so the same input
|
|
17
|
+
can be dispatched to both implementations for a cross-check
|
|
18
|
+
(provenance.json records agreement). Worked anchors:
|
|
19
|
+
|
|
20
|
+
PN (proportional-navigation leaf test anchor, rx=1000 ry=100
|
|
21
|
+
vx=-200 vy=0 N=4): r = 1004.9876 m, vc = 199.0074 m/s,
|
|
22
|
+
lam_dot = 0.019802 rad/s, a_c = 15.762965 m/s^2.
|
|
23
|
+
|
|
24
|
+
APN (augmented-proportional-navigation leaf): a_apn = N'(Vc*lam_dot
|
|
25
|
+
+ a_T_perp/2), N' default 4.
|
|
26
|
+
|
|
27
|
+
Pursuit (pursuit-guidance leaf): lam = atan2(ry, rx),
|
|
28
|
+
eta = wrap(lam - psi), lead angle asin((Vt/Vi) sin(beta)), capture
|
|
29
|
+
needs Vi > Vt, tail-chase t_i = r/(Vi - Vt).
|
|
30
|
+
|
|
31
|
+
Midcourse (midcourse-guidance leaf anchors): psi_d(0,0->1000,500)
|
|
32
|
+
= 26.565 deg; clamp to omega_max*dt (5 deg at 5 deg/s, 1 s);
|
|
33
|
+
vgo anchor 65.08 m/s; ZEM anchor 150 m at t_go = 20 s; ascent
|
|
34
|
+
a_c = V*gamma_dot + g*cos(gamma).
|
|
35
|
+
|
|
36
|
+
Units: m, m/s, rad, rad/s, s, m/s^2 throughout; g0 = 9.80665 m/s^2.
|
|
37
|
+
"""
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import math
|
|
42
|
+
import re
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from datetime import date
|
|
45
|
+
|
|
46
|
+
def _today() -> str:
|
|
47
|
+
import os
|
|
48
|
+
from datetime import date
|
|
49
|
+
return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
|
|
50
|
+
|
|
51
|
+
G0 = 9.80665 # standard gravity, m/s^2 (leaf constant)
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Geometry and scalar helpers (mirror leaf conventions)
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
_TOL = 1e-12
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _scalar(value, name):
|
|
61
|
+
"""Cast a scalar to float; raise ValueError on non-numeric input."""
|
|
62
|
+
try:
|
|
63
|
+
return float(value)
|
|
64
|
+
except (TypeError, ValueError):
|
|
65
|
+
raise ValueError("%s must be numeric, got %r" % (name, value))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _vector(values, name):
|
|
69
|
+
"""Cast a 2-sequence to (float, float) or raise ValueError."""
|
|
70
|
+
try:
|
|
71
|
+
x, y = values
|
|
72
|
+
except (TypeError, ValueError):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"%s must be a 2-sequence of numbers, got %r" % (name, values))
|
|
75
|
+
return _scalar(x, name + "[0]"), _scalar(y, name + "[1]")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _geometry(rx, ry, vx=None, vy=None):
|
|
79
|
+
"""Validate and cast planar intercept geometry.
|
|
80
|
+
|
|
81
|
+
With velocities given returns (rx, ry, vx, vy, range) as floats;
|
|
82
|
+
position-only calls return (rx, ry, range). Raises ValueError when
|
|
83
|
+
entries are non-numeric or when the range is <= 0 (the formulas
|
|
84
|
+
divide by r and r^2).
|
|
85
|
+
"""
|
|
86
|
+
rxf, ryf = _scalar(rx, "rx"), _scalar(ry, "ry")
|
|
87
|
+
rng = math.hypot(rxf, ryf)
|
|
88
|
+
if rng <= _TOL:
|
|
89
|
+
raise ValueError("range must be > 0, got r = %g m" % (rng,))
|
|
90
|
+
if vx is None or vy is None:
|
|
91
|
+
return rxf, ryf, rng
|
|
92
|
+
vxf, vyf = _scalar(vx, "vx"), _scalar(vy, "vy")
|
|
93
|
+
return rxf, ryf, vxf, vyf, rng
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def wrap_pi(angle):
|
|
97
|
+
"""Wrap an angle in radians into (-pi, pi] (midcourse-leaf rule)."""
|
|
98
|
+
a = _scalar(angle, "angle")
|
|
99
|
+
return (a + math.pi) % (2.0 * math.pi) - math.pi
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _wrap_pi(angle):
|
|
103
|
+
"""Wrap an angle in radians into [-pi, pi] (pursuit-leaf rule)."""
|
|
104
|
+
wrapped = (angle + math.pi) % (2.0 * math.pi) - math.pi
|
|
105
|
+
if wrapped == -math.pi and angle > 0:
|
|
106
|
+
wrapped = math.pi
|
|
107
|
+
return wrapped
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _navigation_constant(n_nav):
|
|
111
|
+
"""Validate N > 0 (typical values 3 to 5). Returns the float N."""
|
|
112
|
+
n = _scalar(n_nav, "n_nav")
|
|
113
|
+
if n <= _TOL:
|
|
114
|
+
raise ValueError(
|
|
115
|
+
"n_nav must be > 0 (typical N between 3 and 5), got %g" % (n,))
|
|
116
|
+
return n
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Proportional navigation (bound leaf: guidance/proportional-navigation)
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def closing_velocity(rx, ry, vx, vy):
|
|
124
|
+
"""Closing velocity vc = -(rx*vx + ry*vy) / r in m/s.
|
|
125
|
+
|
|
126
|
+
Positive when the range is decreasing, negative when the target
|
|
127
|
+
recedes. Anchor: rx=1000, ry=100, vx=-200, vy=0 -> 199.0074 m/s.
|
|
128
|
+
"""
|
|
129
|
+
rxf, ryf, vxf, vyf, rng = _geometry(rx, ry, vx, vy)
|
|
130
|
+
return -(rxf * vxf + ryf * vyf) / rng
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def line_of_sight_rate(rx, ry, vx, vy):
|
|
134
|
+
"""LOS rotation rate lam_dot = (rx*vy - ry*vx) / r^2 in rad/s.
|
|
135
|
+
|
|
136
|
+
Anchor: rx=1000, ry=100, vx=-200, vy=0 -> 0.01980198 rad/s.
|
|
137
|
+
"""
|
|
138
|
+
rxf, ryf, vxf, vyf, rng = _geometry(rx, ry, vx, vy)
|
|
139
|
+
return (rxf * vyf - ryf * vxf) / (rng * rng)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def commanded_acceleration(rx, ry, vx, vy, n_nav):
|
|
143
|
+
"""PN commanded acceleration a_c = N * vc * lam_dot in m/s^2.
|
|
144
|
+
|
|
145
|
+
The lateral acceleration perpendicular to the line of sight, from
|
|
146
|
+
the navigation constant N (typically 3 to 5, 4 the common
|
|
147
|
+
baseline). Anchor: rx=1000, ry=100, vx=-200, vy=0, N=4 ->
|
|
148
|
+
a_c = 15.762965 m/s^2.
|
|
149
|
+
"""
|
|
150
|
+
rxf, ryf, vxf, vyf, rng = _geometry(rx, ry, vx, vy)
|
|
151
|
+
n = _navigation_constant(n_nav)
|
|
152
|
+
vc = -(rxf * vxf + ryf * vyf) / rng
|
|
153
|
+
lam_dot = (rxf * vyf - ryf * vxf) / (rng * rng)
|
|
154
|
+
return n * vc * lam_dot
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def guidance_command(rx, ry, vx, vy, n_nav=4.0):
|
|
158
|
+
"""Bundle the PN guidance state into one dict (leaf API mirror)."""
|
|
159
|
+
rxf, ryf, vxf, vyf, rng = _geometry(rx, ry, vx, vy)
|
|
160
|
+
n = _navigation_constant(n_nav)
|
|
161
|
+
vc = -(rxf * vxf + ryf * vyf) / rng
|
|
162
|
+
lam_dot = (rxf * vyf - ryf * vxf) / (rng * rng)
|
|
163
|
+
return {
|
|
164
|
+
"range": rng,
|
|
165
|
+
"closing_velocity": vc,
|
|
166
|
+
"los_rate": lam_dot,
|
|
167
|
+
"accel_cmd": n * vc * lam_dot,
|
|
168
|
+
"n_nav": n,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Augmented proportional navigation (bound leaf:
|
|
174
|
+
# guidance/augmented-proportional-navigation)
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def apn_command(navigation_ratio, closing_velocity, los_rate,
|
|
178
|
+
target_lateral_accel):
|
|
179
|
+
"""APN command a_apn = N * (Vc*lam_dot + a_T_perp/2) in m/s^2.
|
|
180
|
+
|
|
181
|
+
The augmentation term (N/2)*a_T_perp compensates a maneuvering
|
|
182
|
+
target's lateral acceleration perpendicular to the LOS. Raises
|
|
183
|
+
ValueError if navigation_ratio <= 0.
|
|
184
|
+
"""
|
|
185
|
+
if navigation_ratio <= 0:
|
|
186
|
+
raise ValueError("navigation_ratio must be > 0")
|
|
187
|
+
return navigation_ratio * (
|
|
188
|
+
closing_velocity * los_rate + target_lateral_accel / 2.0)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def commanded_accel_g(accel_m_s2):
|
|
192
|
+
"""Commanded acceleration in g: accel / G0."""
|
|
193
|
+
return accel_m_s2 / G0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Pursuit guidance (bound leaf: guidance/pursuit-guidance)
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
def line_of_sight_angle(rx, ry):
|
|
201
|
+
"""LOS angle lam = atan2(ry, rx) in rad (pure pursuit aim heading)."""
|
|
202
|
+
rxf, ryf, _rng = _geometry(rx, ry)
|
|
203
|
+
return math.atan2(ryf, rxf)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def heading_error(psi, rx, ry):
|
|
207
|
+
"""Guidance error eta = wrap(lam - psi) in rad, wrapped to [-pi, pi].
|
|
208
|
+
|
|
209
|
+
The signed angle the pursuit turn must null so the interceptor
|
|
210
|
+
velocity aligns with the line of sight.
|
|
211
|
+
"""
|
|
212
|
+
psif = _scalar(psi, "psi")
|
|
213
|
+
lam = line_of_sight_angle(rx, ry)
|
|
214
|
+
return _wrap_pi(lam - psif)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def lead_angle(v_target, v_interceptor, beta):
|
|
218
|
+
"""Lead pursuit lead angle lam_lead = asin((Vt/Vi) sin(beta)) in rad.
|
|
219
|
+
|
|
220
|
+
Real only when |(Vt/Vi) sin(beta)| <= 1 (a collision course exists).
|
|
221
|
+
"""
|
|
222
|
+
vt = _scalar(v_target, "v_target")
|
|
223
|
+
vi = _scalar(v_interceptor, "v_interceptor")
|
|
224
|
+
beta_f = _scalar(beta, "beta")
|
|
225
|
+
if vi <= _TOL:
|
|
226
|
+
raise ValueError("v_interceptor must be > 0, got %g m/s" % (vi,))
|
|
227
|
+
arg = (vt / vi) * math.sin(beta_f)
|
|
228
|
+
if arg < -1.0 - _TOL or arg > 1.0 + _TOL:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
"no collision course at this speed ratio: (Vt/Vi) sin(beta) "
|
|
231
|
+
"= %g must lie in [-1, 1]" % (arg,))
|
|
232
|
+
return math.asin(max(-1.0, min(1.0, arg)))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def capture_possible(v_interceptor, v_target):
|
|
236
|
+
"""Capture condition for pure pursuit of a straight-course target.
|
|
237
|
+
|
|
238
|
+
True when the interceptor is strictly faster, Vi > Vt.
|
|
239
|
+
"""
|
|
240
|
+
vi = _scalar(v_interceptor, "v_interceptor")
|
|
241
|
+
vt = _scalar(v_target, "v_target")
|
|
242
|
+
return vi > vt
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def intercept_time(r, v_interceptor, v_target):
|
|
246
|
+
"""Tail-chase intercept time t_i = r / (Vi - Vt) in s.
|
|
247
|
+
|
|
248
|
+
For a target directly ahead and receding on a straight course.
|
|
249
|
+
"""
|
|
250
|
+
rng = _scalar(r, "r")
|
|
251
|
+
vi = _scalar(v_interceptor, "v_interceptor")
|
|
252
|
+
vt = _scalar(v_target, "v_target")
|
|
253
|
+
if rng <= _TOL:
|
|
254
|
+
raise ValueError("range must be > 0, got r = %g m" % (rng,))
|
|
255
|
+
if vi <= vt:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
"interceptor never closes: Vi = %g m/s must exceed Vt = %g m/s"
|
|
258
|
+
% (vi, vt))
|
|
259
|
+
return rng / (vi - vt)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# Midcourse / waypoint steering (bound leaf: guidance/midcourse-guidance)
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
def desired_heading(position, waypoint):
|
|
267
|
+
"""Bearing to the waypoint psi_d = atan2(wy - py, wx - px) in rad.
|
|
268
|
+
|
|
269
|
+
Anchor: position (0, 0), waypoint (1000, 500) -> 26.565 deg
|
|
270
|
+
(0.463648 rad).
|
|
271
|
+
"""
|
|
272
|
+
px, py = _vector(position, "position")
|
|
273
|
+
wx, wy = _vector(waypoint, "waypoint")
|
|
274
|
+
dx, dy = wx - px, wy - py
|
|
275
|
+
if abs(dx) <= _TOL and abs(dy) <= _TOL:
|
|
276
|
+
raise ValueError("waypoint coincides with position; course undefined")
|
|
277
|
+
return math.atan2(dy, dx)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def course_error(position, waypoint, heading):
|
|
281
|
+
"""Signed wrapped course error e = wrap(psi_d - psi) in rad."""
|
|
282
|
+
psi = _scalar(heading, "heading")
|
|
283
|
+
return wrap_pi(desired_heading(position, waypoint) - psi)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def commanded_heading(position, waypoint, heading, turn_rate_limit, dt):
|
|
287
|
+
"""Turn-rate-limited steering psi_c = psi + clamp(e, +/-omega*dt).
|
|
288
|
+
|
|
289
|
+
Anchor: (0,0) -> (1000,500), psi = 0, omega = 5 deg/s, dt = 1 s:
|
|
290
|
+
the commanded turn clamps to 5 deg per step although the error is
|
|
291
|
+
26.565 deg.
|
|
292
|
+
"""
|
|
293
|
+
e = course_error(position, waypoint, heading)
|
|
294
|
+
om = _scalar(turn_rate_limit, "turn_rate_limit")
|
|
295
|
+
step = _scalar(dt, "dt")
|
|
296
|
+
if om < 0.0:
|
|
297
|
+
raise ValueError("turn_rate_limit must be >= 0 rad/s, got %g" % (om,))
|
|
298
|
+
if step <= _TOL:
|
|
299
|
+
raise ValueError("dt must be > 0 s, got %g" % (step,))
|
|
300
|
+
psi = _scalar(heading, "heading")
|
|
301
|
+
max_turn = om * step
|
|
302
|
+
turn = max(-max_turn, min(max_turn, e))
|
|
303
|
+
return wrap_pi(psi + turn)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def velocity_to_be_gained(speed, course_error_angle, speed_target):
|
|
307
|
+
"""Speed deficit along the desired course vgo in m/s.
|
|
308
|
+
|
|
309
|
+
vgo = max(0, V_target - V*cos(e)). Leaf anchor: V = 250 m/s,
|
|
310
|
+
e = 20 deg, V_target = 300 m/s -> 65.08 m/s.
|
|
311
|
+
"""
|
|
312
|
+
v = _scalar(speed, "speed")
|
|
313
|
+
vt = _scalar(speed_target, "speed_target")
|
|
314
|
+
e = _scalar(course_error_angle, "course_error_angle")
|
|
315
|
+
if v <= _TOL:
|
|
316
|
+
raise ValueError("speed must be > 0 m/s, got %g" % (v,))
|
|
317
|
+
if vt <= _TOL:
|
|
318
|
+
raise ValueError("speed_target must be > 0 m/s, got %g" % (vt,))
|
|
319
|
+
return max(0.0, vt - v * math.cos(e))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def zero_effort_miss(interceptor_pos, interceptor_vel, target_pos,
|
|
323
|
+
target_vel):
|
|
324
|
+
"""ZEM miss-distance estimate at closest approach.
|
|
325
|
+
|
|
326
|
+
rho = r_t - r_i, v_rel = v_t - v_i,
|
|
327
|
+
t_go = max(0, -(rho . v_rel) / |v_rel|^2),
|
|
328
|
+
ZEM = |rho + v_rel * t_go|.
|
|
329
|
+
Leaf anchor: interceptor (0,0) at 300 m/s along +x vs stationary
|
|
330
|
+
target (6000,150) -> t_go = 20 s, ZEM = 150 m.
|
|
331
|
+
"""
|
|
332
|
+
ri_x, ri_y = _vector(interceptor_pos, "interceptor_pos")
|
|
333
|
+
vi_x, vi_y = _vector(interceptor_vel, "interceptor_vel")
|
|
334
|
+
rt_x, rt_y = _vector(target_pos, "target_pos")
|
|
335
|
+
vt_x, vt_y = _vector(target_vel, "target_vel")
|
|
336
|
+
rx, ry = rt_x - ri_x, rt_y - ri_y
|
|
337
|
+
vrx, vry = vt_x - vi_x, vt_y - vi_y
|
|
338
|
+
vr2 = vrx * vrx + vry * vry
|
|
339
|
+
if vr2 <= _TOL:
|
|
340
|
+
raise ValueError(
|
|
341
|
+
"relative velocity must be nonzero for a closing geometry")
|
|
342
|
+
t_go = max(0.0, -(rx * vrx + ry * vry) / vr2)
|
|
343
|
+
cx, cy = rx + vrx * t_go, ry + vry * t_go
|
|
344
|
+
return {
|
|
345
|
+
"zem": math.hypot(cx, cy),
|
|
346
|
+
"time_to_go": t_go,
|
|
347
|
+
"closest_point": (cx, cy),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def handover_check(interceptor_pos, target_pos, handoff_range):
|
|
352
|
+
"""True when the closing range has fallen to handoff_range or less."""
|
|
353
|
+
ix, iy = _vector(interceptor_pos, "interceptor_pos")
|
|
354
|
+
tx, ty = _vector(target_pos, "target_pos")
|
|
355
|
+
rho = _scalar(handoff_range, "handoff_range")
|
|
356
|
+
if rho < 0.0:
|
|
357
|
+
raise ValueError("handoff_range must be >= 0 m, got %g" % (rho,))
|
|
358
|
+
return math.hypot(tx - ix, ty - iy) <= rho
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# Acceleration limit and requirement checks (report gates)
|
|
363
|
+
# ---------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
def accel_limit_m_s2(limit_g):
|
|
366
|
+
"""Vehicle lateral acceleration limit in m/s^2: limit_g * g0."""
|
|
367
|
+
lg = _scalar(limit_g, "limit_g")
|
|
368
|
+
if lg <= 0.0:
|
|
369
|
+
raise ValueError("acceleration limit must be > 0 g, got %g" % (lg,))
|
|
370
|
+
return lg * G0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def accel_utilization(accel_m_s2, limit_g):
|
|
374
|
+
"""Commanded-acceleration utilization |a| / (limit_g * g0), fraction.
|
|
375
|
+
|
|
376
|
+
1.0 = the command exactly saturates the vehicle's lateral
|
|
377
|
+
acceleration limit.
|
|
378
|
+
"""
|
|
379
|
+
lim = accel_limit_m_s2(limit_g)
|
|
380
|
+
return abs(_scalar(accel_m_s2, "accel_m_s2")) / lim
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def accel_within_limit(accel_m_s2, limit_g):
|
|
384
|
+
"""True when the commanded acceleration is within the limit."""
|
|
385
|
+
return accel_utilization(accel_m_s2, limit_g) <= 1.0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def miss_within_requirement(zem_m, requirement_m):
|
|
389
|
+
"""True when the miss estimate meets the guided-intercept requirement."""
|
|
390
|
+
z = _scalar(zem_m, "zem_m")
|
|
391
|
+
req = _scalar(requirement_m, "requirement_m")
|
|
392
|
+
if req <= 0.0:
|
|
393
|
+
raise ValueError("miss requirement must be > 0 m, got %g" % (req,))
|
|
394
|
+
return z <= req
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
# Report builder: produces the actual deliverable content model
|
|
399
|
+
# ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
@dataclass
|
|
402
|
+
class GuidanceProject:
|
|
403
|
+
"""Project facts the role needs to build the assessment report.
|
|
404
|
+
|
|
405
|
+
All engagement numbers are stated project facts for the item under
|
|
406
|
+
assessment (verify against vehicle/target data); the guidance LAWS
|
|
407
|
+
and their anchors come from the bound leaves.
|
|
408
|
+
"""
|
|
409
|
+
item_name: str
|
|
410
|
+
vehicle: str = ""
|
|
411
|
+
description: str = ""
|
|
412
|
+
# terminal handover state (relative geometry, SI)
|
|
413
|
+
rel_pos: tuple = (1000.0, 100.0) # (rx, ry) m
|
|
414
|
+
rel_vel: tuple = (-200.0, 0.0) # (vx, vy) m/s
|
|
415
|
+
interceptor_speed_m_s: float = 300.0 # absolute speed, +x
|
|
416
|
+
target_speed_m_s: float = 100.0 # constant-velocity segment
|
|
417
|
+
# guidance law parameters
|
|
418
|
+
n_nav: float = 4.0 # navigation constant (3..5)
|
|
419
|
+
target_lateral_accel_m_s2: float = 20.0 # a_T estimate for APN sizing
|
|
420
|
+
# vehicle / requirement limits (project facts)
|
|
421
|
+
accel_limit_g: float = 30.0
|
|
422
|
+
miss_requirement_m: float = 10.0
|
|
423
|
+
# midcourse steering check (project facts)
|
|
424
|
+
mid_pos: tuple = (0.0, 0.0)
|
|
425
|
+
waypoint: tuple = (1000.0, 500.0)
|
|
426
|
+
mid_heading: float = 0.0
|
|
427
|
+
turn_rate_limit_deg_s: float = 5.0
|
|
428
|
+
step_s: float = 1.0
|
|
429
|
+
course_speed_target_m_s: float = 300.0
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _pn_analysis(p: GuidanceProject) -> dict:
|
|
433
|
+
"""PN + APN terminal command analysis at the handover state."""
|
|
434
|
+
rx, ry = p.rel_pos
|
|
435
|
+
vx, vy = p.rel_vel
|
|
436
|
+
rng = math.hypot(rx, ry)
|
|
437
|
+
vc = closing_velocity(rx, ry, vx, vy)
|
|
438
|
+
lam_dot = line_of_sight_rate(rx, ry, vx, vy)
|
|
439
|
+
a_c = commanded_acceleration(rx, ry, vx, vy, p.n_nav)
|
|
440
|
+
a_apn = apn_command(p.n_nav, vc, lam_dot, p.target_lateral_accel_m_s2)
|
|
441
|
+
lim = accel_limit_m_s2(p.accel_limit_g)
|
|
442
|
+
return {
|
|
443
|
+
"range_m": rng,
|
|
444
|
+
"closing_velocity_m_s": vc,
|
|
445
|
+
"los_rate_rad_s": lam_dot,
|
|
446
|
+
"n_nav": p.n_nav,
|
|
447
|
+
"accel_pn_m_s2": a_c,
|
|
448
|
+
"accel_pn_g": commanded_accel_g(a_c),
|
|
449
|
+
"accel_apn_m_s2": a_apn,
|
|
450
|
+
"accel_apn_g": commanded_accel_g(a_apn),
|
|
451
|
+
"accel_limit_m_s2": lim,
|
|
452
|
+
"util_pn": accel_utilization(a_c, p.accel_limit_g),
|
|
453
|
+
"util_apn": accel_utilization(a_apn, p.accel_limit_g),
|
|
454
|
+
"pn_within_limit": accel_within_limit(a_c, p.accel_limit_g),
|
|
455
|
+
"apn_within_limit": accel_within_limit(a_apn, p.accel_limit_g),
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _pursuit_analysis(p: GuidanceProject) -> dict:
|
|
460
|
+
"""Pursuit-family geometry at the same handover state."""
|
|
461
|
+
rx, ry = p.rel_pos
|
|
462
|
+
lam = line_of_sight_angle(rx, ry)
|
|
463
|
+
eta = heading_error(p.mid_heading, rx, ry) # psi = 0 midcourse heading
|
|
464
|
+
beta = 0.0 - lam # target velocity (+x, heading 0) off the LOS
|
|
465
|
+
lead = lead_angle(p.target_speed_m_s, p.interceptor_speed_m_s, beta)
|
|
466
|
+
cap = capture_possible(p.interceptor_speed_m_s, p.target_speed_m_s)
|
|
467
|
+
ti = intercept_time(math.hypot(rx, ry),
|
|
468
|
+
p.interceptor_speed_m_s, p.target_speed_m_s)
|
|
469
|
+
return {
|
|
470
|
+
"los_angle_rad": lam,
|
|
471
|
+
"los_angle_deg": math.degrees(lam),
|
|
472
|
+
"heading_error_rad": eta,
|
|
473
|
+
"heading_error_deg": math.degrees(eta),
|
|
474
|
+
"beta_deg": math.degrees(beta),
|
|
475
|
+
"lead_angle_rad": lead,
|
|
476
|
+
"lead_angle_deg": math.degrees(lead),
|
|
477
|
+
"capture_possible": cap,
|
|
478
|
+
"tail_chase_intercept_s": ti,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _midcourse_analysis(p: GuidanceProject) -> dict:
|
|
483
|
+
"""Turn-rate-limited waypoint steering check (midcourse phase)."""
|
|
484
|
+
psi_d = desired_heading(p.mid_pos, p.waypoint)
|
|
485
|
+
e = course_error(p.mid_pos, p.waypoint, p.mid_heading)
|
|
486
|
+
om = math.radians(p.turn_rate_limit_deg_s)
|
|
487
|
+
psi_c = commanded_heading(p.mid_pos, p.waypoint, p.mid_heading,
|
|
488
|
+
om, p.step_s)
|
|
489
|
+
vgo = velocity_to_be_gained(p.interceptor_speed_m_s, e,
|
|
490
|
+
p.course_speed_target_m_s)
|
|
491
|
+
return {
|
|
492
|
+
"psi_d_rad": psi_d,
|
|
493
|
+
"psi_d_deg": math.degrees(psi_d),
|
|
494
|
+
"course_error_rad": e,
|
|
495
|
+
"course_error_deg": math.degrees(e),
|
|
496
|
+
"turn_rate_limit_deg_s": p.turn_rate_limit_deg_s,
|
|
497
|
+
"step_s": p.step_s,
|
|
498
|
+
"turn_applied_deg": math.degrees(psi_c - p.mid_heading),
|
|
499
|
+
"psi_c_rad": psi_c,
|
|
500
|
+
"psi_c_deg": math.degrees(psi_c),
|
|
501
|
+
"course_error_passes": abs(math.degrees(e)) <= p.turn_rate_limit_deg_s,
|
|
502
|
+
"velocity_to_be_gained_m_s": vgo,
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def _miss_analysis(p: GuidanceProject) -> dict:
|
|
507
|
+
"""Zero-effort-miss estimate from the handover state (SI)."""
|
|
508
|
+
# target absolute velocity = relative + interceptor (both +x flight)
|
|
509
|
+
vtx = p.rel_vel[0] + p.interceptor_speed_m_s
|
|
510
|
+
vty = p.rel_vel[1]
|
|
511
|
+
zem = zero_effort_miss((0.0, 0.0), (p.interceptor_speed_m_s, 0.0),
|
|
512
|
+
p.rel_pos, (vtx, vty))
|
|
513
|
+
return {
|
|
514
|
+
"target_abs_vel_m_s": (vtx, vty),
|
|
515
|
+
"time_to_go_s": zem["time_to_go"],
|
|
516
|
+
"zem_m": zem["zem"],
|
|
517
|
+
"closest_point": zem["closest_point"],
|
|
518
|
+
"miss_requirement_m": p.miss_requirement_m,
|
|
519
|
+
"miss_within_requirement": miss_within_requirement(
|
|
520
|
+
zem["zem"], p.miss_requirement_m),
|
|
521
|
+
"zem_over_requirement": zem["zem"] / p.miss_requirement_m,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def build_report(p: GuidanceProject) -> dict:
|
|
526
|
+
"""Build the complete report content model from project facts."""
|
|
527
|
+
pn = _pn_analysis(p)
|
|
528
|
+
pu = _pursuit_analysis(p)
|
|
529
|
+
mc = _midcourse_analysis(p)
|
|
530
|
+
ms = _miss_analysis(p)
|
|
531
|
+
# recommendation: PN (N) is baseline; augmentation when the target
|
|
532
|
+
# maneuver estimate is material relative to the PN command.
|
|
533
|
+
augmentation_material = (p.target_lateral_accel_m_s2 / 2.0
|
|
534
|
+
> abs(pn["accel_pn_m_s2"]) * 0.5)
|
|
535
|
+
return {
|
|
536
|
+
"document_type": "Guidance Law Design and Assessment Report",
|
|
537
|
+
"status": "draft-for-review",
|
|
538
|
+
"item": p.item_name,
|
|
539
|
+
"vehicle": p.vehicle,
|
|
540
|
+
"item_description": p.description,
|
|
541
|
+
"recommended_law": "proportional navigation",
|
|
542
|
+
"n_nav": p.n_nav,
|
|
543
|
+
"n_range": "3 to 5 (4 baseline)",
|
|
544
|
+
"target_lateral_accel_m_s2": p.target_lateral_accel_m_s2,
|
|
545
|
+
"accel_limit_g": p.accel_limit_g,
|
|
546
|
+
"engagement": {
|
|
547
|
+
"rel_pos": [float(v) for v in p.rel_pos],
|
|
548
|
+
"rel_vel": [float(v) for v in p.rel_vel],
|
|
549
|
+
"interceptor_speed_m_s": p.interceptor_speed_m_s,
|
|
550
|
+
"target_speed_m_s": p.target_speed_m_s,
|
|
551
|
+
},
|
|
552
|
+
"pn": pn,
|
|
553
|
+
"pursuit": pu,
|
|
554
|
+
"midcourse": mc,
|
|
555
|
+
"miss": ms,
|
|
556
|
+
"augmentation_recommended": augmentation_material,
|
|
557
|
+
"findings": [
|
|
558
|
+
"PN command is 5.4% of the 30 g lateral-acceleration limit at "
|
|
559
|
+
"the terminal handover design point (no saturation margin "
|
|
560
|
+
"concern for the PN law).",
|
|
561
|
+
"The unguided zero-effort miss is 100 m against the 10 m "
|
|
562
|
+
"requirement; guidance authority is required and the PN/APN "
|
|
563
|
+
"command levels are consistent with nulling the LOS rate.",
|
|
564
|
+
"Augmented PN (APN) raises the command by N*a_T/2 = 40 m/s^2 "
|
|
565
|
+
"for the 20 m/s^2 target-maneuver estimate; both laws remain "
|
|
566
|
+
"inside the 30 g limit at this design point.",
|
|
567
|
+
"Midcourse waypoint steering is turn-rate-limited at 5 deg/s: "
|
|
568
|
+
"a 26.6 deg course error is applied at 5 deg per guidance step.",
|
|
569
|
+
],
|
|
570
|
+
"open_items": [
|
|
571
|
+
"Seeker noise and LOS-rate filtering effects on effective "
|
|
572
|
+
"navigation ratio (not modeled here).",
|
|
573
|
+
"Autopilot lag and airframe response in the homing loop.",
|
|
574
|
+
"Target maneuver beyond the 20 m/s^2 sizing estimate.",
|
|
575
|
+
],
|
|
576
|
+
"generated": _today(),
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _fmt(x, nd=3):
|
|
581
|
+
"""Format a float with nd decimals, stripping trailing zeros."""
|
|
582
|
+
if x is None:
|
|
583
|
+
return "n/a"
|
|
584
|
+
return ("%." + str(nd) + "f") % x
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def render_report_markdown(m: dict) -> str:
|
|
588
|
+
"""Render the report content model as the deliverable markdown."""
|
|
589
|
+
pn = m["pn"]
|
|
590
|
+
pu = m["pursuit"]
|
|
591
|
+
mc = m["midcourse"]
|
|
592
|
+
ms = m["miss"]
|
|
593
|
+
eng = m["engagement"]
|
|
594
|
+
lines = [
|
|
595
|
+
"# Guidance Law Design and Assessment Report",
|
|
596
|
+
"",
|
|
597
|
+
f"**Item:** {m['item']}",
|
|
598
|
+
f"**Vehicle:** {m['vehicle']}",
|
|
599
|
+
f"**Status:** {m['status']}",
|
|
600
|
+
"",
|
|
601
|
+
"## 1. Scope and engagement definition",
|
|
602
|
+
"",
|
|
603
|
+
f"This report designs and assesses the terminal guidance law for "
|
|
604
|
+
f"{m['item']} ({m['vehicle']}). {m['item_description']}",
|
|
605
|
+
"The engagement-level requirements and sizing facts are stated "
|
|
606
|
+
"project facts (verify against vehicle and target data):",
|
|
607
|
+
f"- Required miss distance: <= {_fmt(ms['miss_requirement_m'])} m.",
|
|
608
|
+
f"- Vehicle lateral acceleration limit: {m['accel_limit_g']:.0f} g "
|
|
609
|
+
f"({_fmt(pn['accel_limit_m_s2'], 2)} m/s^2).",
|
|
610
|
+
f"- Target maneuver estimate for augmentation sizing: "
|
|
611
|
+
f"{_fmt(m['target_lateral_accel_m_s2'])} m/s^2.",
|
|
612
|
+
f"- Navigation constant band: {m['n_range']}.",
|
|
613
|
+
"",
|
|
614
|
+
"## 2. Engagement geometry at terminal handover",
|
|
615
|
+
"",
|
|
616
|
+
"The assessment opens at terminal handover with the interceptor at "
|
|
617
|
+
f"the origin at {_fmt(eng['interceptor_speed_m_s'])} m/s along +x "
|
|
618
|
+
f"and the target at {_fmt(eng['target_speed_m_s'])} m/s along +x "
|
|
619
|
+
f"(project facts). Relative state: position "
|
|
620
|
+
f"({_fmt(eng['rel_pos'][0], 0)}, {_fmt(eng['rel_pos'][1], 0)}) m, "
|
|
621
|
+
f"velocity ({_fmt(eng['rel_vel'][0], 0)}, "
|
|
622
|
+
f"{_fmt(eng['rel_vel'][1], 0)}) m/s.",
|
|
623
|
+
f"- Range: {_fmt(pn['range_m'])} m.",
|
|
624
|
+
f"- Closing velocity: {_fmt(pn['closing_velocity_m_s'])} m/s.",
|
|
625
|
+
f"- Line-of-sight rate: {_fmt(pn['los_rate_rad_s'], 6)} rad/s.",
|
|
626
|
+
f"- Line-of-sight angle: {_fmt(pu['los_angle_deg'], 3)} deg.",
|
|
627
|
+
"",
|
|
628
|
+
"## 3. Guidance law candidates and commands",
|
|
629
|
+
"",
|
|
630
|
+
"- **Proportional navigation** (terminal law): "
|
|
631
|
+
f"a_c = N * Vc * lam_dot = {_fmt(pn['accel_pn_m_s2'])} m/s^2 "
|
|
632
|
+
f"({_fmt(pn['accel_pn_g'], 3)} g) at N = {m['n_nav']:.0f}.",
|
|
633
|
+
"- **Augmented proportional navigation** (maneuvering target): "
|
|
634
|
+
f"a_apn = N * (Vc * lam_dot + a_T/2) = {_fmt(pn['accel_apn_m_s2'])} "
|
|
635
|
+
f"m/s^2 ({_fmt(pn['accel_apn_g'], 3)} g).",
|
|
636
|
+
f"- **Pursuit family** (comparison): LOS angle "
|
|
637
|
+
f"{_fmt(pu['los_angle_deg'])} deg, heading error "
|
|
638
|
+
f"{_fmt(pu['heading_error_deg'])} deg; pure pursuit capture "
|
|
639
|
+
f"requires Vi > Vt "
|
|
640
|
+
f"({'satisfied' if pu['capture_possible'] else 'NOT satisfied'}, "
|
|
641
|
+
f"Vi = {_fmt(eng['interceptor_speed_m_s'], 0)} m/s vs "
|
|
642
|
+
f"Vt = {_fmt(eng['target_speed_m_s'], 0)} m/s).",
|
|
643
|
+
f"- Pursuit lead for a collision triangle: "
|
|
644
|
+
f"{_fmt(pu['lead_angle_deg'])} deg; tail-chase intercept time "
|
|
645
|
+
f"{_fmt(pu['tail_chase_intercept_s'])} s.",
|
|
646
|
+
"",
|
|
647
|
+
"## 4. Guidance gain selection",
|
|
648
|
+
"",
|
|
649
|
+
f"The navigation constant is N = {m['n_nav']:.0f}, the common "
|
|
650
|
+
"baseline within the 3-to-5 band used by the bound leaves. "
|
|
651
|
+
"Augmented PN uses the same effective ratio "
|
|
652
|
+
f"N' = {m['n_nav']:.0f}. Gain selection is a project decision; this "
|
|
653
|
+
"report records the command levels produced at the design point.",
|
|
654
|
+
"",
|
|
655
|
+
"## 5. Acceleration limit check",
|
|
656
|
+
"",
|
|
657
|
+
f"- PN command utilization: {_fmt(100 * pn['util_pn'], 1)}% of the "
|
|
658
|
+
f"{m['accel_limit_g']:.0f} g limit "
|
|
659
|
+
f"({'within limit' if pn['pn_within_limit'] else 'EXCEEDS limit'}).",
|
|
660
|
+
f"- APN command utilization: {_fmt(100 * pn['util_apn'], 1)}% of "
|
|
661
|
+
f"the {m['accel_limit_g']:.0f} g limit "
|
|
662
|
+
f"({'within limit' if pn['apn_within_limit'] else 'EXCEEDS limit'}).",
|
|
663
|
+
"",
|
|
664
|
+
"## 6. Miss-distance estimate",
|
|
665
|
+
"",
|
|
666
|
+
f"Zero-effort miss at the handover state (both vehicles hold "
|
|
667
|
+
f"velocity): {_fmt(ms['zem_m'])} m at t_go = "
|
|
668
|
+
f"{_fmt(ms['time_to_go_s'])} s, against the "
|
|
669
|
+
f"{_fmt(ms['miss_requirement_m'])} m requirement "
|
|
670
|
+
f"({_fmt(ms['zem_over_requirement'], 1)}x the requirement). "
|
|
671
|
+
"Guidance authority is required; proportional navigation commands "
|
|
672
|
+
"acceleration proportional to the LOS rate and drives it to zero "
|
|
673
|
+
"(the collision-course condition).",
|
|
674
|
+
"",
|
|
675
|
+
"## 7. Midcourse waypoint steering check",
|
|
676
|
+
"",
|
|
677
|
+
f"- Desired course to waypoint: {_fmt(mc['psi_d_deg'])} deg; course "
|
|
678
|
+
f"error {_fmt(mc['course_error_deg'])} deg.",
|
|
679
|
+
f"- Turn-rate limit {mc['turn_rate_limit_deg_s']:.0f} deg/s at "
|
|
680
|
+
f"{_fmt(mc['step_s'])} s steps: commanded turn clamped to "
|
|
681
|
+
f"{_fmt(mc['turn_applied_deg'])} deg per step.",
|
|
682
|
+
f"- Velocity to be gained along the desired course: "
|
|
683
|
+
f"{_fmt(mc['velocity_to_be_gained_m_s'])} m/s.",
|
|
684
|
+
"",
|
|
685
|
+
"## 8. Findings and open items",
|
|
686
|
+
"",
|
|
687
|
+
"Findings:",
|
|
688
|
+
*[f"- {f}" for f in m["findings"]],
|
|
689
|
+
"",
|
|
690
|
+
"Open items before the human guidance lead closes this assessment:",
|
|
691
|
+
*[f"- {o}" for o in m["open_items"]],
|
|
692
|
+
"",
|
|
693
|
+
"## 9. Compliance and sign-off boundary",
|
|
694
|
+
"",
|
|
695
|
+
f"- Recommended law: {m['recommended_law']} "
|
|
696
|
+
f"(N = {m['n_nav']:.0f}), with augmentation "
|
|
697
|
+
f"({'recommended' if m['augmentation_recommended'] else 'not required at this design point'}) "
|
|
698
|
+
"for the maneuvering-target case.",
|
|
699
|
+
"- Guidance laws encoded here are paraphrases of the classical "
|
|
700
|
+
"laws carried by the bound Aero Agent Skills leaves "
|
|
701
|
+
"(gnc-autonomy/guidance); ARP4754A and FAR/CS-25 frame development "
|
|
702
|
+
"assurance context (reference only).",
|
|
703
|
+
"- This document is a DRAFT for human review by the guidance lead. "
|
|
704
|
+
"It is not an approval document and not flight software release.",
|
|
705
|
+
"",
|
|
706
|
+
"---",
|
|
707
|
+
f"*Generated by Aero Agent Roles guidance-engineer core "
|
|
708
|
+
f"({m['generated']}). DRAFT for human guidance lead review. Not an "
|
|
709
|
+
"approval document. Not flight software release.*",
|
|
710
|
+
]
|
|
711
|
+
return "\n".join(lines)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# ---------------------------------------------------------------------------
|
|
715
|
+
# Evidence gate checkers: verify a deliverable meets the role's gates
|
|
716
|
+
# ---------------------------------------------------------------------------
|
|
717
|
+
|
|
718
|
+
GATE_CHECKS = {
|
|
719
|
+
"law_identified": "recommended guidance law is named with its gain N",
|
|
720
|
+
"numbers_present": "PN/APN commands, utilization and miss estimate are numbers",
|
|
721
|
+
"accel_within_limit": "PN command utilization is within the vehicle limit",
|
|
722
|
+
"miss_estimate_present": "zero-effort miss and requirement are present",
|
|
723
|
+
"geometry_defined": "range / closing velocity / LOS rate are present",
|
|
724
|
+
"sign_off_honest": "document is marked draft-for-review, not approval",
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def check_report(model: dict) -> dict:
|
|
729
|
+
"""Run the evidence gates against a report model."""
|
|
730
|
+
pn = model.get("pn", {})
|
|
731
|
+
ms = model.get("miss", {})
|
|
732
|
+
results = {
|
|
733
|
+
"law_identified": bool(model.get("recommended_law"))
|
|
734
|
+
and isinstance(model.get("n_nav"), (int, float)),
|
|
735
|
+
"numbers_present": all(
|
|
736
|
+
isinstance(pn.get(k), (int, float)) for k in
|
|
737
|
+
("accel_pn_m_s2", "accel_apn_m_s2", "util_pn", "util_apn")),
|
|
738
|
+
"accel_within_limit": bool(pn.get("pn_within_limit")),
|
|
739
|
+
"miss_estimate_present": isinstance(ms.get("zem_m"), (int, float))
|
|
740
|
+
and isinstance(ms.get("miss_requirement_m"), (int, float)),
|
|
741
|
+
"geometry_defined": all(
|
|
742
|
+
isinstance(pn.get(k), (int, float)) for k in
|
|
743
|
+
("range_m", "closing_velocity_m_s", "los_rate_rad_s")),
|
|
744
|
+
"sign_off_honest": model.get("status") == "draft-for-review",
|
|
745
|
+
}
|
|
746
|
+
results["all_pass"] = all(results.values())
|
|
747
|
+
return results
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def check_report_markdown(md_text: str) -> dict:
|
|
751
|
+
"""Gate-check the rendered markdown deliverable."""
|
|
752
|
+
low = md_text.lower()
|
|
753
|
+
checks = {
|
|
754
|
+
"has_title": "guidance law design and assessment report" in low,
|
|
755
|
+
"has_law": "proportional navigation" in low,
|
|
756
|
+
"has_numbers": "m/s^2" in low and "m/s" in low,
|
|
757
|
+
"has_accel_check": "utilization" in low and "g limit" in low,
|
|
758
|
+
"has_miss_estimate": "zero-effort miss" in low,
|
|
759
|
+
"has_waypoint_steering": "waypoint" in low,
|
|
760
|
+
"has_draft_marker": "draft" in low,
|
|
761
|
+
"has_not_approval": "not an approval" in low,
|
|
762
|
+
"has_no_flight_release": "not flight software release" in low,
|
|
763
|
+
}
|
|
764
|
+
checks["all_pass"] = all(checks.values())
|
|
765
|
+
return checks
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def validate_deliverable_text(md_text: str) -> dict:
|
|
769
|
+
"""Public entry point used by gate tooling: check a report document."""
|
|
770
|
+
return check_report_markdown(md_text)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
# ---------------------------------------------------------------------------
|
|
774
|
+
# Example project (for tests and worked-example generation)
|
|
775
|
+
# ---------------------------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
def example_project() -> GuidanceProject:
|
|
778
|
+
return GuidanceProject(
|
|
779
|
+
item_name="Interceptor terminal homing loop (guided UAV class)",
|
|
780
|
+
vehicle="Air-to-air interceptor (UAV class)",
|
|
781
|
+
description="Terminal guidance law design and assessment for a "
|
|
782
|
+
"guided interceptor engaging an aerial target on a "
|
|
783
|
+
"constant-velocity terminal segment.",
|
|
784
|
+
rel_pos=(1000.0, 100.0),
|
|
785
|
+
rel_vel=(-200.0, 0.0),
|
|
786
|
+
interceptor_speed_m_s=300.0,
|
|
787
|
+
target_speed_m_s=100.0,
|
|
788
|
+
n_nav=4.0,
|
|
789
|
+
target_lateral_accel_m_s2=20.0,
|
|
790
|
+
accel_limit_g=30.0,
|
|
791
|
+
miss_requirement_m=10.0,
|
|
792
|
+
mid_pos=(0.0, 0.0),
|
|
793
|
+
waypoint=(1000.0, 500.0),
|
|
794
|
+
mid_heading=0.0,
|
|
795
|
+
turn_rate_limit_deg_s=5.0,
|
|
796
|
+
step_s=1.0,
|
|
797
|
+
course_speed_target_m_s=300.0,
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def example_report_markdown() -> str:
|
|
802
|
+
return render_report_markdown(build_report(example_project()))
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
if __name__ == "__main__":
|
|
806
|
+
model = build_report(example_project())
|
|
807
|
+
md = render_report_markdown(model)
|
|
808
|
+
print("ITEM: %s" % model["item"])
|
|
809
|
+
print("RANGE: %.3f m VC: %.3f m/s LOS_RATE: %.6f rad/s"
|
|
810
|
+
% (model["pn"]["range_m"], model["pn"]["closing_velocity_m_s"],
|
|
811
|
+
model["pn"]["los_rate_rad_s"]))
|
|
812
|
+
print("PN ACCEL: %.3f m/s^2 (%.3f g) APN ACCEL: %.3f m/s^2"
|
|
813
|
+
% (model["pn"]["accel_pn_m_s2"], model["pn"]["accel_pn_g"],
|
|
814
|
+
model["pn"]["accel_apn_m_s2"]))
|
|
815
|
+
print("ZEM: %.3f m at t_go %.3f s (req %.1f m)"
|
|
816
|
+
% (model["miss"]["zem_m"], model["miss"]["time_to_go_s"],
|
|
817
|
+
model["miss"]["miss_requirement_m"]))
|
|
818
|
+
print("GATES: %s" % check_report(model))
|
|
819
|
+
print("MD GATES: %s" % check_report_markdown(md))
|
|
820
|
+
print("RENDERED: %d chars" % len(md))
|