oscura 0.5.1__py3-none-any.whl → 0.7.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 (497) 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/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.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: