oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/edges.py +325 -65
  5. oscura/analyzers/digital/quality.py +293 -166
  6. oscura/analyzers/digital/timing.py +260 -115
  7. oscura/analyzers/digital/timing_numba.py +334 -0
  8. oscura/analyzers/entropy.py +605 -0
  9. oscura/analyzers/eye/diagram.py +176 -109
  10. oscura/analyzers/eye/metrics.py +5 -5
  11. oscura/analyzers/jitter/__init__.py +6 -4
  12. oscura/analyzers/jitter/ber.py +52 -52
  13. oscura/analyzers/jitter/classification.py +156 -0
  14. oscura/analyzers/jitter/decomposition.py +163 -113
  15. oscura/analyzers/jitter/spectrum.py +80 -64
  16. oscura/analyzers/ml/__init__.py +39 -0
  17. oscura/analyzers/ml/features.py +600 -0
  18. oscura/analyzers/ml/signal_classifier.py +604 -0
  19. oscura/analyzers/packet/daq.py +246 -158
  20. oscura/analyzers/packet/parser.py +12 -1
  21. oscura/analyzers/packet/payload.py +50 -2110
  22. oscura/analyzers/packet/payload_analysis.py +361 -181
  23. oscura/analyzers/packet/payload_patterns.py +133 -70
  24. oscura/analyzers/packet/stream.py +84 -23
  25. oscura/analyzers/patterns/__init__.py +26 -5
  26. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  27. oscura/analyzers/patterns/clustering.py +169 -108
  28. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  29. oscura/analyzers/patterns/discovery.py +1 -1
  30. oscura/analyzers/patterns/matching.py +581 -197
  31. oscura/analyzers/patterns/pattern_mining.py +778 -0
  32. oscura/analyzers/patterns/periodic.py +121 -38
  33. oscura/analyzers/patterns/sequences.py +175 -78
  34. oscura/analyzers/power/conduction.py +1 -1
  35. oscura/analyzers/power/soa.py +6 -6
  36. oscura/analyzers/power/switching.py +250 -110
  37. oscura/analyzers/protocol/__init__.py +17 -1
  38. oscura/analyzers/protocols/base.py +6 -6
  39. oscura/analyzers/protocols/ble/__init__.py +38 -0
  40. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  41. oscura/analyzers/protocols/ble/uuids.py +288 -0
  42. oscura/analyzers/protocols/can.py +257 -127
  43. oscura/analyzers/protocols/can_fd.py +107 -80
  44. oscura/analyzers/protocols/flexray.py +139 -80
  45. oscura/analyzers/protocols/hdlc.py +93 -58
  46. oscura/analyzers/protocols/i2c.py +247 -106
  47. oscura/analyzers/protocols/i2s.py +138 -86
  48. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  49. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  50. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  51. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  52. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  53. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  54. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  55. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  56. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  57. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  58. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  59. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  60. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  61. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  62. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  63. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  64. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  65. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  66. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  67. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  68. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  69. oscura/analyzers/protocols/jtag.py +180 -98
  70. oscura/analyzers/protocols/lin.py +219 -114
  71. oscura/analyzers/protocols/manchester.py +4 -4
  72. oscura/analyzers/protocols/onewire.py +253 -149
  73. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  74. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  75. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  76. oscura/analyzers/protocols/spi.py +192 -95
  77. oscura/analyzers/protocols/swd.py +321 -167
  78. oscura/analyzers/protocols/uart.py +267 -125
  79. oscura/analyzers/protocols/usb.py +235 -131
  80. oscura/analyzers/side_channel/power.py +17 -12
  81. oscura/analyzers/signal/__init__.py +15 -0
  82. oscura/analyzers/signal/timing_analysis.py +1086 -0
  83. oscura/analyzers/signal_integrity/__init__.py +4 -1
  84. oscura/analyzers/signal_integrity/sparams.py +2 -19
  85. oscura/analyzers/spectral/chunked.py +129 -60
  86. oscura/analyzers/spectral/chunked_fft.py +300 -94
  87. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  88. oscura/analyzers/statistical/checksum.py +376 -217
  89. oscura/analyzers/statistical/classification.py +229 -107
  90. oscura/analyzers/statistical/entropy.py +78 -53
  91. oscura/analyzers/statistics/correlation.py +407 -211
  92. oscura/analyzers/statistics/outliers.py +2 -2
  93. oscura/analyzers/statistics/streaming.py +30 -5
  94. oscura/analyzers/validation.py +216 -101
  95. oscura/analyzers/waveform/measurements.py +9 -0
  96. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  97. oscura/analyzers/waveform/spectral.py +500 -228
  98. oscura/api/__init__.py +31 -5
  99. oscura/api/dsl/__init__.py +582 -0
  100. oscura/{dsl → api/dsl}/commands.py +43 -76
  101. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  102. oscura/{dsl → api/dsl}/parser.py +107 -77
  103. oscura/{dsl → api/dsl}/repl.py +2 -2
  104. oscura/api/dsl.py +1 -1
  105. oscura/{integrations → api/integrations}/__init__.py +1 -1
  106. oscura/{integrations → api/integrations}/llm.py +201 -102
  107. oscura/api/operators.py +3 -3
  108. oscura/api/optimization.py +144 -30
  109. oscura/api/rest_server.py +921 -0
  110. oscura/api/server/__init__.py +17 -0
  111. oscura/api/server/dashboard.py +850 -0
  112. oscura/api/server/static/README.md +34 -0
  113. oscura/api/server/templates/base.html +181 -0
  114. oscura/api/server/templates/export.html +120 -0
  115. oscura/api/server/templates/home.html +284 -0
  116. oscura/api/server/templates/protocols.html +58 -0
  117. oscura/api/server/templates/reports.html +43 -0
  118. oscura/api/server/templates/session_detail.html +89 -0
  119. oscura/api/server/templates/sessions.html +83 -0
  120. oscura/api/server/templates/waveforms.html +73 -0
  121. oscura/automotive/__init__.py +8 -1
  122. oscura/automotive/can/__init__.py +10 -0
  123. oscura/automotive/can/checksum.py +3 -1
  124. oscura/automotive/can/dbc_generator.py +590 -0
  125. oscura/automotive/can/message_wrapper.py +121 -74
  126. oscura/automotive/can/patterns.py +98 -21
  127. oscura/automotive/can/session.py +292 -56
  128. oscura/automotive/can/state_machine.py +6 -3
  129. oscura/automotive/can/stimulus_response.py +97 -75
  130. oscura/automotive/dbc/__init__.py +10 -2
  131. oscura/automotive/dbc/generator.py +84 -56
  132. oscura/automotive/dbc/parser.py +6 -6
  133. oscura/automotive/dtc/data.json +17 -102
  134. oscura/automotive/dtc/database.py +2 -2
  135. oscura/automotive/flexray/__init__.py +31 -0
  136. oscura/automotive/flexray/analyzer.py +504 -0
  137. oscura/automotive/flexray/crc.py +185 -0
  138. oscura/automotive/flexray/fibex.py +449 -0
  139. oscura/automotive/j1939/__init__.py +45 -8
  140. oscura/automotive/j1939/analyzer.py +605 -0
  141. oscura/automotive/j1939/spns.py +326 -0
  142. oscura/automotive/j1939/transport.py +306 -0
  143. oscura/automotive/lin/__init__.py +47 -0
  144. oscura/automotive/lin/analyzer.py +612 -0
  145. oscura/automotive/loaders/blf.py +13 -2
  146. oscura/automotive/loaders/csv_can.py +143 -72
  147. oscura/automotive/loaders/dispatcher.py +50 -2
  148. oscura/automotive/loaders/mdf.py +86 -45
  149. oscura/automotive/loaders/pcap.py +111 -61
  150. oscura/automotive/uds/__init__.py +4 -0
  151. oscura/automotive/uds/analyzer.py +725 -0
  152. oscura/automotive/uds/decoder.py +140 -58
  153. oscura/automotive/uds/models.py +7 -1
  154. oscura/automotive/visualization.py +1 -1
  155. oscura/cli/analyze.py +348 -0
  156. oscura/cli/batch.py +142 -122
  157. oscura/cli/benchmark.py +275 -0
  158. oscura/cli/characterize.py +137 -82
  159. oscura/cli/compare.py +224 -131
  160. oscura/cli/completion.py +250 -0
  161. oscura/cli/config_cmd.py +361 -0
  162. oscura/cli/decode.py +164 -87
  163. oscura/cli/export.py +286 -0
  164. oscura/cli/main.py +115 -31
  165. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  166. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  167. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  168. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  169. oscura/cli/progress.py +147 -0
  170. oscura/cli/shell.py +157 -135
  171. oscura/cli/validate_cmd.py +204 -0
  172. oscura/cli/visualize.py +158 -0
  173. oscura/convenience.py +125 -79
  174. oscura/core/__init__.py +4 -2
  175. oscura/core/backend_selector.py +3 -3
  176. oscura/core/cache.py +126 -15
  177. oscura/core/cancellation.py +1 -1
  178. oscura/{config → core/config}/__init__.py +20 -11
  179. oscura/{config → core/config}/defaults.py +1 -1
  180. oscura/{config → core/config}/loader.py +7 -5
  181. oscura/{config → core/config}/memory.py +5 -5
  182. oscura/{config → core/config}/migration.py +1 -1
  183. oscura/{config → core/config}/pipeline.py +99 -23
  184. oscura/{config → core/config}/preferences.py +1 -1
  185. oscura/{config → core/config}/protocol.py +3 -3
  186. oscura/{config → core/config}/schema.py +426 -272
  187. oscura/{config → core/config}/settings.py +1 -1
  188. oscura/{config → core/config}/thresholds.py +195 -153
  189. oscura/core/correlation.py +5 -6
  190. oscura/core/cross_domain.py +0 -2
  191. oscura/core/debug.py +9 -5
  192. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  193. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  194. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  195. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  196. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  197. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  198. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  199. oscura/core/gpu_backend.py +11 -7
  200. oscura/core/log_query.py +101 -11
  201. oscura/core/logging.py +126 -54
  202. oscura/core/logging_advanced.py +5 -5
  203. oscura/core/memory_limits.py +108 -70
  204. oscura/core/memory_monitor.py +2 -2
  205. oscura/core/memory_progress.py +7 -7
  206. oscura/core/memory_warnings.py +1 -1
  207. oscura/core/numba_backend.py +13 -13
  208. oscura/{plugins → core/plugins}/__init__.py +9 -9
  209. oscura/{plugins → core/plugins}/base.py +7 -7
  210. oscura/{plugins → core/plugins}/cli.py +3 -3
  211. oscura/{plugins → core/plugins}/discovery.py +186 -106
  212. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  213. oscura/{plugins → core/plugins}/manager.py +7 -7
  214. oscura/{plugins → core/plugins}/registry.py +3 -3
  215. oscura/{plugins → core/plugins}/versioning.py +1 -1
  216. oscura/core/progress.py +16 -1
  217. oscura/core/provenance.py +8 -2
  218. oscura/{schemas → core/schemas}/__init__.py +2 -2
  219. oscura/{schemas → core/schemas}/device_mapping.json +2 -8
  220. oscura/{schemas → core/schemas}/packet_format.json +4 -24
  221. oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
  222. oscura/core/types.py +4 -0
  223. oscura/core/uncertainty.py +3 -3
  224. oscura/correlation/__init__.py +52 -0
  225. oscura/correlation/multi_protocol.py +811 -0
  226. oscura/discovery/auto_decoder.py +117 -35
  227. oscura/discovery/comparison.py +191 -86
  228. oscura/discovery/quality_validator.py +155 -68
  229. oscura/discovery/signal_detector.py +196 -79
  230. oscura/export/__init__.py +18 -8
  231. oscura/export/kaitai_struct.py +513 -0
  232. oscura/export/scapy_layer.py +801 -0
  233. oscura/export/wireshark/generator.py +1 -1
  234. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  235. oscura/export/wireshark_dissector.py +746 -0
  236. oscura/guidance/wizard.py +207 -111
  237. oscura/hardware/__init__.py +19 -0
  238. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  239. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  240. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  241. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  242. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  243. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  244. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  245. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  246. oscura/hardware/firmware/__init__.py +29 -0
  247. oscura/hardware/firmware/pattern_recognition.py +874 -0
  248. oscura/hardware/hal_detector.py +736 -0
  249. oscura/hardware/security/__init__.py +37 -0
  250. oscura/hardware/security/side_channel_detector.py +1126 -0
  251. oscura/inference/__init__.py +4 -0
  252. oscura/inference/active_learning/observation_table.py +4 -1
  253. oscura/inference/alignment.py +216 -123
  254. oscura/inference/bayesian.py +113 -33
  255. oscura/inference/crc_reverse.py +101 -55
  256. oscura/inference/logic.py +6 -2
  257. oscura/inference/message_format.py +342 -183
  258. oscura/inference/protocol.py +95 -44
  259. oscura/inference/protocol_dsl.py +180 -82
  260. oscura/inference/signal_intelligence.py +1439 -706
  261. oscura/inference/spectral.py +99 -57
  262. oscura/inference/state_machine.py +810 -158
  263. oscura/inference/stream.py +270 -110
  264. oscura/iot/__init__.py +34 -0
  265. oscura/iot/coap/__init__.py +32 -0
  266. oscura/iot/coap/analyzer.py +668 -0
  267. oscura/iot/coap/options.py +212 -0
  268. oscura/iot/lorawan/__init__.py +21 -0
  269. oscura/iot/lorawan/crypto.py +206 -0
  270. oscura/iot/lorawan/decoder.py +801 -0
  271. oscura/iot/lorawan/mac_commands.py +341 -0
  272. oscura/iot/mqtt/__init__.py +27 -0
  273. oscura/iot/mqtt/analyzer.py +999 -0
  274. oscura/iot/mqtt/properties.py +315 -0
  275. oscura/iot/zigbee/__init__.py +31 -0
  276. oscura/iot/zigbee/analyzer.py +615 -0
  277. oscura/iot/zigbee/security.py +153 -0
  278. oscura/iot/zigbee/zcl.py +349 -0
  279. oscura/jupyter/display.py +125 -45
  280. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  281. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  282. oscura/jupyter/exploratory/fuzzy.py +746 -0
  283. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  284. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  285. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  286. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  287. oscura/jupyter/exploratory/sync.py +612 -0
  288. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  289. oscura/jupyter/magic.py +4 -4
  290. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  291. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  292. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  293. oscura/loaders/__init__.py +183 -67
  294. oscura/loaders/binary.py +88 -1
  295. oscura/loaders/chipwhisperer.py +153 -137
  296. oscura/loaders/configurable.py +208 -86
  297. oscura/loaders/csv_loader.py +458 -215
  298. oscura/loaders/hdf5_loader.py +278 -119
  299. oscura/loaders/lazy.py +87 -54
  300. oscura/loaders/mmap_loader.py +1 -1
  301. oscura/loaders/numpy_loader.py +253 -116
  302. oscura/loaders/pcap.py +226 -151
  303. oscura/loaders/rigol.py +110 -49
  304. oscura/loaders/sigrok.py +201 -78
  305. oscura/loaders/tdms.py +81 -58
  306. oscura/loaders/tektronix.py +291 -174
  307. oscura/loaders/touchstone.py +182 -87
  308. oscura/loaders/tss.py +456 -0
  309. oscura/loaders/vcd.py +215 -117
  310. oscura/loaders/wav.py +155 -68
  311. oscura/reporting/__init__.py +9 -0
  312. oscura/reporting/analyze.py +352 -146
  313. oscura/reporting/argument_preparer.py +69 -14
  314. oscura/reporting/auto_report.py +97 -61
  315. oscura/reporting/batch.py +131 -58
  316. oscura/reporting/chart_selection.py +57 -45
  317. oscura/reporting/comparison.py +63 -17
  318. oscura/reporting/content/executive.py +76 -24
  319. oscura/reporting/core_formats/multi_format.py +11 -8
  320. oscura/reporting/engine.py +312 -158
  321. oscura/reporting/enhanced_reports.py +949 -0
  322. oscura/reporting/export.py +86 -43
  323. oscura/reporting/formatting/numbers.py +69 -42
  324. oscura/reporting/html.py +139 -58
  325. oscura/reporting/index.py +137 -65
  326. oscura/reporting/output.py +158 -67
  327. oscura/reporting/pdf.py +67 -102
  328. oscura/reporting/plots.py +191 -112
  329. oscura/reporting/sections.py +88 -47
  330. oscura/reporting/standards.py +104 -61
  331. oscura/reporting/summary_generator.py +75 -55
  332. oscura/reporting/tables.py +138 -54
  333. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  334. oscura/sessions/__init__.py +14 -23
  335. oscura/sessions/base.py +3 -3
  336. oscura/sessions/blackbox.py +106 -10
  337. oscura/sessions/generic.py +2 -2
  338. oscura/sessions/legacy.py +783 -0
  339. oscura/side_channel/__init__.py +63 -0
  340. oscura/side_channel/dpa.py +1025 -0
  341. oscura/utils/__init__.py +15 -1
  342. oscura/utils/bitwise.py +118 -0
  343. oscura/{builders → utils/builders}/__init__.py +1 -1
  344. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  345. oscura/{comparison → utils/comparison}/compare.py +202 -101
  346. oscura/{comparison → utils/comparison}/golden.py +83 -63
  347. oscura/{comparison → utils/comparison}/limits.py +313 -89
  348. oscura/{comparison → utils/comparison}/mask.py +151 -45
  349. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  350. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  351. oscura/{component → utils/component}/__init__.py +3 -3
  352. oscura/{component → utils/component}/impedance.py +122 -58
  353. oscura/{component → utils/component}/reactive.py +165 -168
  354. oscura/{component → utils/component}/transmission_line.py +3 -3
  355. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  356. oscura/{filtering → utils/filtering}/base.py +1 -1
  357. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  358. oscura/{filtering → utils/filtering}/design.py +169 -93
  359. oscura/{filtering → utils/filtering}/filters.py +2 -2
  360. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  361. oscura/utils/geometry.py +31 -0
  362. oscura/utils/imports.py +184 -0
  363. oscura/utils/lazy.py +1 -1
  364. oscura/{math → utils/math}/__init__.py +2 -2
  365. oscura/{math → utils/math}/arithmetic.py +114 -48
  366. oscura/{math → utils/math}/interpolation.py +139 -106
  367. oscura/utils/memory.py +129 -66
  368. oscura/utils/memory_advanced.py +92 -9
  369. oscura/utils/memory_extensions.py +10 -8
  370. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  371. oscura/{optimization → utils/optimization}/search.py +2 -2
  372. oscura/utils/performance/__init__.py +58 -0
  373. oscura/utils/performance/caching.py +889 -0
  374. oscura/utils/performance/lsh_clustering.py +333 -0
  375. oscura/utils/performance/memory_optimizer.py +699 -0
  376. oscura/utils/performance/optimizations.py +675 -0
  377. oscura/utils/performance/parallel.py +654 -0
  378. oscura/utils/performance/profiling.py +661 -0
  379. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  380. oscura/{pipeline → utils/pipeline}/composition.py +1 -1
  381. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  382. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  383. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  384. oscura/{search → utils/search}/__init__.py +3 -3
  385. oscura/{search → utils/search}/anomaly.py +188 -58
  386. oscura/utils/search/context.py +294 -0
  387. oscura/{search → utils/search}/pattern.py +138 -10
  388. oscura/utils/serial.py +51 -0
  389. oscura/utils/storage/__init__.py +61 -0
  390. oscura/utils/storage/database.py +1166 -0
  391. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  392. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  393. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  394. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  395. oscura/{triggering → utils/triggering}/base.py +6 -6
  396. oscura/{triggering → utils/triggering}/edge.py +2 -2
  397. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  398. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  399. oscura/{triggering → utils/triggering}/window.py +2 -2
  400. oscura/utils/validation.py +32 -0
  401. oscura/validation/__init__.py +121 -0
  402. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  403. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  404. oscura/{compliance → validation/compliance}/masks.py +1 -1
  405. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  406. oscura/{compliance → validation/compliance}/testing.py +114 -52
  407. oscura/validation/compliance_tests.py +915 -0
  408. oscura/validation/fuzzer.py +990 -0
  409. oscura/validation/grammar_tests.py +596 -0
  410. oscura/validation/grammar_validator.py +904 -0
  411. oscura/validation/hil_testing.py +977 -0
  412. oscura/{quality → validation/quality}/__init__.py +4 -4
  413. oscura/{quality → validation/quality}/ensemble.py +251 -171
  414. oscura/{quality → validation/quality}/explainer.py +3 -3
  415. oscura/{quality → validation/quality}/scoring.py +1 -1
  416. oscura/{quality → validation/quality}/warnings.py +4 -4
  417. oscura/validation/regression_suite.py +808 -0
  418. oscura/validation/replay.py +788 -0
  419. oscura/{testing → validation/testing}/__init__.py +2 -2
  420. oscura/{testing → validation/testing}/synthetic.py +5 -5
  421. oscura/visualization/__init__.py +9 -0
  422. oscura/visualization/accessibility.py +1 -1
  423. oscura/visualization/annotations.py +64 -67
  424. oscura/visualization/colors.py +7 -7
  425. oscura/visualization/digital.py +180 -81
  426. oscura/visualization/eye.py +236 -85
  427. oscura/visualization/interactive.py +320 -143
  428. oscura/visualization/jitter.py +587 -247
  429. oscura/visualization/layout.py +169 -134
  430. oscura/visualization/optimization.py +103 -52
  431. oscura/visualization/palettes.py +1 -1
  432. oscura/visualization/power.py +427 -211
  433. oscura/visualization/power_extended.py +626 -297
  434. oscura/visualization/presets.py +2 -0
  435. oscura/visualization/protocols.py +495 -181
  436. oscura/visualization/render.py +79 -63
  437. oscura/visualization/reverse_engineering.py +171 -124
  438. oscura/visualization/signal_integrity.py +460 -279
  439. oscura/visualization/specialized.py +190 -100
  440. oscura/visualization/spectral.py +670 -255
  441. oscura/visualization/thumbnails.py +166 -137
  442. oscura/visualization/waveform.py +150 -63
  443. oscura/workflows/__init__.py +3 -0
  444. oscura/{batch → workflows/batch}/__init__.py +5 -5
  445. oscura/{batch → workflows/batch}/advanced.py +150 -75
  446. oscura/workflows/batch/aggregate.py +531 -0
  447. oscura/workflows/batch/analyze.py +236 -0
  448. oscura/{batch → workflows/batch}/logging.py +2 -2
  449. oscura/{batch → workflows/batch}/metrics.py +1 -1
  450. oscura/workflows/complete_re.py +1144 -0
  451. oscura/workflows/compliance.py +44 -54
  452. oscura/workflows/digital.py +197 -51
  453. oscura/workflows/legacy/__init__.py +12 -0
  454. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  455. oscura/workflows/multi_trace.py +9 -9
  456. oscura/workflows/power.py +42 -62
  457. oscura/workflows/protocol.py +82 -49
  458. oscura/workflows/reverse_engineering.py +351 -150
  459. oscura/workflows/signal_integrity.py +157 -82
  460. oscura-0.7.0.dist-info/METADATA +661 -0
  461. oscura-0.7.0.dist-info/RECORD +591 -0
  462. oscura/batch/aggregate.py +0 -300
  463. oscura/batch/analyze.py +0 -139
  464. oscura/dsl/__init__.py +0 -73
  465. oscura/exceptions.py +0 -59
  466. oscura/exploratory/fuzzy.py +0 -513
  467. oscura/exploratory/sync.py +0 -384
  468. oscura/exporters/__init__.py +0 -94
  469. oscura/exporters/csv.py +0 -303
  470. oscura/exporters/exporters.py +0 -44
  471. oscura/exporters/hdf5.py +0 -217
  472. oscura/exporters/html_export.py +0 -701
  473. oscura/exporters/json_export.py +0 -291
  474. oscura/exporters/markdown_export.py +0 -367
  475. oscura/exporters/matlab_export.py +0 -354
  476. oscura/exporters/npz_export.py +0 -219
  477. oscura/exporters/spice_export.py +0 -210
  478. oscura/search/context.py +0 -149
  479. oscura/session/__init__.py +0 -34
  480. oscura/session/annotations.py +0 -289
  481. oscura/session/history.py +0 -313
  482. oscura/session/session.py +0 -520
  483. oscura/workflow/__init__.py +0 -13
  484. oscura-0.5.1.dist-info/METADATA +0 -583
  485. oscura-0.5.1.dist-info/RECORD +0 -481
  486. /oscura/core/{config.py → config/legacy.py} +0 -0
  487. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  488. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  489. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  490. /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
  491. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  492. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  493. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  494. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  495. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
  496. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
  497. {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -5,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
+ }