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,120 @@
|
|
|
1
|
+
# Numerical Methods Verification Memo
|
|
2
|
+
|
|
3
|
+
**Code under verification:** Loads Post-Processor (LPP) (version v2.3.1)
|
|
4
|
+
**Verification basis:** classical numerical analysis (Richardson 1910/1927; Euler-Maclaurin error analysis; Newton/Simpson/Gauss); ASME V&V 20-2009 vocabulary for discretization error
|
|
5
|
+
**Status:** draft-for-review
|
|
6
|
+
|
|
7
|
+
## 1. Scope and method selection
|
|
8
|
+
|
|
9
|
+
The LPP loads post-processor turns the spanwise load distribution into the half-span resultant used by the loads report, inverts the isentropic area-Mach relation for duct sizing, and solves the 2x2 aerodynamic coefficient system A c = b. This memo independently verifies the numerical methods of the item above: the spanwise-load integral, the area-Mach root inversion, and the loads coefficient solve are each recomputed with reference methods and compared against the code's reported values and claimed tolerances. A three-mesh convergence study and a conditioning report bound the discretization and rounding error of the results.
|
|
10
|
+
|
|
11
|
+
| Operation | Code method | Reference method | Why |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| spanwise load intensity w(y) = w0*(1 - (2y/span)^2), half-span integral | composite trapezoid, n=16 panels | composite Simpson | Composite Simpson error scales with h^4 and the rule is exact for polynomials of degree <= 3, beating trapezoid on smooth integrands at moderate panel counts. |
|
|
14
|
+
| area-Mach inversion (A/A* = 1.2, subsonic) | code-internal Newton solver | Newton-Raphson (quadratic, simple root) with bisection bracket cross-check | Bisection guarantees convergence when the bracket straddles zero (linear); Newton gives quadratic convergence from a nearby guess when the derivative is non-zero; agreement of the two independent iterations cross-validates the root. |
|
|
15
|
+
| loads coefficient solve A c = b | code-internal dense solver | Gaussian elimination with partial pivoting | Partial pivoting bounds element growth and avoids zero pivots; the reference residual A x - b is checked at machine precision, and the conditioning report bounds how much backward error the solve amplifies. |
|
|
16
|
+
|
|
17
|
+
## 2. Reference computations
|
|
18
|
+
|
|
19
|
+
### 2.1 Reference integral
|
|
20
|
+
|
|
21
|
+
Integrand: spanwise load intensity w(y) = w0*(1 - (2y/span)^2), half-span integral on [0.000000, 18.000000] m (peak load 4200 N/m, span 36 m).
|
|
22
|
+
- Reference value (composite Simpson): 50400.000000 N
|
|
23
|
+
- Closed-form anchor w0*span/3: 50400.000000 N (the integrand is a quadratic, so Simpson is exact here)
|
|
24
|
+
- Richardson error estimate at the code's panel count: the trapezoid error scales with h^2, giving error(2n) = |I_2n - I_n|/3 = 12.304688 N.
|
|
25
|
+
|
|
26
|
+
### 2.2 Reference root
|
|
27
|
+
|
|
28
|
+
- isentropic area-Mach inversion (A/A* = 1.2, gamma = 1.4), subsonic branch: M* = 0.590248761
|
|
29
|
+
- Reference method: reference Newton-Raphson from x0 = 0.3 + bisection bracket cross-check - converged in 6 iterations, residual |f| = 2.22e-16
|
|
30
|
+
- Derivative at the root f'(M*) = -1.238449 (non-zero: simple root, quadratic convergence expected); root sensitivity per unit residual 1/|f'| = 0.807462
|
|
31
|
+
- Bracket [0.200, 0.990] straddles zero: True
|
|
32
|
+
- Bisection cross-check: M = 0.590248761, agreement with Newton 8.17e-12
|
|
33
|
+
|
|
34
|
+
### 2.3 Reference linear solve
|
|
35
|
+
|
|
36
|
+
- Reference solution c = A^-1 b: 0.285714286, 0.285714286
|
|
37
|
+
- Condition number kappa = s_max/s_min: 7.0000
|
|
38
|
+
- Conditioning assessment: kappa = 7: up to 0.85 decimal digits of input uncertainty can be amplified by the solve
|
|
39
|
+
|
|
40
|
+
## 3. Cross-check against the engineering code
|
|
41
|
+
|
|
42
|
+
Each operation: code-reported value vs reference value; verdict PASS when the discrepancy is within the code's own claimed tolerance.
|
|
43
|
+
|
|
44
|
+
### 3.1 Integration
|
|
45
|
+
|
|
46
|
+
- Operation: spanwise load integral
|
|
47
|
+
- Code method: composite trapezoid, n=16 panels
|
|
48
|
+
- Reference method: composite Simpson, n=64 (exact for degree <= 3) + Richardson error estimate
|
|
49
|
+
- Code-reported value: 50350.781250000 N
|
|
50
|
+
- Reference value (composite Simpson, n=64): 50400.000000000 N
|
|
51
|
+
- Discrepancy: 49.218750000 N
|
|
52
|
+
- Code-claimed tolerance: 1.000 N
|
|
53
|
+
- Richardson estimate of the code's n-panel error: 49.218750 N (from the code's own n and 2n outputs)
|
|
54
|
+
- **Verdict: FAIL**
|
|
55
|
+
- Method fingerprint: the reference engine's trapezoid at the code's panel count reproduces the code output (50350.781250000) - the code's method was identified correctly.
|
|
56
|
+
|
|
57
|
+
### 3.2 Root finding
|
|
58
|
+
|
|
59
|
+
- Operation: isentropic area-Mach inversion (A/A* = 1.2, gamma = 1.4), subsonic branch
|
|
60
|
+
- Code method: code-internal Newton solver
|
|
61
|
+
- Reference method: reference Newton-Raphson from x0 = 0.3 + bisection bracket cross-check
|
|
62
|
+
- Code-reported value: M = 0.590248761
|
|
63
|
+
- Reference value: M* = 0.590248760988 (residual |f| = 2.22e-16)
|
|
64
|
+
- Bisection cross-check: M = 0.590248761 (agreement with Newton 8.17e-12)
|
|
65
|
+
- Discrepancy: 1.15e-11
|
|
66
|
+
- Code-claimed tolerance: 1.00e-08
|
|
67
|
+
- **Verdict: PASS**
|
|
68
|
+
|
|
69
|
+
### 3.3 Linear solve
|
|
70
|
+
|
|
71
|
+
- Operation: loads coefficient solve A c = b
|
|
72
|
+
- Code method: code-internal dense solver
|
|
73
|
+
- Reference method: Gaussian elimination with partial pivoting (reference engine)
|
|
74
|
+
- Code-reported solution: [0.285714000, 0.285714000]
|
|
75
|
+
- Reference solution: [0.285714286, 0.285714286]
|
|
76
|
+
- Discrepancy (max component): 2.86e-07
|
|
77
|
+
- Code-claimed tolerance: 1.00e-06
|
|
78
|
+
- **Verdict: PASS**
|
|
79
|
+
- Condition number kappa = s_max/s_min: 7.0000
|
|
80
|
+
- Conditioning: kappa = 7: up to 0.85 decimal digits of input uncertainty can be amplified by the solve
|
|
81
|
+
|
|
82
|
+
## 4. Convergence and error analysis
|
|
83
|
+
|
|
84
|
+
Three-mesh study of the code's own outputs (refinement ratio r = 2, finest f1 to coarsest f3):
|
|
85
|
+
|
|
86
|
+
- 64: 50396.923828
|
|
87
|
+
- 32: 50387.695312
|
|
88
|
+
- 16: 50350.781250
|
|
89
|
+
- Observed order p = 2.000 (consistent with composite trapezoid (order 2))
|
|
90
|
+
- Richardson extrapolated value: 50400.000000
|
|
91
|
+
- Grid convergence index (GCI, Fs = 1.25): 7.63e-05 fraction (0.0076%)
|
|
92
|
+
- Verdict: monotone converged
|
|
93
|
+
- Code-reported value vs finest mesh: 46.142578 N
|
|
94
|
+
|
|
95
|
+
## 5. Findings and recommendations
|
|
96
|
+
|
|
97
|
+
### F-1 (Discrepancy)
|
|
98
|
+
|
|
99
|
+
spanwise load integral reports 50350.781250 N while the reference integration gives 50400.000000 N: discrepancy 49.218750 N exceeds the code's claimed tolerance of 1 N.
|
|
100
|
+
|
|
101
|
+
Evidence: Richardson error estimate of the code's n-panel output is 49.218750 N (measured discrepancy 49.218750 N); the three-mesh study shows observed order 2.00, so the code's panel count cannot support the claimed tolerance.
|
|
102
|
+
|
|
103
|
+
Recommendation: Refine to n >= 64 panels or switch to a composite Simpson rule, and gate the output with a Richardson error estimate before release.
|
|
104
|
+
|
|
105
|
+
### O-1 (Observation)
|
|
106
|
+
|
|
107
|
+
root-finding (isentropic area-Mach inversion (A/A* = 1.2, gamma = 1.4), subsonic branch) verified within the code's claimed tolerance (discrepancy 1.1549e-11 <= 1e-08).
|
|
108
|
+
|
|
109
|
+
### O-2 (Observation)
|
|
110
|
+
|
|
111
|
+
linear solve (loads coefficient solve A c = b) verified within the code's claimed tolerance (discrepancy 2.85714e-07 <= 1e-06).
|
|
112
|
+
|
|
113
|
+
## 6. Verification limits
|
|
114
|
+
|
|
115
|
+
- Scope: the three numerical operations named above, on the example item's input set. Other code paths (ODE integration, interpolation, least-squares fitting, finite-difference derivatives, uncertainty propagation) follow the same reference-engine workflow when exercised.
|
|
116
|
+
- The code's claimed tolerances are inputs to this memo, not endorsements; a PASS verdict means the claim is supported by the reference computation on this input set.
|
|
117
|
+
- Export classification: uncontrolled technical data (example).
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
*Generated by Aero Agent Roles numerical-analysis-engineer core (2026-09-05). DRAFT for human numerical-analysis review. Not an approval document.*
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test the evidence bundle + skill-dispatch protocol (PROTOCOL.md v1).
|
|
3
|
+
|
|
4
|
+
Verifies:
|
|
5
|
+
- build --bundle emits evidence/{model,gates,provenance}.json
|
|
6
|
+
- bundle JSON parses, model contains the computed reference numbers,
|
|
7
|
+
gates all_pass
|
|
8
|
+
- the ten dispatch rows agree with the bound numerics leaf logic
|
|
9
|
+
modules (delta <= 1e-6) when AeroSkills is present
|
|
10
|
+
- standalone (no AeroSkills) emits the bundle honestly with
|
|
11
|
+
cross_checked=false
|
|
12
|
+
- build --profile prepends a program context header
|
|
13
|
+
- CLI overrides re-verify a different code report
|
|
14
|
+
- check gate-checks a built memo
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import unittest
|
|
23
|
+
|
|
24
|
+
ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
25
|
+
CLI = os.path.join(ROLE_DIR, "cli.py")
|
|
26
|
+
PROFILE = os.path.normpath(os.path.join(ROLE_DIR, "..", "..", "profiles",
|
|
27
|
+
"example-airframer.json"))
|
|
28
|
+
|
|
29
|
+
AEROSKILLS = os.environ.get("AEROSKILLS_DEV", os.path.expanduser("~/AeroSkills"))
|
|
30
|
+
HAS_SKILLS = os.path.isdir(os.path.join(AEROSKILLS, "skills"))
|
|
31
|
+
|
|
32
|
+
EXPECTED_LEAVES = {
|
|
33
|
+
"numerical-integration", "root-finding", "convergence-verification",
|
|
34
|
+
"singular-value-decomposition", "matrix-operations",
|
|
35
|
+
}
|
|
36
|
+
EXPECTED_FUNCS = {"trapezoid", "simpson", "error_estimate_trapezoid",
|
|
37
|
+
"newton_raphson", "bisection", "condition_number", "solve"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run_build(outdir, env_extra=None, extra_args=None):
|
|
41
|
+
env = dict(os.environ)
|
|
42
|
+
if env_extra:
|
|
43
|
+
env.update(env_extra)
|
|
44
|
+
args = [sys.executable, CLI, "build", "--out",
|
|
45
|
+
os.path.join(outdir, "memo.md"), "--bundle"]
|
|
46
|
+
if extra_args:
|
|
47
|
+
args.extend(extra_args)
|
|
48
|
+
r = subprocess.run(args, capture_output=True, text=True, env=env)
|
|
49
|
+
return r
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestBundleProtocol(unittest.TestCase):
|
|
53
|
+
|
|
54
|
+
def setUp(self):
|
|
55
|
+
self.tmp = tempfile.mkdtemp()
|
|
56
|
+
|
|
57
|
+
def tearDown(self):
|
|
58
|
+
shutil.rmtree(self.tmp, ignore_errors=True)
|
|
59
|
+
|
|
60
|
+
def test_bundle_emits_three_files(self):
|
|
61
|
+
r = run_build(self.tmp)
|
|
62
|
+
self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
|
|
63
|
+
ev = os.path.join(self.tmp, "evidence")
|
|
64
|
+
for f in ("model.json", "gates.json", "provenance.json"):
|
|
65
|
+
self.assertTrue(os.path.exists(os.path.join(ev, f)), f + " missing")
|
|
66
|
+
|
|
67
|
+
def test_model_has_numbers_and_status(self):
|
|
68
|
+
run_build(self.tmp)
|
|
69
|
+
m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
|
|
70
|
+
self.assertEqual(m["role"], "numerical-analysis-engineer")
|
|
71
|
+
self.assertEqual(m["deliverable_type"],
|
|
72
|
+
"Numerical Methods Verification Memo")
|
|
73
|
+
self.assertEqual(m.get("status"), "draft-for-review")
|
|
74
|
+
# the model must carry the numbers that appear in the markdown
|
|
75
|
+
self.assertAlmostEqual(m["integration"]["code_value"], 50350.78125,
|
|
76
|
+
places=6)
|
|
77
|
+
self.assertAlmostEqual(m["integration"]["reference_value"], 50400.0,
|
|
78
|
+
places=6)
|
|
79
|
+
self.assertAlmostEqual(
|
|
80
|
+
m["root_finding"]["reference_root"], 0.59024876099, places=9)
|
|
81
|
+
self.assertAlmostEqual(m["convergence"]["observed_order"], 2.0,
|
|
82
|
+
places=6)
|
|
83
|
+
self.assertAlmostEqual(m["linear_solve"]["conditioning"]["kappa"],
|
|
84
|
+
7.0, places=6)
|
|
85
|
+
|
|
86
|
+
def test_gates_all_pass_exit_zero(self):
|
|
87
|
+
r = run_build(self.tmp)
|
|
88
|
+
self.assertEqual(r.returncode, 0)
|
|
89
|
+
g = json.load(open(os.path.join(self.tmp, "evidence", "gates.json")))
|
|
90
|
+
self.assertTrue(g["all_pass"])
|
|
91
|
+
self.assertEqual(g["exit_code"], 0)
|
|
92
|
+
self.assertGreaterEqual(len(g["gates"]), 5)
|
|
93
|
+
|
|
94
|
+
def test_dispatch_crosschecks_when_skills_present(self):
|
|
95
|
+
if not HAS_SKILLS:
|
|
96
|
+
self.skipTest("AeroSkills not present")
|
|
97
|
+
r = run_build(self.tmp)
|
|
98
|
+
self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
|
|
99
|
+
p = json.load(open(os.path.join(self.tmp, "evidence",
|
|
100
|
+
"provenance.json")))
|
|
101
|
+
self.assertTrue(p["cross_checked"])
|
|
102
|
+
rows = [row for row in p["skills"] if row.get("dispatched")
|
|
103
|
+
and not row.get("error")]
|
|
104
|
+
self.assertGreaterEqual(len(rows), 10)
|
|
105
|
+
leafs = {row["leaf"].rsplit("/", 1)[-1] for row in rows}
|
|
106
|
+
self.assertEqual(leafs, EXPECTED_LEAVES)
|
|
107
|
+
funcs = {row["function"] for row in rows}
|
|
108
|
+
self.assertTrue(EXPECTED_FUNCS.issubset(funcs), funcs)
|
|
109
|
+
for row in rows:
|
|
110
|
+
self.assertTrue(row["agrees"],
|
|
111
|
+
"core/skill disagree for %s: %s"
|
|
112
|
+
% (row["function"], row))
|
|
113
|
+
self.assertLessEqual(row["delta"], 1e-6)
|
|
114
|
+
self.assertIn("cross-cutting/numerics/", row["leaf"])
|
|
115
|
+
|
|
116
|
+
def test_standalone_honest_no_skills(self):
|
|
117
|
+
r = run_build(self.tmp, env_extra={"AEROSKILLS_DEV": "/nonexistent"})
|
|
118
|
+
self.assertEqual(r.returncode, 0)
|
|
119
|
+
p = json.load(open(os.path.join(self.tmp, "evidence",
|
|
120
|
+
"provenance.json")))
|
|
121
|
+
self.assertFalse(p["cross_checked"])
|
|
122
|
+
self.assertEqual(len(p["skills"]), 0)
|
|
123
|
+
|
|
124
|
+
def test_profile_header_applied(self):
|
|
125
|
+
if not os.path.exists(PROFILE):
|
|
126
|
+
self.skipTest("profiles/example-airframer.json not present")
|
|
127
|
+
r = run_build(self.tmp, extra_args=["--profile", PROFILE])
|
|
128
|
+
self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
|
|
129
|
+
md = open(os.path.join(self.tmp, "memo.md")).read()
|
|
130
|
+
self.assertIn("Example Airframers Ltd", md)
|
|
131
|
+
self.assertIn("Program context (profile)", md)
|
|
132
|
+
m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
|
|
133
|
+
self.assertEqual(m["profile"]["customer"], "Example Airframers Ltd")
|
|
134
|
+
|
|
135
|
+
def test_cli_overrides_flip_verdict(self):
|
|
136
|
+
# re-verify a code report that reports the correct integral:
|
|
137
|
+
# the integration check must now PASS
|
|
138
|
+
r = run_build(self.tmp, extra_args=["--code-integral", "50400.0"])
|
|
139
|
+
self.assertEqual(r.returncode, 0, (r.stdout + r.stderr)[-800:])
|
|
140
|
+
m = json.load(open(os.path.join(self.tmp, "evidence", "model.json")))
|
|
141
|
+
self.assertEqual(m["integration"]["verdict"], "PASS")
|
|
142
|
+
self.assertTrue(all(c["verdict"] == "PASS" for c in m["checks"]))
|
|
143
|
+
# findings become observations, not discrepancies
|
|
144
|
+
self.assertTrue(all(fd["severity"] == "observation"
|
|
145
|
+
for fd in m["findings"]))
|
|
146
|
+
|
|
147
|
+
def test_check_still_works(self):
|
|
148
|
+
run_build(self.tmp)
|
|
149
|
+
md = os.path.join(self.tmp, "memo.md")
|
|
150
|
+
c = subprocess.run([sys.executable, CLI, "check", "--file", md],
|
|
151
|
+
capture_output=True, text=True)
|
|
152
|
+
self.assertEqual(c.returncode, 0, c.stdout)
|
|
153
|
+
self.assertIn("RESULT: PASS", c.stdout)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
unittest.main()
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test numerical_analysis_core: the executable engine of the
|
|
3
|
+
Numerical Analysis Engineer role.
|
|
4
|
+
|
|
5
|
+
Proves the role can DO its job standalone (no AeroSkills needed):
|
|
6
|
+
reference quadrature + Richardson error estimation, Newton/bisection
|
|
7
|
+
root finding with the area-Mach anchor, pivoted reference solves,
|
|
8
|
+
condition numbers, the three-mesh convergence study (observed order,
|
|
9
|
+
Richardson extrapolation, GCI), the memo builder, evidence-gate checks,
|
|
10
|
+
and the filled template.
|
|
11
|
+
"""
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import unittest
|
|
16
|
+
|
|
17
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
18
|
+
"..", "core"))
|
|
19
|
+
from numerical_analysis_core import ( # noqa: E402
|
|
20
|
+
EPS_MACH, GCI_FS, RULE_SELECTION,
|
|
21
|
+
area_mach_derivative, area_mach_ratio, area_mach_residual, bisection,
|
|
22
|
+
bracket_straddles, build_report, check_report, check_report_markdown,
|
|
23
|
+
condition_number, condition_report, convergence_verdict,
|
|
24
|
+
example_item, example_report_markdown, forward_error_bound,
|
|
25
|
+
grid_convergence_index, lift_intensity, max_abs_component_diff,
|
|
26
|
+
newton_iterations, newton_raphson, observed_order,
|
|
27
|
+
render_report_markdown, richardson_error_n, richardson_error_trapezoid,
|
|
28
|
+
richardson_extrapolation, root_sensitivity, select_integration_rule,
|
|
29
|
+
simpson, solve, trapezoid,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Deterministic across midnight: the committed template carries the
|
|
33
|
+
# generation date 2026-09-05; pin fresh renders to it (CI override wins).
|
|
34
|
+
os.environ.setdefault("ROLE_GEN_DATE", "2026-09-05")
|
|
35
|
+
|
|
36
|
+
ROLE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
37
|
+
TEMPLATE = os.path.join(ROLE_DIR, "templates",
|
|
38
|
+
"verification-memo-template.md")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestQuadratureRules(unittest.TestCase):
|
|
42
|
+
|
|
43
|
+
def test_trapezoid_linear_exact(self):
|
|
44
|
+
# trapezoid is exact for linears: integral of x on [0, 2] = 2
|
|
45
|
+
self.assertAlmostEqual(trapezoid(lambda x: x, 0.0, 2.0, 4), 2.0,
|
|
46
|
+
places=12)
|
|
47
|
+
|
|
48
|
+
def test_quadratic_anchor_x2(self):
|
|
49
|
+
# integral of x^2 on [0, 2] = 8/3; trapezoid converges O(h^2)
|
|
50
|
+
est = trapezoid(lambda x: x * x, 0.0, 2.0, 1000)
|
|
51
|
+
self.assertAlmostEqual(est, 8.0 / 3.0, places=4)
|
|
52
|
+
# Simpson n=2 is exact for the quadratic
|
|
53
|
+
self.assertAlmostEqual(simpson(lambda x: x * x, 0.0, 2.0, 2),
|
|
54
|
+
8.0 / 3.0, places=12)
|
|
55
|
+
|
|
56
|
+
def test_sin_anchor(self):
|
|
57
|
+
# integral of sin(x) on [0, pi] = 2 (leaf textbook anchor)
|
|
58
|
+
self.assertAlmostEqual(simpson(math.sin, 0.0, math.pi, 1000), 2.0,
|
|
59
|
+
places=8)
|
|
60
|
+
|
|
61
|
+
def test_invalid_panels_raise(self):
|
|
62
|
+
with self.assertRaises(ValueError):
|
|
63
|
+
trapezoid(lambda x: x, 0.0, 1.0, 0)
|
|
64
|
+
with self.assertRaises(ValueError):
|
|
65
|
+
simpson(lambda x: x, 0.0, 1.0, 3) # odd n raises
|
|
66
|
+
with self.assertRaises(ValueError):
|
|
67
|
+
trapezoid(lambda x: x, 0.0, 1.0, 2.5)
|
|
68
|
+
|
|
69
|
+
def test_richardson_error_estimate(self):
|
|
70
|
+
# error(2n) = |I_2n - I_n| / 3 ; on the smooth quadratic
|
|
71
|
+
# half-span load at n=16 this is 12.3046875 N (measured)
|
|
72
|
+
f = lambda y: lift_intensity(y, 4200.0, 36.0)
|
|
73
|
+
self.assertAlmostEqual(
|
|
74
|
+
richardson_error_trapezoid(f, 0.0, 18.0, 16), 12.3046875,
|
|
75
|
+
places=9)
|
|
76
|
+
|
|
77
|
+
def test_richardson_error_n_recovers_trapezoid_error(self):
|
|
78
|
+
# for an order-2 rule E_n = |I_n - I_2n|/(1 - 2^-2); on the
|
|
79
|
+
# quadratic integrand the trapezoid error is exactly known:
|
|
80
|
+
# I16 - exact = -49.21875 N
|
|
81
|
+
f = lambda y: lift_intensity(y, 4200.0, 36.0)
|
|
82
|
+
i16 = trapezoid(f, 0.0, 18.0, 16)
|
|
83
|
+
i32 = trapezoid(f, 0.0, 18.0, 32)
|
|
84
|
+
self.assertAlmostEqual(richardson_error_n(i16, i32, 2),
|
|
85
|
+
abs(i16 - 4200.0 * 36.0 / 3.0), places=6)
|
|
86
|
+
self.assertAlmostEqual(richardson_error_n(i16, i32, 2), 49.21875,
|
|
87
|
+
places=6)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestRuleSelection(unittest.TestCase):
|
|
91
|
+
|
|
92
|
+
def test_smooth_selects_simpson(self):
|
|
93
|
+
sel = select_integration_rule("smooth")
|
|
94
|
+
self.assertEqual(sel["rule"], "composite Simpson")
|
|
95
|
+
self.assertEqual(sel["order"], 4)
|
|
96
|
+
|
|
97
|
+
def test_noisy_selects_trapezoid(self):
|
|
98
|
+
sel = select_integration_rule("arbitrary-or-noisy")
|
|
99
|
+
self.assertIn("trapezoid", sel["rule"])
|
|
100
|
+
self.assertEqual(sel["order"], 2)
|
|
101
|
+
|
|
102
|
+
def test_all_classes_have_rationale(self):
|
|
103
|
+
for cls, sel in RULE_SELECTION.items():
|
|
104
|
+
self.assertTrue(sel["rationale"])
|
|
105
|
+
|
|
106
|
+
def test_unknown_class_raises(self):
|
|
107
|
+
with self.assertRaises(ValueError):
|
|
108
|
+
select_integration_rule("not-a-class")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestRootFinding(unittest.TestCase):
|
|
112
|
+
|
|
113
|
+
def test_sqrt2_anchors(self):
|
|
114
|
+
# f = x^2 - 2, root sqrt(2) = 1.4142135623730951
|
|
115
|
+
root_b = bisection(lambda x: x * x - 2.0, 1.0, 2.0)
|
|
116
|
+
root_n = newton_raphson(lambda x: x * x - 2.0, lambda x: 2.0 * x,
|
|
117
|
+
1.5)
|
|
118
|
+
self.assertAlmostEqual(root_b, math.sqrt(2.0), places=9)
|
|
119
|
+
self.assertAlmostEqual(root_n, math.sqrt(2.0), places=9)
|
|
120
|
+
|
|
121
|
+
def test_newton_quadratic_speed(self):
|
|
122
|
+
# three Newton steps reach ~machine precision from x0 = 1.5
|
|
123
|
+
r = newton_iterations(lambda x: x * x - 2.0, lambda x: 2.0 * x, 1.5)
|
|
124
|
+
self.assertLessEqual(r["iterations"], 3)
|
|
125
|
+
self.assertLess(r["residual"], 1e-10)
|
|
126
|
+
|
|
127
|
+
def test_area_mach_subsonic_anchor(self):
|
|
128
|
+
# isentropic area-Mach relation, A/A* = 1.2, gamma = 1.4:
|
|
129
|
+
# subsonic root M = 0.59024876099 (leaf-tested anchor)
|
|
130
|
+
resid = lambda M: area_mach_residual(M, 1.2, 1.4)
|
|
131
|
+
deriv = lambda M: area_mach_derivative(M, 1.4)
|
|
132
|
+
rn = newton_raphson(resid, deriv, 0.3)
|
|
133
|
+
rb = bisection(resid, 0.2, 0.99)
|
|
134
|
+
self.assertAlmostEqual(rn, 0.59024876099, places=9)
|
|
135
|
+
self.assertAlmostEqual(rb, 0.59024876099, places=9)
|
|
136
|
+
self.assertLess(abs(area_mach_residual(rn, 1.2, 1.4)), 1e-10)
|
|
137
|
+
|
|
138
|
+
def test_bracket_behavior(self):
|
|
139
|
+
resid = lambda M: area_mach_residual(M, 1.2, 1.4)
|
|
140
|
+
ok = bracket_straddles(resid, 0.2, 0.99)
|
|
141
|
+
self.assertTrue(ok["straddles"])
|
|
142
|
+
bad = bracket_straddles(resid, 0.05, 0.4) # both sides positive
|
|
143
|
+
self.assertFalse(bad["straddles"])
|
|
144
|
+
with self.assertRaises(ValueError):
|
|
145
|
+
bisection(resid, 0.05, 0.4) # no sign change
|
|
146
|
+
# Newton step undefined when the derivative vanishes before
|
|
147
|
+
# convergence: f = x^3 - 1 from x0 = 0 has df(0) = 0
|
|
148
|
+
with self.assertRaises(ValueError):
|
|
149
|
+
newton_raphson(lambda x: x ** 3 - 1.0, lambda x: 3.0 * x * x, 0.0)
|
|
150
|
+
|
|
151
|
+
def test_root_sensitivity(self):
|
|
152
|
+
# simple root: sensitivity 1/|f'|; multiple root: unbounded
|
|
153
|
+
self.assertAlmostEqual(root_sensitivity(-2.0), 0.5)
|
|
154
|
+
self.assertEqual(root_sensitivity(0.0), float("inf"))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TestLinearSolveAndConditioning(unittest.TestCase):
|
|
158
|
+
|
|
159
|
+
def test_solve_leaf_anchor(self):
|
|
160
|
+
# matrix-operations leaf worked anchor:
|
|
161
|
+
# A x = b with A = [[2,1,-1],[-3,-1,2],[-2,1,2]], b = [8,-11,-3]
|
|
162
|
+
# gives x = [2, 3, -1]
|
|
163
|
+
A = [[2.0, 1.0, -1.0], [-3.0, -1.0, 2.0], [-2.0, 1.0, 2.0]]
|
|
164
|
+
b = [8.0, -11.0, -3.0]
|
|
165
|
+
x = solve(A, b)
|
|
166
|
+
self.assertAlmostEqual(x[0], 2.0, places=10)
|
|
167
|
+
self.assertAlmostEqual(x[1], 3.0, places=10)
|
|
168
|
+
self.assertAlmostEqual(x[2], -1.0, places=10)
|
|
169
|
+
|
|
170
|
+
def test_singular_matrix_raises(self):
|
|
171
|
+
with self.assertRaises(ValueError):
|
|
172
|
+
solve([[1.0, 2.0], [2.0, 4.0]], [1.0, 2.0])
|
|
173
|
+
|
|
174
|
+
def test_max_abs_component_diff(self):
|
|
175
|
+
self.assertAlmostEqual(max_abs_component_diff([1.0, 2.0],
|
|
176
|
+
[1.0, 2.5]), 0.5)
|
|
177
|
+
|
|
178
|
+
def test_condition_number(self):
|
|
179
|
+
# kappa = s_max/s_min: [1.75, 0.25] -> 7
|
|
180
|
+
self.assertAlmostEqual(condition_number([1.75, 0.25]), 7.0)
|
|
181
|
+
self.assertEqual(condition_number([1.0, 0.0]), float("inf"))
|
|
182
|
+
with self.assertRaises(ValueError):
|
|
183
|
+
condition_number([])
|
|
184
|
+
with self.assertRaises(ValueError):
|
|
185
|
+
condition_number([-1.0])
|
|
186
|
+
|
|
187
|
+
def test_condition_report_and_bound(self):
|
|
188
|
+
rep = condition_report([1.75, 0.25])
|
|
189
|
+
self.assertAlmostEqual(rep["kappa"], 7.0)
|
|
190
|
+
self.assertAlmostEqual(rep["digits_lost_max"],
|
|
191
|
+
math.log10(7.0), places=3)
|
|
192
|
+
self.assertFalse(rep["at_precision_limit"])
|
|
193
|
+
# classical forward-error bound: kappa * backward error
|
|
194
|
+
self.assertAlmostEqual(forward_error_bound(7.0, 1e-12), 7e-12)
|
|
195
|
+
# at the precision limit kappa >= 1/eps
|
|
196
|
+
lim = condition_report([EPS_MACH, 1.0])
|
|
197
|
+
self.assertTrue(lim["at_precision_limit"])
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestConvergenceStudy(unittest.TestCase):
|
|
201
|
+
|
|
202
|
+
# three-mesh trapezoid outputs of the half-span quadratic load
|
|
203
|
+
F1, F2, F3 = 50396.923828125, 50387.6953125, 50350.78125
|
|
204
|
+
|
|
205
|
+
def test_observed_order_is_two(self):
|
|
206
|
+
# trapezoid on a quadratic: exactly second order, ratio 4
|
|
207
|
+
p = observed_order(self.F1, self.F2, self.F3, 2.0)
|
|
208
|
+
self.assertAlmostEqual(p, 2.0, places=9)
|
|
209
|
+
|
|
210
|
+
def test_richardson_extrapolation_recovers_exact(self):
|
|
211
|
+
p = observed_order(self.F1, self.F2, self.F3, 2.0)
|
|
212
|
+
exact = richardson_extrapolation(self.F1, self.F2, 2.0, p)
|
|
213
|
+
self.assertAlmostEqual(exact, 50400.0, places=6)
|
|
214
|
+
|
|
215
|
+
def test_gci_fraction(self):
|
|
216
|
+
p = observed_order(self.F1, self.F2, self.F3, 2.0)
|
|
217
|
+
gci = grid_convergence_index(self.F1, self.F2, 2.0, p)
|
|
218
|
+
self.assertAlmostEqual(gci, 7.62986022096e-05, places=12)
|
|
219
|
+
self.assertEqual(GCI_FS, 1.25)
|
|
220
|
+
|
|
221
|
+
def test_convergence_verdicts(self):
|
|
222
|
+
v = convergence_verdict(self.F1, self.F2, self.F3, 2.0)
|
|
223
|
+
self.assertEqual(v["verdict"], "monotone converged")
|
|
224
|
+
self.assertAlmostEqual(v["order"], 2.0, places=9)
|
|
225
|
+
# diverging: |ratio| > 1 on the negative branch
|
|
226
|
+
vd = convergence_verdict(1.0, 0.5, 2.0, 2.0)
|
|
227
|
+
self.assertEqual(vd["verdict"], "diverging")
|
|
228
|
+
# oscillatory: ratio < 0 with |ratio| <= 1
|
|
229
|
+
vo = convergence_verdict(1.0, 0.5, 0.75, 2.0)
|
|
230
|
+
self.assertEqual(vo["verdict"], "oscillatory")
|
|
231
|
+
self.assertIsNone(vo["order"])
|
|
232
|
+
|
|
233
|
+
def test_invalid_studies_raise(self):
|
|
234
|
+
with self.assertRaises(ValueError):
|
|
235
|
+
observed_order(1.0, 2.0, 3.0, 1.0) # r <= 1
|
|
236
|
+
with self.assertRaises(ValueError):
|
|
237
|
+
observed_order(1.0, 1.0, 1.5, 2.0) # f2 == f1
|
|
238
|
+
with self.assertRaises(ValueError):
|
|
239
|
+
observed_order(1.0, 0.5, 0.6, 2.0) # non-monotone
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TestMemoBuilder(unittest.TestCase):
|
|
243
|
+
|
|
244
|
+
def test_example_item_reports(self):
|
|
245
|
+
item = example_item()
|
|
246
|
+
# code reported the n=16 trapezoid value (50350.78125 N) and the
|
|
247
|
+
# subsonic area-Mach root to 9 decimals
|
|
248
|
+
self.assertAlmostEqual(item.code_integral, 50350.78125, places=6)
|
|
249
|
+
self.assertAlmostEqual(item.code_mach, 0.590248761, places=9)
|
|
250
|
+
|
|
251
|
+
def test_model_structure(self):
|
|
252
|
+
model = build_report(example_item())
|
|
253
|
+
self.assertEqual(model["document_type"],
|
|
254
|
+
"Numerical Methods Verification Memo")
|
|
255
|
+
self.assertEqual(model["status"], "draft-for-review")
|
|
256
|
+
self.assertEqual(len(model["checks"]), 3)
|
|
257
|
+
self.assertEqual(len(model["method_selection"]), 3)
|
|
258
|
+
self.assertTrue(model["findings"])
|
|
259
|
+
|
|
260
|
+
def test_example_verdicts(self):
|
|
261
|
+
# integration fails (49.2 N vs 1 N claim); root and solve pass
|
|
262
|
+
int_chk = build_report(example_item())["integration"]
|
|
263
|
+
self.assertEqual(int_chk["verdict"], "FAIL")
|
|
264
|
+
self.assertAlmostEqual(int_chk["discrepancy"], 49.21875, places=6)
|
|
265
|
+
root_chk = build_report(example_item())["root_finding"]
|
|
266
|
+
self.assertEqual(root_chk["verdict"], "PASS")
|
|
267
|
+
self.assertLess(root_chk["discrepancy"], 1e-9)
|
|
268
|
+
solve_chk = build_report(example_item())["linear_solve"]
|
|
269
|
+
self.assertEqual(solve_chk["verdict"], "PASS")
|
|
270
|
+
self.assertAlmostEqual(solve_chk["conditioning"]["kappa"], 7.0)
|
|
271
|
+
|
|
272
|
+
def test_convergence_in_model(self):
|
|
273
|
+
conv = build_report(example_item())["convergence"]
|
|
274
|
+
self.assertAlmostEqual(conv["observed_order"], 2.0, places=6)
|
|
275
|
+
self.assertAlmostEqual(conv["gci_fraction"], 7.62986022096e-05,
|
|
276
|
+
places=12)
|
|
277
|
+
self.assertEqual(conv["verdict"], "monotone converged")
|
|
278
|
+
self.assertAlmostEqual(conv["richardson_extrapolated"], 50400.0,
|
|
279
|
+
places=6)
|
|
280
|
+
|
|
281
|
+
def test_correct_report_flips_verdict(self):
|
|
282
|
+
# re-verify a code report that is actually correct: all PASS
|
|
283
|
+
item = example_item()
|
|
284
|
+
item.code_integral = 50400.0
|
|
285
|
+
model = build_report(item)
|
|
286
|
+
for c in model["checks"]:
|
|
287
|
+
self.assertEqual(c["verdict"], "PASS")
|
|
288
|
+
|
|
289
|
+
def test_core_gates_all_pass(self):
|
|
290
|
+
model = build_report(example_item())
|
|
291
|
+
gates = check_report(model)
|
|
292
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
293
|
+
|
|
294
|
+
def test_markdown_gates_all_pass(self):
|
|
295
|
+
md = example_report_markdown()
|
|
296
|
+
gates = check_report_markdown(md)
|
|
297
|
+
self.assertTrue(gates["all_pass"], gates)
|
|
298
|
+
|
|
299
|
+
def test_sign_off_gate_fails_when_honest_marker_removed(self):
|
|
300
|
+
model = build_report(example_item())
|
|
301
|
+
model["status"] = "approved"
|
|
302
|
+
gates = check_report(model)
|
|
303
|
+
self.assertFalse(gates["sign_off_honest"])
|
|
304
|
+
self.assertFalse(gates["all_pass"])
|
|
305
|
+
|
|
306
|
+
def test_convergence_gate_fails_when_study_missing(self):
|
|
307
|
+
model = build_report(example_item())
|
|
308
|
+
model["convergence"] = {"meshes": []}
|
|
309
|
+
gates = check_report(model)
|
|
310
|
+
self.assertFalse(gates["convergence_study_present"])
|
|
311
|
+
self.assertFalse(gates["all_pass"])
|
|
312
|
+
|
|
313
|
+
def test_render_is_draft_and_not_approval(self):
|
|
314
|
+
md = example_report_markdown()
|
|
315
|
+
self.assertIn("draft", md.lower())
|
|
316
|
+
self.assertIn("not an approval", md.lower())
|
|
317
|
+
self.assertIn("Numerical Methods Verification Memo", md)
|
|
318
|
+
|
|
319
|
+
def test_template_is_the_filled_example(self):
|
|
320
|
+
# ROLE-STANDARD: the template IS the example deliverable
|
|
321
|
+
# generated by the core - byte-identical, zero blanks.
|
|
322
|
+
with open(TEMPLATE) as fh:
|
|
323
|
+
template = fh.read()
|
|
324
|
+
self.assertNotIn("___", template)
|
|
325
|
+
self.assertEqual(template, example_report_markdown())
|
|
326
|
+
|
|
327
|
+
def test_standalone_no_skills_repo(self):
|
|
328
|
+
# Core must work with no AeroSkills present
|
|
329
|
+
os.environ["AEROSKILLS_DEV"] = "/nonexistent"
|
|
330
|
+
model = build_report(example_item())
|
|
331
|
+
md = render_report_markdown(model)
|
|
332
|
+
self.assertGreater(len(md), 2000)
|
|
333
|
+
self.assertTrue(check_report(model)["all_pass"])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
if __name__ == "__main__":
|
|
337
|
+
unittest.main()
|