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
@@ -323,10 +323,10 @@ def iqr_outliers(
323
323
 
324
324
 
325
325
  def detect_outliers(
326
- trace: WaveformTrace | NDArray[np.floating[Any]], # type: ignore[name-defined]
326
+ trace: WaveformTrace | NDArray[np.floating[Any]],
327
327
  *,
328
328
  method: str = "modified_zscore",
329
- **kwargs: Any, # type: ignore[name-defined]
329
+ **kwargs: Any,
330
330
  ) -> OutlierResult:
331
331
  """Detect outliers using specified method.
332
332
 
@@ -46,6 +46,8 @@ class StreamingStats:
46
46
  self.m2 = 0.0 # Sum of squared differences from mean
47
47
  self.min_val = float("inf")
48
48
  self.max_val = float("-inf")
49
+ self.has_pos_inf = False
50
+ self.has_neg_inf = False
49
51
 
50
52
  def update(self, data: NDArray[np.floating[Any]]) -> None:
51
53
  """Update statistics with new data chunk.
@@ -59,10 +61,19 @@ class StreamingStats:
59
61
 
60
62
  for value in data:
61
63
  self.count += 1
62
- delta = value - self.mean
63
- self.mean += delta / self.count
64
- delta2 = value - self.mean
65
- self.m2 += delta * delta2
64
+
65
+ # Track infinities for proper mean calculation
66
+ if np.isposinf(value):
67
+ self.has_pos_inf = True
68
+ elif np.isneginf(value):
69
+ self.has_neg_inf = True
70
+
71
+ # Only update Welford's algorithm for finite values when no inf present
72
+ if not (self.has_pos_inf or self.has_neg_inf):
73
+ delta = value - self.mean
74
+ self.mean += delta / self.count
75
+ delta2 = value - self.mean
76
+ self.m2 += delta * delta2
66
77
 
67
78
  # Update min/max
68
79
  if value < self.min_val:
@@ -76,6 +87,20 @@ class StreamingStats:
76
87
  Returns:
77
88
  StreamingStatsResult with mean, variance, std, min, max, count.
78
89
  """
90
+ # Handle infinities in mean calculation
91
+ if self.has_pos_inf and self.has_neg_inf:
92
+ # Both +inf and -inf present: mean is undefined (NaN)
93
+ mean = float("nan")
94
+ elif self.has_pos_inf:
95
+ # Only +inf present: mean is +inf
96
+ mean = float("inf")
97
+ elif self.has_neg_inf:
98
+ # Only -inf present: mean is -inf
99
+ mean = float("-inf")
100
+ else:
101
+ # No infinities: use Welford's result
102
+ mean = self.mean
103
+
79
104
  if self.count < 2:
80
105
  variance = 0.0
81
106
  std = 0.0
@@ -84,7 +109,7 @@ class StreamingStats:
84
109
  std = np.sqrt(variance)
85
110
 
86
111
  return StreamingStatsResult(
87
- mean=self.mean,
112
+ mean=mean,
88
113
  variance=variance,
89
114
  std=std,
90
115
  min=self.min_val if self.min_val != float("inf") else 0.0,
@@ -20,6 +20,8 @@ from typing import TYPE_CHECKING
20
20
  import numpy as np
21
21
 
22
22
  if TYPE_CHECKING:
23
+ from numpy.typing import NDArray
24
+
23
25
  from oscura.core.types import WaveformTrace
24
26
 
25
27
 
@@ -260,40 +262,66 @@ def get_valid_measurements(trace: WaveformTrace) -> list[str]:
260
262
  ... func = getattr(tk, meas_name)
261
263
  ... result = func(trace)
262
264
  """
263
- valid = []
265
+ valid: list[str] = []
266
+
267
+ _add_basic_measurements(valid, trace)
268
+ _add_edge_measurements(valid, trace)
269
+ _add_frequency_measurements(valid, trace)
270
+ _add_pulse_measurements(valid, trace)
271
+ _add_overshoot_measurements(valid, trace)
272
+ _add_slew_rate_measurement(valid)
273
+
274
+ return valid
275
+
264
276
 
265
- # These almost always work (just need data)
277
+ def _add_basic_measurements(valid: list[str], trace: WaveformTrace) -> None:
278
+ """Add basic measurements that almost always work.
279
+
280
+ Args:
281
+ valid: List to append to.
282
+ trace: Input trace.
283
+ """
266
284
  if len(trace.data) > 0:
267
285
  valid.extend(["mean", "rms"])
268
-
269
286
  if len(trace.data) >= 2:
270
287
  valid.append("amplitude")
271
288
 
272
- # Check edge-based measurements
273
- suitable, _ = is_suitable_for_rise_time_measurement(trace)
274
- if suitable:
275
- valid.append("rise_time")
276
289
 
277
- suitable, _ = is_suitable_for_fall_time_measurement(trace)
278
- if suitable:
290
+ def _add_edge_measurements(valid: list[str], trace: WaveformTrace) -> None:
291
+ """Add edge-based measurements.
292
+
293
+ Args:
294
+ valid: List to append to.
295
+ trace: Input trace.
296
+ """
297
+ if is_suitable_for_rise_time_measurement(trace)[0]:
298
+ valid.append("rise_time")
299
+ if is_suitable_for_fall_time_measurement(trace)[0]:
279
300
  valid.append("fall_time")
280
301
 
281
- # Check frequency/period
282
- suitable, _ = is_suitable_for_frequency_measurement(trace)
283
- if suitable:
284
- valid.extend(["frequency", "period"])
285
302
 
286
- # Check duty cycle
287
- suitable, _ = is_suitable_for_duty_cycle_measurement(trace)
288
- if suitable:
289
- valid.append("duty_cycle")
303
+ def _add_frequency_measurements(valid: list[str], trace: WaveformTrace) -> None:
304
+ """Add frequency/period measurements.
290
305
 
291
- # Check jitter
292
- suitable, _ = is_suitable_for_jitter_measurement(trace)
293
- if suitable:
306
+ Args:
307
+ valid: List to append to.
308
+ trace: Input trace.
309
+ """
310
+ if is_suitable_for_frequency_measurement(trace)[0]:
311
+ valid.extend(["frequency", "period"])
312
+ if is_suitable_for_duty_cycle_measurement(trace)[0]:
313
+ valid.append("duty_cycle")
314
+ if is_suitable_for_jitter_measurement(trace)[0]:
294
315
  valid.extend(["rms_jitter", "peak_to_peak_jitter"])
295
316
 
296
- # Pulse width - needs edges but not necessarily periodic
317
+
318
+ def _add_pulse_measurements(valid: list[str], trace: WaveformTrace) -> None:
319
+ """Add pulse width measurements.
320
+
321
+ Args:
322
+ valid: List to append to.
323
+ trace: Input trace.
324
+ """
297
325
  from oscura.analyzers.waveform.measurements import _find_edges
298
326
 
299
327
  rising = _find_edges(trace, "rising")
@@ -302,7 +330,14 @@ def get_valid_measurements(trace: WaveformTrace) -> list[str]:
302
330
  if len(rising) > 0 and len(falling) > 0:
303
331
  valid.append("pulse_width")
304
332
 
305
- # Overshoot/undershoot - check amplitude
333
+
334
+ def _add_overshoot_measurements(valid: list[str], trace: WaveformTrace) -> None:
335
+ """Add overshoot/undershoot measurements.
336
+
337
+ Args:
338
+ valid: List to append to.
339
+ trace: Input trace.
340
+ """
306
341
  from oscura.analyzers.waveform.measurements import _find_levels
307
342
 
308
343
  if len(trace.data) >= 3:
@@ -310,12 +345,16 @@ def get_valid_measurements(trace: WaveformTrace) -> list[str]:
310
345
  if high - low > 0:
311
346
  valid.extend(["overshoot", "undershoot", "preshoot"])
312
347
 
313
- # Slew rate - similar to rise/fall time
348
+
349
+ def _add_slew_rate_measurement(valid: list[str]) -> None:
350
+ """Add slew rate if edge measurements available.
351
+
352
+ Args:
353
+ valid: List to check and append to.
354
+ """
314
355
  if "rise_time" in valid or "fall_time" in valid:
315
356
  valid.append("slew_rate")
316
357
 
317
- return valid
318
-
319
358
 
320
359
  def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int | str | list[str]]:
321
360
  """Perform comprehensive signal characteristic analysis.
@@ -327,59 +366,75 @@ def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int
327
366
  trace: Input waveform trace.
328
367
 
329
368
  Returns:
330
- Dictionary containing:
331
- - sufficient_samples: bool - at least 16 samples
332
- - has_amplitude: bool - signal has variation
333
- - has_variation: bool - standard deviation > 0
334
- - has_edges: bool - rising or falling edges detected
335
- - is_periodic: bool - signal appears periodic
336
- - edge_count: int - total edges (rising + falling)
337
- - rising_edge_count: int - number of rising edges
338
- - falling_edge_count: int - number of falling edges
339
- - signal_type: str - classified type (dc, periodic_digital, etc.)
340
- - recommended_measurements: list[str] - suggested measurements
369
+ Dictionary containing signal characteristics including sufficient_samples,
370
+ has_amplitude, has_variation, edge counts, periodicity, signal_type.
341
371
 
342
372
  Example:
343
373
  >>> chars = analyze_signal_characteristics(trace)
344
374
  >>> if chars['is_periodic']:
345
375
  ... print("Signal is periodic")
346
- ... print(f"Frequency measurement recommended: {'frequency' in chars['recommended_measurements']}")
347
376
  """
348
377
  from oscura.analyzers.waveform.measurements import _find_edges
349
378
 
350
379
  data = trace.data
380
+ characteristics = _init_characteristics(data)
381
+
382
+ if not characteristics["has_variation"]:
383
+ characteristics["signal_type"] = "dc"
384
+ characteristics["recommended_measurements"] = ["mean", "rms"]
385
+ return characteristics
386
+
387
+ rising_edges = _find_edges(trace, "rising")
388
+ falling_edges = _find_edges(trace, "falling")
389
+ _update_edge_counts(characteristics, rising_edges, falling_edges)
390
+ _check_periodicity(characteristics, rising_edges)
391
+ _classify_signal_type(characteristics, data, len(data))
392
+
393
+ characteristics["recommended_measurements"] = get_valid_measurements(trace)
394
+
395
+ return characteristics
396
+
397
+
398
+ def _init_characteristics(data: NDArray[np.float64]) -> dict[str, bool | int | str | list[str]]:
399
+ """Initialize characteristics dictionary with basic signal properties.
400
+
401
+ Args:
402
+ data: Signal data array.
403
+
404
+ Returns:
405
+ Dictionary with initial characteristics.
406
+ """
351
407
  n = len(data)
408
+ std = np.std(data)
409
+ amplitude = np.max(data) - np.min(data)
352
410
 
353
- characteristics: dict[str, bool | int | str | list[str]] = {
354
- "sufficient_samples": n >= 16,
355
- "has_amplitude": False,
356
- "has_variation": False,
357
- "has_edges": False,
358
- "is_periodic": False,
411
+ characteristics: dict[str, int | str | list[str]] = {
412
+ "sufficient_samples": int(n >= 16),
413
+ "has_amplitude": int(amplitude > 1e-12),
414
+ "has_variation": int(std > 1e-12),
415
+ "has_edges": int(False),
416
+ "is_periodic": int(False),
359
417
  "edge_count": 0,
360
418
  "rising_edge_count": 0,
361
419
  "falling_edge_count": 0,
362
420
  "signal_type": "unknown",
363
421
  "recommended_measurements": [],
364
422
  }
423
+ return characteristics
365
424
 
366
- # Check variation
367
- std = np.std(data)
368
- characteristics["has_variation"] = std > 1e-12
369
-
370
- # Check amplitude
371
- amplitude = np.max(data) - np.min(data)
372
- characteristics["has_amplitude"] = amplitude > 1e-12
373
-
374
- if not characteristics["has_variation"]:
375
- characteristics["signal_type"] = "dc"
376
- characteristics["recommended_measurements"] = ["mean", "rms"]
377
- return characteristics
378
425
 
379
- # Count edges
380
- rising_edges = _find_edges(trace, "rising")
381
- falling_edges = _find_edges(trace, "falling")
426
+ def _update_edge_counts(
427
+ characteristics: dict[str, bool | int | str | list[str]],
428
+ rising_edges: NDArray[np.float64],
429
+ falling_edges: NDArray[np.float64],
430
+ ) -> None:
431
+ """Update edge count statistics in characteristics dictionary.
382
432
 
433
+ Args:
434
+ characteristics: Dictionary to update.
435
+ rising_edges: Array of rising edge indices.
436
+ falling_edges: Array of falling edge indices.
437
+ """
383
438
  rising_edge_count = len(rising_edges)
384
439
  falling_edge_count = len(falling_edges)
385
440
  edge_count = rising_edge_count + falling_edge_count
@@ -389,7 +444,16 @@ def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int
389
444
  characteristics["edge_count"] = edge_count
390
445
  characteristics["has_edges"] = edge_count > 0
391
446
 
392
- # Check periodicity
447
+
448
+ def _check_periodicity(
449
+ characteristics: dict[str, bool | int | str | list[str]], rising_edges: NDArray[np.float64]
450
+ ) -> None:
451
+ """Check if signal is periodic based on edge spacing.
452
+
453
+ Args:
454
+ characteristics: Dictionary to update.
455
+ rising_edges: Array of rising edge indices.
456
+ """
393
457
  if len(rising_edges) >= 3:
394
458
  periods = np.diff(rising_edges)
395
459
  period_cv = np.std(periods) / np.mean(periods) if np.mean(periods) > 0 else float("inf")
@@ -397,30 +461,45 @@ def analyze_signal_characteristics(trace: WaveformTrace) -> dict[str, bool | int
397
461
  if period_cv < 0.2: # Less than 20% variation
398
462
  characteristics["is_periodic"] = True
399
463
 
400
- # Classify signal type
464
+
465
+ def _classify_signal_type(
466
+ characteristics: dict[str, bool | int | str | list[str]], data: NDArray[np.float64], n: int
467
+ ) -> None:
468
+ """Classify signal type based on characteristics.
469
+
470
+ Args:
471
+ characteristics: Dictionary to update.
472
+ data: Signal data array.
473
+ n: Number of samples.
474
+ """
401
475
  if not characteristics["has_edges"]:
402
- # No edges - check if analog periodic
403
- if n >= 16:
404
- fft_result = np.abs(np.fft.rfft(data - np.mean(data)))
405
- peak_power = np.max(fft_result[1:]) if len(fft_result) > 1 else 0
406
- avg_power = np.mean(fft_result[1:]) if len(fft_result) > 1 else 0
407
-
408
- if peak_power > 10 * avg_power:
409
- characteristics["signal_type"] = "periodic_analog"
410
- else:
411
- characteristics["signal_type"] = "noise"
412
- else:
413
- characteristics["signal_type"] = "unknown"
476
+ characteristics["signal_type"] = _classify_no_edge_signal(data, n)
414
477
  elif characteristics["is_periodic"]:
415
478
  characteristics["signal_type"] = "periodic_digital"
416
479
  else:
417
480
  characteristics["signal_type"] = "aperiodic_digital"
418
481
 
419
- # Recommend measurements
420
- recommended = get_valid_measurements(trace)
421
- characteristics["recommended_measurements"] = recommended
422
482
 
423
- return characteristics
483
+ def _classify_no_edge_signal(data: NDArray[np.float64], n: int) -> str:
484
+ """Classify signal without edges (analog or noise).
485
+
486
+ Args:
487
+ data: Signal data array.
488
+ n: Number of samples.
489
+
490
+ Returns:
491
+ Signal type classification string.
492
+ """
493
+ if n >= 16:
494
+ fft_result = np.abs(np.fft.rfft(data - np.mean(data)))
495
+ peak_power = np.max(fft_result[1:]) if len(fft_result) > 1 else 0
496
+ avg_power = np.mean(fft_result[1:]) if len(fft_result) > 1 else 0
497
+
498
+ if peak_power > 10 * avg_power:
499
+ return "periodic_analog"
500
+ else:
501
+ return "noise"
502
+ return "unknown"
424
503
 
425
504
 
426
505
  def get_measurement_requirements(measurement_name: str) -> dict[str, str | int | list[str]]:
@@ -442,7 +521,24 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
442
521
  >>> print(f"Minimum samples: {reqs['min_samples']}")
443
522
  >>> print(f"Required features: {', '.join(reqs['required_features'])}")
444
523
  """
445
- requirements = {
524
+ requirements = _get_all_measurement_requirements()
525
+ default = _get_default_measurement_requirements()
526
+ return requirements.get(measurement_name, default)
527
+
528
+
529
+ def _get_all_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
530
+ """Get complete measurement requirements dictionary."""
531
+ timing_reqs = _get_timing_measurement_requirements()
532
+ amplitude_reqs = _get_amplitude_measurement_requirements()
533
+ jitter_reqs = _get_jitter_measurement_requirements()
534
+ statistical_reqs = _get_statistical_measurement_requirements()
535
+
536
+ return {**timing_reqs, **amplitude_reqs, **jitter_reqs, **statistical_reqs}
537
+
538
+
539
+ def _get_timing_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
540
+ """Get requirements for timing-related measurements."""
541
+ return {
446
542
  "frequency": {
447
543
  "description": "Measures the repetition rate of a periodic signal",
448
544
  "min_samples": 3,
@@ -509,6 +605,19 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
509
605
  "Incomplete pulses",
510
606
  ],
511
607
  },
608
+ "slew_rate": {
609
+ "description": "Measures dV/dt during transitions",
610
+ "min_samples": 3,
611
+ "required_signal_types": ["periodic_digital", "aperiodic_digital"],
612
+ "required_features": ["edges", "amplitude"],
613
+ "common_nan_causes": ["No edges", "No amplitude", "DC signal"],
614
+ },
615
+ }
616
+
617
+
618
+ def _get_amplitude_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
619
+ """Get requirements for amplitude-related measurements."""
620
+ return {
512
621
  "amplitude": {
513
622
  "description": "Measures peak-to-peak voltage",
514
623
  "min_samples": 2,
@@ -516,20 +625,6 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
516
625
  "required_features": [],
517
626
  "common_nan_causes": ["Fewer than 2 samples"],
518
627
  },
519
- "mean": {
520
- "description": "Calculates DC level (average voltage)",
521
- "min_samples": 1,
522
- "required_signal_types": ["all"],
523
- "required_features": [],
524
- "common_nan_causes": ["No data"],
525
- },
526
- "rms": {
527
- "description": "Calculates root-mean-square voltage",
528
- "min_samples": 1,
529
- "required_signal_types": ["all"],
530
- "required_features": [],
531
- "common_nan_causes": ["No data"],
532
- },
533
628
  "overshoot": {
534
629
  "description": "Measures overshoot above high level",
535
630
  "min_samples": 3,
@@ -544,13 +639,12 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
544
639
  "required_features": ["amplitude"],
545
640
  "common_nan_causes": ["No amplitude", "DC signal"],
546
641
  },
547
- "slew_rate": {
548
- "description": "Measures dV/dt during transitions",
549
- "min_samples": 3,
550
- "required_signal_types": ["periodic_digital", "aperiodic_digital"],
551
- "required_features": ["edges", "amplitude"],
552
- "common_nan_causes": ["No edges", "No amplitude", "DC signal"],
553
- },
642
+ }
643
+
644
+
645
+ def _get_jitter_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
646
+ """Get requirements for jitter measurements."""
647
+ return {
554
648
  "rms_jitter": {
555
649
  "description": "Measures timing uncertainty (RMS)",
556
650
  "min_samples": 3,
@@ -575,7 +669,30 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
575
669
  },
576
670
  }
577
671
 
578
- default = {
672
+
673
+ def _get_statistical_measurement_requirements() -> dict[str, dict[str, str | int | list[str]]]:
674
+ """Get requirements for statistical measurements."""
675
+ return {
676
+ "mean": {
677
+ "description": "Calculates DC level (average voltage)",
678
+ "min_samples": 1,
679
+ "required_signal_types": ["all"],
680
+ "required_features": [],
681
+ "common_nan_causes": ["No data"],
682
+ },
683
+ "rms": {
684
+ "description": "Calculates root-mean-square voltage",
685
+ "min_samples": 1,
686
+ "required_signal_types": ["all"],
687
+ "required_features": [],
688
+ "common_nan_causes": ["No data"],
689
+ },
690
+ }
691
+
692
+
693
+ def _get_default_measurement_requirements() -> dict[str, str | int | list[str]]:
694
+ """Get default requirements for undocumented measurements."""
695
+ return {
579
696
  "description": "Measurement not documented",
580
697
  "min_samples": 1,
581
698
  "required_signal_types": ["unknown"],
@@ -583,8 +700,6 @@ def get_measurement_requirements(measurement_name: str) -> dict[str, str | int |
583
700
  "common_nan_causes": ["Check measurement documentation"],
584
701
  }
585
702
 
586
- return requirements.get(measurement_name, default) # type: ignore[return-value]
587
-
588
703
 
589
704
  __all__ = [
590
705
  "analyze_signal_characteristics",
@@ -789,9 +789,18 @@ def _find_levels(data: NDArray[np_floating[Any]]) -> tuple[float, float]:
789
789
  if data.dtype == np.bool_:
790
790
  data = data.astype(np.float64)
791
791
 
792
+ # Check for all-NaN data
793
+ if np.all(np.isnan(data)):
794
+ return float(np.nan), float(np.nan)
795
+
792
796
  # Use percentiles for robust level detection
793
797
  p10, p90 = np.percentile(data, [10, 90])
794
798
 
799
+ # Check for constant or near-constant signal
800
+ data_range = p90 - p10
801
+ if data_range < 1e-10 or np.isnan(data_range): # Essentially constant or NaN
802
+ return float(p10), float(p10)
803
+
795
804
  # Refine using histogram peaks
796
805
  hist, bin_edges = np.histogram(data, bins=50)
797
806
  bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
@@ -70,16 +70,26 @@ def rise_time(
70
70
  uncertainties = []
71
71
 
72
72
  # 1. Time base uncertainty (Type B)
73
- if trace.metadata.calibration_info is not None:
74
- # Use calibration info if available
75
- # Typical scope: 25-50 ppm timebase accuracy
76
- timebase_ppm = 25.0 # Conservative estimate
73
+ if (
74
+ trace.metadata.calibration_info is not None
75
+ and trace.metadata.calibration_info.timebase_accuracy is not None
76
+ ):
77
+ # Use calibration timebase accuracy if available
78
+ timebase_ppm = trace.metadata.calibration_info.timebase_accuracy
77
79
  u_timebase = UncertaintyEstimator.time_base_uncertainty(
78
80
  trace.metadata.sample_rate, timebase_ppm
79
81
  )
80
82
  # Rise time involves 2 samples (start and stop), so uncertainty scales
81
83
  u_timebase_rise = u_timebase * np.sqrt(2)
82
84
  uncertainties.append(u_timebase_rise)
85
+ elif trace.metadata.calibration_info is not None:
86
+ # Use conservative estimate if calibration present but no timebase accuracy
87
+ timebase_ppm = 25.0 # Typical scope: 25-50 ppm
88
+ u_timebase = UncertaintyEstimator.time_base_uncertainty(
89
+ trace.metadata.sample_rate, timebase_ppm
90
+ )
91
+ u_timebase_rise = u_timebase * np.sqrt(2)
92
+ uncertainties.append(u_timebase_rise)
83
93
  else:
84
94
  # No calibration info - use conservative estimate
85
95
  u_timebase_rise = (1.0 / trace.metadata.sample_rate) * 50e-6 # 50 ppm
@@ -146,7 +156,13 @@ def fall_time(
146
156
  uncertainties = []
147
157
 
148
158
  # Time base uncertainty
149
- timebase_ppm = 25.0
159
+ if (
160
+ trace.metadata.calibration_info is not None
161
+ and trace.metadata.calibration_info.timebase_accuracy is not None
162
+ ):
163
+ timebase_ppm = trace.metadata.calibration_info.timebase_accuracy
164
+ else:
165
+ timebase_ppm = 25.0 # Conservative default
150
166
  u_timebase = UncertaintyEstimator.time_base_uncertainty(
151
167
  trace.metadata.sample_rate, timebase_ppm
152
168
  )
@@ -208,7 +224,13 @@ def frequency(
208
224
  uncertainties = []
209
225
 
210
226
  # Time base uncertainty
211
- timebase_ppm = 25.0
227
+ if (
228
+ trace.metadata.calibration_info is not None
229
+ and trace.metadata.calibration_info.timebase_accuracy is not None
230
+ ):
231
+ timebase_ppm = trace.metadata.calibration_info.timebase_accuracy
232
+ else:
233
+ timebase_ppm = 25.0 # Conservative default
212
234
  # Period measurement spans multiple cycles, typically more accurate
213
235
  u_period_timebase = period * (timebase_ppm * 1e-6)
214
236
  uncertainties.append(u_period_timebase)
@@ -292,15 +314,9 @@ def amplitude(
292
314
  uncertainties.append(u_quant)
293
315
 
294
316
  # 3. Signal noise (Type A)
295
- # Estimate from flat regions (if available)
296
- # Simplified: use standard deviation as proxy
297
- if len(trace.data) > 100:
298
- # Sample first and last 50 points (assume flat regions)
299
- noise_start = np.std(trace.data[:50])
300
- noise_end = np.std(trace.data[-50:])
301
- u_noise = np.mean([noise_start, noise_end])
302
- # Amplitude involves max and min, so sqrt(2) factor
303
- uncertainties.append(u_noise * np.sqrt(2))
317
+ # For amplitude (Vpp) measurements, noise is already captured in the peak detection
318
+ # uncertainty. Adding additional noise estimation from "flat regions" is inappropriate
319
+ # for periodic signals where no regions are truly flat. Skip noise term for amplitude.
304
320
 
305
321
  total_uncertainty = UncertaintyEstimator.combined_uncertainty(uncertainties)
306
322