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
@@ -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)