oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,9 +5,9 @@ This module enables efficient pattern matching, anomaly detection, and
5
5
  context extraction for debugging and analysis workflows.
6
6
  """
7
7
 
8
- from oscura.search.anomaly import find_anomalies
9
- from oscura.search.context import extract_context
10
- from oscura.search.pattern import find_pattern
8
+ from oscura.utils.search.anomaly import find_anomalies
9
+ from oscura.utils.search.context import extract_context
10
+ from oscura.utils.search.pattern import find_pattern
11
11
 
12
12
  __all__ = [
13
13
  "extract_context",
@@ -94,36 +94,33 @@ def find_anomalies(
94
94
  if anomaly_type not in valid_types:
95
95
  raise ValueError(f"Invalid anomaly_type '{anomaly_type}'. Must be one of: {valid_types}")
96
96
 
97
- anomalies: list[dict[str, Any]] = []
97
+ return _dispatch_anomaly_detection(
98
+ trace, anomaly_type, threshold, min_width, max_width, sample_rate, context_samples
99
+ )
98
100
 
101
+
102
+ def _dispatch_anomaly_detection(
103
+ trace: NDArray[np.float64],
104
+ anomaly_type: str,
105
+ threshold: float | None,
106
+ min_width: float | None,
107
+ max_width: float | None,
108
+ sample_rate: float | None,
109
+ context_samples: int,
110
+ ) -> list[dict[str, Any]]:
111
+ """Dispatch to appropriate anomaly detection method."""
99
112
  if anomaly_type == "glitch":
100
- anomalies = _detect_glitches(
101
- trace,
102
- threshold=threshold,
103
- min_width=min_width,
104
- max_width=max_width,
105
- sample_rate=sample_rate,
106
- context_samples=context_samples,
113
+ return _detect_glitches(
114
+ trace, threshold, min_width, max_width, sample_rate, context_samples
107
115
  )
108
116
 
109
- elif anomaly_type == "timing":
117
+ if anomaly_type == "timing":
110
118
  if sample_rate is None:
111
119
  raise ValueError("sample_rate required for timing anomaly detection")
120
+ return _detect_timing_violations(trace, sample_rate, min_width, max_width, context_samples)
112
121
 
113
- anomalies = _detect_timing_violations(
114
- trace,
115
- sample_rate=sample_rate,
116
- min_width=min_width,
117
- max_width=max_width,
118
- context_samples=context_samples,
119
- )
120
-
121
- elif anomaly_type == "protocol":
122
- # Protocol error detection would integrate with protocol decoders
123
- # For now, return empty list with note
124
- anomalies = []
125
-
126
- return anomalies
122
+ # Protocol error detection would integrate with protocol decoders
123
+ return []
127
124
 
128
125
 
129
126
  def _detect_glitches(
@@ -134,70 +131,175 @@ def _detect_glitches(
134
131
  sample_rate: float | None,
135
132
  context_samples: int,
136
133
  ) -> list[dict[str, Any]]:
137
- """Detect voltage glitches using derivative method."""
138
- glitches: list[dict[str, Any]] = []
134
+ """Detect voltage glitches using derivative method.
135
+
136
+ Uses median absolute deviation (MAD) for robust auto-thresholding,
137
+ groups consecutive derivative spikes, and filters by duration.
139
138
 
140
- # Auto-threshold if not provided
141
- threshold_value: float
142
- if threshold is None:
143
- # Use 3 sigma as default threshold
144
- threshold_value = float(3 * np.std(trace))
145
- else:
146
- threshold_value = threshold
139
+ Example:
140
+ >>> trace = np.array([1.0, 1.0, 5.0, 1.0, 1.0]) # Glitch at index 2
141
+ >>> glitches = _detect_glitches(trace, None, None, None, 1000.0, 5)
142
+ >>> len(glitches) > 0
143
+ True
144
+ """
145
+ if len(trace) < 2:
146
+ return []
147
147
 
148
148
  # Compute derivative to find rapid changes
149
- derivative = np.diff(trace)
150
- abs_derivative = np.abs(derivative)
149
+ abs_derivative = np.abs(np.diff(trace))
151
150
 
152
- # Find points where derivative exceeds threshold
153
- glitch_candidates = np.where(abs_derivative > threshold_value)[0]
151
+ # Determine threshold
152
+ threshold_value = _compute_glitch_threshold(threshold, abs_derivative)
154
153
 
154
+ # Find glitch candidate points
155
+ glitch_candidates = np.where(abs_derivative > threshold_value)[0]
155
156
  if len(glitch_candidates) == 0:
156
- return glitches
157
+ return []
157
158
 
158
159
  # Group consecutive points into glitch events
159
- glitch_groups = []
160
- current_group = [glitch_candidates[0]]
160
+ glitch_groups = _group_consecutive_indices(glitch_candidates)
161
+
162
+ # Compute baseline once for performance
163
+ baseline = _compute_baseline(trace)
164
+
165
+ # Convert groups to glitch dictionaries
166
+ return _build_glitch_results(
167
+ glitch_groups,
168
+ trace,
169
+ baseline,
170
+ threshold_value,
171
+ min_width,
172
+ max_width,
173
+ sample_rate,
174
+ context_samples,
175
+ )
176
+
177
+
178
+ def _compute_glitch_threshold(
179
+ threshold: float | None, abs_derivative: NDArray[np.float64]
180
+ ) -> float:
181
+ """Compute threshold for glitch detection using MAD.
182
+
183
+ Args:
184
+ threshold: User-provided threshold, or None for auto-threshold.
185
+ abs_derivative: Absolute derivative of signal.
186
+
187
+ Returns:
188
+ Threshold value for glitch detection.
189
+ """
190
+ if threshold is not None:
191
+ return threshold
192
+
193
+ # Use median absolute deviation (MAD) for robust auto-thresholding
194
+ median_deriv = np.median(abs_derivative)
195
+ mad = np.median(np.abs(abs_derivative - median_deriv))
196
+
197
+ # Convert MAD to equivalent std (1.4826 is the constant for normal distribution)
198
+ if mad > 0:
199
+ return float(median_deriv + 3 * 1.4826 * mad)
200
+
201
+ # Fallback: use 75th percentile to avoid catching glitches in threshold
202
+ p75 = np.percentile(abs_derivative, 75)
203
+ if p75 > 0:
204
+ return float(p75)
205
+
206
+ # Last resort: use any non-zero derivative
207
+ return 0.0
208
+
209
+
210
+ def _group_consecutive_indices(indices: NDArray[np.int64]) -> list[list[int]]:
211
+ """Group consecutive indices into separate lists.
212
+
213
+ Args:
214
+ indices: Sorted array of indices.
215
+
216
+ Returns:
217
+ List of groups, where each group contains consecutive indices.
218
+
219
+ Example:
220
+ >>> indices = np.array([1, 2, 3, 7, 8, 10])
221
+ >>> _group_consecutive_indices(indices)
222
+ [[1, 2, 3], [7, 8], [10]]
223
+ """
224
+ if len(indices) == 0:
225
+ return []
226
+
227
+ groups = []
228
+ current_group = [int(indices[0])]
161
229
 
162
- for idx in glitch_candidates[1:]:
230
+ for idx in indices[1:]:
163
231
  if idx == current_group[-1] + 1:
164
- current_group.append(idx)
232
+ current_group.append(int(idx))
165
233
  else:
166
- glitch_groups.append(current_group)
167
- current_group = [idx]
234
+ groups.append(current_group)
235
+ current_group = [int(idx)]
168
236
 
169
237
  if current_group:
170
- glitch_groups.append(current_group)
238
+ groups.append(current_group)
239
+
240
+ return groups
241
+
242
+
243
+ def _compute_baseline(trace: NDArray[np.float64]) -> float:
244
+ """Compute baseline value using median.
171
245
 
172
- # Compute baseline once for all glitches (performance optimization)
173
- # For very large arrays (>1M samples), use percentile approximation
246
+ For very large arrays (>1M samples), uses percentile approximation
247
+ for performance.
248
+
249
+ Args:
250
+ trace: Signal trace.
251
+
252
+ Returns:
253
+ Baseline value (median).
254
+ """
174
255
  if len(trace) > 1_000_000:
175
256
  # Fast approximation: 50th percentile with linear interpolation
176
- baseline = float(np.percentile(trace, 50, method="linear"))
177
- else:
178
- baseline = float(np.median(trace))
257
+ return float(np.percentile(trace, 50, method="linear"))
258
+ return float(np.median(trace))
259
+
260
+
261
+ def _build_glitch_results(
262
+ glitch_groups: list[list[int]],
263
+ trace: NDArray[np.float64],
264
+ baseline: float,
265
+ threshold_value: float,
266
+ min_width: float | None,
267
+ max_width: float | None,
268
+ sample_rate: float | None,
269
+ context_samples: int,
270
+ ) -> list[dict[str, Any]]:
271
+ """Build glitch result dictionaries from detected groups.
272
+
273
+ Args:
274
+ glitch_groups: Groups of consecutive glitch indices.
275
+ trace: Original signal trace.
276
+ baseline: Signal baseline value.
277
+ threshold_value: Detection threshold.
278
+ min_width: Minimum glitch duration (seconds), or None.
279
+ max_width: Maximum glitch duration (seconds), or None.
280
+ sample_rate: Sample rate (Hz), or None.
281
+ context_samples: Number of context samples to include.
282
+
283
+ Returns:
284
+ List of glitch dictionaries with metadata.
285
+ """
286
+ glitches: list[dict[str, Any]] = []
179
287
 
180
- # Filter by width if specified
181
288
  for group in glitch_groups:
182
289
  start_idx = group[0]
183
290
  end_idx = group[-1] + 1
184
291
  duration_samples = end_idx - start_idx
185
292
 
186
293
  # Check width constraints
187
- if sample_rate is not None:
188
- duration_seconds = duration_samples / sample_rate
189
-
190
- if min_width is not None and duration_seconds < min_width:
191
- continue
192
- if max_width is not None and duration_seconds > max_width:
193
- continue
294
+ if not _check_width_constraints(duration_samples, min_width, max_width, sample_rate):
295
+ continue
194
296
 
195
297
  # Extract context
196
298
  ctx_start = max(0, start_idx - context_samples)
197
299
  ctx_end = min(len(trace), end_idx + context_samples)
198
300
  context = trace[ctx_start:ctx_end].copy()
199
301
 
200
- # Compute amplitude deviation (baseline computed once above)
302
+ # Compute amplitude deviation
201
303
  amplitude = np.max(np.abs(trace[start_idx:end_idx] - baseline))
202
304
 
203
305
  # Severity: normalized amplitude
@@ -218,6 +320,34 @@ def _detect_glitches(
218
320
  return glitches
219
321
 
220
322
 
323
+ def _check_width_constraints(
324
+ duration_samples: int,
325
+ min_width: float | None,
326
+ max_width: float | None,
327
+ sample_rate: float | None,
328
+ ) -> bool:
329
+ """Check if glitch duration meets width constraints.
330
+
331
+ Args:
332
+ duration_samples: Glitch duration in samples.
333
+ min_width: Minimum duration (seconds), or None.
334
+ max_width: Maximum duration (seconds), or None.
335
+ sample_rate: Sample rate (Hz), or None.
336
+
337
+ Returns:
338
+ True if glitch meets constraints, False otherwise.
339
+ """
340
+ if sample_rate is None:
341
+ return True
342
+
343
+ duration_seconds = duration_samples / sample_rate
344
+
345
+ if min_width is not None and duration_seconds < min_width:
346
+ return False
347
+
348
+ return not (max_width is not None and duration_seconds > max_width)
349
+
350
+
221
351
  def _detect_timing_violations(
222
352
  trace: NDArray[np.float64],
223
353
  sample_rate: float,
@@ -0,0 +1,294 @@
1
+ """Context extraction around points of interest.
2
+
3
+
4
+ This module provides efficient extraction of signal context around
5
+ events, maintaining original time references for debugging workflows.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ from numpy.typing import NDArray
12
+
13
+
14
+ def extract_context(
15
+ trace: NDArray[np.float64],
16
+ index: int | list[int] | NDArray[np.int_],
17
+ *,
18
+ before: int = 100,
19
+ after: int = 100,
20
+ sample_rate: float | None = None,
21
+ include_metadata: bool = True,
22
+ ) -> dict[str, Any] | list[dict[str, Any]]:
23
+ """Extract signal context around a point of interest.
24
+
25
+ : Context extraction with time reference preservation.
26
+ Supports batch extraction for multiple indices and optional protocol data.
27
+
28
+ Args:
29
+ trace: Input signal trace
30
+ index: Sample index or list of indices to extract context around.
31
+ Can be int, list of ints, or numpy array.
32
+ before: Number of samples to include before index (default: 100)
33
+ after: Number of samples to include after index (default: 100)
34
+ sample_rate: Optional sample rate in Hz for time calculations
35
+ include_metadata: Include metadata dict with context info (default: True)
36
+
37
+ Returns:
38
+ If index is scalar: Single context dictionary
39
+ If index is list/array: List of context dictionaries
40
+
41
+ Each context dictionary contains:
42
+ - data: Extracted sub-trace array
43
+ - start_index: Starting index in original trace
44
+ - end_index: Ending index in original trace
45
+ - center_index: Center index (original query index)
46
+ - time_reference: Time offset if sample_rate provided
47
+ - length: Number of samples in context
48
+
49
+ Raises:
50
+ ValueError: If index is out of bounds
51
+ ValueError: If before or after are negative
52
+
53
+ Examples:
54
+ >>> # Extract context around a glitch
55
+ >>> trace = np.random.randn(1000)
56
+ >>> glitch_index = 500
57
+ >>> context = extract_context(
58
+ ... trace,
59
+ ... glitch_index,
60
+ ... before=50,
61
+ ... after=50,
62
+ ... sample_rate=1e6
63
+ ... )
64
+ >>> print(f"Context length: {len(context['data'])}")
65
+ >>> print(f"Time reference: {context['time_reference']*1e6:.2f} µs")
66
+
67
+ >>> # Batch extraction for multiple events
68
+ >>> event_indices = [100, 200, 300]
69
+ >>> contexts = extract_context(
70
+ ... trace,
71
+ ... event_indices,
72
+ ... before=25,
73
+ ... after=25
74
+ ... )
75
+ >>> print(f"Extracted {len(contexts)} contexts")
76
+
77
+ Notes:
78
+ - Handles edge cases at trace boundaries automatically
79
+ - Context may be shorter than before+after at boundaries
80
+ - Time reference is relative to start of extracted context
81
+ - Original trace is not modified
82
+
83
+ References:
84
+ SRCH-003: Context Extraction
85
+ """
86
+ # Phase 1: Input validation
87
+ _validate_context_params(before, after, trace)
88
+
89
+ # Phase 2: Normalize indices
90
+ indices, return_single = _normalize_indices(index, trace)
91
+
92
+ # Phase 3: Extract contexts
93
+ contexts = [
94
+ _extract_single_context(trace, idx, before, after, sample_rate, include_metadata)
95
+ for idx in indices
96
+ ]
97
+
98
+ # Return single context or list
99
+ return contexts[0] if return_single else contexts
100
+
101
+
102
+ def _validate_context_params(before: int, after: int, trace: NDArray[np.float64]) -> None:
103
+ """Validate context extraction parameters.
104
+
105
+ Args:
106
+ before: Samples before index.
107
+ after: Samples after index.
108
+ trace: Input trace.
109
+
110
+ Raises:
111
+ ValueError: If parameters are invalid.
112
+
113
+ Example:
114
+ >>> trace = np.array([1.0, 2.0, 3.0])
115
+ >>> _validate_context_params(10, 10, trace)
116
+ >>> _validate_context_params(-1, 10, trace)
117
+ Traceback (most recent call last):
118
+ ValueError: before and after must be non-negative
119
+ """
120
+ if before < 0 or after < 0:
121
+ raise ValueError("before and after must be non-negative")
122
+
123
+ if trace.size == 0:
124
+ raise ValueError("Trace cannot be empty")
125
+
126
+
127
+ def _normalize_indices(
128
+ index: int | list[int] | NDArray[np.int_], trace: NDArray[np.float64]
129
+ ) -> tuple[list[int], bool]:
130
+ """Normalize index input to list of integers.
131
+
132
+ Args:
133
+ index: Input index (int, list, or array).
134
+ trace: Trace to validate against.
135
+
136
+ Returns:
137
+ Tuple of (normalized_indices, return_single_flag).
138
+
139
+ Raises:
140
+ ValueError: If any index is out of bounds.
141
+
142
+ Example:
143
+ >>> trace = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
144
+ >>> _normalize_indices(2, trace)
145
+ ([2], True)
146
+ >>> _normalize_indices([1, 3], trace)
147
+ ([1, 3], False)
148
+ """
149
+ # Handle single index vs multiple indices
150
+ if isinstance(index, int | np.integer):
151
+ indices = [int(index)]
152
+ return_single = True
153
+ else:
154
+ indices = [int(i) for i in index]
155
+ return_single = False
156
+
157
+ # Validate indices
158
+ for idx in indices:
159
+ if idx < 0 or idx >= len(trace):
160
+ raise ValueError(f"Index {idx} out of bounds for trace of length {len(trace)}")
161
+
162
+ return indices, return_single
163
+
164
+
165
+ def _extract_single_context(
166
+ trace: NDArray[np.float64],
167
+ idx: int,
168
+ before: int,
169
+ after: int,
170
+ sample_rate: float | None,
171
+ include_metadata: bool,
172
+ ) -> dict[str, Any]:
173
+ """Extract context for a single index.
174
+
175
+ Args:
176
+ trace: Input signal trace.
177
+ idx: Center index to extract around.
178
+ before: Samples before index.
179
+ after: Samples after index.
180
+ sample_rate: Optional sample rate.
181
+ include_metadata: Include metadata dict.
182
+
183
+ Returns:
184
+ Context dictionary with extracted data and metadata.
185
+
186
+ Example:
187
+ >>> trace = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
188
+ >>> ctx = _extract_single_context(trace, 2, 1, 1, None, False)
189
+ >>> ctx['data']
190
+ array([2., 3., 4.])
191
+ """
192
+ # Calculate window bounds with boundary handling
193
+ start_idx, end_idx = _calculate_window_bounds(idx, before, after, len(trace))
194
+
195
+ # Extract data
196
+ data = trace[start_idx:end_idx].copy()
197
+
198
+ # Build context dictionary
199
+ context: dict[str, Any] = {
200
+ "data": data,
201
+ "start_index": start_idx,
202
+ "end_index": end_idx,
203
+ "center_index": idx,
204
+ "length": len(data),
205
+ }
206
+
207
+ # Add time information if requested
208
+ if sample_rate is not None:
209
+ _add_time_information(context, start_idx, sample_rate, len(data))
210
+
211
+ # Add metadata if requested
212
+ if include_metadata:
213
+ _add_boundary_metadata(context, idx, start_idx, end_idx, len(trace))
214
+
215
+ return context
216
+
217
+
218
+ def _calculate_window_bounds(idx: int, before: int, after: int, trace_len: int) -> tuple[int, int]:
219
+ """Calculate window boundaries with edge handling.
220
+
221
+ Args:
222
+ idx: Center index.
223
+ before: Samples before center.
224
+ after: Samples after center.
225
+ trace_len: Length of trace.
226
+
227
+ Returns:
228
+ Tuple of (start_idx, end_idx).
229
+
230
+ Example:
231
+ >>> _calculate_window_bounds(50, 10, 10, 100)
232
+ (40, 61)
233
+ >>> _calculate_window_bounds(5, 10, 10, 100)
234
+ (0, 16)
235
+ """
236
+ start_idx = max(0, idx - before)
237
+ end_idx = min(trace_len, idx + after + 1)
238
+ return start_idx, end_idx
239
+
240
+
241
+ def _add_time_information(
242
+ context: dict[str, Any], start_idx: int, sample_rate: float, data_len: int
243
+ ) -> None:
244
+ """Add time reference information to context.
245
+
246
+ Args:
247
+ context: Context dictionary to update.
248
+ start_idx: Start index in original trace.
249
+ sample_rate: Sample rate in Hz.
250
+ data_len: Length of extracted data.
251
+
252
+ Example:
253
+ >>> ctx = {}
254
+ >>> _add_time_information(ctx, 100, 1e6, 50)
255
+ >>> ctx['time_reference']
256
+ 0.0001
257
+ >>> ctx['sample_rate']
258
+ 1000000.0
259
+ """
260
+ time_offset = start_idx / sample_rate
261
+ context["time_reference"] = time_offset
262
+ context["sample_rate"] = sample_rate
263
+
264
+ # Time array for the context
265
+ dt = 1.0 / sample_rate
266
+ context["time_array"] = np.arange(data_len) * dt + time_offset
267
+
268
+
269
+ def _add_boundary_metadata(
270
+ context: dict[str, Any], idx: int, start_idx: int, end_idx: int, trace_len: int
271
+ ) -> None:
272
+ """Add boundary metadata to context.
273
+
274
+ Args:
275
+ context: Context dictionary to update.
276
+ idx: Center index.
277
+ start_idx: Window start index.
278
+ end_idx: Window end index.
279
+ trace_len: Total trace length.
280
+
281
+ Example:
282
+ >>> ctx = {}
283
+ >>> _add_boundary_metadata(ctx, 5, 0, 11, 100)
284
+ >>> ctx['metadata']['at_start_boundary']
285
+ True
286
+ >>> ctx['metadata']['samples_before']
287
+ 5
288
+ """
289
+ context["metadata"] = {
290
+ "samples_before": idx - start_idx,
291
+ "samples_after": end_idx - idx - 1,
292
+ "at_start_boundary": start_idx == 0,
293
+ "at_end_boundary": end_idx == trace_len,
294
+ }