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
@@ -5,7 +5,7 @@ functions for pass/fail testing against known-good waveforms.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.comparison import create_golden, compare_to_golden
8
+ >>> from oscura.utils.comparison import create_golden, compare_to_golden
9
9
  >>> golden = create_golden(reference_trace)
10
10
  >>> result = compare_to_golden(measured_trace, golden)
11
11
 
@@ -301,8 +301,7 @@ def compare_to_golden(
301
301
  ) -> GoldenComparisonResult:
302
302
  """Compare a trace to a golden reference.
303
303
 
304
- Tests if the measured trace falls within the tolerance bounds
305
- of the golden reference.
304
+ Tests if the measured trace falls within the tolerance bounds.
306
305
 
307
306
  Args:
308
307
  trace: Measured trace to compare.
@@ -315,73 +314,107 @@ def compare_to_golden(
315
314
 
316
315
  Example:
317
316
  >>> result = compare_to_golden(measured, golden)
318
- >>> if result.passed:
319
- ... print("PASS")
317
+ >>> if result.passed: print("PASS")
320
318
  """
319
+ measured, reference, upper, lower = _prepare_data(trace, golden, interpolate)
320
+
321
+ if align and len(measured) > 10:
322
+ measured = _align_signals(measured, reference)
323
+
324
+ violations = _find_violations(measured, upper, lower)
325
+ deviation = measured - reference
326
+ margin = _compute_margin(measured, upper, lower)
327
+ margin_pct = (margin / golden.tolerance * 100) if golden.tolerance > 0 else None
328
+
329
+ statistics = _compute_statistics(measured, reference, deviation)
330
+
331
+ return GoldenComparisonResult(
332
+ passed=violations["count"] == 0,
333
+ num_violations=violations["count"],
334
+ violation_rate=violations["rate"],
335
+ max_deviation=float(np.max(np.abs(deviation))),
336
+ rms_deviation=float(np.sqrt(np.mean(deviation**2))),
337
+ upper_violations=violations["upper"] if len(violations["upper"]) > 0 else None,
338
+ lower_violations=violations["lower"] if len(violations["lower"]) > 0 else None,
339
+ margin=margin,
340
+ margin_percentage=margin_pct,
341
+ statistics=statistics,
342
+ )
343
+
344
+
345
+ def _prepare_data(
346
+ trace: WaveformTrace, golden: GoldenReference, interpolate: bool
347
+ ) -> tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
348
+ """Prepare and align data arrays."""
321
349
  measured = trace.data.astype(np.float64)
322
- reference = golden.data.copy()
323
- upper = golden.upper_bound.copy()
324
- lower = golden.lower_bound.copy()
350
+ reference, upper, lower = (
351
+ golden.data.copy(),
352
+ golden.upper_bound.copy(),
353
+ golden.lower_bound.copy(),
354
+ )
325
355
 
326
- # Handle length mismatch
327
356
  if len(measured) != len(reference):
328
357
  if interpolate:
329
- # Interpolate measured to match reference length
330
- x_measured = np.linspace(0, 1, len(measured))
331
- x_reference = np.linspace(0, 1, len(reference))
358
+ x_measured, x_reference = (
359
+ np.linspace(0, 1, len(measured)),
360
+ np.linspace(0, 1, len(reference)),
361
+ )
332
362
  measured = np.interp(x_reference, x_measured, measured)
333
363
  else:
334
- # Truncate to shorter length
335
364
  min_len = min(len(measured), len(reference))
336
- measured = measured[:min_len]
337
- reference = reference[:min_len]
338
- upper = upper[:min_len]
339
- lower = lower[:min_len]
365
+ measured, reference, upper, lower = (
366
+ measured[:min_len],
367
+ reference[:min_len],
368
+ upper[:min_len],
369
+ lower[:min_len],
370
+ )
340
371
 
341
- # Optionally align by cross-correlation
342
- if align and len(measured) > 10:
343
- from scipy import signal as sp_signal
372
+ return measured, reference, upper, lower
344
373
 
345
- corr = sp_signal.correlate(measured, reference, mode="same")
346
- shift = len(measured) // 2 - np.argmax(corr)
347
- if abs(shift) < len(measured) // 4: # Only shift if reasonable
348
- measured = np.roll(measured, -shift)
349
374
 
350
- # Find violations
351
- upper_viol = np.where(measured > upper)[0]
352
- lower_viol = np.where(measured < lower)[0]
353
- all_violations = np.union1d(upper_viol, lower_viol)
375
+ def _align_signals(
376
+ measured: NDArray[np.float64], reference: NDArray[np.float64]
377
+ ) -> NDArray[np.float64]:
378
+ """Align signals using cross-correlation."""
379
+ from scipy import signal as sp_signal
354
380
 
355
- num_violations = len(all_violations)
356
- violation_rate = num_violations / len(measured) if len(measured) > 0 else 0.0
381
+ corr = sp_signal.correlate(measured, reference, mode="same")
382
+ shift = len(measured) // 2 - np.argmax(corr)
383
+ return np.roll(measured, -shift) if abs(shift) < len(measured) // 4 else measured
357
384
 
358
- # Compute deviation statistics
359
- deviation = measured - reference
360
- max_deviation = float(np.max(np.abs(deviation)))
361
- rms_deviation = float(np.sqrt(np.mean(deviation**2)))
362
385
 
363
- # Compute margin
364
- upper_margin = float(np.min(upper - measured))
365
- lower_margin = float(np.min(measured - lower))
366
- margin = min(upper_margin, lower_margin)
386
+ def _find_violations(
387
+ measured: NDArray[np.float64], upper: NDArray[np.float64], lower: NDArray[np.float64]
388
+ ) -> dict[str, Any]:
389
+ """Find tolerance violations."""
390
+ upper_viol, lower_viol = np.where(measured > upper)[0], np.where(measured < lower)[0]
391
+ all_violations = np.union1d(upper_viol, lower_viol)
392
+ return {
393
+ "upper": upper_viol,
394
+ "lower": lower_viol,
395
+ "count": len(all_violations),
396
+ "rate": len(all_violations) / len(measured) if len(measured) > 0 else 0.0,
397
+ }
367
398
 
368
- # Margin as percentage of tolerance
369
- margin_pct = (margin / golden.tolerance * 100) if golden.tolerance > 0 else None
370
399
 
371
- # Additional statistics
372
- # Handle constant data (zero std) for correlation calculation
373
- measured_std = np.std(measured)
374
- reference_std = np.std(reference)
400
+ def _compute_margin(
401
+ measured: NDArray[np.float64], upper: NDArray[np.float64], lower: NDArray[np.float64]
402
+ ) -> float:
403
+ """Compute minimum margin to tolerance bounds."""
404
+ return min(float(np.min(upper - measured)), float(np.min(measured - lower)))
405
+
406
+
407
+ def _compute_statistics(
408
+ measured: NDArray[np.float64], reference: NDArray[np.float64], deviation: NDArray[np.float64]
409
+ ) -> dict[str, float]:
410
+ """Compute deviation and correlation statistics."""
411
+ measured_std, reference_std = np.std(measured), np.std(reference)
375
412
  if measured_std == 0 or reference_std == 0:
376
- # For constant data, correlation is undefined (NaN) or 1.0 if both are equal
377
- if np.allclose(measured, reference):
378
- correlation = 1.0
379
- else:
380
- correlation = float("nan")
413
+ correlation = 1.0 if np.allclose(measured, reference) else float("nan")
381
414
  else:
382
415
  correlation = float(np.corrcoef(measured, reference)[0, 1])
383
416
 
384
- statistics = {
417
+ return {
385
418
  "mean_deviation": float(np.mean(deviation)),
386
419
  "std_deviation": float(np.std(deviation)),
387
420
  "max_positive_deviation": float(np.max(deviation)),
@@ -389,19 +422,6 @@ def compare_to_golden(
389
422
  "correlation": correlation,
390
423
  }
391
424
 
392
- return GoldenComparisonResult(
393
- passed=num_violations == 0,
394
- num_violations=num_violations,
395
- violation_rate=violation_rate,
396
- max_deviation=max_deviation,
397
- rms_deviation=rms_deviation,
398
- upper_violations=upper_viol if len(upper_viol) > 0 else None,
399
- lower_violations=lower_viol if len(lower_viol) > 0 else None,
400
- margin=margin,
401
- margin_percentage=margin_pct,
402
- statistics=statistics,
403
- )
404
-
405
425
 
406
426
  def batch_compare_to_golden(
407
427
  traces: list[WaveformTrace],
@@ -5,7 +5,7 @@ bounds, pass/fail determination, and margin analysis.
5
5
 
6
6
 
7
7
  Example:
8
- >>> from oscura.comparison import check_limits, margin_analysis
8
+ >>> from oscura.utils.comparison import check_limits, margin_analysis
9
9
  >>> result = check_limits(trace, upper=1.5, lower=-0.5)
10
10
  >>> margins = margin_analysis(trace, limits)
11
11
  """
@@ -188,74 +188,35 @@ def check_limits(
188
188
  >>> if not result.passed:
189
189
  ... print(f"{result.num_violations} violations found")
190
190
  """
191
- # Get data
192
- if isinstance(trace, WaveformTrace):
193
- data = trace.data.astype(np.float64)
194
- else:
195
- data = np.asarray(trace, dtype=np.float64)
191
+ # Extract data array
192
+ data = _extract_data_array(trace)
196
193
 
197
- # Create or use limits
198
- if limits is None:
199
- if upper is None and lower is None:
200
- raise ValueError("Must specify limits or upper/lower bounds")
201
- limits = LimitSpec(upper=upper, lower=lower)
194
+ # Get or create limit specification
195
+ limits = _get_or_create_limits(limits, upper, lower)
202
196
 
203
- # Handle relative limits
204
- actual_upper = limits.upper
205
- actual_lower = limits.lower
206
- if limits.mode == "relative" and reference is not None:
207
- if actual_upper is not None:
208
- actual_upper = reference + actual_upper
209
- if actual_lower is not None:
210
- actual_lower = reference + actual_lower
197
+ # Apply relative mode adjustment if needed
198
+ actual_upper, actual_lower = _apply_relative_limits(limits, reference)
211
199
 
212
- # Find violations
213
- upper_viol = np.array([], dtype=np.int64)
214
- lower_viol = np.array([], dtype=np.int64)
200
+ # Find violations in data
201
+ upper_viol, lower_viol = _find_violations(data, actual_upper, actual_lower)
215
202
 
216
- if actual_upper is not None:
217
- upper_viol = np.where(data > actual_upper)[0]
218
- if actual_lower is not None:
219
- lower_viol = np.where(data < actual_lower)[0]
203
+ # Compute violation statistics
204
+ num_violations, violation_rate = _compute_violation_stats(upper_viol, lower_viol, len(data))
220
205
 
221
- # Combine violations
222
- all_violations = np.union1d(upper_viol, lower_viol)
223
- num_violations = len(all_violations)
224
- violation_rate = num_violations / len(data) if len(data) > 0 else 0.0
225
-
226
- # Compute statistics
206
+ # Compute data range statistics
227
207
  max_val = float(np.max(data))
228
208
  min_val = float(np.min(data))
229
209
 
230
- # Compute margins
231
- upper_margin = None
232
- lower_margin = None
233
- if actual_upper is not None:
234
- upper_margin = float(actual_upper - max_val)
235
- if actual_lower is not None:
236
- lower_margin = float(min_val - actual_lower)
210
+ # Compute margins to limits
211
+ upper_margin, lower_margin = _compute_limit_margins(
212
+ actual_upper, actual_lower, max_val, min_val
213
+ )
237
214
 
238
215
  # Compute margin percentage
239
- margin_pct = None
240
- if actual_upper is not None and actual_lower is not None:
241
- limit_range = actual_upper - actual_lower
242
- if limit_range > 0:
243
- min_margin = min(
244
- upper_margin if upper_margin is not None else float("inf"),
245
- lower_margin if lower_margin is not None else float("inf"),
246
- )
247
- margin_pct = (min_margin / limit_range) * 100.0
216
+ margin_pct = _compute_margin_pct(actual_upper, actual_lower, upper_margin, lower_margin)
248
217
 
249
- # Check guardband
250
- within_guardband = False
251
- if num_violations == 0:
252
- # Check if within guardband
253
- if limits.upper_guardband > 0 and upper_margin is not None:
254
- if upper_margin < limits.upper_guardband:
255
- within_guardband = True
256
- if limits.lower_guardband > 0 and lower_margin is not None:
257
- if lower_margin < limits.lower_guardband:
258
- within_guardband = True
218
+ # Check guardband status
219
+ within_guardband = _check_guardband_status(num_violations, limits, upper_margin, lower_margin)
259
220
 
260
221
  return LimitTestResult(
261
222
  passed=num_violations == 0,
@@ -321,16 +282,238 @@ def margin_analysis(
321
282
  >>> margins = margin_analysis(trace, limits)
322
283
  >>> print(f"Margin: {margins.margin_percentage:.1f}%")
323
284
  """
324
- # Get data
285
+ # Extract data array
286
+ data = _extract_data_array(trace)
287
+ max_val = float(np.max(data))
288
+ min_val = float(np.min(data))
289
+
290
+ # Calculate margins to limits
291
+ upper_margin, lower_margin = _calculate_margins(limits, max_val, min_val)
292
+
293
+ # Determine critical limit and minimum margin
294
+ min_margin, critical_limit = _find_critical_limit(upper_margin, lower_margin)
295
+
296
+ # Calculate margin as percentage of limit range
297
+ margin_pct = _calculate_margin_percentage(limits, upper_margin, lower_margin, min_margin)
298
+
299
+ # Determine pass/warning/fail status
300
+ margin_status, warning = _determine_margin_status(min_margin, margin_pct, warning_threshold_pct)
301
+
302
+ return MarginAnalysis(
303
+ upper_margin=upper_margin,
304
+ lower_margin=lower_margin,
305
+ min_margin=min_margin,
306
+ margin_percentage=margin_pct,
307
+ critical_limit=critical_limit,
308
+ warning=warning,
309
+ margin_status=margin_status,
310
+ )
311
+
312
+
313
+ def _get_or_create_limits(
314
+ limits: LimitSpec | None, upper: float | None, lower: float | None
315
+ ) -> LimitSpec:
316
+ """Get existing limits or create from upper/lower bounds.
317
+
318
+ Args:
319
+ limits: Existing LimitSpec or None.
320
+ upper: Upper limit value.
321
+ lower: Lower limit value.
322
+
323
+ Returns:
324
+ LimitSpec instance.
325
+
326
+ Raises:
327
+ ValueError: If no limits specified.
328
+ """
329
+ if limits is None:
330
+ if upper is None and lower is None:
331
+ raise ValueError("Must specify limits or upper/lower bounds")
332
+ limits = LimitSpec(upper=upper, lower=lower)
333
+ return limits
334
+
335
+
336
+ def _apply_relative_limits(
337
+ limits: LimitSpec, reference: float | None
338
+ ) -> tuple[float | None, float | None]:
339
+ """Apply relative mode adjustment to limits.
340
+
341
+ Args:
342
+ limits: Limit specification.
343
+ reference: Reference value for relative limits.
344
+
345
+ Returns:
346
+ Tuple of (actual_upper, actual_lower).
347
+ """
348
+ actual_upper = limits.upper
349
+ actual_lower = limits.lower
350
+
351
+ if limits.mode == "relative" and reference is not None:
352
+ if actual_upper is not None:
353
+ actual_upper = reference + actual_upper
354
+ if actual_lower is not None:
355
+ actual_lower = reference + actual_lower
356
+
357
+ return (actual_upper, actual_lower)
358
+
359
+
360
+ def _find_violations(
361
+ data: NDArray[np.float64], actual_upper: float | None, actual_lower: float | None
362
+ ) -> tuple[NDArray[np.int64], NDArray[np.int64]]:
363
+ """Find samples violating upper and lower limits.
364
+
365
+ Args:
366
+ data: Data array to check.
367
+ actual_upper: Upper limit (None if no upper limit).
368
+ actual_lower: Lower limit (None if no lower limit).
369
+
370
+ Returns:
371
+ Tuple of (upper_violations, lower_violations) index arrays.
372
+ """
373
+ upper_viol = np.array([], dtype=np.int64)
374
+ lower_viol = np.array([], dtype=np.int64)
375
+
376
+ if actual_upper is not None:
377
+ upper_viol = np.where(data > actual_upper)[0]
378
+ if actual_lower is not None:
379
+ lower_viol = np.where(data < actual_lower)[0]
380
+
381
+ return (upper_viol, lower_viol)
382
+
383
+
384
+ def _compute_violation_stats(
385
+ upper_viol: NDArray[np.int64], lower_viol: NDArray[np.int64], data_length: int
386
+ ) -> tuple[int, float]:
387
+ """Compute violation count and rate.
388
+
389
+ Args:
390
+ upper_viol: Upper limit violations.
391
+ lower_viol: Lower limit violations.
392
+ data_length: Total number of samples.
393
+
394
+ Returns:
395
+ Tuple of (num_violations, violation_rate).
396
+ """
397
+ all_violations = np.union1d(upper_viol, lower_viol)
398
+ num_violations = len(all_violations)
399
+ violation_rate = num_violations / data_length if data_length > 0 else 0.0
400
+ return (num_violations, violation_rate)
401
+
402
+
403
+ def _compute_limit_margins(
404
+ actual_upper: float | None, actual_lower: float | None, max_val: float, min_val: float
405
+ ) -> tuple[float | None, float | None]:
406
+ """Compute margins to upper and lower limits.
407
+
408
+ Args:
409
+ actual_upper: Upper limit.
410
+ actual_lower: Lower limit.
411
+ max_val: Maximum value in data.
412
+ min_val: Minimum value in data.
413
+
414
+ Returns:
415
+ Tuple of (upper_margin, lower_margin).
416
+ """
417
+ upper_margin = None
418
+ lower_margin = None
419
+
420
+ if actual_upper is not None:
421
+ upper_margin = float(actual_upper - max_val)
422
+ if actual_lower is not None:
423
+ lower_margin = float(min_val - actual_lower)
424
+
425
+ return (upper_margin, lower_margin)
426
+
427
+
428
+ def _compute_margin_pct(
429
+ actual_upper: float | None,
430
+ actual_lower: float | None,
431
+ upper_margin: float | None,
432
+ lower_margin: float | None,
433
+ ) -> float | None:
434
+ """Compute margin as percentage of limit range.
435
+
436
+ Args:
437
+ actual_upper: Upper limit.
438
+ actual_lower: Lower limit.
439
+ upper_margin: Margin to upper limit.
440
+ lower_margin: Margin to lower limit.
441
+
442
+ Returns:
443
+ Margin percentage or None if cannot compute.
444
+ """
445
+ if actual_upper is not None and actual_lower is not None:
446
+ limit_range = actual_upper - actual_lower
447
+ if limit_range > 0:
448
+ min_margin = min(
449
+ upper_margin if upper_margin is not None else float("inf"),
450
+ lower_margin if lower_margin is not None else float("inf"),
451
+ )
452
+ return (min_margin / limit_range) * 100.0
453
+ return None
454
+
455
+
456
+ def _check_guardband_status(
457
+ num_violations: int,
458
+ limits: LimitSpec,
459
+ upper_margin: float | None,
460
+ lower_margin: float | None,
461
+ ) -> bool:
462
+ """Check if data is within guardband region.
463
+
464
+ Args:
465
+ num_violations: Number of limit violations.
466
+ limits: Limit specification with guardbands.
467
+ upper_margin: Margin to upper limit.
468
+ lower_margin: Margin to lower limit.
469
+
470
+ Returns:
471
+ True if within guardband but outside tight limits.
472
+ """
473
+ if num_violations > 0:
474
+ return False
475
+
476
+ within_guardband = False
477
+
478
+ if limits.upper_guardband > 0 and upper_margin is not None:
479
+ if upper_margin < limits.upper_guardband:
480
+ within_guardband = True
481
+
482
+ if limits.lower_guardband > 0 and lower_margin is not None:
483
+ if lower_margin < limits.lower_guardband:
484
+ within_guardband = True
485
+
486
+ return within_guardband
487
+
488
+
489
+ def _extract_data_array(trace: WaveformTrace | NDArray[np.floating[Any]]) -> NDArray[np.float64]:
490
+ """Extract numpy array from trace or array input.
491
+
492
+ Args:
493
+ trace: WaveformTrace object or numpy array.
494
+
495
+ Returns:
496
+ Data as float64 numpy array.
497
+ """
325
498
  if isinstance(trace, WaveformTrace):
326
- data = trace.data.astype(np.float64)
499
+ return trace.data.astype(np.float64)
327
500
  else:
328
- data = np.asarray(trace, dtype=np.float64)
501
+ return np.asarray(trace, dtype=np.float64)
329
502
 
330
- max_val = float(np.max(data))
331
- min_val = float(np.min(data))
332
503
 
333
- # Compute margins
504
+ def _calculate_margins(
505
+ limits: LimitSpec, max_val: float, min_val: float
506
+ ) -> tuple[float | None, float | None]:
507
+ """Calculate margin to upper and lower limits.
508
+
509
+ Args:
510
+ limits: Specification limits.
511
+ max_val: Maximum value in data.
512
+ min_val: Minimum value in data.
513
+
514
+ Returns:
515
+ Tuple of (upper_margin, lower_margin). None if limit not defined.
516
+ """
334
517
  upper_margin = None
335
518
  lower_margin = None
336
519
 
@@ -339,8 +522,25 @@ def margin_analysis(
339
522
  if limits.lower is not None:
340
523
  lower_margin = min_val - limits.lower
341
524
 
342
- # Determine minimum margin and critical limit
343
- margins = []
525
+ return (upper_margin, lower_margin)
526
+
527
+
528
+ def _find_critical_limit(
529
+ upper_margin: float | None, lower_margin: float | None
530
+ ) -> tuple[float, Literal["upper", "lower", "both", "none"]]:
531
+ """Find minimum margin and identify critical limit.
532
+
533
+ Args:
534
+ upper_margin: Margin to upper limit (None if no upper limit).
535
+ lower_margin: Margin to lower limit (None if no lower limit).
536
+
537
+ Returns:
538
+ Tuple of (minimum_margin, critical_limit_name).
539
+
540
+ Raises:
541
+ AnalysisError: If no limits defined.
542
+ """
543
+ margins: list[tuple[str, float]] = []
344
544
  if upper_margin is not None:
345
545
  margins.append(("upper", upper_margin))
346
546
  if lower_margin is not None:
@@ -353,39 +553,63 @@ def margin_analysis(
353
553
  min_margin_tuple = min(margins, key=lambda x: x[1])
354
554
  min_margin = min_margin_tuple[1]
355
555
 
356
- # Determine critical limit
556
+ # Determine critical limit (both if equal margins)
357
557
  if len(margins) == 2 and abs(margins[0][1] - margins[1][1]) < 1e-10:
358
558
  critical_limit: Literal["upper", "lower", "both", "none"] = "both"
359
559
  else:
360
560
  critical_limit = min_margin_tuple[0] # type: ignore[assignment]
361
561
 
362
- # Compute margin percentage
363
- margin_pct = 0.0
562
+ return (min_margin, critical_limit)
563
+
564
+
565
+ def _calculate_margin_percentage(
566
+ limits: LimitSpec,
567
+ upper_margin: float | None,
568
+ lower_margin: float | None,
569
+ min_margin: float,
570
+ ) -> float:
571
+ """Calculate margin as percentage of limit range.
572
+
573
+ Args:
574
+ limits: Specification limits.
575
+ upper_margin: Margin to upper limit.
576
+ lower_margin: Margin to lower limit.
577
+ min_margin: Minimum of upper/lower margins.
578
+
579
+ Returns:
580
+ Margin percentage (0-100+).
581
+ """
582
+ # Prefer range-based percentage if both limits defined
364
583
  if limits.upper is not None and limits.lower is not None:
365
584
  limit_range = limits.upper - limits.lower
366
585
  if limit_range > 0:
367
- margin_pct = (min_margin / limit_range) * 100.0
368
- elif limits.upper is not None and upper_margin is not None:
369
- margin_pct = (upper_margin / abs(limits.upper)) * 100.0 if limits.upper != 0 else 0
586
+ return (min_margin / limit_range) * 100.0
587
+
588
+ # Single limit: use absolute value
589
+ if limits.upper is not None and upper_margin is not None:
590
+ return (upper_margin / abs(limits.upper)) * 100.0 if limits.upper != 0 else 0.0
370
591
  elif limits.lower is not None and lower_margin is not None:
371
- margin_pct = (lower_margin / abs(limits.lower)) * 100.0 if limits.lower != 0 else 0
592
+ return (lower_margin / abs(limits.lower)) * 100.0 if limits.lower != 0 else 0.0
593
+
594
+ return 0.0
595
+
596
+
597
+ def _determine_margin_status(
598
+ min_margin: float, margin_pct: float, warning_threshold_pct: float
599
+ ) -> tuple[Literal["pass", "warning", "fail"], bool]:
600
+ """Determine margin status (pass/warning/fail).
601
+
602
+ Args:
603
+ min_margin: Minimum margin value.
604
+ margin_pct: Margin as percentage.
605
+ warning_threshold_pct: Warning threshold percentage.
372
606
 
373
- # Determine status
374
- warning = False
607
+ Returns:
608
+ Tuple of (status, warning_flag).
609
+ """
375
610
  if min_margin < 0:
376
- margin_status: Literal["pass", "warning", "fail"] = "fail"
611
+ return ("fail", False)
377
612
  elif margin_pct < warning_threshold_pct:
378
- margin_status = "warning"
379
- warning = True
613
+ return ("warning", True)
380
614
  else:
381
- margin_status = "pass"
382
-
383
- return MarginAnalysis(
384
- upper_margin=upper_margin,
385
- lower_margin=lower_margin,
386
- min_margin=min_margin,
387
- margin_percentage=margin_pct,
388
- critical_limit=critical_limit,
389
- warning=warning,
390
- margin_status=margin_status,
391
- )
615
+ return ("pass", False)