oscura 0.0.1__py3-none-any.whl → 0.1.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 (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,711 @@
1
+ """Tektronix WFM file loader.
2
+
3
+ This module provides loading of Tektronix oscilloscope .wfm files
4
+ using the tm_data_types library when available, with fallback to
5
+ basic binary parsing.
6
+
7
+ Supports both analog and digital waveforms from Tektronix oscilloscopes
8
+ including mixed-signal instruments.
9
+
10
+
11
+ Example:
12
+ >>> from oscura.loaders.tektronix import load_tektronix_wfm
13
+ >>> trace = load_tektronix_wfm("TEK00001.wfm")
14
+ >>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
15
+
16
+ >>> # Load digital waveform
17
+ >>> digital_trace = load_tektronix_wfm("digital_capture.wfm")
18
+ >>> print(f"Digital trace: {len(digital_trace.data)} samples")
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import contextlib
24
+ import logging
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING, Any, Union
27
+
28
+ import numpy as np
29
+ from numpy.typing import NDArray
30
+
31
+ from oscura.core.exceptions import FormatError, LoaderError
32
+ from oscura.core.types import DigitalTrace, IQTrace, TraceMetadata, WaveformTrace
33
+
34
+ if TYPE_CHECKING:
35
+ from os import PathLike
36
+
37
+ # Logger for debug output
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # Try to import tm_data_types for full Tektronix support
41
+ try:
42
+ import tm_data_types # type: ignore[import-untyped, import-not-found]
43
+
44
+ TM_DATA_TYPES_AVAILABLE = True
45
+ except ImportError:
46
+ TM_DATA_TYPES_AVAILABLE = False
47
+
48
+ # Type alias for return type
49
+ TektronixTrace = Union[WaveformTrace, DigitalTrace, IQTrace]
50
+
51
+ # Minimum file size for valid WFM files
52
+ MIN_WFM_FILE_SIZE = 512
53
+
54
+
55
+ def load_tektronix_wfm(
56
+ path: str | PathLike[str],
57
+ *,
58
+ channel: int = 0,
59
+ ) -> TektronixTrace:
60
+ """Load a Tektronix oscilloscope WFM file.
61
+
62
+ Extracts waveform data and metadata from Tektronix .wfm files.
63
+ Uses the tm_data_types library when available for full support,
64
+ otherwise falls back to basic binary parsing.
65
+
66
+ Supports both analog and digital waveforms from mixed-signal
67
+ oscilloscopes (channels 5-8 are typically digital on MSO scopes).
68
+
69
+ Args:
70
+ path: Path to the Tektronix .wfm file.
71
+ channel: Channel index for multi-channel files (default: 0).
72
+
73
+ Returns:
74
+ WaveformTrace for analog waveforms or DigitalTrace for digital waveforms.
75
+
76
+ Raises:
77
+ LoaderError: If the file cannot be loaded.
78
+ FormatError: If the file is not a valid Tektronix WFM file.
79
+
80
+ Example:
81
+ >>> trace = load_tektronix_wfm("TEK00001.wfm")
82
+ >>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
83
+ >>> print(f"Channel: {trace.metadata.channel_name}")
84
+
85
+ >>> # Check trace type
86
+ >>> if isinstance(trace, DigitalTrace):
87
+ ... print("Digital waveform loaded")
88
+ """
89
+ path = Path(path)
90
+
91
+ if not path.exists():
92
+ raise LoaderError(
93
+ "File not found",
94
+ file_path=str(path),
95
+ )
96
+
97
+ # File size validation
98
+ file_size = path.stat().st_size
99
+ if file_size < MIN_WFM_FILE_SIZE:
100
+ raise FormatError(
101
+ f"File too small ({file_size} bytes), may be empty or corrupted",
102
+ file_path=str(path),
103
+ expected=f"At least {MIN_WFM_FILE_SIZE} bytes",
104
+ got=f"{file_size} bytes",
105
+ )
106
+
107
+ logger.debug("Loading Tektronix WFM file: %s (%d bytes)", path, file_size)
108
+
109
+ if TM_DATA_TYPES_AVAILABLE:
110
+ return _load_with_tm_data_types(path, channel=channel)
111
+ else:
112
+ return _load_basic(path, channel=channel)
113
+
114
+
115
+ def _load_with_tm_data_types(
116
+ path: Path,
117
+ *,
118
+ channel: int = 0,
119
+ ) -> TektronixTrace:
120
+ """Load Tektronix WFM using tm_data_types library.
121
+
122
+ Handles multiple waveform formats:
123
+ - Multi-channel container with analog_waveforms
124
+ - Direct AnalogWaveform with y_axis_values
125
+ - Legacy format with y_data
126
+ - DigitalWaveform with y_axis_byte_values
127
+
128
+ Args:
129
+ path: Path to the WFM file.
130
+ channel: Channel index.
131
+
132
+ Returns:
133
+ WaveformTrace for analog data or DigitalTrace for digital data.
134
+
135
+ Raises:
136
+ FormatError: If the file format is not recognized or invalid.
137
+ LoaderError: If the file cannot be loaded.
138
+ """
139
+ try:
140
+ # Use tm_data_types to read the file
141
+ wfm = tm_data_types.read_file(str(path))
142
+
143
+ # Log object information for debugging
144
+ wfm_type = type(wfm).__name__
145
+ available_attrs = [attr for attr in dir(wfm) if not attr.startswith("_")]
146
+ logger.debug("WFM object type: %s", wfm_type)
147
+ logger.debug("WFM attributes: %s", available_attrs[:20]) # First 20 attrs
148
+
149
+ # Check for digital waveforms attribute
150
+ if hasattr(wfm, "digital_waveforms"):
151
+ logger.debug("Digital waveforms found: %d", len(wfm.digital_waveforms))
152
+
153
+ # Extract waveform data - handle different file formats
154
+ # Path 1: Multi-channel container format (wrapped analog)
155
+ if hasattr(wfm, "analog_waveforms") and len(wfm.analog_waveforms) > channel:
156
+ logger.debug("Loading from analog_waveforms[%d]", channel)
157
+ waveform = wfm.analog_waveforms[channel]
158
+ data = np.array(waveform.y_data, dtype=np.float64)
159
+ sample_rate = 1.0 / waveform.x_increment if waveform.x_increment > 0 else 1e6
160
+ vertical_scale = getattr(waveform, "y_scale", None)
161
+ vertical_offset = getattr(waveform, "y_offset", None)
162
+ channel_name = getattr(waveform, "name", f"CH{channel + 1}")
163
+
164
+ return _build_waveform_trace(
165
+ data=data,
166
+ sample_rate=sample_rate,
167
+ vertical_scale=vertical_scale,
168
+ vertical_offset=vertical_offset,
169
+ channel_name=channel_name,
170
+ path=path,
171
+ wfm=wfm,
172
+ )
173
+
174
+ # Path 2: Direct AnalogWaveform format (tm_data_types 0.3.0+)
175
+ elif hasattr(wfm, "y_axis_values") and wfm_type == "AnalogWaveform":
176
+ logger.debug("Loading direct AnalogWaveform with y_axis_values")
177
+ # Extract raw integer values
178
+ y_raw = np.array(wfm.y_axis_values, dtype=np.float64)
179
+ # Reconstruct voltage values using offset and spacing
180
+ y_spacing = float(wfm.y_axis_spacing) if wfm.y_axis_spacing else 1.0
181
+ y_offset = float(wfm.y_axis_offset) if wfm.y_axis_offset else 0.0
182
+ data = y_raw * y_spacing + y_offset
183
+
184
+ x_spacing = float(wfm.x_axis_spacing) if wfm.x_axis_spacing else 1e-6
185
+ sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
186
+ vertical_offset = y_offset
187
+ channel_name = (
188
+ wfm.source_name
189
+ if hasattr(wfm, "source_name") and wfm.source_name
190
+ else f"CH{channel + 1}"
191
+ )
192
+
193
+ return _build_waveform_trace(
194
+ data=data,
195
+ sample_rate=sample_rate,
196
+ vertical_scale=None,
197
+ vertical_offset=vertical_offset,
198
+ channel_name=channel_name,
199
+ path=path,
200
+ wfm=wfm,
201
+ )
202
+
203
+ # Path 3: DigitalWaveform format
204
+ elif wfm_type == "DigitalWaveform" or hasattr(wfm, "y_axis_byte_values"):
205
+ logger.debug("Loading DigitalWaveform with y_axis_byte_values")
206
+ return _load_digital_waveform(wfm, path, channel)
207
+
208
+ # Path 4: Legacy single channel format with y_data
209
+ elif hasattr(wfm, "y_data"):
210
+ logger.debug("Loading legacy format with y_data")
211
+ data = np.array(wfm.y_data, dtype=np.float64)
212
+ x_increment = getattr(wfm, "x_increment", 1e-6)
213
+ sample_rate = 1.0 / x_increment if x_increment > 0 else 1e6
214
+ vertical_scale = getattr(wfm, "y_scale", None)
215
+ vertical_offset = getattr(wfm, "y_offset", None)
216
+ channel_name = getattr(wfm, "name", "CH1")
217
+
218
+ return _build_waveform_trace(
219
+ data=data,
220
+ sample_rate=sample_rate,
221
+ vertical_scale=vertical_scale,
222
+ vertical_offset=vertical_offset,
223
+ channel_name=channel_name,
224
+ path=path,
225
+ wfm=wfm,
226
+ )
227
+
228
+ # Path 5: Check for wrapped digital waveforms
229
+ elif hasattr(wfm, "digital_waveforms") and len(wfm.digital_waveforms) > channel:
230
+ logger.debug("Loading from digital_waveforms[%d]", channel)
231
+ digital_wfm = wfm.digital_waveforms[channel]
232
+ return _load_digital_waveform(digital_wfm, path, channel)
233
+
234
+ # Path 6: IQWaveform format (I/Q data)
235
+ elif wfm_type == "IQWaveform" or (
236
+ hasattr(wfm, "i_axis_values") and hasattr(wfm, "q_axis_values")
237
+ ):
238
+ logger.debug("Loading IQWaveform with i_axis_values and q_axis_values")
239
+ return _load_iq_waveform(wfm, path)
240
+
241
+ # No recognized format - provide detailed error
242
+ raise FormatError(
243
+ f"No waveform data found. Object type: {wfm_type}. "
244
+ f"Available attributes: {', '.join(available_attrs[:15])}",
245
+ file_path=str(path),
246
+ expected="Tektronix analog or digital waveform data",
247
+ fix_hint=(
248
+ "This file may use an unsupported Tektronix format variant. "
249
+ "Check that tm_data_types is up to date: pip install -U tm_data_types"
250
+ ),
251
+ )
252
+
253
+ except Exception as e:
254
+ if isinstance(e, LoaderError | FormatError):
255
+ raise
256
+ raise LoaderError(
257
+ "Failed to load Tektronix WFM file",
258
+ file_path=str(path),
259
+ details=str(e),
260
+ fix_hint="Ensure the file is a valid Tektronix WFM format.",
261
+ ) from e
262
+
263
+
264
+ def _build_waveform_trace(
265
+ data: NDArray[np.float64],
266
+ sample_rate: float,
267
+ vertical_scale: float | None,
268
+ vertical_offset: float | None,
269
+ channel_name: str,
270
+ path: Path,
271
+ wfm: Any,
272
+ ) -> WaveformTrace:
273
+ """Build a WaveformTrace from extracted data.
274
+
275
+ Args:
276
+ data: Waveform sample data.
277
+ sample_rate: Sample rate in Hz.
278
+ vertical_scale: Vertical scale in volts/div.
279
+ vertical_offset: Vertical offset in volts.
280
+ channel_name: Channel name.
281
+ path: Source file path.
282
+ wfm: Original waveform object for trigger info extraction.
283
+
284
+ Returns:
285
+ Constructed WaveformTrace.
286
+ """
287
+ # Extract acquisition time if available
288
+ acquisition_time = None
289
+ if hasattr(wfm, "date_time"):
290
+ with contextlib.suppress(ValueError, AttributeError):
291
+ acquisition_time = wfm.date_time
292
+
293
+ metadata = TraceMetadata(
294
+ sample_rate=sample_rate,
295
+ vertical_scale=vertical_scale,
296
+ vertical_offset=vertical_offset,
297
+ acquisition_time=acquisition_time,
298
+ source_file=str(path),
299
+ channel_name=channel_name,
300
+ trigger_info=_extract_trigger_info(wfm),
301
+ )
302
+
303
+ return WaveformTrace(data=data, metadata=metadata)
304
+
305
+
306
+ def _load_digital_waveform(
307
+ wfm: Any,
308
+ path: Path,
309
+ channel: int = 0,
310
+ ) -> DigitalTrace:
311
+ """Load a digital waveform from tm_data_types object.
312
+
313
+ Handles DigitalWaveform objects with y_axis_byte_values attribute,
314
+ commonly used for digital/logic analyzer captures on mixed-signal
315
+ oscilloscopes.
316
+
317
+ Args:
318
+ wfm: DigitalWaveform object from tm_data_types.
319
+ path: Source file path.
320
+ channel: Channel index.
321
+
322
+ Returns:
323
+ DigitalTrace with boolean sample data.
324
+
325
+ Raises:
326
+ FormatError: If DigitalWaveform has no recognized data attribute.
327
+ """
328
+ logger.debug("Extracting digital waveform data")
329
+
330
+ # Extract digital sample data
331
+ if hasattr(wfm, "y_axis_byte_values"):
332
+ # y_axis_byte_values contains byte-level digital data
333
+ raw_bytes = wfm.y_axis_byte_values
334
+ # Convert bytes to numpy array and interpret as boolean
335
+ # Each byte typically represents a logic state (0 = low, non-zero = high)
336
+ byte_array = np.frombuffer(bytes(raw_bytes), dtype=np.uint8)
337
+ data = byte_array.astype(np.bool_)
338
+ logger.debug("Loaded %d digital samples from y_axis_byte_values", len(data))
339
+ elif hasattr(wfm, "samples"):
340
+ # Alternative attribute name
341
+ data = np.array(wfm.samples, dtype=np.bool_)
342
+ logger.debug("Loaded %d digital samples from samples", len(data))
343
+ else:
344
+ # Try to find any data attribute
345
+ for attr in ["data", "digital_data", "logic_data"]:
346
+ if hasattr(wfm, attr):
347
+ data = np.array(getattr(wfm, attr), dtype=np.bool_)
348
+ logger.debug("Loaded %d digital samples from %s", len(data), attr)
349
+ break
350
+ else:
351
+ raise FormatError(
352
+ "DigitalWaveform has no recognized data attribute",
353
+ file_path=str(path),
354
+ expected="y_axis_byte_values, samples, or data attribute",
355
+ )
356
+
357
+ # Extract timing information
358
+ x_spacing = 1e-6 # Default 1 microsecond per sample
359
+ if hasattr(wfm, "x_axis_spacing") and wfm.x_axis_spacing:
360
+ x_spacing = float(wfm.x_axis_spacing)
361
+ elif hasattr(wfm, "horizontal_spacing") and wfm.horizontal_spacing:
362
+ x_spacing = float(wfm.horizontal_spacing)
363
+
364
+ sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
365
+
366
+ # Extract channel name
367
+ channel_name = f"D{channel + 1}" # Digital channels typically labeled D1, D2, etc.
368
+ if hasattr(wfm, "source_name") and wfm.source_name:
369
+ channel_name = wfm.source_name
370
+ elif hasattr(wfm, "name") and wfm.name:
371
+ channel_name = wfm.name
372
+
373
+ # Build metadata
374
+ metadata = TraceMetadata(
375
+ sample_rate=sample_rate,
376
+ source_file=str(path),
377
+ channel_name=channel_name,
378
+ )
379
+
380
+ # Extract edge information if available
381
+ edges = None
382
+ if hasattr(wfm, "edges"):
383
+ try:
384
+ edges = [(float(ts), bool(is_rising)) for ts, is_rising in wfm.edges]
385
+ except (TypeError, ValueError):
386
+ pass
387
+
388
+ return DigitalTrace(data=data, metadata=metadata, edges=edges)
389
+
390
+
391
+ def _load_iq_waveform(
392
+ wfm: Any,
393
+ path: Path,
394
+ ) -> IQTrace:
395
+ """Load I/Q waveform data from tm_data_types IQWaveform object.
396
+
397
+ Handles IQWaveform objects with i_axis_values and q_axis_values,
398
+ commonly used for RF and software-defined radio captures.
399
+
400
+ Args:
401
+ wfm: IQWaveform object from tm_data_types.
402
+ path: Source file path.
403
+
404
+ Returns:
405
+ IQTrace with I and Q component data.
406
+ """
407
+ logger.debug("Extracting I/Q waveform data")
408
+
409
+ # Extract I/Q data
410
+ i_data = np.array(wfm.i_axis_values, dtype=np.float64)
411
+ q_data = np.array(wfm.q_axis_values, dtype=np.float64)
412
+
413
+ logger.debug("Loaded %d I/Q samples", len(i_data))
414
+
415
+ # Apply scaling if available
416
+ if hasattr(wfm, "iq_axis_spacing") and wfm.iq_axis_spacing:
417
+ iq_spacing = float(wfm.iq_axis_spacing)
418
+ i_data = i_data * iq_spacing
419
+ q_data = q_data * iq_spacing
420
+ if hasattr(wfm, "iq_axis_offset") and wfm.iq_axis_offset:
421
+ iq_offset = float(wfm.iq_axis_offset)
422
+ i_data = i_data + iq_offset
423
+ q_data = q_data + iq_offset
424
+
425
+ # Extract timing information
426
+ x_spacing = 1e-6 # Default 1 microsecond per sample
427
+ if hasattr(wfm, "x_axis_spacing") and wfm.x_axis_spacing:
428
+ x_spacing = float(wfm.x_axis_spacing)
429
+
430
+ sample_rate = 1.0 / x_spacing if x_spacing > 0 else 1e6
431
+
432
+ # Extract channel name
433
+ channel_name = "IQ1"
434
+ if hasattr(wfm, "source_name") and wfm.source_name:
435
+ channel_name = wfm.source_name
436
+
437
+ # Build metadata
438
+ metadata = TraceMetadata(
439
+ sample_rate=sample_rate,
440
+ source_file=str(path),
441
+ channel_name=channel_name,
442
+ )
443
+
444
+ return IQTrace(i_data=i_data, q_data=q_data, metadata=metadata)
445
+
446
+
447
+ def _load_basic(
448
+ path: Path,
449
+ *,
450
+ channel: int = 0,
451
+ ) -> WaveformTrace:
452
+ """Basic Tektronix WFM loader without tm_data_types.
453
+
454
+ This is a simplified loader that reads the basic waveform data
455
+ from Tektronix WFM files, including support for WFM#003 format.
456
+ For full feature support, install tm_data_types.
457
+
458
+ Args:
459
+ path: Path to the WFM file.
460
+ channel: Channel index (ignored in basic mode).
461
+
462
+ Returns:
463
+ WaveformTrace with basic metadata.
464
+
465
+ Raises:
466
+ FormatError: If the file format is invalid or cannot be parsed.
467
+ LoaderError: If the file cannot be read.
468
+ """
469
+ try:
470
+ with open(path, "rb") as f:
471
+ # Read full file for format detection
472
+ file_data = f.read()
473
+
474
+ if len(file_data) < MIN_WFM_FILE_SIZE:
475
+ raise FormatError(
476
+ "File too small to be a valid Tektronix WFM",
477
+ file_path=str(path),
478
+ expected=f"At least {MIN_WFM_FILE_SIZE} bytes",
479
+ got=f"{len(file_data)} bytes",
480
+ )
481
+
482
+ # Detect WFM format version
483
+ if file_data[2:10] == b":WFM#003":
484
+ return _parse_wfm003(file_data, path, channel)
485
+ else:
486
+ # Legacy WFM format (older versions)
487
+ return _parse_wfm_legacy(file_data, path, channel)
488
+
489
+ except OSError as e:
490
+ raise LoaderError(
491
+ "Failed to read Tektronix WFM file",
492
+ file_path=str(path),
493
+ details=str(e),
494
+ ) from e
495
+ except Exception as e:
496
+ if isinstance(e, LoaderError | FormatError):
497
+ raise
498
+ raise LoaderError(
499
+ "Failed to parse Tektronix WFM file",
500
+ file_path=str(path),
501
+ details=str(e),
502
+ fix_hint="Install tm_data_types for full Tektronix support: pip install tm_data_types",
503
+ ) from e
504
+
505
+
506
+ def _parse_wfm003(
507
+ file_data: bytes,
508
+ path: Path,
509
+ channel: int = 0,
510
+ ) -> WaveformTrace:
511
+ """Parse Tektronix WFM#003 format files.
512
+
513
+ WFM#003 is a binary format used by Tektronix oscilloscopes.
514
+ The file structure consists of:
515
+ - Static file header (first ~80 bytes)
516
+ - Main waveform header (~838 bytes total)
517
+ - Waveform data (int16 samples)
518
+ - Optional metadata footer (tekmeta!)
519
+
520
+ Args:
521
+ file_data: Raw file bytes.
522
+ path: Path to file (for error messages).
523
+ channel: Channel index.
524
+
525
+ Returns:
526
+ WaveformTrace with extracted data and metadata.
527
+
528
+ Raises:
529
+ FormatError: If the file signature is invalid or no waveform data found.
530
+ """
531
+ import struct
532
+
533
+ # Validate signature
534
+ signature = file_data[2:10]
535
+ if signature != b":WFM#003":
536
+ raise FormatError(
537
+ "Invalid WFM#003 signature",
538
+ file_path=str(path),
539
+ expected=":WFM#003",
540
+ got=signature.decode("latin-1", errors="replace"),
541
+ )
542
+
543
+ # WFM#003 files have a fixed header size of 838 bytes
544
+ # This is consistent across all WFM#003 files
545
+ header_size = 838
546
+
547
+ # Find metadata footer (tekmeta!) if present
548
+ # This helps us determine where waveform data ends
549
+ footer_start = len(file_data)
550
+ if b"tekmeta!" in file_data:
551
+ footer_start = file_data.find(b"tekmeta!")
552
+
553
+ # Extract waveform data region
554
+ data_start = header_size
555
+ data_end = footer_start
556
+ waveform_bytes = file_data[data_start:data_end]
557
+
558
+ if len(waveform_bytes) < 2:
559
+ raise FormatError(
560
+ "No waveform data found in WFM#003 file",
561
+ file_path=str(path),
562
+ )
563
+
564
+ # WFM#003 data is stored as int16 (16-bit signed integers)
565
+ # Ensure we have an even number of bytes
566
+ if len(waveform_bytes) % 2 != 0:
567
+ waveform_bytes = waveform_bytes[:-1]
568
+
569
+ # Parse as int16 little-endian
570
+ data = np.frombuffer(waveform_bytes, dtype=np.int16).astype(np.float64)
571
+
572
+ # Try to extract metadata from header
573
+ sample_rate = 1e6 # Default 1 MSa/s
574
+ vertical_scale = None
575
+ vertical_offset = None
576
+ channel_name = f"CH{channel + 1}"
577
+
578
+ # Try to find sample interval in header
579
+ # The header contains doubles at various offsets
580
+ # Sample interval is typically found in the horizontal dimension info
581
+ try:
582
+ # Search for reasonable sample interval values (doubles in header)
583
+ for offset in range(16, min(header_size - 8, 200), 8):
584
+ val = struct.unpack("<d", file_data[offset : offset + 8])[0]
585
+ # Sample intervals are typically 1e-12 to 1e-3 (1ps to 1ms)
586
+ if 1e-12 < val < 1e-3:
587
+ sample_rate = 1.0 / val
588
+ break
589
+ except (struct.error, ZeroDivisionError):
590
+ pass
591
+
592
+ # Try to extract vertical scale/offset
593
+ # These are also doubles in the header
594
+ try:
595
+ # Vertical scale is often in a specific range
596
+ for offset in range(16, min(header_size - 8, 400), 8):
597
+ val = struct.unpack("<d", file_data[offset : offset + 8])[0]
598
+ # Vertical scale is typically 1e-9 to 1e3 (nV to kV range)
599
+ if 1e-9 < abs(val) < 1e3 and vertical_scale is None:
600
+ vertical_scale = abs(val)
601
+ # Offset might be nearby
602
+ next_val = struct.unpack("<d", file_data[offset + 8 : offset + 16])[0]
603
+ if abs(next_val) < 1e6:
604
+ vertical_offset = next_val
605
+ break
606
+ except struct.error:
607
+ pass
608
+
609
+ # Build metadata
610
+ metadata = TraceMetadata(
611
+ sample_rate=sample_rate,
612
+ vertical_scale=vertical_scale,
613
+ vertical_offset=vertical_offset,
614
+ source_file=str(path),
615
+ channel_name=channel_name,
616
+ )
617
+
618
+ return WaveformTrace(data=data, metadata=metadata)
619
+
620
+
621
+ def _parse_wfm_legacy(
622
+ file_data: bytes,
623
+ path: Path,
624
+ channel: int = 0,
625
+ ) -> WaveformTrace:
626
+ """Parse legacy Tektronix WFM formats (pre-WFM#003).
627
+
628
+ Args:
629
+ file_data: Raw file bytes.
630
+ path: Path to file (for error messages).
631
+ channel: Channel index.
632
+
633
+ Returns:
634
+ WaveformTrace with extracted data and metadata.
635
+
636
+ Raises:
637
+ FormatError: If no waveform data is found in the file.
638
+ """
639
+ import struct
640
+
641
+ # Default values
642
+ sample_rate = 1e6 # Default 1 MSa/s
643
+ vertical_scale = None
644
+ vertical_offset = None
645
+
646
+ # Try to find sample interval in header (little-endian double at offset ~40)
647
+ try:
648
+ # Sample interval is typically at offset 40 in many WFM versions
649
+ sample_interval_bytes = file_data[40:48]
650
+ if len(sample_interval_bytes) == 8:
651
+ sample_interval = struct.unpack("<d", sample_interval_bytes)[0]
652
+ if 0 < sample_interval < 1: # Sanity check
653
+ sample_rate = 1.0 / sample_interval
654
+ except (struct.error, ZeroDivisionError):
655
+ pass
656
+
657
+ # Read waveform data - assume rest of file is float32 samples after 512-byte header
658
+ header_size = 512
659
+ data_size = len(file_data) - header_size
660
+
661
+ if data_size <= 0:
662
+ raise FormatError(
663
+ "No waveform data in file",
664
+ file_path=str(path),
665
+ )
666
+
667
+ raw_data = file_data[header_size:]
668
+
669
+ # Try to interpret as float32 or int16
670
+ try:
671
+ # Try float32 first (common in Tektronix files)
672
+ data = np.frombuffer(raw_data, dtype=np.float32).astype(np.float64)
673
+ except ValueError:
674
+ # Fall back to int16
675
+ data = np.frombuffer(raw_data, dtype=np.int16).astype(np.float64)
676
+ data = data / 32768.0 # Normalize to -1 to 1
677
+
678
+ # Build metadata
679
+ metadata = TraceMetadata(
680
+ sample_rate=sample_rate,
681
+ vertical_scale=vertical_scale,
682
+ vertical_offset=vertical_offset,
683
+ source_file=str(path),
684
+ channel_name=f"CH{channel + 1}",
685
+ )
686
+
687
+ return WaveformTrace(data=data, metadata=metadata)
688
+
689
+
690
+ def _extract_trigger_info(wfm: Any) -> dict[str, Any] | None:
691
+ """Extract trigger information from Tektronix waveform object.
692
+
693
+ Args:
694
+ wfm: Tektronix waveform object from tm_data_types.
695
+
696
+ Returns:
697
+ Dictionary of trigger settings or None.
698
+ """
699
+ trigger_info: dict[str, Any] = {}
700
+
701
+ if hasattr(wfm, "trigger_level"):
702
+ trigger_info["level"] = wfm.trigger_level
703
+ if hasattr(wfm, "trigger_slope"):
704
+ trigger_info["slope"] = wfm.trigger_slope
705
+ if hasattr(wfm, "trigger_position"):
706
+ trigger_info["position"] = wfm.trigger_position
707
+
708
+ return trigger_info if trigger_info else None
709
+
710
+
711
+ __all__ = ["TektronixTrace", "load_tektronix_wfm"]