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,1121 @@
1
+ #!/usr/bin/env python3
2
+ """data_bus_avionics_core.py - Data Bus / Avionics Network Engineer core.
3
+
4
+ This is the role's ENGINE: given an aircraft bus architecture it decodes
5
+ ARINC 429 and MIL-STD-1553 words, budgets ARINC 429 label-rate schedules
6
+ into bus load and percent utilization, computes MIL-STD-1553 bus loading
7
+ from a bus-controller minor-frame schedule, sizes ARINC 664 AFDX virtual
8
+ links against the 100 Mbps link, and BUILDS the Avionics Data Bus Loading
9
+ and Protocol Assessment deliverable. It also gate-checks deliverables.
10
+ Standalone: no external repo needed.
11
+
12
+ Domain rules encoded here mirror the bound AeroSkills data-bus leaves
13
+ (AeroSkills avionics/data-bus/*, themselves paraphrases of the public
14
+ protocol practice) so the same quantity can be computed two independent
15
+ ways and cross-checked in provenance:
16
+
17
+ - ARINC 429 (Mark 33 DITS): every transmitted word occupies 36 bit-times
18
+ on the bus (32 data bits + 4-bit inter-word gap); high-speed link is
19
+ 100 kbps (about 2777.8 words/s), low-speed 12.5 kbps (about 347.2
20
+ words/s); common design guideline 80% utilization. Word: bits 1-8
21
+ octal label, 9-10 SDI, 11-29 data, 30-31 SSM, 32 odd parity (integer
22
+ bit 0 = first bit transmitted).
23
+ - MIL-STD-1553B: 1 Mbps command/response bus; 20-bit words (16 info +
24
+ 3-bit sync + odd parity); message = command/status overhead + data
25
+ words; each wire word occupies a 24 us slot (20 us word + 4 us gap);
26
+ loading is schedule time over the minor frame against an 80% budget.
27
+ - ARINC 664 Part 7 AFDX: VL bandwidth = max_frame_bytes*8/(BAG seconds)
28
+ with BAG in {1,2,4,8,16,32,64,128} ms and frame 64-1518 bytes; the VL
29
+ set must fit the 100 Mbps link; latency = 2 serializations + switches
30
+ x switch delay; jitter slack = budget - measured.
31
+
32
+ ARINC 429 / ARINC 664 text is proprietary and never reproduced; the
33
+ field layouts and timing figures above are common-knowledge protocol
34
+ summaries. MIL-STD-1553 is US public-domain work, paraphrase used.
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import re
39
+ from dataclasses import dataclass, field
40
+ from datetime import date
41
+
42
+ def _today() -> str:
43
+ import os
44
+ from datetime import date
45
+ return os.environ.get("ROLE_GEN_DATE", date.today().isoformat())
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Domain constants (grounded in the bound data-bus leaves)
49
+ # ---------------------------------------------------------------------------
50
+
51
+ # ARINC 429 loading model (leaf avionics/data-bus/arinc429-bus-loading)
52
+ A429_BITS_PER_WORD = 36.0 # 32 data bits + 4-bit gap
53
+ A429_RATE_100_KBPS = 100000.0 # high-speed link
54
+ A429_RATE_12_5_KBPS = 12500.0 # low-speed link
55
+ A429_GUIDELINE_PCT = 80.0 # common design guideline
56
+
57
+ # ARINC 429 word layout (leaf avionics/data-bus/arinc429-protocol).
58
+ # Integer bit 0 is the first bit transmitted (ARINC bit 1).
59
+ A429_LABEL_MASK = 0xFF # ARINC bits 1-8
60
+ A429_SDI_MASK = 0b11 # ARINC bits 9-10
61
+ A429_DATA_MASK = 0x7FFFF # ARINC bits 11-29 (19-bit field)
62
+ A429_SSM_MASK = 0b11 # ARINC bits 30-31
63
+ A429_PARITY_BIT = 31 # ARINC bit 32
64
+ A429_BNR_SIGN_BIT = 1 << 18
65
+ A429_BNR_POS_MAX = (1 << 18) - 1
66
+ A429_BNR_NEG_MIN = -(1 << 18)
67
+
68
+ # MIL-STD-1553 loading model (leaf avionics/data-bus/mil-std-1553-bus-loading)
69
+ M1553_WORD_TIME_US = 20.0 # 20 bit times at 1 Mbps
70
+ M1553_WORD_GAP_US = 4.0 # inter-word gap
71
+ M1553_WORD_SLOT_US = M1553_WORD_TIME_US + M1553_WORD_GAP_US # 24.0 us
72
+ M1553_FRAME_US_DEFAULT = 5000.0 # 5 ms minor frame
73
+ M1553_BUDGET_FRACTION = 0.80
74
+ M1553_KINDS = ("BCRT", "RTBC", "RTRT")
75
+
76
+ # MIL-STD-1553 word layout (leaf avionics/data-bus/mil-std-1553)
77
+ M1553_WORD_MASK = (1 << 20) - 1
78
+ M1553_RT_MASK = 0x1F
79
+ M1553_DATA16_MASK = 0xFFFF
80
+ M1553_SYNC_COMMAND = 0b100
81
+ M1553_SYNC_DATA = 0b011
82
+ M1553_MODE_SUBADDRESSES = (0, 0x1F)
83
+ M1553_BROADCAST_RT = 0x1F
84
+
85
+ # ARINC 664 AFDX (leaf avionics/data-bus/arinc664-afdx)
86
+ AFDX_LEGAL_BAGS = (1, 2, 4, 8, 16, 32, 64, 128)
87
+ AFDX_MIN_FRAME = 64
88
+ AFDX_MAX_FRAME = 1518
89
+ AFDX_LINK_RATE = 100_000_000.0 # 100 Mbps full-duplex link
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Guards (stdlib, shared)
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ def _number(value, name):
97
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
98
+ raise ValueError("%s must be a number, got %r" % (name, value))
99
+ return float(value)
100
+
101
+
102
+ def _require_int(value, name, lo, hi):
103
+ if isinstance(value, bool) or not isinstance(value, int):
104
+ raise ValueError("%s must be an integer, got %r" % (name, value))
105
+ if not (lo <= value <= hi):
106
+ raise ValueError("%s %d out of range [%d, %d]" % (name, value, lo, hi))
107
+ return value
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # ARINC 429 protocol (word decode/encode, parity, BNR)
112
+ # ---------------------------------------------------------------------------
113
+
114
+ def a429_parity_ok(word):
115
+ """True when a 32-bit word carries odd parity (odd number of 1 bits)."""
116
+ word = _require_int(word, "word", 0, 0xFFFFFFFF)
117
+ return (bin(word).count("1") % 2) == 1
118
+
119
+
120
+ def a429_decode_word(word):
121
+ """Split a 32-bit ARINC 429 word into label, SDI, data, SSM, parity."""
122
+ word = _require_int(word, "word", 0, 0xFFFFFFFF)
123
+ return {
124
+ "label": word & A429_LABEL_MASK,
125
+ "sdi": (word >> 8) & A429_SDI_MASK,
126
+ "data": (word >> 10) & A429_DATA_MASK,
127
+ "ssm": (word >> 29) & A429_SSM_MASK,
128
+ "parity": (word >> A429_PARITY_BIT) & 1,
129
+ "parity_ok": a429_parity_ok(word),
130
+ }
131
+
132
+
133
+ def a429_build_word(label, sdi, data, ssm, parity=None):
134
+ """Pack label, SDI, 19-bit data, SSM, and odd parity into 32 bits.
135
+
136
+ Worked anchor: build_word(8, 1, 1234, 3) -> 1611876616 (0x60134908).
137
+ """
138
+ if isinstance(label, str):
139
+ text = label.strip()
140
+ if not re.match(r"^[0-7]+$", text):
141
+ raise ValueError("label %r is not octal digits (0-7)" % (label,))
142
+ label = int(text, 8)
143
+ label = _require_int(label, "label", 0, A429_LABEL_MASK)
144
+ sdi = _require_int(sdi, "sdi", 0, A429_SDI_MASK)
145
+ data = _require_int(data, "data", 0, A429_DATA_MASK)
146
+ ssm = _require_int(ssm, "ssm", 0, A429_SSM_MASK)
147
+ payload = label | (sdi << 8) | (data << 10) | (ssm << 29)
148
+ if parity is None:
149
+ parity = 0 if (bin(payload & 0x7FFFFFFF).count("1") % 2) else 1
150
+ else:
151
+ parity = _require_int(parity, "parity", 0, 1)
152
+ return payload | (parity << A429_PARITY_BIT)
153
+
154
+
155
+ def a429_bnr_encode(value, scale_factor):
156
+ """Encode a signed BNR value into the 19-bit data field (two's complement).
157
+
158
+ Worked anchor: 123.4 at LSB 0.1 -> field 1234; -12.3 -> field 524165.
159
+ """
160
+ value = _number(value, "value")
161
+ scale = _number(scale_factor, "scale_factor")
162
+ if scale <= 0.0:
163
+ raise ValueError("scale_factor must be positive, got %r" % (scale,))
164
+ raw = int(round(value / scale))
165
+ if not (A429_BNR_NEG_MIN <= raw <= A429_BNR_POS_MAX):
166
+ raise ValueError(
167
+ "value %.6f does not fit BNR range at scale %.6f" % (value, scale))
168
+ if raw < 0:
169
+ raw += 1 << 19
170
+ return raw
171
+
172
+
173
+ def a429_bnr_decode(data_field, scale_factor):
174
+ """Decode a 19-bit BNR data field to its signed engineering value."""
175
+ data_field = _require_int(data_field, "data_field", 0, A429_DATA_MASK)
176
+ scale = _number(scale_factor, "scale_factor")
177
+ if scale <= 0.0:
178
+ raise ValueError("scale_factor must be positive, got %r" % (scale,))
179
+ raw = data_field - (1 << 19) if data_field & A429_BNR_SIGN_BIT else data_field
180
+ return raw * scale
181
+
182
+
183
+ # ---------------------------------------------------------------------------
184
+ # ARINC 429 bus loading (label rate schedule -> utilization)
185
+ # ---------------------------------------------------------------------------
186
+
187
+ def a429_total_word_rate(label_rates):
188
+ """Sum a label rate schedule (dict {label: words/s} or list) into wps."""
189
+ rates = list(label_rates.values()) if isinstance(label_rates, dict) \
190
+ else list(label_rates)
191
+ if not rates:
192
+ raise ValueError("the label rate schedule is empty")
193
+ for r in rates:
194
+ if r < 0:
195
+ raise ValueError("label rates must be non-negative, got %r" % (r,))
196
+ return sum(float(r) for r in rates)
197
+
198
+
199
+ def a429_bus_load_bps(total_words_per_s):
200
+ """Bus load in bits/s: every word occupies 36 bit-times on the bus."""
201
+ if total_words_per_s < 0:
202
+ raise ValueError("words per second must be non-negative")
203
+ return float(total_words_per_s) * A429_BITS_PER_WORD
204
+
205
+
206
+ def a429_word_capacity(link_rate_bps=A429_RATE_100_KBPS):
207
+ """Word-per-second capacity of the link: rate / 36 bit-times.
208
+
209
+ About 2777.8 words/s at 100 kbps, 347.2 words/s at 12.5 kbps.
210
+ """
211
+ if link_rate_bps <= 0:
212
+ raise ValueError("link rate must be positive")
213
+ return float(link_rate_bps) / A429_BITS_PER_WORD
214
+
215
+
216
+ def a429_utilization_pct(total_words_per_s, link_rate_bps=A429_RATE_100_KBPS):
217
+ """Percent of the link occupied by the word schedule."""
218
+ if total_words_per_s < 0:
219
+ raise ValueError("words per second must be non-negative")
220
+ if link_rate_bps <= 0:
221
+ raise ValueError("link rate must be positive")
222
+ return a429_bus_load_bps(total_words_per_s) / float(link_rate_bps) * 100.0
223
+
224
+
225
+ def a429_loading_summary(label_rates, link_rate_bps=A429_RATE_100_KBPS):
226
+ """Full ARINC 429 bus loading budget for a label rate schedule.
227
+
228
+ Returns total_words_per_s, load_bps, utilization_pct, capacity_wps,
229
+ capacity_verdict (FITS/OVER against 100% of the link), guideline_pct,
230
+ guideline_verdict (FITS/VIOLATION against the 80% design guideline),
231
+ headroom_pct (margin to the guideline, floored at 0).
232
+ """
233
+ total = a429_total_word_rate(label_rates)
234
+ if link_rate_bps <= 0:
235
+ raise ValueError("link rate must be positive")
236
+ load_bps = a429_bus_load_bps(total)
237
+ utilization_pct = a429_utilization_pct(total, link_rate_bps)
238
+ capacity_wps = a429_word_capacity(link_rate_bps)
239
+ return {
240
+ "total_words_per_s": total,
241
+ "load_bps": load_bps,
242
+ "utilization_pct": utilization_pct,
243
+ "capacity_wps": capacity_wps,
244
+ "capacity_verdict": "OVER" if utilization_pct > 100.0 else "FITS",
245
+ "guideline_pct": A429_GUIDELINE_PCT,
246
+ "guideline_verdict": "VIOLATION"
247
+ if utilization_pct > A429_GUIDELINE_PCT else "FITS",
248
+ "headroom_pct": max(0.0, A429_GUIDELINE_PCT - utilization_pct),
249
+ }
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # MIL-STD-1553 protocol (command / status / data words, classification)
254
+ # ---------------------------------------------------------------------------
255
+
256
+ def m1553_parity_ok(word):
257
+ """True when a 20-bit word carries odd parity."""
258
+ word = _require_int(word, "word", 0, M1553_WORD_MASK)
259
+ return (bin(word).count("1") % 2) == 1
260
+
261
+
262
+ def _m1553_parity_bit(data16, sync):
263
+ ones = bin(data16 & M1553_DATA16_MASK).count("1") \
264
+ + bin(sync & 0b111).count("1")
265
+ return 0 if (ones % 2) else 1
266
+
267
+
268
+ def m1553_encode_command_word(rt_address, subaddress, word_count,
269
+ transmit_receive):
270
+ """Pack RT address, T/R, subaddress, word count, odd parity (20 bits).
271
+
272
+ Worked anchor: (5, 12, 16, 1) -> 93316, odd parity.
273
+ """
274
+ rt = _require_int(rt_address, "rt_address", 0, M1553_RT_MASK)
275
+ sa = _require_int(subaddress, "subaddress", 0, M1553_RT_MASK)
276
+ wc = _require_int(word_count, "word_count", 0, M1553_RT_MASK)
277
+ tr = _require_int(transmit_receive, "transmit_receive", 0, 1)
278
+ data16 = (rt << 11) | (tr << 10) | (sa << 5) | wc
279
+ parity = _m1553_parity_bit(data16, M1553_SYNC_COMMAND)
280
+ return (parity << 19) | (data16 << 3) | M1553_SYNC_COMMAND
281
+
282
+
283
+ def m1553_decode_command_word(word):
284
+ """Split a 20-bit command word; raises on a non-command sync pattern."""
285
+ word = _require_int(word, "word", 0, M1553_WORD_MASK)
286
+ if (word & 0b111) != M1553_SYNC_COMMAND:
287
+ raise ValueError("word 0x%X is not a command/status sync word" % word)
288
+ data16 = (word >> 3) & M1553_DATA16_MASK
289
+ return {
290
+ "rt_address": (data16 >> 11) & M1553_RT_MASK,
291
+ "transmit_receive": (data16 >> 10) & 1,
292
+ "subaddress": (data16 >> 5) & M1553_RT_MASK,
293
+ "word_count": data16 & M1553_RT_MASK,
294
+ "parity": (word >> 19) & 1,
295
+ "parity_ok": m1553_parity_ok(word),
296
+ }
297
+
298
+
299
+ def m1553_encode_data_word(data):
300
+ """Pack 16 data bits and odd parity into a 20-bit data word.
301
+
302
+ Worked anchor: 0x7FFF -> 262139.
303
+ """
304
+ data = _require_int(data, "data", 0, M1553_DATA16_MASK)
305
+ parity = _m1553_parity_bit(data, M1553_SYNC_DATA)
306
+ return (parity << 19) | (data << 3) | M1553_SYNC_DATA
307
+
308
+
309
+ def m1553_encode_status_word(rt_address, message_error=0, instrumentation=0,
310
+ service_request=0, broadcast_received=0, busy=0,
311
+ subsystem_flag=0,
312
+ dynamic_bus_control_acceptance=0,
313
+ terminal_flag=0):
314
+ """Pack RT address and status flags with odd parity.
315
+
316
+ Worked anchor: encode_status_word(5, busy=1) -> 606276.
317
+ """
318
+ rt = _require_int(rt_address, "rt_address", 0, M1553_RT_MASK)
319
+ me = _require_int(message_error, "message_error", 0, 1)
320
+ inst = _require_int(instrumentation, "instrumentation", 0, 1)
321
+ sr = _require_int(service_request, "service_request", 0, 1)
322
+ bcr = _require_int(broadcast_received, "broadcast_received", 0, 1)
323
+ bsy = _require_int(busy, "busy", 0, 1)
324
+ ssf = _require_int(subsystem_flag, "subsystem_flag", 0, 1)
325
+ dbca = _require_int(dynamic_bus_control_acceptance,
326
+ "dynamic_bus_control_acceptance", 0, 1)
327
+ tfl = _require_int(terminal_flag, "terminal_flag", 0, 1)
328
+ data16 = ((rt << 11) | (me << 10) | (inst << 9) | (sr << 8) | (bcr << 4)
329
+ | (bsy << 3) | (ssf << 2) | (dbca << 1) | tfl)
330
+ parity = _m1553_parity_bit(data16, M1553_SYNC_COMMAND)
331
+ return (parity << 19) | (data16 << 3) | M1553_SYNC_COMMAND
332
+
333
+
334
+ def m1553_decode_status_word(word):
335
+ """Split a 20-bit status word into RT address and status flags.
336
+
337
+ Flag layout in transmission order: message error, instrumentation,
338
+ service request, 3 reserved zeros, broadcast received, busy,
339
+ subsystem flag, dynamic bus control acceptance, terminal flag.
340
+ Worked anchor: 606276 -> RT 5 with busy=1.
341
+ """
342
+ word = _require_int(word, "word", 0, M1553_WORD_MASK)
343
+ if (word & 0b111) != M1553_SYNC_COMMAND:
344
+ raise ValueError("word 0x%X is not a status sync word" % word)
345
+ data16 = (word >> 3) & M1553_DATA16_MASK
346
+ return {
347
+ "rt_address": (data16 >> 11) & M1553_RT_MASK,
348
+ "message_error": (data16 >> 10) & 1,
349
+ "instrumentation": (data16 >> 9) & 1,
350
+ "service_request": (data16 >> 8) & 1,
351
+ "broadcast_received": (data16 >> 4) & 1,
352
+ "busy": (data16 >> 3) & 1,
353
+ "subsystem_flag": (data16 >> 2) & 1,
354
+ "dynamic_bus_control_acceptance": (data16 >> 1) & 1,
355
+ "terminal_flag": data16 & 1,
356
+ "parity": (word >> 19) & 1,
357
+ "parity_ok": m1553_parity_ok(word),
358
+ }
359
+
360
+
361
+ def m1553_classify_message(rt_address, subaddress, word_count,
362
+ transmit_receive):
363
+ """Classify the message a command word initiates.
364
+
365
+ Returns broadcast (RT 31), mode-code (subaddress 0/31), rt-to-bc
366
+ (T/R 1), or bc-to-rt (T/R 0).
367
+ """
368
+ rt = _require_int(rt_address, "rt_address", 0, M1553_RT_MASK)
369
+ sa = _require_int(subaddress, "subaddress", 0, M1553_RT_MASK)
370
+ _require_int(word_count, "word_count", 0, M1553_RT_MASK)
371
+ tr = _require_int(transmit_receive, "transmit_receive", 0, 1)
372
+ if rt == M1553_BROADCAST_RT:
373
+ return "broadcast"
374
+ if sa in M1553_MODE_SUBADDRESSES:
375
+ return "mode-code"
376
+ if tr == 1:
377
+ return "rt-to-bc"
378
+ return "bc-to-rt"
379
+
380
+
381
+ # ---------------------------------------------------------------------------
382
+ # MIL-STD-1553 bus loading (BC minor-frame schedule -> utilization)
383
+ # ---------------------------------------------------------------------------
384
+
385
+ def m1553_wire_words(kind, data_words):
386
+ """Wire words on the bus for one message: overhead + data words.
387
+
388
+ BCRT/RTBC = 1 command + 1 status + data; RTRT = 2 commands + 1 status
389
+ + data. data_words must be an integer in 1..32.
390
+ """
391
+ if kind not in M1553_KINDS:
392
+ raise ValueError("unknown message kind %r, use BCRT, RTBC or RTRT"
393
+ % (kind,))
394
+ if not isinstance(data_words, int):
395
+ raise ValueError("data_words must be an integer")
396
+ if data_words < 1 or data_words > 32:
397
+ raise ValueError("data_words must be in 1..32")
398
+ return data_words + (3 if kind == "RTRT" else 2)
399
+
400
+
401
+ def m1553_message_time_us(kind, data_words):
402
+ """Bus time in us for one message: wire words x 24 us slot."""
403
+ return float(m1553_wire_words(kind, data_words)) * M1553_WORD_SLOT_US
404
+
405
+
406
+ def m1553_schedule_utilization(messages, frame_us=M1553_FRAME_US_DEFAULT):
407
+ """Utilization of a minor frame by a (kind, data_words) schedule.
408
+
409
+ Returns total_us, utilization_fraction, utilization_pct, budget_us
410
+ (80% of the frame), headroom_us, headroom_pct, verdict (FITS when the
411
+ schedule fits the 80% budget, OVER otherwise).
412
+ """
413
+ if frame_us <= 0:
414
+ raise ValueError("frame_us must be positive")
415
+ if not messages:
416
+ raise ValueError("schedule is empty, nothing to load")
417
+ total_us = sum(m1553_message_time_us(kind, words)
418
+ for kind, words in messages)
419
+ budget_us = M1553_BUDGET_FRACTION * frame_us
420
+ utilization_fraction = total_us / frame_us
421
+ headroom_us = budget_us - total_us
422
+ return {
423
+ "total_us": total_us,
424
+ "total_wire_words": sum(m1553_wire_words(kind, words)
425
+ for kind, words in messages),
426
+ "utilization_fraction": utilization_fraction,
427
+ "utilization_pct": utilization_fraction * 100.0,
428
+ "budget_us": budget_us,
429
+ "headroom_us": headroom_us,
430
+ "headroom_pct": headroom_us / frame_us * 100.0,
431
+ "verdict": "FITS" if total_us <= budget_us else "OVER",
432
+ }
433
+
434
+
435
+ # ---------------------------------------------------------------------------
436
+ # ARINC 664 AFDX (VL bandwidth, link budget, timing)
437
+ # ---------------------------------------------------------------------------
438
+
439
+ def _require_bag(bag_ms):
440
+ bag_ms = _require_int(bag_ms, "bag_ms", 1, 128)
441
+ if bag_ms not in AFDX_LEGAL_BAGS:
442
+ raise ValueError("bag_ms %d is not a legal BAG (%s ms)"
443
+ % (bag_ms, ", ".join(str(b) for b in AFDX_LEGAL_BAGS)))
444
+ return bag_ms
445
+
446
+
447
+ def _require_frame(frame_bytes):
448
+ return _require_int(frame_bytes, "max_frame_bytes",
449
+ AFDX_MIN_FRAME, AFDX_MAX_FRAME)
450
+
451
+
452
+ def afdx_vl_bandwidth(bag_ms, max_frame_bytes):
453
+ """Bandwidth of one virtual link: frame*8/(BAG seconds).
454
+
455
+ Worked anchors: (4 ms, 1518) -> 3036000.0 bps; (128 ms, 1518) ->
456
+ 94875.0 bps.
457
+ """
458
+ bag = _require_bag(bag_ms)
459
+ frame = _require_frame(max_frame_bytes)
460
+ return frame * 8 / (bag / 1000.0)
461
+
462
+
463
+ def afdx_link_utilization_pct(vl_specs, link_rate_bps=AFDX_LINK_RATE):
464
+ """Percent of the link consumed by a (bag_ms, frame_bytes) VL set.
465
+
466
+ Raises ValueError when the set oversubscribes the link (mirrors the
467
+ leaf: an over-budget set is a configuration error, not a percentage).
468
+ """
469
+ link = _number(link_rate_bps, "link_rate_bps")
470
+ if link <= 0.0:
471
+ raise ValueError("link_rate_bps must be positive")
472
+ total = 0.0
473
+ for spec in vl_specs:
474
+ try:
475
+ bag_ms, frame_bytes = spec
476
+ except (TypeError, ValueError):
477
+ raise ValueError("vl entry %r is not a (bag_ms, frame_bytes) pair"
478
+ % (spec,))
479
+ total += afdx_vl_bandwidth(bag_ms, frame_bytes)
480
+ if total > link:
481
+ raise ValueError(
482
+ "virtual link set needs %.0f bps but the link carries %.0f bps"
483
+ " (oversubscribed)" % (total, link))
484
+ return total / link * 100.0
485
+
486
+
487
+ def afdx_transmission_time_us(frame_bytes, link_rate_bps=AFDX_LINK_RATE):
488
+ """Serialization time of one frame in us: frame*8/link.
489
+
490
+ Worked anchor: 1518 bytes at 100 Mbps -> 121.44 us; 64 bytes -> 5.12 us.
491
+ """
492
+ frame = _require_frame(frame_bytes)
493
+ link = _number(link_rate_bps, "link_rate_bps")
494
+ if link <= 0.0:
495
+ raise ValueError("link_rate_bps must be positive")
496
+ return frame * 8 / link * 1e6
497
+
498
+
499
+ def afdx_end_to_end_latency_us(frame_bytes, switch_count, switch_delay_us,
500
+ link_rate_bps=AFDX_LINK_RATE):
501
+ """Worst-case one-way latency in us: 2 serializations + switch delays.
502
+
503
+ Worked anchor: 1518 bytes, 2 switches at 150 us -> 542.88 us.
504
+ """
505
+ tx_us = afdx_transmission_time_us(frame_bytes, link_rate_bps)
506
+ switches = _require_int(switch_count, "switch_count", 0, 100)
507
+ delay = _number(switch_delay_us, "switch_delay_us")
508
+ if delay < 0.0:
509
+ raise ValueError("switch_delay_us must be non-negative")
510
+ return 2 * tx_us + switches * delay
511
+
512
+
513
+ def afdx_jitter_slack(measured_max_jitter_us, jitter_budget_us):
514
+ """Slack between measured max jitter and the budget, in us.
515
+
516
+ Worked anchor: (420.0, 500.0) -> 80.0 (compliant); negative = violation.
517
+ """
518
+ measured = _number(measured_max_jitter_us, "measured_max_jitter_us")
519
+ budget = _number(jitter_budget_us, "jitter_budget_us")
520
+ if measured < 0.0:
521
+ raise ValueError("measured jitter must be non-negative")
522
+ if budget <= 0.0:
523
+ raise ValueError("jitter budget must be positive")
524
+ return budget - measured
525
+
526
+
527
+ def afdx_largest_bag_for_bandwidth(bandwidth_bps, max_frame_bytes):
528
+ """Largest legal BAG (ms) whose VL bandwidth meets the requirement.
529
+
530
+ Worked anchor: 1.0 Mbps at 1518 bytes -> 8 ms; raises when even the
531
+ 1 ms BAG is insufficient.
532
+ """
533
+ needed = _number(bandwidth_bps, "bandwidth_bps")
534
+ frame = _require_frame(max_frame_bytes)
535
+ if needed <= 0.0:
536
+ raise ValueError("bandwidth_bps must be positive")
537
+ max_rate = afdx_vl_bandwidth(1, frame)
538
+ if needed > max_rate:
539
+ raise ValueError(
540
+ "bandwidth %.0f bps exceeds the 1 ms BAG capacity at %d bytes"
541
+ % (needed, frame))
542
+ chosen = AFDX_LEGAL_BAGS[0]
543
+ for bag in AFDX_LEGAL_BAGS:
544
+ if afdx_vl_bandwidth(bag, frame) >= needed:
545
+ chosen = bag
546
+ return chosen
547
+
548
+
549
+ # ---------------------------------------------------------------------------
550
+ # Example architecture + analysis + deliverable
551
+ # ---------------------------------------------------------------------------
552
+
553
+ @dataclass
554
+ class A429Line:
555
+ """One ARINC 429 transmit line: single transmitter, N receivers."""
556
+ name: str
557
+ transmitter: str
558
+ receivers: str
559
+ link_rate_bps: float = A429_RATE_100_KBPS
560
+ label_rates: dict = field(default_factory=dict) # {label: words/s}
561
+
562
+
563
+ @dataclass
564
+ class M1553Bus:
565
+ """One MIL-STD-1553 bus: BC schedule per minor frame."""
566
+ name: str
567
+ controller: str
568
+ remote_terminals: str
569
+ frame_us: float = M1553_FRAME_US_DEFAULT
570
+ messages: list = field(default_factory=list) # [(kind, data_words)]
571
+
572
+
573
+ @dataclass
574
+ class AfdxNetwork:
575
+ """One AFDX network: dual-redundant VL set (network A and B)."""
576
+ name: str
577
+ vl_specs: list = field(default_factory=list) # [(bag_ms, frame_bytes)]
578
+ jitter_measured_us: float = 0.0
579
+ jitter_budget_us: float = 0.0
580
+ switch_count: int = 0
581
+ switch_delay_us: float = 0.0
582
+
583
+
584
+ @dataclass
585
+ class DataBusItem:
586
+ """Project facts the role needs to build the assessment."""
587
+ aircraft: str
588
+ description: str = ""
589
+ arinc429_lines: list = field(default_factory=list)
590
+ m1553_bus: M1553Bus | None = None
591
+ afdx_network: AfdxNetwork | None = None
592
+ a429_decode_sample: int = 0 # received word for conformance
593
+ a429_sample_expected_label: str = ""
594
+ a429_bnr_scale: float = 0.1
595
+ m1553_command_sample: int = 0 # received command word
596
+ m1553_status_sample: int = 0 # received status word
597
+ certification_basis: str = "MIL-STD-1553B / ARINC 429 / ARINC 664 practice"
598
+
599
+
600
+ def analyze_architecture(item: DataBusItem) -> dict:
601
+ """Run every computation for the architecture; returns the results."""
602
+ lines = []
603
+ for bus in item.arinc429_lines:
604
+ summary = a429_loading_summary(bus.label_rates, bus.link_rate_bps)
605
+ lines.append({
606
+ "name": bus.name,
607
+ "transmitter": bus.transmitter,
608
+ "receivers": bus.receivers,
609
+ "link_rate_bps": bus.link_rate_bps,
610
+ "label_rates": {str(k): float(v) for k, v in bus.label_rates.items()},
611
+ "summary": summary,
612
+ })
613
+
614
+ m1553 = None
615
+ if item.m1553_bus:
616
+ bus = item.m1553_bus
617
+ rows = [{
618
+ "kind": kind, "data_words": words,
619
+ "wire_words": m1553_wire_words(kind, words),
620
+ "time_us": m1553_message_time_us(kind, words),
621
+ } for kind, words in bus.messages]
622
+ schedule = m1553_schedule_utilization(bus.messages, bus.frame_us)
623
+ m1553 = {
624
+ "name": bus.name,
625
+ "controller": bus.controller,
626
+ "remote_terminals": bus.remote_terminals,
627
+ "frame_us": bus.frame_us,
628
+ "messages": rows,
629
+ "schedule": schedule,
630
+ }
631
+
632
+ afdx = None
633
+ if item.afdx_network:
634
+ net = item.afdx_network
635
+ vl_rows = [{
636
+ "bag_ms": bag, "frame_bytes": frame,
637
+ "bandwidth_bps": afdx_vl_bandwidth(bag, frame),
638
+ } for bag, frame in net.vl_specs]
639
+ total_bw = sum(r["bandwidth_bps"] for r in vl_rows)
640
+ try:
641
+ utilization_pct = afdx_link_utilization_pct(net.vl_specs)
642
+ over = False
643
+ error = ""
644
+ except ValueError as e:
645
+ utilization_pct = total_bw / AFDX_LINK_RATE * 100.0
646
+ over = True
647
+ error = str(e)
648
+ check = None
649
+ if net.vl_specs:
650
+ check = {
651
+ "frame_bytes": net.vl_specs[0][1],
652
+ "latency_us": afdx_end_to_end_latency_us(
653
+ net.vl_specs[0][1], net.switch_count,
654
+ net.switch_delay_us),
655
+ "jitter_slack_us": afdx_jitter_slack(
656
+ net.jitter_measured_us, net.jitter_budget_us),
657
+ }
658
+ afdx = {
659
+ "name": net.name,
660
+ "vl_count": len(vl_rows),
661
+ "vl_rows": vl_rows,
662
+ "total_bandwidth_bps": total_bw,
663
+ "link_rate_bps": AFDX_LINK_RATE,
664
+ "utilization_pct": utilization_pct,
665
+ "oversubscribed": over,
666
+ "error": error,
667
+ "redundancy": "dual independent networks A and B, same VL set",
668
+ "switch_count": net.switch_count,
669
+ "switch_delay_us": net.switch_delay_us,
670
+ "jitter_measured_us": net.jitter_measured_us,
671
+ "jitter_budget_us": net.jitter_budget_us,
672
+ "checks": check,
673
+ }
674
+
675
+ # Protocol conformance samples (decode received words on the bench)
676
+ a429_word = a429_decode_word(item.a429_decode_sample) if \
677
+ item.a429_decode_sample else None
678
+ a429_bnr = None
679
+ if a429_word is not None:
680
+ a429_bnr = {
681
+ "data_field": a429_word["data"],
682
+ "scale": item.a429_bnr_scale,
683
+ "engineering_value": a429_bnr_decode(a429_word["data"],
684
+ item.a429_bnr_scale),
685
+ }
686
+ m1553_cmd = m1553_decode_command_word(item.m1553_command_sample) if \
687
+ item.m1553_command_sample else None
688
+ m1553_st = m1553_decode_status_word(item.m1553_status_sample) if \
689
+ item.m1553_status_sample else None
690
+
691
+ return {
692
+ "arinc429_lines": lines,
693
+ "m1553": m1553,
694
+ "afdx": afdx,
695
+ "conformance": {
696
+ "a429_word": a429_word,
697
+ "a429_word_hex": "0x%08X" % item.a429_decode_sample
698
+ if item.a429_decode_sample else "",
699
+ "a429_expected_label": item.a429_sample_expected_label,
700
+ "a429_bnr": a429_bnr,
701
+ "m1553_command": m1553_cmd,
702
+ "m1553_status": m1553_st,
703
+ },
704
+ }
705
+
706
+
707
+ def build_assessment(item: DataBusItem, results: dict) -> dict:
708
+ """Build the complete content model from facts + computed results."""
709
+ observations = []
710
+ for line in results["arinc429_lines"]:
711
+ s = line["summary"]
712
+ if s["guideline_verdict"] == "VIOLATION":
713
+ observations.append(
714
+ "%s exceeds the %g%% design guideline at %.2f%% utilization "
715
+ "(headroom 0): re-rate or offload labels."
716
+ % (line["name"], s["guideline_pct"], s["utilization_pct"]))
717
+ elif s["headroom_pct"] < 10.0:
718
+ observations.append(
719
+ "%s sits at %.2f%% utilization with only %.2f%% headroom to "
720
+ "the %g%% guideline: growth requires a re-rate or a new line."
721
+ % (line["name"], s["utilization_pct"], s["headroom_pct"],
722
+ s["guideline_pct"]))
723
+ m1553 = results.get("m1553") or {}
724
+ if m1553:
725
+ sch = m1553["schedule"]
726
+ if sch["verdict"] == "OVER":
727
+ observations.append(
728
+ "%s schedule OVER the %g%% budget (%.2f%%): move messages "
729
+ "to the spare bus or lengthen the minor frame."
730
+ % (m1553["name"], M1553_BUDGET_FRACTION * 100.0,
731
+ sch["utilization_pct"]))
732
+ afdx = results.get("afdx")
733
+ if afdx and afdx["oversubscribed"]:
734
+ observations.append(
735
+ "%s VL set oversubscribes the 100 Mbps link (%.2f%%): increase "
736
+ "BAGs or reduce frame sizes. %s" % (afdx["name"],
737
+ afdx["utilization_pct"],
738
+ afdx["error"]))
739
+ return {
740
+ "document_type": "Avionics Data Bus Loading and Protocol Assessment",
741
+ "status": "draft-for-review",
742
+ "aircraft": item.aircraft,
743
+ "description": item.description,
744
+ "certification_basis": item.certification_basis,
745
+ "arinc429": results["arinc429_lines"],
746
+ "m1553": m1553,
747
+ "afdx": afdx,
748
+ "conformance": results["conformance"],
749
+ "observations": observations,
750
+ "generated": _today(),
751
+ }
752
+
753
+
754
+ def _fmt_link(rate):
755
+ if rate >= 1e6:
756
+ return "%g Mbps" % (rate / 1e6)
757
+ if rate >= 1e3:
758
+ return "%g kbps" % (rate / 1e3)
759
+ return "%g bps" % rate
760
+
761
+
762
+ def render_assessment_markdown(model: dict) -> str:
763
+ """Render the content model as the deliverable markdown document."""
764
+ lines = [
765
+ "# Avionics Data Bus Loading and Protocol Assessment",
766
+ "",
767
+ f"**Aircraft / program:** {model['aircraft']}",
768
+ f"**Status:** {model['status']}",
769
+ f"**Protocol basis:** {model['certification_basis']}",
770
+ "",
771
+ "## 1. Scope",
772
+ "",
773
+ "This assessment budgets every ARINC 429 transmit line, the "
774
+ "MIL-STD-1553 bus-controller minor-frame schedule, and the ARINC "
775
+ "664 AFDX virtual-link set of the aircraft bus architecture, and "
776
+ "checks received-word conformance (parity, label, word fields) "
777
+ "against the interface control documents."
778
+ + (f" {model['description']}" if model.get("description") else ""),
779
+ "",
780
+ "Loading gates: ARINC 429 and MIL-STD-1553 utilization against the "
781
+ f"{A429_GUIDELINE_PCT:.0f}% design guideline "
782
+ "(the leaf practice; ARINC 429 capacity is the link itself), "
783
+ "AFDX VL set against the 100 Mbps link.",
784
+ "",
785
+ "## 2. ARINC 429 bus loading",
786
+ "",
787
+ ]
788
+ for bus in model["arinc429"]:
789
+ s = bus["summary"]
790
+ lines += [
791
+ f"### {bus['name']} ({bus['transmitter']} -> "
792
+ f"{bus['receivers']}, {_fmt_link(bus['link_rate_bps'])})",
793
+ "",
794
+ "| Label | Rate (words/s) |",
795
+ "|---|---|",
796
+ ]
797
+ for label, rate in sorted(bus["label_rates"].items()):
798
+ lines.append(f"| {label} | {rate:g} |")
799
+ lines += [
800
+ "",
801
+ f"- Total word rate: **{s['total_words_per_s']:.2f} words/s**",
802
+ f"- Bus load: **{s['load_bps']:.0f} bps** "
803
+ f"({A429_BITS_PER_WORD:.0f} bit-times per word)",
804
+ f"- Bus utilization: **{s['utilization_pct']:.2f}%** of "
805
+ f"{_fmt_link(bus['link_rate_bps'])} "
806
+ f"(capacity {s['capacity_wps']:.1f} words/s)",
807
+ f"- Capacity verdict: **{s['capacity_verdict']}**; design "
808
+ f"guideline ({s['guideline_pct']:.0f}%): "
809
+ f"**{s['guideline_verdict']}**",
810
+ f"- Headroom to guideline: **{s['headroom_pct']:.2f}%**",
811
+ "",
812
+ ]
813
+ lines += ["## 3. MIL-STD-1553 bus loading", ""]
814
+ m1553 = model.get("m1553")
815
+ if m1553:
816
+ sch = m1553["schedule"]
817
+ lines += [
818
+ f"### {m1553['name']} (BC {m1553['controller']}; RTs "
819
+ f"{m1553['remote_terminals']}; minor frame "
820
+ f"{m1553['frame_us']:.0f} us)",
821
+ "",
822
+ "| Message | Data words | Wire words | Bus time (us) |",
823
+ "|---|---|---|---|",
824
+ ]
825
+ for row in m1553["messages"]:
826
+ lines.append(f"| {row['kind']} | {row['data_words']} | "
827
+ f"{row['wire_words']} | {row['time_us']:.0f} |")
828
+ lines += [
829
+ "",
830
+ f"- Schedule time: **{sch['total_us']:.0f} us** over "
831
+ f"{m1553['frame_us']:.0f} us "
832
+ f"({sch['total_wire_words']} wire words x "
833
+ f"{M1553_WORD_SLOT_US:.0f} us)",
834
+ f"- Bus utilization: **{sch['utilization_pct']:.2f}%**",
835
+ f"- Budget ({M1553_BUDGET_FRACTION * 100:.0f}% of frame): "
836
+ f"{sch['budget_us']:.0f} us; headroom "
837
+ f"{sch['headroom_us']:.0f} us "
838
+ f"({sch['headroom_pct']:.2f}%)",
839
+ f"- Verdict: **{sch['verdict']}**",
840
+ "",
841
+ ]
842
+ else:
843
+ lines += ["No MIL-STD-1553 bus in this architecture.", ""]
844
+
845
+ lines += ["## 4. ARINC 664 AFDX network", ""]
846
+ afdx = model.get("afdx")
847
+ if afdx:
848
+ lines += [
849
+ f"### {afdx['name']} ({afdx['redundancy']})",
850
+ "",
851
+ f"- Virtual links: {afdx['vl_count']}; total bandwidth "
852
+ f"**{afdx['total_bandwidth_bps'] / 1e6:.3f} Mbps**",
853
+ f"- Link utilization: **{afdx['utilization_pct']:.2f}%** of "
854
+ f"100 Mbps",
855
+ f"- Link budget verdict: "
856
+ f"**{'OVER' if afdx['oversubscribed'] else 'FITS'}**",
857
+ "",
858
+ "| VLs | BAG (ms) | Frame (bytes) | Bandwidth (Mbps) |",
859
+ "|---|---|---|---|",
860
+ ]
861
+ # summarize repeated (bag, frame) pairs
862
+ counts = {}
863
+ for row in afdx["vl_rows"]:
864
+ key = (row["bag_ms"], row["frame_bytes"])
865
+ counts[key] = counts.get(key, 0) + 1
866
+ for (bag, frame), n in sorted(counts.items()):
867
+ bw = afdx_vl_bandwidth(bag, frame) / 1e6
868
+ lines.append(f"| {n} | {bag} | {frame} | {bw:.3f} |")
869
+ if afdx["checks"]:
870
+ chk = afdx["checks"]
871
+ lines += [
872
+ "",
873
+ f"- Worst-case latency ({chk['frame_bytes']}-byte frame, "
874
+ f"{afdx['switch_count']} switch(es) at "
875
+ f"{afdx['switch_delay_us']:.0f} us): "
876
+ f"**{chk['latency_us']:.2f} us**",
877
+ f"- Jitter: measured {afdx['jitter_measured_us']:.0f} us vs "
878
+ f"budget {afdx['jitter_budget_us']:.0f} us -> slack "
879
+ f"**{chk['jitter_slack_us']:.1f} us**",
880
+ "",
881
+ ]
882
+ else:
883
+ lines += ["No AFDX network in this architecture.", ""]
884
+
885
+ lines += ["## 5. Protocol conformance checks", ""]
886
+ conf = model["conformance"]
887
+ aw = conf.get("a429_word")
888
+ if a429_word_present := aw:
889
+ bnr = conf.get("a429_bnr") or {}
890
+ label_ok = (aw["label"] == _oct_label(conf.get("a429_expected_label", "")))
891
+ lines += [
892
+ f"- ARINC 429 received word {conf.get('a429_word_hex', '')}: "
893
+ f"label {aw['label']:03o} ({aw['label']}), SDI {aw['sdi']}, "
894
+ f"data field {aw['data']}"
895
+ + (f" (BNR {bnr.get('engineering_value', 0):g} at "
896
+ f"LSB {bnr.get('scale', 0.1):g})" if bnr else "")
897
+ + f", SSM {aw['ssm']}, parity {'OK' if aw['parity_ok'] else 'FAIL'}",
898
+ f" - Expected label {conf.get('a429_expected_label', '')} "
899
+ f"({_oct_label(conf.get('a429_expected_label', ''))}): "
900
+ f"{'MATCH' if label_ok else 'MISMATCH'}",
901
+ ]
902
+ mc = conf.get("m1553_command")
903
+ if mc:
904
+ cls = m1553_classify_message(mc["rt_address"], mc["subaddress"],
905
+ mc["word_count"], mc["transmit_receive"])
906
+ lines += [
907
+ f"- MIL-STD-1553 command word {mc['word_count']} data words, "
908
+ f"RT {mc['rt_address']} subaddress {mc['subaddress']}, "
909
+ f"{'RT-to-BC' if mc['transmit_receive'] else 'BC-to-RT'}: "
910
+ f"classified **{cls}**, parity "
911
+ f"{'OK' if mc['parity_ok'] else 'FAIL'}",
912
+ ]
913
+ ms = conf.get("m1553_status")
914
+ if ms:
915
+ lines += [
916
+ f"- MIL-STD-1553 status word RT {ms['rt_address']}: busy="
917
+ f"{ms['busy']}, terminal_flag={ms['terminal_flag']}, parity "
918
+ f"{'OK' if ms['parity_ok'] else 'FAIL'}",
919
+ ]
920
+ lines += [""]
921
+
922
+ lines += ["## 6. Findings and observations", ""]
923
+ obs = model.get("observations") or []
924
+ if obs:
925
+ for i, o in enumerate(obs, 1):
926
+ lines.append(f"- **O-{i}** {o}")
927
+ else:
928
+ lines.append("- No loading or conformance observations: all buses "
929
+ "fit their design guidelines.")
930
+ lines += [
931
+ "",
932
+ "## 7. Conclusion",
933
+ "",
934
+ "The bus architecture loading and received-word conformance results "
935
+ "above are computed by the data-bus-avionics-engineer core from the "
936
+ "bound ARINC 429, MIL-STD-1553 and ARINC 664 AFDX leaf rules "
937
+ "(36 bit-times per ARINC 429 word; 24 us per MIL-STD-1553 wire "
938
+ "word; frame x 8 / BAG for AFDX VL bandwidth). Numbers are "
939
+ "cross-checked against the AeroSkills leaf logic when the skills "
940
+ "library is present and recorded in the evidence bundle.",
941
+ "",
942
+ "---",
943
+ f"*Generated by Aero Agent Roles data-bus-avionics-engineer core "
944
+ f"({model['generated']}). DRAFT for human avionics/data-bus "
945
+ "engineer review. Not an approval document.*",
946
+ ]
947
+ return "\n".join(lines)
948
+
949
+
950
+ def _oct_label(label_str: str) -> int:
951
+ """Octal label string -> int (empty -> 0)."""
952
+ if not label_str:
953
+ return 0
954
+ return int(label_str, 8)
955
+
956
+
957
+ # ---------------------------------------------------------------------------
958
+ # Evidence gates
959
+ # ---------------------------------------------------------------------------
960
+
961
+ GATE_CHECKS = {
962
+ "arinc429_loads_computed": "every ARINC 429 line has numeric load + utilization",
963
+ "arinc429_conformance_ok": "received word parity OK and label matches ICD",
964
+ "m1553_schedule_computed": "1553 schedule utilization is a number in range",
965
+ "m1553_conformance_ok": "command/status words decode with odd parity OK",
966
+ "afdx_budget_checkable": "AFDX VL set present and link budget verdict known",
967
+ "observations_listed": "findings/observations section is present",
968
+ "sign_off_honest": "document is marked draft-for-review, not approval",
969
+ }
970
+
971
+
972
+ def check_assessment(model: dict) -> dict:
973
+ """Run the evidence gates against a content model."""
974
+ a429 = model.get("arinc429") or []
975
+ conf = model.get("conformance") or {}
976
+ a429_ok = bool(a429) and all(
977
+ isinstance(b.get("summary", {}).get("utilization_pct"), (int, float))
978
+ for b in a429)
979
+ aw = conf.get("a429_word") or {}
980
+ a429_conf = bool(aw.get("parity_ok")) and bool(aw) and (
981
+ aw.get("label") == _oct_label(conf.get("a429_expected_label", "")))
982
+ m1553 = model.get("m1553") or {}
983
+ sch = m1553.get("schedule") or {}
984
+ m1553_ok = bool(m1553) and isinstance(sch.get("utilization_pct"),
985
+ (int, float)) and \
986
+ 0.0 <= sch.get("utilization_pct", -1) <= 100.0
987
+ mc = conf.get("m1553_command") or {}
988
+ ms = conf.get("m1553_status") or {}
989
+ m1553_conf = bool(mc.get("parity_ok")) and bool(ms.get("parity_ok"))
990
+ afdx = model.get("afdx")
991
+ afdx_ok = bool(afdx) and "oversubscribed" in afdx
992
+ results = {
993
+ "arinc429_loads_computed": a429_ok,
994
+ "arinc429_conformance_ok": a429_conf,
995
+ "m1553_schedule_computed": m1553_ok,
996
+ "m1553_conformance_ok": m1553_conf,
997
+ "afdx_budget_checkable": afdx_ok,
998
+ "observations_listed": isinstance(model.get("observations"), list),
999
+ "sign_off_honest": model.get("status") == "draft-for-review",
1000
+ }
1001
+ results["all_pass"] = all(results.values())
1002
+ return results
1003
+
1004
+
1005
+ def check_assessment_markdown(md_text: str) -> dict:
1006
+ """Gate-check the rendered markdown deliverable."""
1007
+ low = md_text.lower()
1008
+ checks = {
1009
+ "has_title": "avionics data bus loading and protocol assessment" in low,
1010
+ "has_a429": "arinc 429" in low and "utilization" in low,
1011
+ "has_m1553": "mil-std-1553" in low and "utilization" in low,
1012
+ "has_afdx": "afdx" in low,
1013
+ "has_percent": "%" in low,
1014
+ "has_conformance": "parity" in low,
1015
+ "has_draft_marker": "draft" in low,
1016
+ "has_not_approval": "not an approval" in low,
1017
+ }
1018
+ checks["all_pass"] = all(checks.values())
1019
+ return checks
1020
+
1021
+
1022
+ def validate_deliverable_text(md_text: str) -> dict:
1023
+ """Public entry point used by gate tooling: check an assessment doc."""
1024
+ return check_assessment_markdown(md_text)
1025
+
1026
+
1027
+ # ---------------------------------------------------------------------------
1028
+ # Example item: EX-100 reference bus architecture (worked example)
1029
+ # ---------------------------------------------------------------------------
1030
+
1031
+ def example_item() -> DataBusItem:
1032
+ """Reference example: transport avionics bus architecture with
1033
+ ARINC 429 TX lines, one MIL-STD-1553 bus, and an AFDX VL set."""
1034
+ return DataBusItem(
1035
+ aircraft="EX-100 Reference Example Transport",
1036
+ description="Flight deck avionics data bus architecture: ADC, IRS "
1037
+ "and legacy utility concentrator ARINC 429 transmit "
1038
+ "lines, a dual-redundant MIL-STD-1553 bus under one "
1039
+ "bus controller, and a growth AFDX core network.",
1040
+ arinc429_lines=[
1041
+ A429Line(
1042
+ name="A429-1 ADC/FMS data",
1043
+ transmitter="ADC-1",
1044
+ receivers="FMS-1, EFIS-1",
1045
+ link_rate_bps=A429_RATE_100_KBPS,
1046
+ label_rates={
1047
+ "010": 25.0, "011": 25.0, "036": 50.0, "100": 50.0,
1048
+ "203": 12.5, "210": 25.0, "231": 25.0, "320": 12.5,
1049
+ "365": 12.5, "370": 12.5,
1050
+ }),
1051
+ A429Line(
1052
+ name="A429-2 IRS/FCC data",
1053
+ transmitter="IRS-1",
1054
+ receivers="FCC-1, EFIS-1",
1055
+ link_rate_bps=A429_RATE_100_KBPS,
1056
+ label_rates={
1057
+ "320": 25.0, "365": 25.0, "366": 25.0, "367": 25.0,
1058
+ "370": 25.0, "371": 25.0, "372": 25.0, "375": 12.5,
1059
+ "376": 12.5,
1060
+ }),
1061
+ A429Line(
1062
+ name="A429-3 fuel/utility (low speed)",
1063
+ transmitter="UTA-1",
1064
+ receivers="EICAS-1",
1065
+ link_rate_bps=A429_RATE_12_5_KBPS,
1066
+ label_rates={
1067
+ "250": 20.0, "251": 20.0, "252": 20.0, "253": 20.0,
1068
+ "254": 20.0, "255": 20.0, "256": 20.0, "257": 20.0,
1069
+ "260": 20.0, "261": 20.0, "262": 20.0, "263": 20.0,
1070
+ "264": 20.0,
1071
+ }),
1072
+ ],
1073
+ m1553_bus=M1553Bus(
1074
+ name="1553-B1 mission bus",
1075
+ controller="Mission Computer (MC-1)",
1076
+ remote_terminals="RT 1 (FMS-1), RT 2 (NAV-1), RT 3 (FCC-1)",
1077
+ frame_us=M1553_FRAME_US_DEFAULT,
1078
+ messages=[
1079
+ ("BCRT", 32), ("RTBC", 16), ("BCRT", 16), ("RTRT", 8),
1080
+ ("RTBC", 8), ("BCRT", 8), ("BCRT", 4), ("RTRT", 4),
1081
+ ]),
1082
+ afdx_network=AfdxNetwork(
1083
+ name="AFDX core network (growth)",
1084
+ vl_specs=[(16, 1518)] * 6 + [(32, 1518)] * 4 + [(128, 1518)] * 2
1085
+ + [(4, 1518), (8, 1518)],
1086
+ jitter_measured_us=420.0,
1087
+ jitter_budget_us=500.0,
1088
+ switch_count=2,
1089
+ switch_delay_us=150.0,
1090
+ ),
1091
+ a429_decode_sample=0x60134908, # label 010, SDI 1, data 1234
1092
+ a429_sample_expected_label="010",
1093
+ a429_bnr_scale=0.1,
1094
+ m1553_command_sample=93316, # RT5 SA12 WC16 RT-to-BC
1095
+ m1553_status_sample=606276, # RT5 busy
1096
+ certification_basis="MIL-STD-1553B / ARINC 429 Mark 33 DITS / "
1097
+ "ARINC 664 Part 7 protocol practice",
1098
+ )
1099
+
1100
+
1101
+ def example_assessment_markdown() -> str:
1102
+ item = example_item()
1103
+ return render_assessment_markdown(build_assessment(item,
1104
+ analyze_architecture(item)))
1105
+
1106
+
1107
+ if __name__ == "__main__":
1108
+ item = example_item()
1109
+ results = analyze_architecture(item)
1110
+ model = build_assessment(item, results)
1111
+ md = render_assessment_markdown(model)
1112
+ a429_max = max(b["summary"]["utilization_pct"]
1113
+ for b in model["arinc429"])
1114
+ print("ARINC 429 lines: %d, max utilization %.2f%%"
1115
+ % (len(model["arinc429"]), a429_max))
1116
+ print("MIL-STD-1553 utilization: %.2f%%"
1117
+ % model["m1553"]["schedule"]["utilization_pct"])
1118
+ print("AFDX utilization: %.2f%%, oversubscribed=%s"
1119
+ % (model["afdx"]["utilization_pct"], model["afdx"]["oversubscribed"]))
1120
+ print("GATES: %s" % check_assessment(model))
1121
+ print("RENDERED: %d chars" % len(md))