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