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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -173,109 +173,46 @@ def _load_with_pandas(
173
173
  skip_rows: int,
174
174
  encoding: str,
175
175
  ) -> WaveformTrace:
176
- """Load CSV using pandas for better parsing."""
177
- try:
178
- # Auto-detect delimiter if not specified
179
- if delimiter is None:
180
- delimiter = _detect_delimiter(path, encoding)
176
+ """Load CSV using pandas for better parsing.
177
+
178
+ Args:
179
+ path: Path to CSV file.
180
+ time_column: Name or index of time column (None for auto-detect).
181
+ voltage_column: Name or index of voltage column (None for auto-detect).
182
+ sample_rate: Override sample rate (None to compute from time column).
183
+ delimiter: Column delimiter (None for auto-detect).
184
+ skip_rows: Number of rows to skip before header.
185
+ encoding: File encoding.
186
+
187
+ Returns:
188
+ WaveformTrace containing waveform data and metadata.
181
189
 
190
+ Raises:
191
+ FormatError: If CSV format is invalid or missing data.
192
+ LoaderError: If file cannot be loaded.
193
+ """
194
+ try:
182
195
  # Read CSV with pandas
183
- df = pd.read_csv(
184
- path,
185
- delimiter=delimiter,
186
- skiprows=skip_rows,
187
- encoding=encoding,
188
- engine="python", # More flexible parsing
196
+ delimiter = delimiter or _detect_delimiter(path, encoding)
197
+ df = _read_csv_with_pandas(path, delimiter, skip_rows, encoding)
198
+
199
+ # Find time and voltage columns
200
+ time_data, time_col_name = _find_pandas_time_column(df, time_column)
201
+ voltage_data, voltage_col_name = _find_pandas_voltage_column(
202
+ df, voltage_column, time_col_name, path
189
203
  )
190
204
 
191
- if df.empty:
192
- raise FormatError(
193
- "CSV file is empty",
194
- file_path=str(path),
195
- )
196
-
197
- # Find time column
198
- time_data = None
199
- time_col_name = None
200
-
201
- if time_column is not None:
202
- if isinstance(time_column, int):
203
- if time_column < len(df.columns):
204
- time_col_name = df.columns[time_column]
205
- time_data = df.iloc[:, time_column].values
206
- elif time_column in df.columns:
207
- time_col_name = time_column
208
- time_data = df[time_column].values
209
- else:
210
- # Auto-detect time column
211
- for col in df.columns:
212
- col_lower = col.lower().strip()
213
- if col_lower in [n.lower() for n in TIME_COLUMN_NAMES]:
214
- time_col_name = col
215
- time_data = df[col].values
216
- break
205
+ # Compute sample rate from time data if not provided
206
+ detected_sample_rate = _compute_sample_rate_from_array(sample_rate, time_data)
217
207
 
218
- # Find voltage column
219
- voltage_data = None
220
- voltage_col_name = None
221
-
222
- if voltage_column is not None:
223
- if isinstance(voltage_column, int):
224
- if voltage_column < len(df.columns):
225
- voltage_col_name = df.columns[voltage_column]
226
- voltage_data = df.iloc[:, voltage_column].values
227
- elif voltage_column in df.columns:
228
- voltage_col_name = voltage_column
229
- voltage_data = df[voltage_column].values
230
- else:
231
- # Auto-detect voltage column (first non-time numeric column)
232
- for col in df.columns:
233
- if col == time_col_name:
234
- continue
235
- col_lower = col.lower().strip()
236
- # Check if numeric
237
- if pd.api.types.is_numeric_dtype(df[col]):
238
- # Prefer columns with voltage-like names
239
- if col_lower in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
240
- voltage_col_name = col
241
- voltage_data = df[col].values
242
- break
243
- elif voltage_data is None:
244
- voltage_col_name = col
245
- voltage_data = df[col].values
246
-
247
- if voltage_data is None:
248
- raise FormatError(
249
- "No voltage data found in CSV",
250
- file_path=str(path),
251
- expected="Numeric column for voltage data",
252
- got=f"Columns: {', '.join(df.columns)}",
253
- )
254
-
255
- # Convert to float64
256
- data = np.asarray(voltage_data, dtype=np.float64)
257
-
258
- # Determine sample rate
259
- detected_sample_rate = sample_rate
260
- if detected_sample_rate is None and time_data is not None:
261
- time_data = np.asarray(time_data, dtype=np.float64)
262
- if len(time_data) > 1:
263
- # Calculate sample rate from time intervals
264
- dt = np.median(np.diff(time_data))
265
- if dt > 0:
266
- detected_sample_rate = 1.0 / dt
267
-
268
- if detected_sample_rate is None:
269
- detected_sample_rate = 1e6 # Default to 1 MSa/s
270
-
271
- # Build metadata
208
+ # Create metadata and trace
272
209
  metadata = TraceMetadata(
273
210
  sample_rate=detected_sample_rate,
274
211
  source_file=str(path),
275
212
  channel_name=voltage_col_name or "CH1",
276
213
  )
277
214
 
278
- return WaveformTrace(data=data, metadata=metadata)
215
+ return WaveformTrace(data=np.asarray(voltage_data, dtype=np.float64), metadata=metadata)
279
216
 
280
217
  except pd.errors.ParserError as e:
281
218
  raise FormatError(
@@ -293,6 +230,151 @@ def _load_with_pandas(
293
230
  ) from e
294
231
 
295
232
 
233
+ def _read_csv_with_pandas(path: Path, delimiter: str, skip_rows: int, encoding: str) -> Any:
234
+ """Read CSV file using pandas.
235
+
236
+ Args:
237
+ path: Path to CSV file.
238
+ delimiter: Column delimiter.
239
+ skip_rows: Number of rows to skip before header.
240
+ encoding: File encoding.
241
+
242
+ Returns:
243
+ Pandas DataFrame.
244
+
245
+ Raises:
246
+ FormatError: If CSV is empty.
247
+ """
248
+ df = pd.read_csv(
249
+ path,
250
+ delimiter=delimiter,
251
+ skiprows=skip_rows,
252
+ encoding=encoding,
253
+ engine="python", # More flexible parsing
254
+ )
255
+
256
+ if df.empty:
257
+ raise FormatError("CSV file is empty", file_path=str(path))
258
+
259
+ return df
260
+
261
+
262
+ def _find_pandas_time_column(
263
+ df: Any, time_column: str | int | None
264
+ ) -> tuple[Any | None, str | None]:
265
+ """Find time column in pandas DataFrame.
266
+
267
+ Args:
268
+ df: Pandas DataFrame.
269
+ time_column: User-specified time column name or index.
270
+
271
+ Returns:
272
+ Tuple of (time_data, time_column_name).
273
+ """
274
+ time_data = None
275
+ time_col_name = None
276
+
277
+ if time_column is not None:
278
+ if isinstance(time_column, int):
279
+ if time_column < len(df.columns):
280
+ time_col_name = df.columns[time_column]
281
+ time_data = df.iloc[:, time_column].values
282
+ elif time_column in df.columns:
283
+ time_col_name = time_column
284
+ time_data = df[time_column].values
285
+ else:
286
+ # Auto-detect time column
287
+ for col in df.columns:
288
+ col_lower = col.lower().strip()
289
+ if col_lower in [n.lower() for n in TIME_COLUMN_NAMES]:
290
+ time_col_name = col
291
+ time_data = df[col].values
292
+ break
293
+
294
+ return time_data, time_col_name
295
+
296
+
297
+ def _find_pandas_voltage_column(
298
+ df: Any, voltage_column: str | int | None, time_col_name: str | None, path: Path
299
+ ) -> tuple[Any, str | None]:
300
+ """Find voltage column in pandas DataFrame.
301
+
302
+ Args:
303
+ df: Pandas DataFrame.
304
+ voltage_column: User-specified voltage column name or index.
305
+ time_col_name: Name of time column (to exclude from voltage search).
306
+ path: Path to CSV file (for error reporting).
307
+
308
+ Returns:
309
+ Tuple of (voltage_data, voltage_column_name).
310
+
311
+ Raises:
312
+ FormatError: If no voltage data found.
313
+ """
314
+ voltage_data = None
315
+ voltage_col_name = None
316
+
317
+ if voltage_column is not None:
318
+ if isinstance(voltage_column, int):
319
+ if voltage_column < len(df.columns):
320
+ voltage_col_name = df.columns[voltage_column]
321
+ voltage_data = df.iloc[:, voltage_column].values
322
+ elif voltage_column in df.columns:
323
+ voltage_col_name = voltage_column
324
+ voltage_data = df[voltage_column].values
325
+ else:
326
+ # Auto-detect voltage column (first non-time numeric column)
327
+ for col in df.columns:
328
+ if col == time_col_name:
329
+ continue
330
+
331
+ col_lower = col.lower().strip()
332
+
333
+ # Check if numeric
334
+ if pd.api.types.is_numeric_dtype(df[col]):
335
+ # Prefer columns with voltage-like names
336
+ if col_lower in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
337
+ voltage_col_name = col
338
+ voltage_data = df[col].values
339
+ break
340
+ elif voltage_data is None:
341
+ voltage_col_name = col
342
+ voltage_data = df[col].values
343
+
344
+ if voltage_data is None:
345
+ raise FormatError(
346
+ "No voltage data found in CSV",
347
+ file_path=str(path),
348
+ expected="Numeric column for voltage data",
349
+ got=f"Columns: {', '.join(df.columns)}",
350
+ )
351
+
352
+ return voltage_data, voltage_col_name
353
+
354
+
355
+ def _compute_sample_rate_from_array(sample_rate: float | None, time_data: Any | None) -> float:
356
+ """Compute sample rate from numpy array or use override.
357
+
358
+ Args:
359
+ sample_rate: User-specified sample rate (None to compute).
360
+ time_data: Numpy array of time values.
361
+
362
+ Returns:
363
+ Sample rate in Hz. Defaults to 1 MHz if cannot be computed.
364
+ """
365
+ if sample_rate is not None:
366
+ return sample_rate
367
+
368
+ if time_data is not None:
369
+ time_arr = np.asarray(time_data, dtype=np.float64)
370
+ if len(time_arr) > 1:
371
+ dt = float(np.median(np.diff(time_arr)))
372
+ if dt > 0:
373
+ return 1.0 / dt
374
+
375
+ return 1e6 # Default to 1 MSa/s
376
+
377
+
296
378
  def _load_basic(
297
379
  path: Path,
298
380
  *,
@@ -303,137 +385,55 @@ def _load_basic(
303
385
  skip_rows: int,
304
386
  encoding: str,
305
387
  ) -> WaveformTrace:
306
- """Basic CSV loader without pandas."""
388
+ """Basic CSV loader without pandas.
389
+
390
+ Args:
391
+ path: Path to CSV file.
392
+ time_column: Name or index of time column (None for auto-detect).
393
+ voltage_column: Name or index of voltage column (None for auto-detect).
394
+ sample_rate: Override sample rate (None to compute from time column).
395
+ delimiter: Column delimiter (None for auto-detect).
396
+ skip_rows: Number of rows to skip before header.
397
+ encoding: File encoding.
398
+
399
+ Returns:
400
+ WaveformTrace containing waveform data and metadata.
401
+
402
+ Raises:
403
+ FormatError: If CSV format is invalid or missing data.
404
+ LoaderError: If file cannot be loaded.
405
+ """
307
406
  try:
308
- with open(path, encoding=encoding) as f:
309
- # Skip rows
310
- for _ in range(skip_rows):
311
- next(f)
312
-
313
- content = f.read()
314
-
315
- # Auto-detect delimiter
316
- if delimiter is None:
317
- delimiter = _detect_delimiter_from_content(content)
318
-
319
- # Parse CSV
320
- reader = csv.reader(StringIO(content), delimiter=delimiter)
321
- rows = list(reader)
322
-
323
- if not rows:
324
- raise FormatError("CSV file is empty", file_path=str(path))
325
-
326
- # Detect header
327
- header = None
328
- data_start = 0
329
- first_row = rows[0]
330
-
331
- # Check if first row is a header (contains non-numeric values)
332
- is_header = False
333
- for cell in first_row:
334
- try:
335
- float(cell)
336
- except ValueError:
337
- if cell.strip(): # Non-empty, non-numeric
338
- is_header = True
339
- break
407
+ # Read and parse CSV
408
+ content = _read_file_content(path, skip_rows, encoding)
409
+ delimiter = delimiter or _detect_delimiter_from_content(content)
410
+ rows = _parse_csv_rows(content, delimiter, path)
340
411
 
341
- if is_header:
342
- header = [cell.strip() for cell in first_row]
343
- data_start = 1
344
-
345
- # Determine column indices
346
- time_idx = None
347
- voltage_idx = None
348
-
349
- if header:
350
- # Find columns by name
351
- if time_column is not None:
352
- if isinstance(time_column, int):
353
- time_idx = time_column
354
- elif time_column in header:
355
- time_idx = header.index(time_column)
356
- else:
357
- # Auto-detect
358
- for i, col in enumerate(header):
359
- if col.lower() in [n.lower() for n in TIME_COLUMN_NAMES]:
360
- time_idx = i
361
- break
362
-
363
- if voltage_column is not None:
364
- if isinstance(voltage_column, int):
365
- voltage_idx = voltage_column
366
- elif voltage_column in header:
367
- voltage_idx = header.index(voltage_column)
368
- else:
369
- # Auto-detect (first column that's not time)
370
- for i, col in enumerate(header):
371
- if i == time_idx:
372
- continue
373
- if col.lower() in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
374
- voltage_idx = i
375
- break
376
- if voltage_idx is None:
377
- voltage_idx = 1 if time_idx == 0 else 0
378
- else:
379
- # No header - use indices
380
- if isinstance(time_column, int):
381
- time_idx = time_column
382
- else:
383
- time_idx = 0 # Assume first column is time
384
-
385
- if isinstance(voltage_column, int):
386
- voltage_idx = voltage_column
387
- else:
388
- voltage_idx = 1 # Assume second column is voltage
389
-
390
- # Extract data
391
- time_data = []
392
- voltage_data = []
393
-
394
- for row in rows[data_start:]:
395
- if not row:
396
- continue
397
- try:
398
- if voltage_idx is not None and voltage_idx < len(row):
399
- voltage_data.append(float(row[voltage_idx]))
400
- if time_idx is not None and time_idx < len(row):
401
- time_data.append(float(row[time_idx]))
402
- except (ValueError, IndexError):
403
- continue # Skip malformed rows
404
-
405
- if not voltage_data:
406
- raise FormatError(
407
- "No valid voltage data found in CSV",
408
- file_path=str(path),
409
- )
410
-
411
- data = np.array(voltage_data, dtype=np.float64)
412
-
413
- # Determine sample rate
414
- detected_sample_rate = sample_rate
415
- if detected_sample_rate is None and time_data:
416
- time_arr = np.array(time_data, dtype=np.float64)
417
- if len(time_arr) > 1:
418
- dt = np.median(np.diff(time_arr))
419
- if dt > 0:
420
- detected_sample_rate = 1.0 / dt
421
-
422
- if detected_sample_rate is None:
423
- detected_sample_rate = 1e6
424
-
425
- # Channel name
426
- channel_name = "CH1"
427
- if header and voltage_idx is not None and voltage_idx < len(header):
428
- channel_name = header[voltage_idx]
412
+ # Detect header and determine data start position
413
+ header, data_start = _detect_header(rows)
414
+
415
+ # Find column indices for time and voltage data
416
+ time_idx, voltage_idx = _determine_column_indices(header, time_column, voltage_column)
417
+
418
+ # Extract numeric data from rows
419
+ time_data, voltage_data = _extract_data_from_rows(
420
+ rows, data_start, time_idx, voltage_idx, path
421
+ )
422
+
423
+ # Calculate sample rate from time data if not provided
424
+ detected_sample_rate = _compute_sample_rate(sample_rate, time_data)
429
425
 
426
+ # Build channel name from header if available
427
+ channel_name = _get_channel_name(header, voltage_idx)
428
+
429
+ # Create metadata and trace
430
430
  metadata = TraceMetadata(
431
431
  sample_rate=detected_sample_rate,
432
432
  source_file=str(path),
433
433
  channel_name=channel_name,
434
434
  )
435
435
 
436
- return WaveformTrace(data=data, metadata=metadata)
436
+ return WaveformTrace(data=np.array(voltage_data, dtype=np.float64), metadata=metadata)
437
437
 
438
438
  except Exception as e:
439
439
  if isinstance(e, LoaderError | FormatError):
@@ -445,10 +445,253 @@ def _load_basic(
445
445
  ) from e
446
446
 
447
447
 
448
+ def _read_file_content(path: Path, skip_rows: int, encoding: str) -> str:
449
+ """Read file content after skipping specified rows.
450
+
451
+ Args:
452
+ path: Path to CSV file.
453
+ skip_rows: Number of rows to skip.
454
+ encoding: File encoding.
455
+
456
+ Returns:
457
+ File content as string.
458
+ """
459
+ with open(path, encoding=encoding, buffering=65536) as f:
460
+ for _ in range(skip_rows):
461
+ next(f)
462
+ return f.read()
463
+
464
+
465
+ def _parse_csv_rows(content: str, delimiter: str, path: Path) -> list[list[str]]:
466
+ """Parse CSV content into rows.
467
+
468
+ Args:
469
+ content: CSV file content.
470
+ delimiter: Column delimiter.
471
+ path: Path to CSV file (for error reporting).
472
+
473
+ Returns:
474
+ List of rows, where each row is a list of cell values.
475
+
476
+ Raises:
477
+ FormatError: If CSV is empty.
478
+ """
479
+ reader = csv.reader(StringIO(content), delimiter=delimiter)
480
+ rows = list(reader)
481
+
482
+ if not rows:
483
+ raise FormatError("CSV file is empty", file_path=str(path))
484
+
485
+ return rows
486
+
487
+
488
+ def _detect_header(rows: list[list[str]]) -> tuple[list[str] | None, int]:
489
+ """Detect if first row is a header row.
490
+
491
+ Args:
492
+ rows: Parsed CSV rows.
493
+
494
+ Returns:
495
+ Tuple of (header row, data start index).
496
+ If no header detected, returns (None, 0).
497
+ """
498
+ first_row = rows[0]
499
+
500
+ # Check if first row contains non-numeric values (indicates header)
501
+ for cell in first_row:
502
+ try:
503
+ float(cell)
504
+ except ValueError:
505
+ if cell.strip(): # Non-empty, non-numeric
506
+ return [cell.strip() for cell in first_row], 1
507
+
508
+ return None, 0
509
+
510
+
511
+ def _determine_column_indices(
512
+ header: list[str] | None,
513
+ time_column: str | int | None,
514
+ voltage_column: str | int | None,
515
+ ) -> tuple[int | None, int | None]:
516
+ """Determine column indices for time and voltage data.
517
+
518
+ Args:
519
+ header: Header row if detected, None otherwise.
520
+ time_column: User-specified time column name or index.
521
+ voltage_column: User-specified voltage column name or index.
522
+
523
+ Returns:
524
+ Tuple of (time_index, voltage_index).
525
+ """
526
+ if header:
527
+ time_idx = _find_time_column_index(header, time_column)
528
+ voltage_idx = _find_voltage_column_index(header, voltage_column, time_idx)
529
+ else:
530
+ time_idx = _get_index_or_default(time_column, 0)
531
+ voltage_idx = _get_index_or_default(voltage_column, 1)
532
+
533
+ return time_idx, voltage_idx
534
+
535
+
536
+ def _find_time_column_index(header: list[str], time_column: str | int | None) -> int | None:
537
+ """Find time column index from header.
538
+
539
+ Args:
540
+ header: Header row.
541
+ time_column: User-specified time column name or index.
542
+
543
+ Returns:
544
+ Time column index, or None if not found.
545
+ """
546
+ if time_column is not None:
547
+ if isinstance(time_column, int):
548
+ return time_column
549
+ if time_column in header:
550
+ return header.index(time_column)
551
+ else:
552
+ # Auto-detect
553
+ for i, col in enumerate(header):
554
+ if col.lower() in [n.lower() for n in TIME_COLUMN_NAMES]:
555
+ return i
556
+
557
+ return None
558
+
559
+
560
+ def _find_voltage_column_index(
561
+ header: list[str], voltage_column: str | int | None, time_idx: int | None
562
+ ) -> int | None:
563
+ """Find voltage column index from header.
564
+
565
+ Args:
566
+ header: Header row.
567
+ voltage_column: User-specified voltage column name or index.
568
+ time_idx: Time column index (to exclude from voltage search).
569
+
570
+ Returns:
571
+ Voltage column index.
572
+ """
573
+ if voltage_column is not None:
574
+ if isinstance(voltage_column, int):
575
+ return voltage_column
576
+ if voltage_column in header:
577
+ return header.index(voltage_column)
578
+ else:
579
+ # Auto-detect (first column that's not time)
580
+ for i, col in enumerate(header):
581
+ if i == time_idx:
582
+ continue
583
+ if col.lower() in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
584
+ return i
585
+
586
+ # Default: column 1 if time is 0, otherwise column 0
587
+ return 1 if time_idx == 0 else 0
588
+
589
+ return None
590
+
591
+
592
+ def _get_index_or_default(column: str | int | None, default: int) -> int:
593
+ """Get column index or return default.
594
+
595
+ Args:
596
+ column: User-specified column (int or string).
597
+ default: Default index if column is not an int.
598
+
599
+ Returns:
600
+ Column index.
601
+ """
602
+ if isinstance(column, int):
603
+ return column
604
+ return default
605
+
606
+
607
+ def _extract_data_from_rows(
608
+ rows: list[list[str]],
609
+ data_start: int,
610
+ time_idx: int | None,
611
+ voltage_idx: int | None,
612
+ path: Path,
613
+ ) -> tuple[list[float], list[float]]:
614
+ """Extract numeric data from CSV rows.
615
+
616
+ Args:
617
+ rows: Parsed CSV rows.
618
+ data_start: Index of first data row.
619
+ time_idx: Time column index.
620
+ voltage_idx: Voltage column index.
621
+ path: Path to CSV file (for error reporting).
622
+
623
+ Returns:
624
+ Tuple of (time_data, voltage_data) lists.
625
+
626
+ Raises:
627
+ FormatError: If no valid voltage data found.
628
+ """
629
+ time_data: list[float] = []
630
+ voltage_data: list[float] = []
631
+
632
+ for row in rows[data_start:]:
633
+ if not row:
634
+ continue
635
+
636
+ try:
637
+ if voltage_idx is not None and voltage_idx < len(row):
638
+ voltage_data.append(float(row[voltage_idx]))
639
+ if time_idx is not None and time_idx < len(row):
640
+ time_data.append(float(row[time_idx]))
641
+ except (ValueError, IndexError):
642
+ continue # Skip malformed rows
643
+
644
+ if not voltage_data:
645
+ raise FormatError(
646
+ "No valid voltage data found in CSV",
647
+ file_path=str(path),
648
+ )
649
+
650
+ return time_data, voltage_data
651
+
652
+
653
+ def _compute_sample_rate(sample_rate: float | None, time_data: list[float]) -> float:
654
+ """Compute sample rate from time data or use override.
655
+
656
+ Args:
657
+ sample_rate: User-specified sample rate (None to compute).
658
+ time_data: List of time values.
659
+
660
+ Returns:
661
+ Sample rate in Hz. Defaults to 1 MHz if cannot be computed.
662
+ """
663
+ if sample_rate is not None:
664
+ return sample_rate
665
+
666
+ if time_data:
667
+ time_arr = np.array(time_data, dtype=np.float64)
668
+ if len(time_arr) > 1:
669
+ dt = float(np.median(np.diff(time_arr)))
670
+ if dt > 0:
671
+ return 1.0 / dt
672
+
673
+ return 1e6 # Default to 1 MSa/s
674
+
675
+
676
+ def _get_channel_name(header: list[str] | None, voltage_idx: int | None) -> str:
677
+ """Get channel name from header or use default.
678
+
679
+ Args:
680
+ header: Header row if available.
681
+ voltage_idx: Voltage column index.
682
+
683
+ Returns:
684
+ Channel name string.
685
+ """
686
+ if header and voltage_idx is not None and voltage_idx < len(header):
687
+ return header[voltage_idx]
688
+ return "CH1"
689
+
690
+
448
691
  def _detect_delimiter(path: Path, encoding: str) -> str:
449
692
  """Detect the delimiter used in a CSV file."""
450
693
  try:
451
- with open(path, encoding=encoding) as f:
694
+ with open(path, encoding=encoding, buffering=65536) as f:
452
695
  sample = f.read(4096)
453
696
  return _detect_delimiter_from_content(sample)
454
697
  except Exception: