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,943 @@
1
+ """Waveform timing and amplitude measurements.
2
+
3
+ This module provides IEEE 181-2011 and IEEE 1057-2017 compliant
4
+ waveform measurements including rise/fall time, period, frequency,
5
+ amplitude, and RMS.
6
+
7
+
8
+ Example:
9
+ >>> from oscura.analyzers.waveform.measurements import rise_time, measure
10
+ >>> t_rise = rise_time(trace)
11
+ >>> results = measure(trace, parameters=["rise_time", "frequency"])
12
+
13
+ References:
14
+ IEEE 181-2011: Standard for Transitional Waveform Definitions
15
+ IEEE 1057-2017: Standard for Digitizing Waveform Recorders
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import TYPE_CHECKING, Any, Literal, overload
21
+
22
+ import numpy as np
23
+ from numpy import floating as np_floating
24
+
25
+ if TYPE_CHECKING:
26
+ from numpy.typing import NDArray
27
+
28
+ from oscura.core.types import WaveformTrace
29
+
30
+
31
+ def rise_time(
32
+ trace: WaveformTrace,
33
+ *,
34
+ ref_levels: tuple[float, float] = (0.1, 0.9),
35
+ ) -> float | np_floating[Any]:
36
+ """Measure rise time between reference levels.
37
+
38
+ Computes the time for a signal to transition from the lower
39
+ reference level to the upper reference level, per IEEE 181-2011.
40
+
41
+ Args:
42
+ trace: Input waveform trace.
43
+ ref_levels: Reference levels as fractions (0.0 to 1.0).
44
+ Default (0.1, 0.9) for 10%-90% rise time.
45
+
46
+ Returns:
47
+ Rise time in seconds, or np.nan if no valid rising edge found.
48
+
49
+ Example:
50
+ >>> t_rise = rise_time(trace)
51
+ >>> print(f"Rise time: {t_rise * 1e9:.2f} ns")
52
+
53
+ References:
54
+ IEEE 181-2011 Section 5.2
55
+ """
56
+ if len(trace.data) < 3:
57
+ return np.nan
58
+
59
+ data = trace.data
60
+ low, high = _find_levels(data)
61
+ amplitude = high - low
62
+
63
+ if amplitude <= 0:
64
+ return np.nan
65
+
66
+ # Calculate reference voltages
67
+ low_ref = low + ref_levels[0] * amplitude
68
+ high_ref = low + ref_levels[1] * amplitude
69
+
70
+ # Find rising edge: where signal crosses from below low_ref to above high_ref
71
+ sample_period = trace.metadata.time_base
72
+
73
+ # Find first crossing of low reference (going up)
74
+ below_low = data < low_ref
75
+ above_low = data >= low_ref
76
+
77
+ # Find transitions from below to above low_ref
78
+ transitions = np.where(below_low[:-1] & above_low[1:])[0]
79
+
80
+ if len(transitions) == 0:
81
+ return np.nan
82
+
83
+ best_rise_time: float | np_floating[Any] = np.nan
84
+
85
+ for start_idx in transitions:
86
+ # Find where signal crosses high reference
87
+ remaining = data[start_idx:]
88
+ above_high = remaining >= high_ref
89
+
90
+ if not np.any(above_high):
91
+ continue
92
+
93
+ end_offset = np.argmax(above_high)
94
+ end_idx = start_idx + end_offset
95
+
96
+ # Ensure monotonic rise (no dips)
97
+ segment = data[start_idx : end_idx + 1]
98
+ if len(segment) < 2:
99
+ continue
100
+
101
+ # Interpolate for sub-sample accuracy
102
+ t_low = _interpolate_crossing_time(data, start_idx, low_ref, sample_period, rising=True)
103
+ t_high = _interpolate_crossing_time(data, end_idx - 1, high_ref, sample_period, rising=True)
104
+
105
+ if t_low is not None and t_high is not None:
106
+ rt = t_high - t_low
107
+ if rt > 0 and (np.isnan(best_rise_time) or rt < best_rise_time):
108
+ best_rise_time = rt
109
+
110
+ return best_rise_time
111
+
112
+
113
+ def fall_time(
114
+ trace: WaveformTrace,
115
+ *,
116
+ ref_levels: tuple[float, float] = (0.9, 0.1),
117
+ ) -> float | np_floating[Any]:
118
+ """Measure fall time between reference levels.
119
+
120
+ Computes the time for a signal to transition from the upper
121
+ reference level to the lower reference level, per IEEE 181-2011.
122
+
123
+ Args:
124
+ trace: Input waveform trace.
125
+ ref_levels: Reference levels as fractions (0.0 to 1.0).
126
+ Default (0.9, 0.1) for 90%-10% fall time.
127
+
128
+ Returns:
129
+ Fall time in seconds, or np.nan if no valid falling edge found.
130
+
131
+ Example:
132
+ >>> t_fall = fall_time(trace)
133
+ >>> print(f"Fall time: {t_fall * 1e9:.2f} ns")
134
+
135
+ References:
136
+ IEEE 181-2011 Section 5.2
137
+ """
138
+ if len(trace.data) < 3:
139
+ return np.nan
140
+
141
+ data = trace.data
142
+ low, high = _find_levels(data)
143
+ amplitude = high - low
144
+
145
+ if amplitude <= 0:
146
+ return np.nan
147
+
148
+ # Calculate reference voltages (note: ref_levels[0] is the higher one for fall)
149
+ high_ref = low + ref_levels[0] * amplitude
150
+ low_ref = low + ref_levels[1] * amplitude
151
+
152
+ sample_period = trace.metadata.time_base
153
+
154
+ # Find where signal is above high reference
155
+ above_high = data >= high_ref
156
+ below_high = data < high_ref
157
+
158
+ # Find transitions from above to below high_ref
159
+ transitions = np.where(above_high[:-1] & below_high[1:])[0]
160
+
161
+ if len(transitions) == 0:
162
+ return np.nan
163
+
164
+ best_fall_time: float | np_floating[Any] = np.nan
165
+
166
+ for start_idx in transitions:
167
+ # Find where signal crosses low reference
168
+ remaining = data[start_idx:]
169
+ below_low = remaining <= low_ref
170
+
171
+ if not np.any(below_low):
172
+ continue
173
+
174
+ end_offset = np.argmax(below_low)
175
+ end_idx = start_idx + end_offset
176
+
177
+ segment = data[start_idx : end_idx + 1]
178
+ if len(segment) < 2:
179
+ continue
180
+
181
+ # Interpolate for sub-sample accuracy
182
+ t_high = _interpolate_crossing_time(data, start_idx, high_ref, sample_period, rising=False)
183
+ t_low = _interpolate_crossing_time(data, end_idx - 1, low_ref, sample_period, rising=False)
184
+
185
+ if t_high is not None and t_low is not None:
186
+ ft = t_low - t_high
187
+ if ft > 0 and (np.isnan(best_fall_time) or ft < best_fall_time):
188
+ best_fall_time = ft
189
+
190
+ return best_fall_time
191
+
192
+
193
+ @overload
194
+ def period(
195
+ trace: WaveformTrace,
196
+ *,
197
+ edge_type: Literal["rising", "falling"] = "rising",
198
+ return_all: Literal[False] = False,
199
+ ) -> float | np_floating[Any]: ...
200
+
201
+
202
+ @overload
203
+ def period(
204
+ trace: WaveformTrace,
205
+ *,
206
+ edge_type: Literal["rising", "falling"] = "rising",
207
+ return_all: Literal[True],
208
+ ) -> NDArray[np.float64]: ...
209
+
210
+
211
+ def period(
212
+ trace: WaveformTrace,
213
+ *,
214
+ edge_type: Literal["rising", "falling"] = "rising",
215
+ return_all: bool = False,
216
+ ) -> float | np_floating[Any] | NDArray[np.float64]:
217
+ """Measure signal period between consecutive edges.
218
+
219
+ Computes the time between consecutive rising or falling edges.
220
+
221
+ Args:
222
+ trace: Input waveform trace.
223
+ edge_type: Type of edges to use ("rising" or "falling").
224
+ return_all: If True, return array of all periods. If False, return mean.
225
+
226
+ Returns:
227
+ Period in seconds (mean if return_all=False), or array of periods.
228
+
229
+ Example:
230
+ >>> T = period(trace)
231
+ >>> print(f"Period: {T * 1e6:.2f} us")
232
+
233
+ References:
234
+ IEEE 181-2011 Section 5.3
235
+ """
236
+ edges = _find_edges(trace, edge_type)
237
+
238
+ if len(edges) < 2:
239
+ if return_all:
240
+ return np.array([], dtype=np.float64)
241
+ return np.nan
242
+
243
+ periods = np.diff(edges)
244
+
245
+ if return_all:
246
+ return periods
247
+ return float(np.mean(periods))
248
+
249
+
250
+ def frequency(
251
+ trace: WaveformTrace,
252
+ *,
253
+ method: Literal["edge", "fft"] = "edge",
254
+ ) -> float | np_floating[Any]:
255
+ """Measure signal frequency.
256
+
257
+ Computes frequency either from edge-to-edge period or using FFT.
258
+
259
+ Args:
260
+ trace: Input waveform trace.
261
+ method: Measurement method:
262
+ - "edge": 1/period from edge timing (default, more accurate)
263
+ - "fft": Peak of FFT magnitude spectrum
264
+
265
+ Returns:
266
+ Frequency in Hz, or np.nan if measurement not possible.
267
+
268
+ Raises:
269
+ ValueError: If method is not one of the supported types.
270
+
271
+ Example:
272
+ >>> f = frequency(trace)
273
+ >>> print(f"Frequency: {f / 1e6:.3f} MHz")
274
+
275
+ References:
276
+ IEEE 181-2011 Section 5.3
277
+ """
278
+ if method == "edge":
279
+ T = period(trace, edge_type="rising", return_all=False)
280
+ if np.isnan(T) or T <= 0:
281
+ return np.nan
282
+ return 1.0 / T
283
+
284
+ elif method == "fft":
285
+ if len(trace.data) < 16:
286
+ return np.nan
287
+
288
+ data = trace.data - np.mean(trace.data) # Remove DC
289
+ n = len(data)
290
+ fft_mag = np.abs(np.fft.rfft(data))
291
+
292
+ # Find peak (skip DC component)
293
+ peak_idx = np.argmax(fft_mag[1:]) + 1
294
+
295
+ # Calculate frequency
296
+ freq_resolution = trace.metadata.sample_rate / n
297
+ return float(peak_idx * freq_resolution)
298
+
299
+ else:
300
+ raise ValueError(f"Unknown method: {method}")
301
+
302
+
303
+ def duty_cycle(
304
+ trace: WaveformTrace,
305
+ *,
306
+ percentage: bool = False,
307
+ ) -> float | np_floating[Any]:
308
+ """Measure duty cycle.
309
+
310
+ Computes duty cycle as the ratio of positive pulse width to period.
311
+
312
+ Args:
313
+ trace: Input waveform trace.
314
+ percentage: If True, return as percentage (0-100). If False, return ratio (0-1).
315
+
316
+ Returns:
317
+ Duty cycle as ratio or percentage.
318
+
319
+ Example:
320
+ >>> dc = duty_cycle(trace, percentage=True)
321
+ >>> print(f"Duty cycle: {dc:.1f}%")
322
+
323
+ References:
324
+ IEEE 181-2011 Section 5.4
325
+ """
326
+ pw_pos = pulse_width(trace, polarity="positive", return_all=False)
327
+ T = period(trace, edge_type="rising", return_all=False)
328
+
329
+ if np.isnan(pw_pos) or np.isnan(T) or T <= 0:
330
+ return np.nan
331
+
332
+ dc = pw_pos / T
333
+
334
+ if percentage:
335
+ return dc * 100
336
+ return dc
337
+
338
+
339
+ @overload
340
+ def pulse_width(
341
+ trace: WaveformTrace,
342
+ *,
343
+ polarity: Literal["positive", "negative"] = "positive",
344
+ ref_level: float = 0.5,
345
+ return_all: Literal[False] = False,
346
+ ) -> float | np_floating[Any]: ...
347
+
348
+
349
+ @overload
350
+ def pulse_width(
351
+ trace: WaveformTrace,
352
+ *,
353
+ polarity: Literal["positive", "negative"] = "positive",
354
+ ref_level: float = 0.5,
355
+ return_all: Literal[True],
356
+ ) -> NDArray[np.float64]: ...
357
+
358
+
359
+ def pulse_width(
360
+ trace: WaveformTrace,
361
+ *,
362
+ polarity: Literal["positive", "negative"] = "positive",
363
+ ref_level: float = 0.5,
364
+ return_all: bool = False,
365
+ ) -> float | np_floating[Any] | NDArray[np.float64]:
366
+ """Measure pulse width.
367
+
368
+ Computes positive or negative pulse width at the specified reference level.
369
+
370
+ Args:
371
+ trace: Input waveform trace.
372
+ polarity: "positive" for high pulses, "negative" for low pulses.
373
+ ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
374
+ return_all: If True, return array of all widths. If False, return mean.
375
+
376
+ Returns:
377
+ Pulse width in seconds.
378
+
379
+ Example:
380
+ >>> pw = pulse_width(trace, polarity="positive")
381
+ >>> print(f"Pulse width: {pw * 1e6:.2f} us")
382
+
383
+ References:
384
+ IEEE 181-2011 Section 5.4
385
+ """
386
+ rising_edges = _find_edges(trace, "rising", ref_level)
387
+ falling_edges = _find_edges(trace, "falling", ref_level)
388
+
389
+ if len(rising_edges) == 0 or len(falling_edges) == 0:
390
+ if return_all:
391
+ return np.array([], dtype=np.float64)
392
+ return np.nan
393
+
394
+ widths: list[float] = []
395
+
396
+ if polarity == "positive":
397
+ # Rising to falling
398
+ for r in rising_edges:
399
+ # Find next falling edge after this rising edge
400
+ next_falling = falling_edges[falling_edges > r]
401
+ if len(next_falling) > 0:
402
+ widths.append(next_falling[0] - r)
403
+ else:
404
+ # Falling to rising
405
+ for f in falling_edges:
406
+ # Find next rising edge after this falling edge
407
+ next_rising = rising_edges[rising_edges > f]
408
+ if len(next_rising) > 0:
409
+ widths.append(next_rising[0] - f)
410
+
411
+ if len(widths) == 0:
412
+ if return_all:
413
+ return np.array([], dtype=np.float64)
414
+ return np.nan
415
+
416
+ widths_arr = np.array(widths, dtype=np.float64)
417
+
418
+ if return_all:
419
+ return widths_arr
420
+ return float(np.mean(widths_arr))
421
+
422
+
423
+ def overshoot(trace: WaveformTrace) -> float | np_floating[Any]:
424
+ """Measure overshoot percentage.
425
+
426
+ Computes overshoot as (max - high) / amplitude * 100%.
427
+
428
+ Args:
429
+ trace: Input waveform trace.
430
+
431
+ Returns:
432
+ Overshoot as percentage, or np.nan if not applicable.
433
+
434
+ Example:
435
+ >>> os = overshoot(trace)
436
+ >>> print(f"Overshoot: {os:.1f}%")
437
+
438
+ References:
439
+ IEEE 181-2011 Section 5.5
440
+ """
441
+ if len(trace.data) < 3:
442
+ return np.nan
443
+
444
+ data = trace.data
445
+ low, high = _find_levels(data)
446
+ amplitude = high - low
447
+
448
+ if amplitude <= 0:
449
+ return np.nan
450
+
451
+ max_val = np.max(data)
452
+
453
+ if max_val <= high:
454
+ return 0.0
455
+
456
+ return float((max_val - high) / amplitude * 100)
457
+
458
+
459
+ def undershoot(trace: WaveformTrace) -> float | np_floating[Any]:
460
+ """Measure undershoot percentage.
461
+
462
+ Computes undershoot as (low - min) / amplitude * 100%.
463
+
464
+ Args:
465
+ trace: Input waveform trace.
466
+
467
+ Returns:
468
+ Undershoot as percentage, or np.nan if not applicable.
469
+
470
+ Example:
471
+ >>> us = undershoot(trace)
472
+ >>> print(f"Undershoot: {us:.1f}%")
473
+
474
+ References:
475
+ IEEE 181-2011 Section 5.5
476
+ """
477
+ if len(trace.data) < 3:
478
+ return np.nan
479
+
480
+ data = trace.data
481
+ low, high = _find_levels(data)
482
+ amplitude = high - low
483
+
484
+ if amplitude <= 0:
485
+ return np.nan
486
+
487
+ min_val = np.min(data)
488
+
489
+ if min_val >= low:
490
+ return 0.0
491
+
492
+ return float((low - min_val) / amplitude * 100)
493
+
494
+
495
+ def preshoot(
496
+ trace: WaveformTrace,
497
+ *,
498
+ edge_type: Literal["rising", "falling"] = "rising",
499
+ ) -> float | np_floating[Any]:
500
+ """Measure preshoot percentage.
501
+
502
+ Computes preshoot before transitions as percentage of amplitude.
503
+
504
+ Args:
505
+ trace: Input waveform trace.
506
+ edge_type: Type of edge to analyze ("rising" or "falling").
507
+
508
+ Returns:
509
+ Preshoot as percentage, or np.nan if not applicable.
510
+
511
+ Example:
512
+ >>> ps = preshoot(trace)
513
+ >>> print(f"Preshoot: {ps:.1f}%")
514
+
515
+ References:
516
+ IEEE 181-2011 Section 5.5
517
+ """
518
+ if len(trace.data) < 10:
519
+ return np.nan
520
+
521
+ # Convert memoryview to ndarray if needed
522
+ data = np.asarray(trace.data)
523
+ low, high = _find_levels(data)
524
+ amplitude = high - low
525
+
526
+ if amplitude <= 0:
527
+ return np.nan
528
+
529
+ # Find edge crossings at 50%
530
+ mid = (low + high) / 2
531
+
532
+ if edge_type == "rising":
533
+ # Look for minimum before rising edge that goes below low level
534
+ crossings = np.where((data[:-1] < mid) & (data[1:] >= mid))[0]
535
+ if len(crossings) == 0:
536
+ return np.nan
537
+
538
+ max_preshoot = 0.0
539
+ for idx in crossings:
540
+ # Look at samples before crossing
541
+ pre_samples = max(0, idx - 10)
542
+ pre_region = data[pre_samples:idx]
543
+ if len(pre_region) > 0:
544
+ min_pre = np.min(pre_region)
545
+ if min_pre < low:
546
+ preshoot_val = (low - min_pre) / amplitude * 100
547
+ max_preshoot = max(max_preshoot, preshoot_val)
548
+
549
+ return max_preshoot
550
+
551
+ else: # falling
552
+ crossings = np.where((data[:-1] >= mid) & (data[1:] < mid))[0]
553
+ if len(crossings) == 0:
554
+ return np.nan
555
+
556
+ max_preshoot = 0.0
557
+ for idx in crossings:
558
+ pre_samples = max(0, idx - 10)
559
+ pre_region = data[pre_samples:idx]
560
+ if len(pre_region) > 0:
561
+ max_pre = np.max(pre_region)
562
+ if max_pre > high:
563
+ preshoot_val = (max_pre - high) / amplitude * 100
564
+ max_preshoot = max(max_preshoot, preshoot_val)
565
+
566
+ return max_preshoot
567
+
568
+
569
+ def amplitude(trace: WaveformTrace) -> float | np_floating[Any]:
570
+ """Measure peak-to-peak amplitude.
571
+
572
+ Computes Vpp as the difference between histogram-based high and low levels.
573
+
574
+ Args:
575
+ trace: Input waveform trace.
576
+
577
+ Returns:
578
+ Amplitude in volts (or input units).
579
+
580
+ Example:
581
+ >>> vpp = amplitude(trace)
582
+ >>> print(f"Amplitude: {vpp:.3f} V")
583
+
584
+ References:
585
+ IEEE 1057-2017 Section 4.2
586
+ """
587
+ if len(trace.data) < 2:
588
+ return np.nan
589
+
590
+ low, high = _find_levels(trace.data)
591
+ return high - low
592
+
593
+
594
+ def rms(
595
+ trace: WaveformTrace,
596
+ *,
597
+ ac_coupled: bool = False,
598
+ nan_policy: Literal["propagate", "omit", "raise"] = "propagate",
599
+ ) -> float | np_floating[Any]:
600
+ """Compute RMS voltage.
601
+
602
+ Calculates root-mean-square voltage of the waveform.
603
+
604
+ Args:
605
+ trace: Input waveform trace.
606
+ ac_coupled: If True, remove DC offset before computing RMS.
607
+ nan_policy: How to handle NaN values:
608
+ - "propagate": Return NaN if any NaN present (default, NumPy behavior)
609
+ - "omit": Ignore NaN values in calculation
610
+ - "raise": Raise ValueError if any NaN present
611
+
612
+ Returns:
613
+ RMS voltage in volts (or input units).
614
+
615
+ Raises:
616
+ ValueError: If nan_policy="raise" and data contains NaN.
617
+
618
+ Example:
619
+ >>> v_rms = rms(trace)
620
+ >>> print(f"RMS: {v_rms:.3f} V")
621
+
622
+ >>> # Handle traces with NaN values
623
+ >>> v_rms = rms(trace, nan_policy="omit")
624
+
625
+
626
+ References:
627
+ IEEE 1057-2017 Section 4.3
628
+ """
629
+ if len(trace.data) == 0:
630
+ return np.nan
631
+
632
+ # Convert memoryview to ndarray if needed
633
+ data = np.asarray(trace.data)
634
+
635
+ # Handle NaN based on policy
636
+ if nan_policy == "raise":
637
+ if np.any(np.isnan(data)):
638
+ raise ValueError("Input data contains NaN values")
639
+ elif nan_policy == "omit":
640
+ # Use nanmean and nansum for NaN-safe calculation
641
+ if ac_coupled:
642
+ data = data - np.nanmean(data)
643
+ return float(np.sqrt(np.nanmean(data**2)))
644
+ # else propagate - default NumPy behavior
645
+
646
+ if ac_coupled:
647
+ data = data - np.mean(data)
648
+
649
+ return float(np.sqrt(np.mean(data**2)))
650
+
651
+
652
+ def mean(
653
+ trace: WaveformTrace,
654
+ *,
655
+ nan_policy: Literal["propagate", "omit", "raise"] = "propagate",
656
+ ) -> float | np_floating[Any]:
657
+ """Compute mean (DC) voltage.
658
+
659
+ Calculates arithmetic mean of the waveform.
660
+
661
+ Args:
662
+ trace: Input waveform trace.
663
+ nan_policy: How to handle NaN values:
664
+ - "propagate": Return NaN if any NaN present (default, NumPy behavior)
665
+ - "omit": Ignore NaN values in calculation
666
+ - "raise": Raise ValueError if any NaN present
667
+
668
+ Returns:
669
+ Mean voltage in volts (or input units).
670
+
671
+ Raises:
672
+ ValueError: If nan_policy="raise" and data contains NaN.
673
+
674
+ Example:
675
+ >>> v_dc = mean(trace)
676
+ >>> print(f"DC: {v_dc:.3f} V")
677
+
678
+ >>> # Handle traces with NaN values
679
+ >>> v_dc = mean(trace, nan_policy="omit")
680
+
681
+
682
+ References:
683
+ IEEE 1057-2017 Section 4.3
684
+ """
685
+ if len(trace.data) == 0:
686
+ return np.nan
687
+
688
+ # Convert memoryview to ndarray if needed
689
+ data = np.asarray(trace.data)
690
+
691
+ # Handle NaN based on policy
692
+ if nan_policy == "raise":
693
+ if np.any(np.isnan(data)):
694
+ raise ValueError("Input data contains NaN values")
695
+ return float(np.mean(data))
696
+ elif nan_policy == "omit":
697
+ return float(np.nanmean(data))
698
+ else: # propagate
699
+ return float(np.mean(data))
700
+
701
+
702
+ def measure(
703
+ trace: WaveformTrace,
704
+ *,
705
+ parameters: list[str] | None = None,
706
+ include_units: bool = True,
707
+ ) -> dict[str, Any]:
708
+ """Compute multiple waveform measurements.
709
+
710
+ Unified function for computing all or selected waveform measurements.
711
+
712
+ Args:
713
+ trace: Input waveform trace.
714
+ parameters: List of measurement names to compute. If None, compute all.
715
+ Valid names: rise_time, fall_time, period, frequency, duty_cycle,
716
+ amplitude, rms, mean, overshoot, undershoot, preshoot
717
+ include_units: If True, include units in output.
718
+
719
+ Returns:
720
+ Dictionary mapping measurement names to values (and units if requested).
721
+
722
+ Example:
723
+ >>> results = measure(trace)
724
+ >>> print(f"Rise time: {results['rise_time']['value']} {results['rise_time']['unit']}")
725
+
726
+ >>> results = measure(trace, parameters=["frequency", "amplitude"])
727
+
728
+ References:
729
+ IEEE 181-2011, IEEE 1057-2017
730
+ """
731
+ all_measurements = {
732
+ "rise_time": (rise_time, "s"),
733
+ "fall_time": (fall_time, "s"),
734
+ "period": (lambda t: period(t, return_all=False), "s"),
735
+ "frequency": (frequency, "Hz"),
736
+ "duty_cycle": (lambda t: duty_cycle(t, percentage=True), "%"),
737
+ "pulse_width_pos": (
738
+ lambda t: pulse_width(t, polarity="positive", return_all=False),
739
+ "s",
740
+ ),
741
+ "pulse_width_neg": (
742
+ lambda t: pulse_width(t, polarity="negative", return_all=False),
743
+ "s",
744
+ ),
745
+ "amplitude": (amplitude, "V"),
746
+ "rms": (rms, "V"),
747
+ "mean": (mean, "V"),
748
+ "overshoot": (overshoot, "%"),
749
+ "undershoot": (undershoot, "%"),
750
+ "preshoot": (preshoot, "%"),
751
+ }
752
+
753
+ if parameters is None:
754
+ selected = all_measurements
755
+ else:
756
+ selected = {k: v for k, v in all_measurements.items() if k in parameters}
757
+
758
+ results: dict[str, Any] = {}
759
+
760
+ for name, (func, unit) in selected.items():
761
+ try:
762
+ value = func(trace) # type: ignore[operator]
763
+ except Exception:
764
+ value = np.nan
765
+
766
+ if include_units:
767
+ results[name] = {"value": value, "unit": unit}
768
+ else:
769
+ results[name] = value
770
+
771
+ return results
772
+
773
+
774
+ # =============================================================================
775
+ # Helper Functions
776
+ # =============================================================================
777
+
778
+
779
+ def _find_levels(data: NDArray[np_floating[Any]]) -> tuple[float, float]:
780
+ """Find low and high levels using histogram method.
781
+
782
+ Args:
783
+ data: Waveform data array.
784
+
785
+ Returns:
786
+ Tuple of (low_level, high_level).
787
+ """
788
+ # Convert boolean data to float if needed (for digital signals)
789
+ if data.dtype == np.bool_:
790
+ data = data.astype(np.float64)
791
+
792
+ # Use percentiles for robust level detection
793
+ p10, p90 = np.percentile(data, [10, 90])
794
+
795
+ # Refine using histogram peaks
796
+ hist, bin_edges = np.histogram(data, bins=50)
797
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
798
+
799
+ # Find peaks in lower and upper halves
800
+ mid_idx = len(hist) // 2
801
+ low_idx = np.argmax(hist[:mid_idx])
802
+ high_idx = mid_idx + np.argmax(hist[mid_idx:])
803
+
804
+ low = bin_centers[low_idx]
805
+ high = bin_centers[high_idx]
806
+
807
+ # Sanity check
808
+ if high <= low:
809
+ return float(p10), float(p90)
810
+
811
+ return float(low), float(high)
812
+
813
+
814
+ def _find_edges(
815
+ trace: WaveformTrace,
816
+ edge_type: Literal["rising", "falling"],
817
+ ref_level: float = 0.5,
818
+ ) -> NDArray[np.float64]:
819
+ """Find edge timestamps in a waveform.
820
+
821
+ Args:
822
+ trace: Input waveform.
823
+ edge_type: Type of edges to find.
824
+ ref_level: Reference level as fraction (0.0 to 1.0). Default 0.5 (50%).
825
+
826
+ Returns:
827
+ Array of edge timestamps in seconds.
828
+ """
829
+ data = trace.data
830
+ sample_period = trace.metadata.time_base
831
+
832
+ if len(data) < 3:
833
+ return np.array([], dtype=np.float64)
834
+
835
+ # Convert boolean data to float for arithmetic (NumPy 2.0+ compatibility)
836
+ if data.dtype == bool:
837
+ data = data.astype(np.float64)
838
+
839
+ low, high = _find_levels(data)
840
+ # Use ref_level parameter to compute threshold
841
+ mid = low + ref_level * (high - low)
842
+
843
+ if edge_type == "rising":
844
+ crossings = np.where((data[:-1] < mid) & (data[1:] >= mid))[0]
845
+ else:
846
+ crossings = np.where((data[:-1] >= mid) & (data[1:] < mid))[0]
847
+
848
+ # Convert to timestamps with interpolation
849
+ timestamps = np.zeros(len(crossings), dtype=np.float64)
850
+
851
+ for i, idx in enumerate(crossings):
852
+ base_time = idx * sample_period
853
+
854
+ # Linear interpolation
855
+ if idx < len(data) - 1:
856
+ v1, v2 = data[idx], data[idx + 1]
857
+ if abs(v2 - v1) > 1e-12:
858
+ t_offset = (mid - v1) / (v2 - v1) * sample_period
859
+ t_offset = max(0, min(sample_period, t_offset))
860
+ timestamps[i] = base_time + t_offset
861
+ else:
862
+ timestamps[i] = base_time + sample_period / 2
863
+ else:
864
+ timestamps[i] = base_time
865
+
866
+ return timestamps
867
+
868
+
869
+ def _interpolate_crossing_time(
870
+ data: NDArray[np_floating[Any]],
871
+ idx: int,
872
+ threshold: float,
873
+ sample_period: float,
874
+ rising: bool,
875
+ ) -> float | None:
876
+ """Interpolate threshold crossing time.
877
+
878
+ Args:
879
+ data: Waveform data.
880
+ idx: Sample index near crossing.
881
+ threshold: Threshold level.
882
+ sample_period: Time between samples.
883
+ rising: True for rising edge, False for falling.
884
+
885
+ Returns:
886
+ Time of crossing in seconds, or None if not found.
887
+ """
888
+ if idx < 0 or idx >= len(data) - 1:
889
+ return None
890
+
891
+ v1, v2 = data[idx], data[idx + 1]
892
+
893
+ # Check direction
894
+ if rising and not (v1 < threshold <= v2):
895
+ # Search nearby
896
+ for offset in range(-2, 3):
897
+ check_idx = idx + offset
898
+ if 0 <= check_idx < len(data) - 1:
899
+ v1, v2 = data[check_idx], data[check_idx + 1]
900
+ if v1 < threshold <= v2:
901
+ idx = check_idx
902
+ break
903
+ else:
904
+ return None
905
+
906
+ if not rising and not (v1 >= threshold > v2):
907
+ for offset in range(-2, 3):
908
+ check_idx = idx + offset
909
+ if 0 <= check_idx < len(data) - 1:
910
+ v1, v2 = data[check_idx], data[check_idx + 1]
911
+ if v1 >= threshold > v2:
912
+ idx = check_idx
913
+ break
914
+ else:
915
+ return None
916
+
917
+ v1, v2 = data[idx], data[idx + 1]
918
+ dv = v2 - v1
919
+
920
+ if abs(dv) < 1e-12:
921
+ t_offset = sample_period / 2
922
+ else:
923
+ t_offset = (threshold - v1) / dv * sample_period
924
+ t_offset = max(0, min(sample_period, t_offset))
925
+
926
+ return idx * sample_period + t_offset
927
+
928
+
929
+ __all__ = [
930
+ "amplitude",
931
+ "duty_cycle",
932
+ "fall_time",
933
+ "frequency",
934
+ "mean",
935
+ "measure",
936
+ "overshoot",
937
+ "period",
938
+ "preshoot",
939
+ "pulse_width",
940
+ "rise_time",
941
+ "rms",
942
+ "undershoot",
943
+ ]