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
@@ -1,210 +0,0 @@
1
- """SPICE PWL export functionality for Oscura.
2
-
3
- This module provides export to SPICE Piece-Wise Linear (PWL) format for
4
- use in circuit simulation tools like LTspice, ngspice, Cadence, etc.
5
-
6
-
7
- Example:
8
- >>> from oscura.exporters.spice_export import export_pwl
9
- >>> export_pwl(trace, "stimulus.pwl")
10
- >>> # Use in SPICE: V1 in 0 PWL file=stimulus.pwl
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from pathlib import Path
16
- from typing import TYPE_CHECKING, Any
17
-
18
- import numpy as np
19
-
20
- from oscura.core.types import DigitalTrace, WaveformTrace
21
-
22
- if TYPE_CHECKING:
23
- from numpy.typing import NDArray
24
-
25
-
26
- def export_pwl(
27
- data: WaveformTrace | DigitalTrace | NDArray[Any] | tuple[NDArray[Any], NDArray[Any]],
28
- path: str | Path,
29
- *,
30
- time_scale: float = 1.0,
31
- amplitude_scale: float = 1.0,
32
- time_offset: float = 0.0,
33
- amplitude_offset: float = 0.0,
34
- precision: int = 12,
35
- comment: str | None = None,
36
- downsample: int = 1,
37
- format_style: str = "ltspice",
38
- ) -> None:
39
- """Export data to SPICE PWL (Piece-Wise Linear) format.
40
-
41
- Creates a PWL file that can be used as a stimulus source in SPICE
42
- circuit simulators. The format consists of time-value pairs.
43
-
44
- Args:
45
- data: Data to export. Can be:
46
- - WaveformTrace or DigitalTrace
47
- - NumPy array (uses trace.time_vector or generates index-based time)
48
- - Tuple of (time_array, value_array)
49
- path: Output file path.
50
- time_scale: Scaling factor for time values (e.g., 1e-9 for ns).
51
- amplitude_scale: Scaling factor for amplitude values.
52
- time_offset: Offset to add to all time values.
53
- amplitude_offset: Offset to add to all amplitude values.
54
- precision: Decimal precision for output values.
55
- comment: Optional comment to include at top of file.
56
- downsample: Downsample factor to reduce file size (1 = no downsampling).
57
- format_style: Output format style:
58
- - "ltspice": LTspice compatible (time value pairs)
59
- - "ngspice": ngspice compatible (same as ltspice)
60
- - "hspice": HSPICE compatible (with header)
61
-
62
- Raises:
63
- TypeError: If data type is not supported.
64
-
65
- Example:
66
- >>> # Export as stimulus for simulation
67
- >>> export_pwl(trace, "input.pwl")
68
- >>> # In LTspice: V1 in 0 PWL file=input.pwl
69
-
70
- >>> # Scale time to nanoseconds for display
71
- >>> export_pwl(trace, "input.pwl", time_scale=1e9)
72
-
73
- References:
74
- EXP-005
75
- """
76
- path = Path(path)
77
-
78
- # Extract time and value arrays
79
- if isinstance(data, WaveformTrace | DigitalTrace):
80
- time = data.time_vector
81
- values = data.data
82
- elif isinstance(data, tuple) and len(data) == 2:
83
- time, values = data
84
- elif isinstance(data, np.ndarray):
85
- # Generate time based on array index (assume 1 unit per sample)
86
- time = np.arange(len(data), dtype=np.float64)
87
- values = data
88
- else:
89
- raise TypeError(f"Unsupported data type: {type(data)}")
90
-
91
- # Apply downsampling
92
- if downsample > 1:
93
- time = time[::downsample]
94
- values = values[::downsample]
95
-
96
- # Apply scaling and offset
97
- time = time * time_scale + time_offset
98
- values = values * amplitude_scale + amplitude_offset
99
-
100
- # Write to file
101
- with open(path, "w") as f:
102
- # Write comment/header
103
- if format_style == "hspice":
104
- f.write("* HSPICE PWL Data\n")
105
- if comment:
106
- f.write(f"* {comment}\n")
107
- f.write(f"* Points: {len(time)}\n")
108
- f.write("*\n")
109
- elif comment:
110
- f.write(f"; {comment}\n")
111
-
112
- # Write time-value pairs
113
- fmt = f"{{:.{precision}g}} {{:.{precision}g}}\n"
114
- f.writelines(fmt.format(t, v) for t, v in zip(time, values, strict=False))
115
-
116
-
117
- def export_pwl_multi(
118
- traces: dict[str, WaveformTrace | DigitalTrace | NDArray[Any]],
119
- path: str | Path,
120
- *,
121
- time_scale: float = 1.0,
122
- amplitude_scale: float = 1.0,
123
- precision: int = 12,
124
- downsample: int = 1,
125
- ) -> None:
126
- """Export multiple traces to individual PWL files.
127
-
128
- Creates separate PWL files for each trace in the dictionary,
129
- with filenames based on the dictionary keys.
130
-
131
- Args:
132
- traces: Dictionary mapping signal names to trace data.
133
- path: Output directory path.
134
- time_scale: Scaling factor for time values.
135
- amplitude_scale: Scaling factor for amplitude values.
136
- precision: Decimal precision for output values.
137
- downsample: Downsample factor to reduce file size.
138
-
139
- Example:
140
- >>> traces = {
141
- ... "clk": clock_trace,
142
- ... "data": data_trace,
143
- ... "reset": reset_trace,
144
- ... }
145
- >>> export_pwl_multi(traces, "stimuli/")
146
- >>> # Creates: stimuli/clk.pwl, stimuli/data.pwl, stimuli/reset.pwl
147
-
148
- References:
149
- EXP-005
150
- """
151
- path = Path(path)
152
- path.mkdir(parents=True, exist_ok=True)
153
-
154
- for name, data in traces.items():
155
- # Sanitize filename
156
- safe_name = "".join(c if c.isalnum() or c in "_-" else "_" for c in name)
157
- file_path = path / f"{safe_name}.pwl"
158
-
159
- export_pwl(
160
- data,
161
- file_path,
162
- time_scale=time_scale,
163
- amplitude_scale=amplitude_scale,
164
- precision=precision,
165
- downsample=downsample,
166
- comment=f"Signal: {name}",
167
- )
168
-
169
-
170
- def generate_spice_source(
171
- pwl_path: str | Path,
172
- node_positive: str = "in",
173
- node_negative: str = "0",
174
- source_name: str = "V1",
175
- source_type: str = "voltage",
176
- ) -> str:
177
- """Generate SPICE source definition for a PWL file.
178
-
179
- Args:
180
- pwl_path: Path to PWL file.
181
- node_positive: Positive node name.
182
- node_negative: Negative node name (usually "0" for ground).
183
- source_name: Source instance name.
184
- source_type: Source type ("voltage" or "current").
185
-
186
- Returns:
187
- SPICE source definition string.
188
-
189
- Example:
190
- >>> line = generate_spice_source("input.pwl", "in", "0", "V1")
191
- >>> print(line)
192
- V1 in 0 PWL file=input.pwl
193
-
194
- References:
195
- EXP-005
196
- """
197
- prefix = "V" if source_type == "voltage" else "I"
198
-
199
- # Ensure source name starts with correct prefix
200
- if not source_name.upper().startswith(prefix):
201
- source_name = f"{prefix}{source_name}"
202
-
203
- return f"{source_name} {node_positive} {node_negative} PWL file={pwl_path}"
204
-
205
-
206
- __all__ = [
207
- "export_pwl",
208
- "export_pwl_multi",
209
- "generate_spice_source",
210
- ]
oscura/search/context.py DELETED
@@ -1,149 +0,0 @@
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
- if before < 0 or after < 0:
87
- raise ValueError("before and after must be non-negative")
88
-
89
- if trace.size == 0:
90
- raise ValueError("Trace cannot be empty")
91
-
92
- # Handle single index vs multiple indices
93
- if isinstance(index, int | np.integer):
94
- indices = [int(index)]
95
- return_single = True
96
- else:
97
- indices = [int(i) for i in index]
98
- return_single = False
99
-
100
- # Validate indices
101
- for idx in indices:
102
- if idx < 0 or idx >= len(trace):
103
- raise ValueError(f"Index {idx} out of bounds for trace of length {len(trace)}")
104
-
105
- # Extract contexts
106
- contexts = []
107
-
108
- for idx in indices:
109
- # Calculate window bounds with boundary handling
110
- start_idx = max(0, idx - before)
111
- end_idx = min(len(trace), idx + after + 1)
112
-
113
- # Extract data
114
- data = trace[start_idx:end_idx].copy()
115
-
116
- # Build context dictionary
117
- context: dict[str, Any] = {
118
- "data": data,
119
- "start_index": start_idx,
120
- "end_index": end_idx,
121
- "center_index": idx,
122
- "length": len(data),
123
- }
124
-
125
- # Add time reference if sample rate provided
126
- if sample_rate is not None:
127
- time_offset = start_idx / sample_rate
128
- context["time_reference"] = time_offset
129
- context["sample_rate"] = sample_rate
130
-
131
- # Time array for the context
132
- dt = 1.0 / sample_rate
133
- context["time_array"] = np.arange(len(data)) * dt + time_offset
134
-
135
- if include_metadata:
136
- context["metadata"] = {
137
- "samples_before": idx - start_idx,
138
- "samples_after": end_idx - idx - 1,
139
- "at_start_boundary": start_idx == 0,
140
- "at_end_boundary": end_idx == len(trace),
141
- }
142
-
143
- contexts.append(context)
144
-
145
- # Return single context or list
146
- if return_single:
147
- return contexts[0]
148
- else:
149
- return contexts
@@ -1,34 +0,0 @@
1
- """Session management for Oscura analysis sessions.
2
-
3
- This module provides session save/restore, trace annotations, and
4
- operation history tracking.
5
-
6
-
7
- Example:
8
- >>> import oscura as osc
9
- >>> session = osc.Session()
10
- >>> session.load_trace('capture.wfm')
11
- >>> session.annotate(time=1.5e-6, text='Glitch here')
12
- >>> session.save('debug_session.tks')
13
- >>>
14
- >>> # Later...
15
- >>> session = osc.load_session('debug_session.tks')
16
- >>> print(session.annotations)
17
- """
18
-
19
- from oscura.session.annotations import Annotation, AnnotationLayer, AnnotationType
20
- from oscura.session.history import HistoryEntry, OperationHistory
21
- from oscura.session.session import Session, load_session
22
-
23
- __all__ = [
24
- # Annotations (SESS-002)
25
- "Annotation",
26
- "AnnotationLayer",
27
- "AnnotationType",
28
- # History (SESS-003)
29
- "HistoryEntry",
30
- "OperationHistory",
31
- # Session (SESS-001)
32
- "Session",
33
- "load_session",
34
- ]
@@ -1,289 +0,0 @@
1
- """Trace annotation support.
2
-
3
- This module provides annotation capabilities for marking points of interest
4
- in signal traces.
5
-
6
-
7
- Example:
8
- >>> layer = AnnotationLayer("Debug Markers")
9
- >>> layer.add(Annotation(time=1.5e-6, text="Glitch detected"))
10
- >>> layer.add(Annotation(time_range=(2e-6, 3e-6), text="Data packet"))
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from dataclasses import dataclass, field
16
- from datetime import datetime
17
- from enum import Enum
18
- from typing import Any
19
-
20
-
21
- class AnnotationType(Enum):
22
- """Types of annotations."""
23
-
24
- POINT = "point" # Single time point
25
- RANGE = "range" # Time range
26
- VERTICAL = "vertical" # Vertical line
27
- HORIZONTAL = "horizontal" # Horizontal line
28
- REGION = "region" # 2D region (time + amplitude)
29
- TEXT = "text" # Free-floating text
30
-
31
-
32
- @dataclass
33
- class Annotation:
34
- """Single annotation on a trace.
35
-
36
- Attributes:
37
- text: Annotation text/label
38
- time: Time point (for point annotations)
39
- time_range: (start, end) time range
40
- amplitude: Amplitude value (for horizontal lines)
41
- amplitude_range: (min, max) amplitude range
42
- annotation_type: Type of annotation
43
- color: Display color (hex or name)
44
- style: Line style ('solid', 'dashed', 'dotted')
45
- visible: Whether annotation is visible
46
- created_at: Creation timestamp
47
- metadata: Additional metadata
48
- """
49
-
50
- text: str
51
- time: float | None = None
52
- time_range: tuple[float, float] | None = None
53
- amplitude: float | None = None
54
- amplitude_range: tuple[float, float] | None = None
55
- annotation_type: AnnotationType = AnnotationType.POINT
56
- color: str = "#FF6B6B"
57
- style: str = "solid"
58
- visible: bool = True
59
- created_at: datetime = field(default_factory=datetime.now)
60
- metadata: dict[str, Any] = field(default_factory=dict)
61
-
62
- def __post_init__(self) -> None:
63
- """Infer annotation type from provided parameters."""
64
- if self.annotation_type == AnnotationType.POINT:
65
- if self.time_range is not None:
66
- self.annotation_type = AnnotationType.RANGE
67
- elif self.amplitude is not None and self.time is None:
68
- self.annotation_type = AnnotationType.HORIZONTAL
69
- elif self.amplitude_range is not None and self.time_range is not None:
70
- self.annotation_type = AnnotationType.REGION # type: ignore[unreachable]
71
-
72
- @property
73
- def start_time(self) -> float | None:
74
- """Get start time for range annotations."""
75
- if self.time_range:
76
- return self.time_range[0]
77
- return self.time
78
-
79
- @property
80
- def end_time(self) -> float | None:
81
- """Get end time for range annotations."""
82
- if self.time_range:
83
- return self.time_range[1]
84
- return self.time
85
-
86
- def to_dict(self) -> dict[str, Any]:
87
- """Convert to dictionary for serialization."""
88
- return {
89
- "text": self.text,
90
- "time": self.time,
91
- "time_range": self.time_range,
92
- "amplitude": self.amplitude,
93
- "amplitude_range": self.amplitude_range,
94
- "annotation_type": self.annotation_type.value,
95
- "color": self.color,
96
- "style": self.style,
97
- "visible": self.visible,
98
- "created_at": self.created_at.isoformat(),
99
- "metadata": self.metadata,
100
- }
101
-
102
- @classmethod
103
- def from_dict(cls, data: dict[str, Any]) -> Annotation:
104
- """Create from dictionary."""
105
- data = data.copy()
106
- data["annotation_type"] = AnnotationType(data.get("annotation_type", "point"))
107
- if "created_at" in data and isinstance(data["created_at"], str):
108
- data["created_at"] = datetime.fromisoformat(data["created_at"])
109
- return cls(**data)
110
-
111
-
112
- @dataclass
113
- class AnnotationLayer:
114
- """Collection of related annotations.
115
-
116
- Attributes:
117
- name: Layer name
118
- annotations: List of annotations
119
- visible: Whether layer is visible
120
- locked: Whether layer is locked (read-only)
121
- color: Default color for new annotations
122
- description: Layer description
123
- """
124
-
125
- name: str
126
- annotations: list[Annotation] = field(default_factory=list)
127
- visible: bool = True
128
- locked: bool = False
129
- color: str = "#FF6B6B"
130
- description: str = ""
131
-
132
- def add(
133
- self,
134
- annotation: Annotation | None = None,
135
- *,
136
- text: str = "",
137
- time: float | None = None,
138
- time_range: tuple[float, float] | None = None,
139
- **kwargs: Any,
140
- ) -> Annotation:
141
- """Add annotation to layer.
142
-
143
- Args:
144
- annotation: Pre-built Annotation object.
145
- text: Annotation text (if not using pre-built).
146
- time: Time point.
147
- time_range: Time range.
148
- **kwargs: Additional Annotation parameters.
149
-
150
- Returns:
151
- Added annotation.
152
-
153
- Raises:
154
- ValueError: If layer is locked.
155
- """
156
- if self.locked:
157
- raise ValueError(f"Layer '{self.name}' is locked")
158
-
159
- if annotation is None:
160
- annotation = Annotation(
161
- text=text,
162
- time=time,
163
- time_range=time_range,
164
- color=kwargs.pop("color", self.color),
165
- **kwargs,
166
- )
167
-
168
- self.annotations.append(annotation)
169
- return annotation
170
-
171
- def remove(self, annotation: Annotation) -> bool:
172
- """Remove annotation from layer.
173
-
174
- Args:
175
- annotation: Annotation to remove.
176
-
177
- Returns:
178
- True if removed, False if not found.
179
-
180
- Raises:
181
- ValueError: If layer is locked.
182
- """
183
- if self.locked:
184
- raise ValueError(f"Layer '{self.name}' is locked")
185
-
186
- try:
187
- self.annotations.remove(annotation)
188
- return True
189
- except ValueError:
190
- return False
191
-
192
- def find_at_time(
193
- self,
194
- time: float,
195
- tolerance: float = 0.0,
196
- ) -> list[Annotation]:
197
- """Find annotations at or near a specific time.
198
-
199
- Args:
200
- time: Time to search.
201
- tolerance: Time tolerance for matching.
202
-
203
- Returns:
204
- List of matching annotations.
205
- """
206
- matches = []
207
- for ann in self.annotations:
208
- if ann.time is not None:
209
- if abs(ann.time - time) <= tolerance:
210
- matches.append(ann)
211
- elif ann.time_range is not None and (
212
- ann.time_range[0] - tolerance <= time <= ann.time_range[1] + tolerance
213
- ):
214
- matches.append(ann)
215
- return matches
216
-
217
- def find_in_range(
218
- self,
219
- start_time: float,
220
- end_time: float,
221
- ) -> list[Annotation]:
222
- """Find annotations within a time range.
223
-
224
- Args:
225
- start_time: Range start.
226
- end_time: Range end.
227
-
228
- Returns:
229
- List of annotations within range.
230
- """
231
- matches = []
232
- for ann in self.annotations:
233
- ann_start = ann.start_time
234
- ann_end = ann.end_time
235
-
236
- if ann_start is not None and (
237
- start_time <= ann_start <= end_time
238
- or (ann_end is not None and ann_start <= end_time and ann_end >= start_time)
239
- ):
240
- matches.append(ann)
241
-
242
- return matches
243
-
244
- def clear(self) -> int:
245
- """Remove all annotations.
246
-
247
- Returns:
248
- Number of annotations removed.
249
-
250
- Raises:
251
- ValueError: If layer is locked.
252
- """
253
- if self.locked:
254
- raise ValueError(f"Layer '{self.name}' is locked")
255
-
256
- count = len(self.annotations)
257
- self.annotations.clear()
258
- return count
259
-
260
- def to_dict(self) -> dict[str, Any]:
261
- """Convert to dictionary for serialization."""
262
- return {
263
- "name": self.name,
264
- "annotations": [a.to_dict() for a in self.annotations],
265
- "visible": self.visible,
266
- "locked": self.locked,
267
- "color": self.color,
268
- "description": self.description,
269
- }
270
-
271
- @classmethod
272
- def from_dict(cls, data: dict[str, Any]) -> AnnotationLayer:
273
- """Create from dictionary."""
274
- annotations = [Annotation.from_dict(a) for a in data.get("annotations", [])]
275
- return cls(
276
- name=data["name"],
277
- annotations=annotations,
278
- visible=data.get("visible", True),
279
- locked=data.get("locked", False),
280
- color=data.get("color", "#FF6B6B"),
281
- description=data.get("description", ""),
282
- )
283
-
284
-
285
- __all__ = [
286
- "Annotation",
287
- "AnnotationLayer",
288
- "AnnotationType",
289
- ]