oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -87,9 +87,22 @@ class FormatStandards:
87
87
  References:
88
88
  REPORT-001: Professional Formatting Standards
89
89
  """
90
+ root_vars = self._generate_css_variables()
91
+ base_styles = self._generate_css_base_styles()
92
+ component_styles = self._generate_css_component_styles()
93
+ print_styles = self._generate_css_print_styles()
94
+
90
95
  return f"""
91
96
  /* Oscura Professional Report Styles - REPORT-001 */
92
- :root {{
97
+ {root_vars}
98
+ {base_styles}
99
+ {component_styles}
100
+ {print_styles}
101
+ """
102
+
103
+ def _generate_css_variables(self) -> str:
104
+ """Generate CSS custom properties."""
105
+ return f""":root {{
93
106
  --heading-font: {self.heading_font};
94
107
  --body-font: {self.body_font};
95
108
  --code-font: {self.code_font};
@@ -109,158 +122,186 @@ class FormatStandards:
109
122
  --color-critical-bg: #ffebee;
110
123
  --color-warning-bg: #fff3e0;
111
124
  --color-info-bg: #e3f2fd;
112
- }}
125
+ }}"""
113
126
 
114
- body {{
127
+ def _generate_css_base_styles(self) -> str:
128
+ """Generate base body and typography CSS."""
129
+ return """
130
+ body {
115
131
  font-family: var(--body-font);
116
132
  font-size: var(--body-size);
117
133
  line-height: var(--line-spacing);
118
134
  margin: var(--margin);
119
135
  max-width: 8.5in;
120
136
  color: #333;
121
- }}
137
+ }
122
138
 
123
- h1, h2, h3, h4 {{
139
+ h1, h2, h3, h4 {
124
140
  font-family: var(--heading-font);
125
141
  line-height: 1.2;
126
142
  margin-top: 1.5em;
127
143
  margin-bottom: 0.5em;
128
- }}
144
+ }
129
145
 
130
- h1 {{ font-size: var(--h1-size); }}
131
- h2 {{ font-size: var(--h2-size); }}
132
- h3 {{ font-size: var(--h3-size); }}
146
+ h1 { font-size: var(--h1-size); }
147
+ h2 { font-size: var(--h2-size); }
148
+ h3 { font-size: var(--h3-size); }
133
149
 
134
- .report-title {{
150
+ .report-title {
135
151
  font-size: var(--title-size);
136
152
  text-align: center;
137
153
  margin-bottom: 2em;
138
- }}
154
+ }
139
155
 
140
- code, pre {{
156
+ code, pre {
141
157
  font-family: var(--code-font);
142
158
  font-size: 0.9em;
143
159
  background-color: #f5f5f5;
144
160
  padding: 2px 4px;
145
161
  border-radius: 3px;
146
- }}
162
+ }"""
163
+
164
+ def _generate_css_component_styles(self) -> str:
165
+ """Generate component CSS (tables, severity, etc)."""
166
+ tables = self._generate_css_tables()
167
+ indicators = self._generate_css_indicators()
168
+ callouts = self._generate_css_callouts()
169
+ summary = self._generate_css_executive_summary()
170
+ watermark = self._generate_css_watermark()
147
171
 
172
+ return f"{tables}\n{indicators}\n{callouts}\n{summary}\n{watermark}"
173
+
174
+ def _generate_css_tables(self) -> str:
175
+ """Generate table CSS."""
176
+ return """
148
177
  /* Table styles */
149
- table {{
178
+ table {
150
179
  border-collapse: collapse;
151
180
  width: 100%;
152
181
  margin: 1em 0;
153
- }}
182
+ }
154
183
 
155
- th, td {{
184
+ th, td {
156
185
  border: 1px solid #ddd;
157
186
  padding: 8px;
158
187
  text-align: left;
159
- }}
188
+ }
160
189
 
161
- th {{
190
+ th {
162
191
  background-color: #f2f2f2;
163
192
  font-weight: bold;
164
- }}
193
+ }
165
194
 
166
- tr:nth-child(even) {{
195
+ tr:nth-child(even) {
167
196
  background-color: #f9f9f9;
168
- }}
197
+ }"""
169
198
 
199
+ def _generate_css_indicators(self) -> str:
200
+ """Generate pass/fail and severity indicator CSS."""
201
+ return """
170
202
  /* Pass/Fail indicators (REPORT-002) */
171
- .pass {{
203
+ .pass {
172
204
  color: var(--color-pass);
173
- }}
205
+ }
174
206
 
175
- .fail {{
207
+ .fail {
176
208
  color: var(--color-fail);
177
- }}
209
+ }
178
210
 
179
- .warning {{
211
+ .warning {
180
212
  color: var(--color-warning);
181
- }}
213
+ }
182
214
 
183
215
  /* Severity indicators (REPORT-002) */
184
- .severity-critical {{
216
+ .severity-critical {
185
217
  background-color: var(--color-critical-bg);
186
218
  border-left: 4px solid var(--color-fail);
187
219
  padding: 10px;
188
220
  margin: 10px 0;
189
- }}
221
+ }
190
222
 
191
- .severity-warning {{
223
+ .severity-warning {
192
224
  background-color: var(--color-warning-bg);
193
225
  border-left: 4px solid var(--color-warning);
194
226
  padding: 10px;
195
227
  margin: 10px 0;
196
- }}
228
+ }
197
229
 
198
- .severity-info {{
230
+ .severity-info {
199
231
  background-color: var(--color-info-bg);
200
232
  border-left: 4px solid var(--color-info);
201
233
  padding: 10px;
202
234
  margin: 10px 0;
203
- }}
235
+ }"""
204
236
 
237
+ def _generate_css_callouts(self) -> str:
238
+ """Generate callout box CSS."""
239
+ return """
205
240
  /* Callout box (REPORT-002) */
206
- .callout {{
241
+ .callout {
207
242
  border: 1px solid #ddd;
208
243
  border-radius: 4px;
209
244
  padding: 15px;
210
245
  margin: 15px 0;
211
246
  background-color: #fafafa;
212
- }}
247
+ }
213
248
 
214
- .callout.key-finding {{
249
+ .callout.key-finding {
215
250
  border-color: var(--color-info);
216
251
  background-color: var(--color-info-bg);
217
- }}
252
+ }
218
253
 
219
254
  /* Highlighting for out-of-spec values */
220
- .out-of-spec {{
255
+ .out-of-spec {
221
256
  background-color: rgba(255, 235, 59, 0.15);
222
257
  padding: 2px 4px;
223
258
  border-radius: 2px;
224
- }}
259
+ }"""
225
260
 
261
+ def _generate_css_executive_summary(self) -> str:
262
+ """Generate executive summary CSS."""
263
+ return """
226
264
  /* Executive summary styles (REPORT-004) */
227
- .executive-summary {{
265
+ .executive-summary {
228
266
  background-color: #f5f5f5;
229
267
  padding: 20px;
230
268
  margin: 20px 0;
231
269
  border-radius: 4px;
232
- }}
270
+ }
233
271
 
234
- .executive-summary h2 {{
272
+ .executive-summary h2 {
235
273
  margin-top: 0;
236
- }}
274
+ }
237
275
 
238
- .key-findings {{
276
+ .key-findings {
239
277
  list-style-type: none;
240
278
  padding-left: 0;
241
- }}
279
+ }
242
280
 
243
- .key-findings li {{
281
+ .key-findings li {
244
282
  padding: 5px 0;
245
283
  padding-left: 25px;
246
284
  position: relative;
247
- }}
285
+ }
248
286
 
249
- .key-findings li::before {{
287
+ .key-findings li::before {
250
288
  content: "";
251
289
  position: absolute;
252
290
  left: 0;
253
291
  top: 8px;
254
292
  width: 16px;
255
293
  height: 16px;
256
- }}
294
+ }
257
295
 
258
- .key-findings li.critical::before {{
296
+ .key-findings li.critical::before {
259
297
  content: "!";
260
298
  color: var(--color-fail);
261
299
  font-weight: bold;
262
- }}
300
+ }"""
263
301
 
302
+ def _generate_css_watermark(self) -> str:
303
+ """Generate watermark CSS."""
304
+ return f"""
264
305
  /* Watermark */
265
306
  .watermark {{
266
307
  position: fixed;
@@ -271,21 +312,23 @@ tr:nth-child(even) {{
271
312
  color: rgba(0, 0, 0, {self.watermark_opacity});
272
313
  pointer-events: none;
273
314
  z-index: 1000;
274
- }}
315
+ }}"""
275
316
 
317
+ def _generate_css_print_styles(self) -> str:
318
+ """Generate print media query CSS."""
319
+ return """
276
320
  /* Print styles */
277
- @media print {{
278
- body {{
321
+ @media print {
322
+ body {
279
323
  margin: 0;
280
- }}
281
- .page-break {{
324
+ }
325
+ .page-break {
282
326
  page-break-before: always;
283
- }}
284
- .no-print {{
327
+ }
328
+ .no-print {
285
329
  display: none;
286
- }}
287
- }}
288
- """
330
+ }
331
+ }"""
289
332
 
290
333
 
291
334
  @dataclass
@@ -215,57 +215,16 @@ def _format_frequency(freq_hz: float) -> str:
215
215
  return f"{freq_hz:.1f} Hz"
216
216
 
217
217
 
218
- def generate_summary(
218
+ def _build_findings(
219
+ signal_type: str,
220
+ type_confidence: float,
221
+ quality_level: str,
222
+ quality_issues: list[str],
219
223
  trace: WaveformTrace,
220
- *,
221
- context: dict[str, Any] | None = None,
222
- detail_level: str = "summary",
223
- max_words: int = 200,
224
- include_sections: list[str] | None = None,
225
- ) -> Summary:
226
- """Generate natural language summary of signal analysis.
227
-
228
- Creates a plain-English description of the signal and analysis results,
229
- avoiding technical jargon and explaining findings in accessible terms.
230
-
231
- Args:
232
- trace: Waveform to summarize.
233
- context: Optional analysis context (characterization, anomalies, etc.).
234
- detail_level: Summary detail level ("summary", "intermediate", "expert").
235
- max_words: Maximum word count for summary text.
236
- include_sections: Sections to include (default: all).
237
-
238
- Returns:
239
- Summary object with natural language description.
240
-
241
- Example:
242
- >>> trace = load("uart_signal.wfm")
243
- >>> summary = generate_summary(trace)
244
- >>> print(summary.text)
245
- This is a digital signal with two voltage levels...
246
-
247
- References:
248
- DISC-003: Natural Language Summaries
249
- """
250
- context = context or {}
251
- include_sections = include_sections or ["overview", "findings", "recommendations"]
252
-
253
- # Characterize signal type
254
- signal_type, type_confidence = _characterize_signal_type(trace)
255
-
256
- # Assess quality
257
- quality_level, quality_issues = _assess_quality(trace)
258
-
259
- # Build overview
260
- sample_rate = trace.metadata.sample_rate
261
- duration_ms = len(trace.data) / sample_rate * 1000
262
-
263
- overview = f"This is a {signal_type} signal captured at {_format_frequency(sample_rate)} sample rate for {duration_ms:.1f} milliseconds."
264
-
265
- # Build findings
224
+ ) -> list[Finding]:
225
+ """Build findings list from analysis results."""
266
226
  findings = []
267
227
 
268
- # Signal type finding
269
228
  findings.append(
270
229
  Finding(
271
230
  title="Signal Type",
@@ -275,7 +234,6 @@ def generate_summary(
275
234
  )
276
235
  )
277
236
 
278
- # Quality finding
279
237
  quality_desc = f"Signal quality is {quality_level}"
280
238
  if quality_issues:
281
239
  quality_desc += f" with {len(quality_issues)} issue(s) noted"
@@ -289,7 +247,6 @@ def generate_summary(
289
247
  )
290
248
  )
291
249
 
292
- # Voltage levels
293
250
  v_min = float(np.min(trace.data))
294
251
  v_max = float(np.max(trace.data))
295
252
  v_range = v_max - v_min
@@ -303,7 +260,11 @@ def generate_summary(
303
260
  )
304
261
  )
305
262
 
306
- # Build recommendations
263
+ return findings
264
+
265
+
266
+ def _build_recommendations(signal_type: str, quality_issues: list[str]) -> list[str]:
267
+ """Build recommendations from analysis results."""
307
268
  recommendations = []
308
269
 
309
270
  if "very short" in str(quality_issues).lower():
@@ -322,14 +283,24 @@ def generate_summary(
322
283
  elif signal_type in ["analog", "periodic analog"] and not recommendations:
323
284
  recommendations.append("Consider spectral analysis to identify frequency components")
324
285
 
325
- # Build complete summary text
286
+ return recommendations
287
+
288
+
289
+ def _build_summary_text(
290
+ overview: str,
291
+ findings: list[Finding],
292
+ recommendations: list[str],
293
+ include_sections: list[str],
294
+ max_words: int,
295
+ ) -> str:
296
+ """Build complete summary text from components."""
326
297
  summary_parts = []
327
298
 
328
299
  if "overview" in include_sections:
329
300
  summary_parts.append(overview)
330
301
 
331
302
  if "findings" in include_sections and findings:
332
- key_findings = findings[:3] # Top 3 findings
303
+ key_findings = findings[:3]
333
304
  findings_text = " ".join(
334
305
  [f"{finding.title}: {finding.description}." for finding in key_findings]
335
306
  )
@@ -341,13 +312,62 @@ def generate_summary(
341
312
 
342
313
  full_text = " ".join(summary_parts)
343
314
 
344
- # Truncate to max_words if needed
345
315
  words = full_text.split()
346
316
  if len(words) > max_words:
347
317
  words = words[:max_words]
348
318
  full_text = " ".join(words) + "..."
349
319
 
350
- # Calculate statistics
320
+ return full_text
321
+
322
+
323
+ def generate_summary(
324
+ trace: WaveformTrace,
325
+ *,
326
+ context: dict[str, Any] | None = None,
327
+ detail_level: str = "summary",
328
+ max_words: int = 200,
329
+ include_sections: list[str] | None = None,
330
+ ) -> Summary:
331
+ """Generate natural language summary of signal analysis.
332
+
333
+ Creates a plain-English description of the signal and analysis results,
334
+ avoiding technical jargon and explaining findings in accessible terms.
335
+
336
+ Args:
337
+ trace: Waveform to summarize.
338
+ context: Optional analysis context (characterization, anomalies, etc.).
339
+ detail_level: Summary detail level ("summary", "intermediate", "expert").
340
+ max_words: Maximum word count for summary text.
341
+ include_sections: Sections to include (default: all).
342
+
343
+ Returns:
344
+ Summary object with natural language description.
345
+
346
+ Example:
347
+ >>> trace = load("uart_signal.wfm")
348
+ >>> summary = generate_summary(trace)
349
+ >>> print(summary.text)
350
+ This is a digital signal with two voltage levels...
351
+
352
+ References:
353
+ DISC-003: Natural Language Summaries
354
+ """
355
+ context = context or {}
356
+ include_sections = include_sections or ["overview", "findings", "recommendations"]
357
+
358
+ signal_type, type_confidence = _characterize_signal_type(trace)
359
+ quality_level, quality_issues = _assess_quality(trace)
360
+
361
+ sample_rate = trace.metadata.sample_rate
362
+ duration_ms = len(trace.data) / sample_rate * 1000
363
+ overview = f"This is a {signal_type} signal captured at {_format_frequency(sample_rate)} sample rate for {duration_ms:.1f} milliseconds."
364
+
365
+ findings = _build_findings(signal_type, type_confidence, quality_level, quality_issues, trace)
366
+ recommendations = _build_recommendations(signal_type, quality_issues)
367
+
368
+ full_text = _build_summary_text(
369
+ overview, findings, recommendations, include_sections, max_words
370
+ )
351
371
  word_count = len(full_text.split())
352
372
  grade_level = _estimate_grade_level(full_text)
353
373
 
@@ -21,6 +21,138 @@ if TYPE_CHECKING:
21
21
  from numpy.typing import NDArray
22
22
 
23
23
 
24
+ def _build_table_headers(show_spec: bool, show_margin: bool, show_status: bool) -> list[str]:
25
+ """Build table headers based on display options.
26
+
27
+ Args:
28
+ show_spec: Include specification column.
29
+ show_margin: Include margin column.
30
+ show_status: Include status column.
31
+
32
+ Returns:
33
+ List of header strings.
34
+ """
35
+ headers = ["Parameter", "Value"]
36
+ if show_spec:
37
+ headers.append("Specification")
38
+ if show_margin:
39
+ headers.append("Margin")
40
+ if show_status:
41
+ headers.append("Status")
42
+ return headers
43
+
44
+
45
+ def _format_value_cell(value: Any, unit: str, formatter: NumberFormatter) -> str:
46
+ """Format measurement value cell.
47
+
48
+ Args:
49
+ value: Measurement value.
50
+ unit: Unit string.
51
+ formatter: Number formatter instance.
52
+
53
+ Returns:
54
+ Formatted value string.
55
+ """
56
+ if value is None:
57
+ return "N/A"
58
+ return formatter.format(value, unit)
59
+
60
+
61
+ def _format_spec_cell(spec: Any, spec_type: str, unit: str, formatter: NumberFormatter) -> str:
62
+ """Format specification cell with comparison operator.
63
+
64
+ Args:
65
+ spec: Specification value.
66
+ spec_type: Spec type ("max", "min", or "exact").
67
+ unit: Unit string.
68
+ formatter: Number formatter instance.
69
+
70
+ Returns:
71
+ Formatted specification string.
72
+ """
73
+ if spec is None:
74
+ return "-"
75
+
76
+ prefix = "<" if spec_type == "max" else ">" if spec_type == "min" else "="
77
+ return f"{prefix}{formatter.format(spec, unit)}"
78
+
79
+
80
+ def _calculate_margin(value: float, spec: float, spec_type: str) -> str:
81
+ """Calculate margin percentage between value and spec.
82
+
83
+ Args:
84
+ value: Measured value.
85
+ spec: Specification limit.
86
+ spec_type: Specification type ("max" or "min").
87
+
88
+ Returns:
89
+ Formatted margin percentage string.
90
+ """
91
+ if spec == 0:
92
+ return "-"
93
+
94
+ if spec_type == "max":
95
+ margin = (spec - value) / spec * 100
96
+ else:
97
+ margin = (value - spec) / spec * 100
98
+
99
+ return f"{margin:.1f}%"
100
+
101
+
102
+ def _build_measurement_row(
103
+ name: str,
104
+ meas: dict[str, Any],
105
+ formatter: NumberFormatter,
106
+ show_spec: bool,
107
+ show_margin: bool,
108
+ show_status: bool,
109
+ ) -> list[str]:
110
+ """Build single measurement table row.
111
+
112
+ Args:
113
+ name: Measurement parameter name.
114
+ meas: Measurement data dictionary.
115
+ formatter: Number formatter instance.
116
+ show_spec: Include specification column.
117
+ show_margin: Include margin column.
118
+ show_status: Include status column.
119
+
120
+ Returns:
121
+ List of cell values for table row.
122
+ """
123
+ row = [name]
124
+ value = meas.get("value")
125
+ unit = meas.get("unit", "")
126
+
127
+ # Value column
128
+ row.append(_format_value_cell(value, unit, formatter))
129
+
130
+ # Specification column
131
+ if show_spec:
132
+ spec = meas.get("spec")
133
+ spec_type = meas.get("spec_type", "max")
134
+ row.append(_format_spec_cell(spec, spec_type, unit, formatter))
135
+
136
+ # Margin column
137
+ if show_margin:
138
+ spec = meas.get("spec")
139
+ if value is not None and spec is not None:
140
+ spec_type = meas.get("spec_type", "max")
141
+ row.append(_calculate_margin(value, spec, spec_type))
142
+ else:
143
+ row.append("-")
144
+
145
+ # Status column
146
+ if show_status:
147
+ passed = meas.get("passed", True)
148
+ if value is None:
149
+ row.append("N/A")
150
+ else:
151
+ row.append("✓ PASS" if passed else "✗ FAIL")
152
+
153
+ return row
154
+
155
+
24
156
  def create_measurement_table(
25
157
  measurements: dict[str, Any],
26
158
  *,
@@ -56,62 +188,14 @@ def create_measurement_table(
56
188
  References:
57
189
  REPORT-004, REPORT-006
58
190
  """
59
- # Build table headers
60
- headers = ["Parameter", "Value"]
61
- if show_spec:
62
- headers.append("Specification")
63
- if show_margin:
64
- headers.append("Margin")
65
- if show_status:
66
- headers.append("Status")
67
-
68
- # Build table rows
69
- rows = []
191
+ headers = _build_table_headers(show_spec, show_margin, show_status)
70
192
  formatter = NumberFormatter(sig_figs=3)
71
193
 
72
- for name, meas in measurements.items():
73
- row = [name]
74
-
75
- # Value
76
- value = meas.get("value")
77
- unit = meas.get("unit", "")
78
- if value is not None:
79
- row.append(formatter.format(value, unit))
80
- else:
81
- row.append("N/A")
82
-
83
- # Specification
84
- if show_spec:
85
- spec = meas.get("spec")
86
- spec_type = meas.get("spec_type", "max")
87
- if spec is not None:
88
- prefix = "<" if spec_type == "max" else ">" if spec_type == "min" else "="
89
- row.append(f"{prefix}{formatter.format(spec, unit)}")
90
- else:
91
- row.append("-")
92
-
93
- # Margin
94
- if show_margin:
95
- spec = meas.get("spec")
96
- if value is not None and spec is not None and spec != 0:
97
- spec_type = meas.get("spec_type", "max")
98
- if spec_type == "max":
99
- margin = (spec - value) / spec * 100
100
- else:
101
- margin = (value - spec) / spec * 100
102
- row.append(f"{margin:.1f}%")
103
- else:
104
- row.append("-")
105
-
106
- # Status
107
- if show_status:
108
- passed = meas.get("passed", True)
109
- if value is None:
110
- row.append("N/A")
111
- else:
112
- row.append("✓ PASS" if passed else "✗ FAIL")
113
-
114
- rows.append(row)
194
+ # Build rows
195
+ rows = [
196
+ _build_measurement_row(name, meas, formatter, show_spec, show_margin, show_status)
197
+ for name, meas in measurements.items()
198
+ ]
115
199
 
116
200
  # Sort if requested
117
201
  if sort_by and sort_by in headers: