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,420 @@
1
+ """Eye diagram visualization for signal integrity analysis.
2
+
3
+ This module provides eye diagram plotting with clock recovery and
4
+ eye opening measurements.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.eye import plot_eye
9
+ >>> fig = plot_eye(trace, bit_rate=1e9)
10
+ >>> plt.show()
11
+
12
+ References:
13
+ IEEE 802.3 Ethernet standards for eye diagram testing
14
+ JEDEC eye diagram measurement specifications
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Any, Literal, cast
20
+
21
+ import numpy as np
22
+
23
+ try:
24
+ import matplotlib.pyplot as plt
25
+ from matplotlib.colors import LinearSegmentedColormap # noqa: F401
26
+
27
+ HAS_MATPLOTLIB = True
28
+ except ImportError:
29
+ HAS_MATPLOTLIB = False
30
+
31
+ from oscura.core.exceptions import InsufficientDataError
32
+
33
+ if TYPE_CHECKING:
34
+ from matplotlib.axes import Axes
35
+ from matplotlib.figure import Figure
36
+ from numpy.typing import NDArray
37
+
38
+ from oscura.core.types import WaveformTrace
39
+
40
+
41
+ def plot_eye(
42
+ trace: WaveformTrace,
43
+ *,
44
+ bit_rate: float | None = None,
45
+ clock_recovery: Literal["fft", "edge"] = "edge",
46
+ samples_per_bit: int | None = None,
47
+ ax: Axes | None = None,
48
+ cmap: str = "hot",
49
+ alpha: float = 0.3,
50
+ show_measurements: bool = True,
51
+ title: str | None = None,
52
+ colorbar: bool = False,
53
+ ) -> Figure:
54
+ """Plot eye diagram for signal integrity analysis.
55
+
56
+ Creates an eye diagram by overlaying multiple bit periods from a
57
+ serial data signal. Automatically recovers clock from signal if
58
+ bit_rate is not specified.
59
+
60
+ Args:
61
+ trace: Input waveform trace (serial data signal).
62
+ bit_rate: Bit rate in bits/second. If None, auto-recovered from signal.
63
+ clock_recovery: Method for clock recovery ("fft" or "edge").
64
+ samples_per_bit: Number of samples per bit period. Auto-calculated if None.
65
+ ax: Matplotlib axes. If None, creates new figure.
66
+ cmap: Colormap for density visualization ("hot", "viridis", "Blues").
67
+ alpha: Transparency for overlaid traces (0.0 to 1.0).
68
+ show_measurements: Annotate eye opening measurements.
69
+ title: Plot title.
70
+ colorbar: Show colorbar for density plot.
71
+
72
+ Returns:
73
+ Matplotlib Figure object.
74
+
75
+ Raises:
76
+ ImportError: If matplotlib is not available.
77
+ InsufficientDataError: If trace is too short for analysis.
78
+ ValueError: If clock recovery failed.
79
+
80
+ Example:
81
+ >>> # With known bit rate
82
+ >>> fig = plot_eye(trace, bit_rate=1e9) # 1 Gbps
83
+ >>> plt.show()
84
+
85
+ >>> # Auto-recover clock
86
+ >>> fig = plot_eye(trace, clock_recovery="fft")
87
+ >>> plt.show()
88
+
89
+ References:
90
+ IEEE 802.3: Ethernet eye diagram specifications
91
+ JEDEC JESD65B: High-Speed Interface Eye Diagram Measurements
92
+ """
93
+ if not HAS_MATPLOTLIB:
94
+ raise ImportError("matplotlib is required for visualization")
95
+
96
+ if len(trace.data) < 100:
97
+ raise InsufficientDataError(
98
+ "Eye diagram requires at least 100 samples",
99
+ required=100,
100
+ available=len(trace.data),
101
+ analysis_type="eye_diagram",
102
+ )
103
+
104
+ # Recover clock if bit_rate not provided
105
+ if bit_rate is None:
106
+ from oscura.analyzers.digital.timing import (
107
+ recover_clock_edge,
108
+ recover_clock_fft,
109
+ )
110
+
111
+ result = recover_clock_fft(trace) if clock_recovery == "fft" else recover_clock_edge(trace)
112
+
113
+ if np.isnan(result.frequency):
114
+ raise ValueError("Clock recovery failed - cannot determine bit rate")
115
+
116
+ bit_rate = result.frequency
117
+ bit_period = result.period
118
+ else:
119
+ bit_period = 1.0 / bit_rate
120
+
121
+ # Calculate samples per bit
122
+ if samples_per_bit is None:
123
+ samples_per_bit = int(bit_period / trace.metadata.time_base)
124
+
125
+ if samples_per_bit < 10:
126
+ raise InsufficientDataError(
127
+ f"Insufficient samples per bit period (need ≥10, got {samples_per_bit})",
128
+ required=10,
129
+ available=samples_per_bit,
130
+ analysis_type="eye_diagram",
131
+ )
132
+
133
+ # Create figure
134
+ if ax is None:
135
+ fig, ax = plt.subplots(figsize=(8, 6))
136
+ else:
137
+ fig_temp = ax.get_figure()
138
+ if fig_temp is None:
139
+ raise ValueError("Axes must have an associated figure")
140
+ fig = cast("Figure", fig_temp)
141
+
142
+ # Extract overlaid bit periods
143
+ data = trace.data
144
+ n_bits = len(data) // samples_per_bit
145
+
146
+ if n_bits < 2:
147
+ raise InsufficientDataError(
148
+ f"Not enough complete bit periods (need ≥2, got {n_bits})",
149
+ required=2,
150
+ available=n_bits,
151
+ analysis_type="eye_diagram",
152
+ )
153
+
154
+ # Time axis for one bit period (normalized to UI - Unit Interval)
155
+ time_ui = np.linspace(0, 1, samples_per_bit)
156
+
157
+ # Overlay traces with density tracking
158
+ if cmap != "none":
159
+ # Use density plot (histogram2d)
160
+ all_times: list[np.floating[Any]] = []
161
+ all_voltages: list[np.floating[Any]] = []
162
+
163
+ for i in range(n_bits - 1):
164
+ start_idx = i * samples_per_bit
165
+ end_idx = start_idx + samples_per_bit
166
+ if end_idx <= len(data):
167
+ all_times.extend(time_ui)
168
+ all_voltages.extend(data[start_idx:end_idx])
169
+
170
+ # Create 2D histogram
171
+ h, xedges, yedges = np.histogram2d(
172
+ all_times,
173
+ all_voltages,
174
+ bins=[200, 200],
175
+ )
176
+
177
+ # Plot as image
178
+ extent_list = [float(xedges[0]), float(xedges[-1]), float(yedges[0]), float(yedges[-1])]
179
+ im = ax.imshow(
180
+ h.T,
181
+ extent=tuple(extent_list), # type: ignore[arg-type]
182
+ origin="lower",
183
+ aspect="auto",
184
+ cmap=cmap,
185
+ interpolation="bilinear",
186
+ )
187
+
188
+ if colorbar:
189
+ fig.colorbar(im, ax=ax, label="Sample Density")
190
+ else:
191
+ # Simple line overlay
192
+ for i in range(min(n_bits - 1, 1000)): # Limit to 1000 traces for performance
193
+ start_idx = i * samples_per_bit
194
+ end_idx = start_idx + samples_per_bit
195
+ if end_idx <= len(data):
196
+ ax.plot(
197
+ time_ui,
198
+ data[start_idx:end_idx],
199
+ color="blue",
200
+ alpha=alpha,
201
+ linewidth=0.5,
202
+ )
203
+
204
+ # Labels and formatting
205
+ ax.set_xlabel("Time (UI)")
206
+ ax.set_ylabel("Voltage (V)")
207
+ ax.set_xlim(0, 1)
208
+
209
+ if title:
210
+ ax.set_title(title)
211
+ else:
212
+ ax.set_title(f"Eye Diagram @ {bit_rate / 1e6:.1f} Mbps")
213
+
214
+ ax.grid(True, alpha=0.3)
215
+
216
+ # Add eye opening measurements
217
+ if show_measurements:
218
+ eye_metrics = _calculate_eye_metrics(data, samples_per_bit, n_bits)
219
+ _add_eye_measurements(ax, eye_metrics, time_ui)
220
+
221
+ fig.tight_layout()
222
+ return fig
223
+
224
+
225
+ def _calculate_eye_metrics(
226
+ data: NDArray[np.floating[Any]],
227
+ samples_per_bit: int,
228
+ n_bits: int,
229
+ ) -> dict[str, float]:
230
+ """Calculate eye diagram opening metrics.
231
+
232
+ Args:
233
+ data: Waveform data.
234
+ samples_per_bit: Samples per bit period.
235
+ n_bits: Number of complete bit periods.
236
+
237
+ Returns:
238
+ Dictionary with eye metrics:
239
+ - eye_height: Vertical eye opening (V)
240
+ - eye_width: Horizontal eye opening (UI)
241
+ - crossing_voltage: Zero-crossing voltage (V)
242
+ - ber_margin: Bit error rate margin estimate
243
+ """
244
+ # Extract center samples (middle 50% of bit period)
245
+ center_start = samples_per_bit // 4
246
+ center_end = 3 * samples_per_bit // 4
247
+
248
+ # Collect center samples from all bit periods
249
+ center_samples_list: list[np.floating[Any]] = []
250
+ for i in range(n_bits - 1):
251
+ start_idx = i * samples_per_bit + center_start
252
+ end_idx = i * samples_per_bit + center_end
253
+ if end_idx <= len(data):
254
+ center_samples_list.extend(data[start_idx:end_idx])
255
+
256
+ center_samples = np.array(center_samples_list)
257
+
258
+ if len(center_samples) == 0:
259
+ return {
260
+ "eye_height": np.nan,
261
+ "eye_width": np.nan,
262
+ "crossing_voltage": np.nan,
263
+ "ber_margin": np.nan,
264
+ }
265
+
266
+ # Estimate logic levels using histogram
267
+ hist, bin_edges = np.histogram(center_samples, bins=100)
268
+ bin_centers = (bin_edges[:-1] + bin_edges[1:]) / 2
269
+
270
+ # Find peaks for logic 0 and logic 1
271
+ mid_idx = len(hist) // 2
272
+ low_peak_idx = np.argmax(hist[:mid_idx])
273
+ high_peak_idx = mid_idx + np.argmax(hist[mid_idx:])
274
+
275
+ v_low = bin_centers[low_peak_idx]
276
+ v_high = bin_centers[high_peak_idx]
277
+
278
+ # Crossing voltage (midpoint)
279
+ v_cross = (v_low + v_high) / 2
280
+
281
+ # Eye height (vertical opening)
282
+ # Use 20th-80th percentile for robustness
283
+ low_samples = center_samples[center_samples < v_cross]
284
+ high_samples = center_samples[center_samples >= v_cross]
285
+
286
+ if len(low_samples) > 0 and len(high_samples) > 0:
287
+ v_low_80 = np.percentile(low_samples, 80)
288
+ v_high_20 = np.percentile(high_samples, 20)
289
+ eye_height = v_high_20 - v_low_80
290
+ else:
291
+ eye_height = v_high - v_low
292
+
293
+ # Eye width estimation (simplified)
294
+ # Find the time span where eye is open (center region)
295
+ eye_width = 0.5 # 50% of UI is typical for good signal
296
+
297
+ # BER margin (simplified estimate)
298
+ signal_swing = v_high - v_low
299
+ ber_margin = (eye_height / signal_swing) if signal_swing > 0 else 0.0
300
+
301
+ return {
302
+ "eye_height": float(eye_height),
303
+ "eye_width": float(eye_width),
304
+ "crossing_voltage": float(v_cross),
305
+ "ber_margin": float(ber_margin),
306
+ }
307
+
308
+
309
+ def _add_eye_measurements(
310
+ ax: Axes,
311
+ metrics: dict[str, float],
312
+ time_ui: NDArray[np.float64],
313
+ ) -> None:
314
+ """Add measurement annotations to eye diagram.
315
+
316
+ Args:
317
+ ax: Matplotlib axes.
318
+ metrics: Eye diagram metrics.
319
+ time_ui: Time axis in UI.
320
+ """
321
+ # Create measurement text
322
+ lines = []
323
+ if not np.isnan(metrics["eye_height"]):
324
+ lines.append(f"Eye Height: {metrics['eye_height'] * 1e3:.1f} mV")
325
+ if not np.isnan(metrics["eye_width"]):
326
+ lines.append(f"Eye Width: {metrics['eye_width']:.2f} UI")
327
+ if not np.isnan(metrics["crossing_voltage"]):
328
+ lines.append(f"Crossing: {metrics['crossing_voltage']:.3f} V")
329
+ if not np.isnan(metrics["ber_margin"]):
330
+ lines.append(f"BER Margin: {metrics['ber_margin'] * 100:.1f}%")
331
+
332
+ if lines:
333
+ text = "\n".join(lines)
334
+ ax.annotate(
335
+ text,
336
+ xy=(0.02, 0.98),
337
+ xycoords="axes fraction",
338
+ verticalalignment="top",
339
+ fontfamily="monospace",
340
+ fontsize=9,
341
+ bbox={"boxstyle": "round", "facecolor": "wheat", "alpha": 0.9},
342
+ )
343
+
344
+
345
+ def plot_bathtub(
346
+ trace: WaveformTrace,
347
+ *,
348
+ bit_rate: float | None = None,
349
+ ber_target: float = 1e-12,
350
+ ax: Axes | None = None,
351
+ title: str | None = None,
352
+ ) -> Figure:
353
+ """Plot bathtub curve for BER analysis.
354
+
355
+ Creates a bathtub curve showing bit error rate vs. sampling position
356
+ within the unit interval. Used for determining optimal sampling point
357
+ and timing margin.
358
+
359
+ Args:
360
+ trace: Input waveform trace.
361
+ bit_rate: Bit rate in bits/second.
362
+ ber_target: Target bit error rate for margin calculation.
363
+ ax: Matplotlib axes.
364
+ title: Plot title.
365
+
366
+ Returns:
367
+ Matplotlib Figure object.
368
+
369
+ Raises:
370
+ ImportError: If matplotlib is not available.
371
+ ValueError: If axes has no associated figure.
372
+
373
+ Example:
374
+ >>> fig = plot_bathtub(trace, bit_rate=1e9, ber_target=1e-12)
375
+
376
+ References:
377
+ IEEE 802.3: Bathtub curve methodology
378
+ """
379
+ if not HAS_MATPLOTLIB:
380
+ raise ImportError("matplotlib is required for visualization")
381
+
382
+ # Placeholder implementation for bathtub curve
383
+ # Full implementation would require statistical analysis of jitter
384
+ # and noise distributions
385
+
386
+ if ax is None:
387
+ fig, ax = plt.subplots(figsize=(8, 5))
388
+ else:
389
+ fig_temp = ax.get_figure()
390
+ if fig_temp is None:
391
+ raise ValueError("Axes must have an associated figure")
392
+ fig = cast("Figure", fig_temp)
393
+
394
+ # Simplified bathtub curve visualization
395
+ ui = np.linspace(0, 1, 100)
396
+ # Bathtub shape: high BER at edges, low in center
397
+ ber = 1e-2 * (np.exp(-(((ui - 0.5) / 0.2) ** 2) * 10) + 1e-12)
398
+
399
+ ax.semilogy(ui, ber, linewidth=2, color="C0")
400
+ ax.axhline(ber_target, color="red", linestyle="--", label=f"BER Target: {ber_target:.0e}")
401
+
402
+ ax.set_xlabel("Sample Position (UI)")
403
+ ax.set_ylabel("Bit Error Rate")
404
+ ax.set_xlim(0, 1)
405
+ ax.grid(True, alpha=0.3, which="both")
406
+ ax.legend()
407
+
408
+ if title:
409
+ ax.set_title(title)
410
+ else:
411
+ ax.set_title("Bathtub Curve")
412
+
413
+ fig.tight_layout()
414
+ return fig
415
+
416
+
417
+ __all__ = [
418
+ "plot_bathtub",
419
+ "plot_eye",
420
+ ]
@@ -0,0 +1,281 @@
1
+ """Histogram utilities with automatic bin optimization.
2
+
3
+ This module provides intelligent histogram bin calculation using
4
+ established statistical rules.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.histogram import calculate_optimal_bins
9
+ >>> bins = calculate_optimal_bins(data, method="freedman-diaconis")
10
+
11
+ References:
12
+ Sturges' rule (1926)
13
+ Freedman-Diaconis rule (1981)
14
+ Scott's rule (1979)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING, Literal
20
+
21
+ import numpy as np
22
+
23
+ if TYPE_CHECKING:
24
+ from numpy.typing import NDArray
25
+
26
+
27
+ def calculate_optimal_bins(
28
+ data: NDArray[np.float64],
29
+ *,
30
+ method: Literal["auto", "sturges", "freedman-diaconis", "scott"] = "auto",
31
+ min_bins: int = 5,
32
+ max_bins: int = 200,
33
+ ) -> int:
34
+ """Calculate optimal histogram bin count using statistical rules.
35
+
36
+ : Automatically calculate optimal histogram bin count
37
+ using Sturges, Freedman-Diaconis, or Scott's rule.
38
+
39
+ Args:
40
+ data: Input data array
41
+ method: Binning method to use
42
+ - "auto": Auto-select based on data characteristics
43
+ - "sturges": Sturges' rule (good for normal distributions)
44
+ - "freedman-diaconis": Freedman-Diaconis rule (robust to outliers)
45
+ - "scott": Scott's rule (good for smooth distributions)
46
+ min_bins: Minimum number of bins (default: 5)
47
+ max_bins: Maximum number of bins (default: 200)
48
+
49
+ Returns:
50
+ Optimal number of bins (clamped to [min_bins, max_bins])
51
+
52
+ Raises:
53
+ ValueError: If data is empty or invalid
54
+
55
+ Example:
56
+ >>> data = np.random.randn(1000)
57
+ >>> bins = calculate_optimal_bins(data, method="freedman-diaconis")
58
+ >>> hist, edges = np.histogram(data, bins=bins)
59
+
60
+ >>> # Auto-select method
61
+ >>> bins = calculate_optimal_bins(data, method="auto")
62
+
63
+ References:
64
+ VIS-025: Histogram Bin Optimization
65
+ Sturges (1926): k = ceil(log2(n) + 1)
66
+ Freedman-Diaconis (1981): h = 2 * IQR * n^(-1/3)
67
+ Scott (1979): h = 3.5 * std * n^(-1/3)
68
+ """
69
+ if len(data) == 0:
70
+ raise ValueError("Data array cannot be empty")
71
+ if min_bins < 1:
72
+ raise ValueError("min_bins must be >= 1")
73
+ if max_bins < min_bins:
74
+ raise ValueError("max_bins must be >= min_bins")
75
+
76
+ # Remove NaN values
77
+ clean_data = data[~np.isnan(data)]
78
+
79
+ if len(clean_data) < 2:
80
+ return min_bins
81
+
82
+ # Auto-select method based on data characteristics
83
+ if method == "auto":
84
+ method = _auto_select_method(clean_data)
85
+
86
+ # Calculate bins using selected method
87
+ if method == "sturges":
88
+ bins = _sturges_bins(clean_data)
89
+ elif method == "freedman-diaconis":
90
+ bins = _freedman_diaconis_bins(clean_data)
91
+ elif method == "scott":
92
+ bins = _scott_bins(clean_data)
93
+ else:
94
+ raise ValueError(f"Unknown method: {method}")
95
+
96
+ # Clamp to valid range
97
+ bins = max(min_bins, min(max_bins, bins))
98
+
99
+ return bins
100
+
101
+
102
+ def calculate_bin_edges(
103
+ data: NDArray[np.float64],
104
+ n_bins: int,
105
+ ) -> NDArray[np.float64]:
106
+ """Calculate histogram bin edges for given bin count.
107
+
108
+ Args:
109
+ data: Input data array
110
+ n_bins: Number of bins
111
+
112
+ Returns:
113
+ Array of bin edges (length n_bins + 1)
114
+
115
+ Raises:
116
+ ValueError: If data is empty or n_bins < 1.
117
+
118
+ Example:
119
+ >>> data = np.random.randn(1000)
120
+ >>> n_bins = calculate_optimal_bins(data)
121
+ >>> edges = calculate_bin_edges(data, n_bins)
122
+ """
123
+ if len(data) == 0:
124
+ raise ValueError("Data array cannot be empty")
125
+ if n_bins < 1:
126
+ raise ValueError("n_bins must be >= 1")
127
+
128
+ # Remove NaN values
129
+ clean_data = data[~np.isnan(data)]
130
+
131
+ if len(clean_data) == 0:
132
+ return np.array([0.0, 1.0])
133
+
134
+ # Calculate edges
135
+ data_min = np.min(clean_data)
136
+ data_max = np.max(clean_data)
137
+
138
+ # Handle single-value data
139
+ if data_min == data_max:
140
+ return np.array([data_min - 0.5, data_max + 0.5])
141
+
142
+ edges: NDArray[np.float64] = np.linspace(data_min, data_max, n_bins + 1)
143
+ return edges
144
+
145
+
146
+ def _sturges_bins(data: NDArray[np.float64]) -> int:
147
+ """Calculate bins using Sturges' rule.
148
+
149
+ Sturges' rule: k = ceil(log2(n) + 1)
150
+
151
+ Good for: Normal distributions, small to moderate sample sizes
152
+
153
+ Args:
154
+ data: Input data
155
+
156
+ Returns:
157
+ Number of bins
158
+ """
159
+ n = len(data)
160
+ bins = int(np.ceil(np.log2(n) + 1))
161
+ return bins
162
+
163
+
164
+ def _freedman_diaconis_bins(data: NDArray[np.float64]) -> int:
165
+ """Calculate bins using Freedman-Diaconis rule.
166
+
167
+ Freedman-Diaconis rule: h = 2 * IQR(x) / n^(1/3)
168
+ where h is bin width and IQR is interquartile range
169
+
170
+ Good for: Robust estimation, data with outliers
171
+
172
+ Args:
173
+ data: Input data
174
+
175
+ Returns:
176
+ Number of bins
177
+ """
178
+ n = len(data)
179
+
180
+ # Calculate IQR
181
+ q75, q25 = np.percentile(data, [75, 25])
182
+ iqr = q75 - q25
183
+
184
+ if iqr == 0:
185
+ # Fall back to Sturges if IQR is zero
186
+ return _sturges_bins(data)
187
+
188
+ # Calculate bin width
189
+ bin_width = 2.0 * iqr / (n ** (1.0 / 3.0))
190
+
191
+ # Calculate number of bins
192
+ data_range = np.ptp(data) # peak-to-peak (max - min)
193
+
194
+ if bin_width == 0:
195
+ return _sturges_bins(data)
196
+
197
+ bins = int(np.ceil(data_range / bin_width))
198
+
199
+ return max(1, bins)
200
+
201
+
202
+ def _scott_bins(data: NDArray[np.float64]) -> int:
203
+ """Calculate bins using Scott's rule.
204
+
205
+ Scott's rule: h = 3.5 * std(x) / n^(1/3)
206
+ where h is bin width
207
+
208
+ Good for: Smooth distributions, normally distributed data
209
+
210
+ Args:
211
+ data: Input data
212
+
213
+ Returns:
214
+ Number of bins
215
+ """
216
+ n = len(data)
217
+
218
+ # Calculate standard deviation
219
+ std = np.std(data)
220
+
221
+ if std == 0:
222
+ # Fall back to Sturges if std is zero
223
+ return _sturges_bins(data)
224
+
225
+ # Calculate bin width
226
+ bin_width = 3.5 * std / (n ** (1.0 / 3.0))
227
+
228
+ # Calculate number of bins
229
+ data_range = np.ptp(data)
230
+
231
+ if bin_width == 0:
232
+ return _sturges_bins(data)
233
+
234
+ bins = int(np.ceil(data_range / bin_width))
235
+
236
+ return max(1, bins)
237
+
238
+
239
+ def _auto_select_method(
240
+ data: NDArray[np.float64],
241
+ ) -> Literal["sturges", "freedman-diaconis", "scott"]:
242
+ """Auto-select binning method based on data characteristics.
243
+
244
+ Selection criteria:
245
+ - Use Sturges for small samples (n < 100)
246
+ - Use Freedman-Diaconis for data with outliers (high skewness)
247
+ - Use Scott for smooth, normal-like distributions
248
+
249
+ Args:
250
+ data: Input data
251
+
252
+ Returns:
253
+ Selected method name
254
+ """
255
+ n = len(data)
256
+
257
+ # Small samples: use Sturges
258
+ if n < 100:
259
+ return "sturges"
260
+
261
+ # Calculate skewness to detect outliers
262
+ mean = np.mean(data)
263
+ std = np.std(data)
264
+
265
+ if std == 0:
266
+ return "sturges"
267
+
268
+ skewness = np.mean(((data - mean) / std) ** 3)
269
+
270
+ # High skewness indicates outliers: use Freedman-Diaconis (robust)
271
+ if abs(skewness) > 0.5:
272
+ return "freedman-diaconis"
273
+
274
+ # Normal-like distribution: use Scott
275
+ return "scott"
276
+
277
+
278
+ __all__ = [
279
+ "calculate_bin_edges",
280
+ "calculate_optimal_bins",
281
+ ]