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.
Files changed (170) hide show
  1. package/NOTICE +18 -0
  2. package/README.md +49 -0
  3. package/bin/aero-agent-roles.js +127 -0
  4. package/lib/catalog.js +97 -0
  5. package/lib/install.js +48 -0
  6. package/lib/mcp.js +199 -0
  7. package/manifest.json +516 -0
  8. package/package.json +44 -0
  9. package/roles/aerodynamics-engineer/ROLE.md +153 -0
  10. package/roles/aerodynamics-engineer/SOURCES.md +12 -0
  11. package/roles/aerodynamics-engineer/cli.py +143 -0
  12. package/roles/aerodynamics-engineer/core/aerodynamics_core.py +1218 -0
  13. package/roles/aerodynamics-engineer/templates/aero-design-report-template.md +72 -0
  14. package/roles/aerodynamics-engineer/tests/test_aerodynamics_core.py +227 -0
  15. package/roles/aerodynamics-engineer/tests/test_role_aerodynamics.py +94 -0
  16. package/roles/aircraft-design-engineer/ROLE.md +138 -0
  17. package/roles/aircraft-design-engineer/SOURCES.md +12 -0
  18. package/roles/aircraft-design-engineer/cli.py +152 -0
  19. package/roles/aircraft-design-engineer/core/aircraft_design_core.py +1278 -0
  20. package/roles/aircraft-design-engineer/templates/concept-design-template.md +99 -0
  21. package/roles/aircraft-design-engineer/tests/test_aircraft_design_core.py +269 -0
  22. package/roles/aircraft-design-engineer/tests/test_role_aircraft_design_engineer.py +106 -0
  23. package/roles/airworthiness-compliance-engineer/ROLE.md +122 -0
  24. package/roles/airworthiness-compliance-engineer/SOURCES.md +16 -0
  25. package/roles/airworthiness-compliance-engineer/cli.py +146 -0
  26. package/roles/airworthiness-compliance-engineer/core/airworthiness_core.py +894 -0
  27. package/roles/airworthiness-compliance-engineer/templates/compliance-matrix-template.md +46 -0
  28. package/roles/airworthiness-compliance-engineer/tests/test_airworthiness_core.py +224 -0
  29. package/roles/airworthiness-compliance-engineer/tests/test_role_airworthiness.py +85 -0
  30. package/roles/as9100-quality-auditor/ROLE.md +128 -0
  31. package/roles/as9100-quality-auditor/SOURCES.md +16 -0
  32. package/roles/as9100-quality-auditor/cli.py +136 -0
  33. package/roles/as9100-quality-auditor/core/as9100_core.py +857 -0
  34. package/roles/as9100-quality-auditor/templates/findings-report-template.md +119 -0
  35. package/roles/as9100-quality-auditor/tests/test_as9100_cli_profile.py +105 -0
  36. package/roles/as9100-quality-auditor/tests/test_as9100_core.py +204 -0
  37. package/roles/as9100-quality-auditor/tests/test_role_as9100.py +90 -0
  38. package/roles/data-bus-avionics-engineer/ROLE.md +146 -0
  39. package/roles/data-bus-avionics-engineer/SOURCES.md +23 -0
  40. package/roles/data-bus-avionics-engineer/cli.py +311 -0
  41. package/roles/data-bus-avionics-engineer/core/data_bus_avionics_core.py +1121 -0
  42. package/roles/data-bus-avionics-engineer/templates/bus-assessment-template.md +134 -0
  43. package/roles/data-bus-avionics-engineer/tests/test_bundle_protocol.py +137 -0
  44. package/roles/data-bus-avionics-engineer/tests/test_data_bus_avionics_core.py +261 -0
  45. package/roles/data-bus-avionics-engineer/tests/test_role_data_bus_avionics_engineer.py +121 -0
  46. package/roles/do160-environmental-engineer/ROLE.md +136 -0
  47. package/roles/do160-environmental-engineer/SOURCES.md +30 -0
  48. package/roles/do160-environmental-engineer/cli.py +147 -0
  49. package/roles/do160-environmental-engineer/core/do160_environmental_core.py +1246 -0
  50. package/roles/do160-environmental-engineer/templates/environmental-qualification-plan-template.md +114 -0
  51. package/roles/do160-environmental-engineer/tests/test_do160_environmental_core.py +312 -0
  52. package/roles/do160-environmental-engineer/tests/test_role_do160_environmental_engineer.py +281 -0
  53. package/roles/do178c-cert-engineer/ROLE.md +120 -0
  54. package/roles/do178c-cert-engineer/SOURCES.md +16 -0
  55. package/roles/do178c-cert-engineer/cli.py +129 -0
  56. package/roles/do178c-cert-engineer/core/do178c_core.py +411 -0
  57. package/roles/do178c-cert-engineer/templates/psac-template.md +74 -0
  58. package/roles/do178c-cert-engineer/tests/test_do178c_cert_engineer_core.py +91 -0
  59. package/roles/do178c-cert-engineer/tests/test_role_do178c.py +109 -0
  60. package/roles/do254-hardware-engineer/ROLE.md +127 -0
  61. package/roles/do254-hardware-engineer/SOURCES.md +19 -0
  62. package/roles/do254-hardware-engineer/cli.py +308 -0
  63. package/roles/do254-hardware-engineer/core/do254_hardware_core.py +761 -0
  64. package/roles/do254-hardware-engineer/templates/phac-template.md +87 -0
  65. package/roles/do254-hardware-engineer/tests/test_do254_bundle_profile.py +152 -0
  66. package/roles/do254-hardware-engineer/tests/test_do254_hardware_core.py +200 -0
  67. package/roles/do254-hardware-engineer/tests/test_role_do254_hardware.py +108 -0
  68. package/roles/engineering-analysis-engineer/ROLE.md +154 -0
  69. package/roles/engineering-analysis-engineer/SOURCES.md +15 -0
  70. package/roles/engineering-analysis-engineer/cli.py +151 -0
  71. package/roles/engineering-analysis-engineer/core/engineering_analysis_engineer_core.py +866 -0
  72. package/roles/engineering-analysis-engineer/templates/analysis-memo-template.md +73 -0
  73. package/roles/engineering-analysis-engineer/tests/test_engineering_analysis_engineer_core.py +367 -0
  74. package/roles/engineering-analysis-engineer/tests/test_role_engineering_analysis_engineer.py +113 -0
  75. package/roles/flight-management-engineer/ROLE.md +160 -0
  76. package/roles/flight-management-engineer/SOURCES.md +30 -0
  77. package/roles/flight-management-engineer/cli.py +258 -0
  78. package/roles/flight-management-engineer/core/flight_management_core.py +1473 -0
  79. package/roles/flight-management-engineer/templates/route-assessment-template.md +94 -0
  80. package/roles/flight-management-engineer/tests/test_flight_management_core.py +437 -0
  81. package/roles/flight-management-engineer/tests/test_role_flight_management_engineer.py +123 -0
  82. package/roles/flight-mechanics-engineer/ROLE.md +162 -0
  83. package/roles/flight-mechanics-engineer/SOURCES.md +51 -0
  84. package/roles/flight-mechanics-engineer/cli.py +149 -0
  85. package/roles/flight-mechanics-engineer/core/flight_mechanics_core.py +1507 -0
  86. package/roles/flight-mechanics-engineer/templates/perf-sc-report-template.md +82 -0
  87. package/roles/flight-mechanics-engineer/tests/test_flight_mechanics_core.py +277 -0
  88. package/roles/flight-mechanics-engineer/tests/test_role_flight_mechanics_engineer.py +141 -0
  89. package/roles/flight-test-engineer/ROLE.md +137 -0
  90. package/roles/flight-test-engineer/SOURCES.md +24 -0
  91. package/roles/flight-test-engineer/cli.py +135 -0
  92. package/roles/flight-test-engineer/core/flight_test_core.py +931 -0
  93. package/roles/flight-test-engineer/templates/flight-test-template.md +143 -0
  94. package/roles/flight-test-engineer/tests/test_flight_test_engineer_core.py +227 -0
  95. package/roles/flight-test-engineer/tests/test_role_flight_test_engineer.py +150 -0
  96. package/roles/flight-test-performance-engineer/ROLE.md +165 -0
  97. package/roles/flight-test-performance-engineer/SOURCES.md +20 -0
  98. package/roles/flight-test-performance-engineer/cli.py +319 -0
  99. package/roles/flight-test-performance-engineer/core/flight_test_performance_core.py +1398 -0
  100. package/roles/flight-test-performance-engineer/templates/performance-report-template.md +105 -0
  101. package/roles/flight-test-performance-engineer/tests/test_flight_test_performance_engineer_core.py +308 -0
  102. package/roles/flight-test-performance-engineer/tests/test_role_flight_test_performance_engineer.py +165 -0
  103. package/roles/gnc-engineer/ROLE.md +141 -0
  104. package/roles/gnc-engineer/SOURCES.md +35 -0
  105. package/roles/gnc-engineer/cli.py +141 -0
  106. package/roles/gnc-engineer/core/gnc_core.py +1074 -0
  107. package/roles/gnc-engineer/templates/gnc-report-template.md +105 -0
  108. package/roles/gnc-engineer/tests/test_gnc_engineer_core.py +238 -0
  109. package/roles/gnc-engineer/tests/test_role_gnc_engineer.py +142 -0
  110. package/roles/guidance-engineer/ROLE.md +156 -0
  111. package/roles/guidance-engineer/SOURCES.md +43 -0
  112. package/roles/guidance-engineer/cli.py +280 -0
  113. package/roles/guidance-engineer/core/guidance_core.py +820 -0
  114. package/roles/guidance-engineer/templates/guidance-report-template.md +70 -0
  115. package/roles/guidance-engineer/tests/test_guidance_engineer_core.py +269 -0
  116. package/roles/guidance-engineer/tests/test_role_guidance_engineer.py +146 -0
  117. package/roles/high-speed-aerodynamics-engineer/ROLE.md +161 -0
  118. package/roles/high-speed-aerodynamics-engineer/SOURCES.md +18 -0
  119. package/roles/high-speed-aerodynamics-engineer/cli.py +282 -0
  120. package/roles/high-speed-aerodynamics-engineer/core/high_speed_aero_core.py +1566 -0
  121. package/roles/high-speed-aerodynamics-engineer/templates/high-speed-memo-template.md +75 -0
  122. package/roles/high-speed-aerodynamics-engineer/tests/test_bundle_protocol.py +103 -0
  123. package/roles/high-speed-aerodynamics-engineer/tests/test_high_speed_aero_core.py +280 -0
  124. package/roles/high-speed-aerodynamics-engineer/tests/test_role_high_speed_aerodynamics.py +125 -0
  125. package/roles/ndt-engineer/ROLE.md +157 -0
  126. package/roles/ndt-engineer/SOURCES.md +18 -0
  127. package/roles/ndt-engineer/cli.py +316 -0
  128. package/roles/ndt-engineer/core/ndt_core.py +1590 -0
  129. package/roles/ndt-engineer/templates/ndt-plan-template.md +109 -0
  130. package/roles/ndt-engineer/tests/test_ndt_cli_bundle.py +183 -0
  131. package/roles/ndt-engineer/tests/test_ndt_core.py +437 -0
  132. package/roles/ndt-engineer/tests/test_role_ndt_engineer.py +148 -0
  133. package/roles/numerical-analysis-engineer/ROLE.md +158 -0
  134. package/roles/numerical-analysis-engineer/SOURCES.md +19 -0
  135. package/roles/numerical-analysis-engineer/cli.py +414 -0
  136. package/roles/numerical-analysis-engineer/core/numerical_analysis_core.py +1220 -0
  137. package/roles/numerical-analysis-engineer/templates/verification-memo-template.md +120 -0
  138. package/roles/numerical-analysis-engineer/tests/test_bundle_protocol.py +157 -0
  139. package/roles/numerical-analysis-engineer/tests/test_numerical_analysis_core.py +337 -0
  140. package/roles/numerical-analysis-engineer/tests/test_role_numerical_analysis_engineer.py +117 -0
  141. package/roles/propulsion-engineer/ROLE.md +163 -0
  142. package/roles/propulsion-engineer/SOURCES.md +15 -0
  143. package/roles/propulsion-engineer/cli.py +168 -0
  144. package/roles/propulsion-engineer/core/propulsion_core.py +1291 -0
  145. package/roles/propulsion-engineer/templates/propulsion-report-template.md +92 -0
  146. package/roles/propulsion-engineer/tests/test_propulsion_engineer_core.py +294 -0
  147. package/roles/propulsion-engineer/tests/test_role_propulsion_engineer.py +106 -0
  148. package/roles/space-systems-engineer/ROLE.md +161 -0
  149. package/roles/space-systems-engineer/SOURCES.md +49 -0
  150. package/roles/space-systems-engineer/cli.py +173 -0
  151. package/roles/space-systems-engineer/core/space_systems_core.py +980 -0
  152. package/roles/space-systems-engineer/templates/space-systems-template.md +88 -0
  153. package/roles/space-systems-engineer/tests/test_role_space_systems_engineer.py +117 -0
  154. package/roles/space-systems-engineer/tests/test_space_systems_engineer_core.py +247 -0
  155. package/roles/state-estimation-engineer/ROLE.md +154 -0
  156. package/roles/state-estimation-engineer/SOURCES.md +27 -0
  157. package/roles/state-estimation-engineer/cli.py +326 -0
  158. package/roles/state-estimation-engineer/core/state_estimation_core.py +958 -0
  159. package/roles/state-estimation-engineer/templates/nav-state-estimator-design-template.md +76 -0
  160. package/roles/state-estimation-engineer/tests/test_bundle_protocol.py +110 -0
  161. package/roles/state-estimation-engineer/tests/test_role_state_estimation_engineer.py +127 -0
  162. package/roles/state-estimation-engineer/tests/test_state_estimation_engineer_core.py +231 -0
  163. package/roles/structures-loads-engineer/ROLE.md +171 -0
  164. package/roles/structures-loads-engineer/SOURCES.md +44 -0
  165. package/roles/structures-loads-engineer/cli.py +219 -0
  166. package/roles/structures-loads-engineer/core/structures_loads_core.py +1112 -0
  167. package/roles/structures-loads-engineer/templates/loads-strength-report-template.md +96 -0
  168. package/roles/structures-loads-engineer/tests/test_bundle_protocol.py +101 -0
  169. package/roles/structures-loads-engineer/tests/test_role_structures_loads_engineer.py +147 -0
  170. 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))