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,374 @@
1
+ """Enhanced annotation placement with collision detection.
2
+
3
+ This module provides intelligent annotation placement with collision avoidance,
4
+ priority-based positioning, and dynamic hiding at different zoom levels.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.annotations import place_annotations
9
+ >>> placed = place_annotations(annotations, viewport=(0, 10), density_limit=20)
10
+
11
+ References:
12
+ - Force-directed graph layout (Fruchterman-Reingold)
13
+ - Greedy placement with priority
14
+ - Leader line routing algorithms
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+
21
+ import numpy as np
22
+
23
+
24
+ @dataclass
25
+ class Annotation:
26
+ """Annotation specification with position and metadata.
27
+
28
+ Attributes:
29
+ text: Annotation text
30
+ x: X coordinate in data units
31
+ y: Y coordinate in data units
32
+ bbox_width: Bounding box width in pixels
33
+ bbox_height: Bounding box height in pixels
34
+ priority: Priority for placement (0-1, higher is more important)
35
+ anchor: Preferred anchor position
36
+ metadata: Additional metadata
37
+ """
38
+
39
+ text: str
40
+ x: float
41
+ y: float
42
+ bbox_width: float = 60.0
43
+ bbox_height: float = 20.0
44
+ priority: float = 0.5
45
+ anchor: str = "auto"
46
+ metadata: dict | None = None # type: ignore[type-arg]
47
+
48
+ def __post_init__(self): # type: ignore[no-untyped-def]
49
+ if self.metadata is None:
50
+ self.metadata = {}
51
+
52
+
53
+ @dataclass
54
+ class PlacedAnnotation:
55
+ """Annotation with optimized placement and leader line.
56
+
57
+ Attributes:
58
+ annotation: Original annotation
59
+ display_x: Optimized X position in data units
60
+ display_y: Optimized Y position in data units
61
+ visible: Whether annotation is visible at current zoom
62
+ needs_leader: Whether a leader line is needed
63
+ leader_points: Points for leader line (if needed)
64
+ """
65
+
66
+ annotation: Annotation
67
+ display_x: float
68
+ display_y: float
69
+ visible: bool = True
70
+ needs_leader: bool = False
71
+ leader_points: list[tuple[float, float]] | None = None
72
+
73
+
74
+ def place_annotations(
75
+ annotations: list[Annotation],
76
+ *,
77
+ viewport: tuple[float, float] | None = None,
78
+ density_limit: int = 20,
79
+ collision_threshold: float = 5.0,
80
+ max_iterations: int = 50,
81
+ ) -> list[PlacedAnnotation]:
82
+ """Place annotations with collision detection and density limiting.
83
+
84
+ Enhanced version with viewport-aware density limiting and dynamic hiding.
85
+
86
+ Args:
87
+ annotations: List of annotations to place.
88
+ viewport: Viewport range (x_min, x_max) for density calculation (None = all visible).
89
+ density_limit: Maximum annotations per viewport.
90
+ collision_threshold: Minimum spacing in pixels.
91
+ max_iterations: Maximum iterations for collision resolution.
92
+
93
+ Returns:
94
+ List of PlacedAnnotation with optimized positions.
95
+
96
+ Example:
97
+ >>> annots = [
98
+ ... Annotation("Peak", 5.0, 1.0, priority=0.9),
99
+ ... Annotation("Min", 3.0, -0.5, priority=0.7),
100
+ ... ]
101
+ >>> placed = place_annotations(annots, density_limit=10)
102
+
103
+ References:
104
+ VIS-016: Annotation Placement Intelligence (enhanced)
105
+ """
106
+ if len(annotations) == 0:
107
+ return []
108
+
109
+ # Filter by viewport if specified
110
+ if viewport is not None:
111
+ x_min, x_max = viewport
112
+ visible_annots = [a for a in annotations if x_min <= a.x <= x_max]
113
+ else:
114
+ visible_annots = annotations
115
+
116
+ # Apply density limiting - keep only top priority annotations
117
+ if len(visible_annots) > density_limit:
118
+ # Sort by priority (descending)
119
+ sorted_annots = sorted(
120
+ visible_annots,
121
+ key=lambda a: a.priority,
122
+ reverse=True,
123
+ )
124
+ visible_annots = sorted_annots[:density_limit]
125
+
126
+ # Initialize placed annotations at anchor points
127
+ placed = []
128
+ for annot in visible_annots:
129
+ placed.append(
130
+ PlacedAnnotation(
131
+ annotation=annot,
132
+ display_x=annot.x,
133
+ display_y=annot.y,
134
+ visible=True,
135
+ needs_leader=False,
136
+ )
137
+ )
138
+
139
+ # Resolve collisions using iterative adjustment
140
+ for _iteration in range(max_iterations):
141
+ moved = False
142
+
143
+ # Check all pairs for collisions
144
+ for i in range(len(placed)):
145
+ for j in range(i + 1, len(placed)):
146
+ if _check_collision(placed[i], placed[j], collision_threshold):
147
+ # Resolve collision by moving lower-priority annotation
148
+ if placed[i].annotation.priority >= placed[j].annotation.priority:
149
+ moved = _move_annotation(placed[j], placed[i], collision_threshold) or moved
150
+ else:
151
+ moved = _move_annotation(placed[i], placed[j], collision_threshold) or moved
152
+
153
+ # Converged if nothing moved
154
+ if not moved:
155
+ break
156
+
157
+ # Determine which annotations need leader lines
158
+ leader_threshold = 30.0 # pixels
159
+
160
+ for p in placed:
161
+ dx = abs(p.display_x - p.annotation.x)
162
+ dy = abs(p.display_y - p.annotation.y)
163
+ displacement = np.sqrt(dx**2 + dy**2)
164
+
165
+ if displacement > leader_threshold:
166
+ p.needs_leader = True
167
+ p.leader_points = _generate_leader_line(
168
+ (p.annotation.x, p.annotation.y),
169
+ (p.display_x, p.display_y),
170
+ )
171
+
172
+ return placed
173
+
174
+
175
+ def _check_collision(
176
+ p1: PlacedAnnotation,
177
+ p2: PlacedAnnotation,
178
+ threshold: float,
179
+ ) -> bool:
180
+ """Check if two annotations collide.
181
+
182
+ Args:
183
+ p1: First annotation
184
+ p2: Second annotation
185
+ threshold: Minimum spacing threshold
186
+
187
+ Returns:
188
+ True if annotations collide
189
+ """
190
+ # Bounding box collision detection
191
+ dx = abs(p2.display_x - p1.display_x)
192
+ dy = abs(p2.display_y - p1.display_y)
193
+
194
+ # Minimum separation (sum of half-widths + threshold)
195
+ min_dx = (p1.annotation.bbox_width + p2.annotation.bbox_width) / 2 + threshold
196
+ min_dy = (p1.annotation.bbox_height + p2.annotation.bbox_height) / 2 + threshold
197
+
198
+ return dx < min_dx and dy < min_dy
199
+
200
+
201
+ def _move_annotation(
202
+ to_move: PlacedAnnotation,
203
+ fixed: PlacedAnnotation,
204
+ threshold: float,
205
+ ) -> bool:
206
+ """Move annotation away from collision.
207
+
208
+ Args:
209
+ to_move: Annotation to move
210
+ fixed: Fixed annotation to move away from
211
+ threshold: Minimum spacing
212
+
213
+ Returns:
214
+ True if annotation was moved
215
+ """
216
+ dx = to_move.display_x - fixed.display_x
217
+ dy = to_move.display_y - fixed.display_y
218
+
219
+ distance = np.sqrt(dx**2 + dy**2)
220
+
221
+ if distance < 1e-6:
222
+ # Randomize if overlapping exactly
223
+ dx = np.random.randn() * 10
224
+ dy = np.random.randn() * 10
225
+ distance = np.sqrt(dx**2 + dy**2)
226
+
227
+ # Required separation
228
+ min_dx = (to_move.annotation.bbox_width + fixed.annotation.bbox_width) / 2 + threshold
229
+ min_dy = (to_move.annotation.bbox_height + fixed.annotation.bbox_height) / 2 + threshold
230
+ min_dist = np.sqrt(min_dx**2 + min_dy**2)
231
+
232
+ # Move away if too close
233
+ if distance < min_dist:
234
+ # Move proportionally to required distance
235
+ scale = min_dist / distance
236
+ new_x = fixed.display_x + dx * scale
237
+ new_y = fixed.display_y + dy * scale
238
+
239
+ # Apply with damping to avoid oscillation
240
+ damping = 0.5
241
+ to_move.display_x += (new_x - to_move.display_x) * damping
242
+ to_move.display_y += (new_y - to_move.display_y) * damping
243
+
244
+ return True
245
+
246
+ return False
247
+
248
+
249
+ def _generate_leader_line(
250
+ anchor: tuple[float, float],
251
+ label: tuple[float, float],
252
+ ) -> list[tuple[float, float]]:
253
+ """Generate orthogonal leader line from anchor to label.
254
+
255
+ Args:
256
+ anchor: Anchor point (x, y)
257
+ label: Label position (x, y)
258
+
259
+ Returns:
260
+ List of points for leader line
261
+ """
262
+ ax, ay = anchor
263
+ lx, ly = label
264
+
265
+ # L-shaped leader line
266
+ dx = abs(lx - ax)
267
+ dy = abs(ly - ay)
268
+
269
+ if dx > dy:
270
+ # Horizontal-first
271
+ mid = (lx, ay)
272
+ else:
273
+ # Vertical-first
274
+ mid = (ax, ly)
275
+
276
+ return [anchor, mid, label]
277
+
278
+
279
+ def filter_by_zoom_level(
280
+ placed: list[PlacedAnnotation],
281
+ zoom_range: tuple[float, float],
282
+ *,
283
+ min_width_for_display: float = 0.1,
284
+ ) -> list[PlacedAnnotation]:
285
+ """Filter annotations based on zoom level.
286
+
287
+ Hide annotations when zoom range is too large for readability.
288
+
289
+ Args:
290
+ placed: List of placed annotations.
291
+ zoom_range: Current zoom range (x_min, x_max).
292
+ min_width_for_display: Minimum zoom width to display annotations.
293
+
294
+ Returns:
295
+ Filtered list with visibility updated.
296
+
297
+ Example:
298
+ >>> # Hide annotations when zoomed out too far
299
+ >>> filtered = filter_by_zoom_level(placed, (0, 1000), min_width_for_display=1.0)
300
+
301
+ References:
302
+ VIS-016: Annotation Placement Intelligence (dynamic hiding)
303
+ """
304
+ x_min, x_max = zoom_range
305
+ zoom_width = x_max - x_min
306
+
307
+ result = []
308
+ for p in placed:
309
+ # Update visibility based on zoom level
310
+ if zoom_width < min_width_for_display:
311
+ p.visible = True
312
+ else:
313
+ # Hide if outside viewport or too zoomed out
314
+ in_viewport = x_min <= p.annotation.x <= x_max
315
+ p.visible = in_viewport
316
+
317
+ result.append(p)
318
+
319
+ return result
320
+
321
+
322
+ def create_priority_annotation( # type: ignore[no-untyped-def]
323
+ text: str,
324
+ x: float,
325
+ y: float,
326
+ *,
327
+ importance: str = "normal",
328
+ **kwargs,
329
+ ) -> Annotation:
330
+ """Create annotation with priority based on importance level.
331
+
332
+ Args:
333
+ text: Annotation text.
334
+ x: X position in data units.
335
+ y: Y position in data units.
336
+ importance: Importance level ("critical", "high", "normal", "low").
337
+ **kwargs: Additional Annotation parameters.
338
+
339
+ Returns:
340
+ Annotation with appropriate priority.
341
+
342
+ Example:
343
+ >>> peak_annot = create_priority_annotation(
344
+ ... "Critical Peak", 5.0, 1.0, importance="critical"
345
+ ... )
346
+
347
+ References:
348
+ VIS-016: Annotation Placement Intelligence (priority-based positioning)
349
+ """
350
+ priority_map = {
351
+ "critical": 1.0,
352
+ "high": 0.8,
353
+ "normal": 0.5,
354
+ "low": 0.2,
355
+ }
356
+
357
+ priority = priority_map.get(importance, 0.5)
358
+
359
+ return Annotation(
360
+ text=text,
361
+ x=x,
362
+ y=y,
363
+ priority=priority,
364
+ **kwargs,
365
+ )
366
+
367
+
368
+ __all__ = [
369
+ "Annotation",
370
+ "PlacedAnnotation",
371
+ "create_priority_annotation",
372
+ "filter_by_zoom_level",
373
+ "place_annotations",
374
+ ]
@@ -0,0 +1,305 @@
1
+ """Intelligent axis scaling and range optimization.
2
+
3
+ This module provides enhanced Y-axis scaling with nice number rounding,
4
+ outlier exclusion, and per-channel scaling for multi-channel plots.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.visualization.axis_scaling import calculate_axis_limits
9
+ >>> y_min, y_max = calculate_axis_limits(signal, nice_numbers=True)
10
+
11
+ References:
12
+ - Wilkinson's tick placement algorithm
13
+ - Percentile-based outlier detection
14
+ - IEEE publication best practices
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_axis_limits(
28
+ data: NDArray[np.float64],
29
+ *,
30
+ nice_numbers: bool = True,
31
+ outlier_percentile: float = 1.0,
32
+ margin_percent: float = 5.0,
33
+ symmetric: bool = False,
34
+ zero_centered: bool = False,
35
+ ) -> tuple[float, float]:
36
+ """Calculate optimal axis limits with nice number rounding.
37
+
38
+ Enhanced version with nice number rounding for publication-quality plots.
39
+
40
+ Args:
41
+ data: Signal data array.
42
+ nice_numbers: Round limits to nice numbers (1, 2, 5 × 10^n). # noqa: RUF002
43
+ outlier_percentile: Percentile for outlier exclusion (default 1% each side).
44
+ margin_percent: Margin as percentage of data range (default 5%).
45
+ symmetric: Use symmetric range ±max for bipolar signals.
46
+ zero_centered: Force zero to be centered in range.
47
+
48
+ Returns:
49
+ Tuple of (y_min, y_max) with nice rounded values.
50
+
51
+ Raises:
52
+ ValueError: If data is empty or all NaN.
53
+
54
+ Example:
55
+ >>> signal = np.array([1.234, 5.678, 9.012])
56
+ >>> y_min, y_max = calculate_axis_limits(signal, nice_numbers=True)
57
+ >>> # Returns nice values like (0.0, 10.0) instead of (1.234, 9.012)
58
+
59
+ References:
60
+ VIS-013: Auto Y-Axis Range Optimization
61
+ Wilkinson (1999): The Grammar of Graphics
62
+ """
63
+ if len(data) == 0:
64
+ raise ValueError("Data array is empty")
65
+
66
+ # Remove NaN values
67
+ clean_data = data[~np.isnan(data)]
68
+
69
+ if len(clean_data) == 0:
70
+ raise ValueError("Data contains only NaN values")
71
+
72
+ # Exclude outliers using percentiles
73
+ lower_pct = outlier_percentile
74
+ upper_pct = 100.0 - outlier_percentile
75
+
76
+ data_min = np.percentile(clean_data, lower_pct)
77
+ data_max = np.percentile(clean_data, upper_pct)
78
+ data_range = data_max - data_min
79
+
80
+ # Apply margin
81
+ margin = margin_percent / 100.0
82
+ margin_value = data_range * margin
83
+
84
+ if symmetric:
85
+ # Symmetric range: ±max
86
+ max_abs = max(abs(data_min), abs(data_max))
87
+ y_min = -(max_abs + margin_value)
88
+ y_max = max_abs + margin_value
89
+ elif zero_centered:
90
+ # Force zero to be centered
91
+ max_extent = max(abs(data_min), abs(data_max)) + margin_value
92
+ y_min = -max_extent
93
+ y_max = max_extent
94
+ else:
95
+ # Asymmetric range
96
+ y_min = data_min - margin_value
97
+ y_max = data_max + margin_value
98
+
99
+ # Round to nice numbers if requested
100
+ if nice_numbers:
101
+ y_min = _round_to_nice_number(y_min, direction="down")
102
+ y_max = _round_to_nice_number(y_max, direction="up")
103
+
104
+ return (float(y_min), float(y_max))
105
+
106
+
107
+ def calculate_multi_channel_limits( # type: ignore[no-untyped-def]
108
+ channels: list[NDArray[np.float64]],
109
+ *,
110
+ mode: Literal["per_channel", "common", "grouped"] = "per_channel",
111
+ nice_numbers: bool = True,
112
+ **kwargs,
113
+ ) -> list[tuple[float, float]]:
114
+ """Calculate axis limits for multiple channels.
115
+
116
+ Args:
117
+ channels: List of channel data arrays.
118
+ mode: Scaling mode:
119
+ - "per_channel": Independent ranges per channel
120
+ - "common": Single range for all channels
121
+ - "grouped": Group similar ranges
122
+ nice_numbers: Round to nice numbers.
123
+ **kwargs: Additional arguments passed to calculate_axis_limits.
124
+
125
+ Returns:
126
+ List of (y_min, y_max) tuples, one per channel.
127
+
128
+ Raises:
129
+ ValueError: If unknown mode specified.
130
+
131
+ Example:
132
+ >>> ch1 = np.array([0, 1, 2])
133
+ >>> ch2 = np.array([0, 10, 20])
134
+ >>> limits = calculate_multi_channel_limits([ch1, ch2], mode="per_channel")
135
+
136
+ References:
137
+ VIS-013: Auto Y-Axis Range Optimization (per-channel scaling)
138
+ VIS-015: Multi-Channel Stack Optimization
139
+ """
140
+ if len(channels) == 0:
141
+ return []
142
+
143
+ if mode == "per_channel":
144
+ # Independent ranges
145
+ return [calculate_axis_limits(ch, nice_numbers=nice_numbers, **kwargs) for ch in channels]
146
+
147
+ elif mode == "common":
148
+ # Single range for all channels
149
+ all_data = np.concatenate([ch for ch in channels if len(ch) > 0])
150
+ if len(all_data) == 0:
151
+ return [(0.0, 1.0)] * len(channels)
152
+
153
+ common_limits = calculate_axis_limits(all_data, nice_numbers=nice_numbers, **kwargs)
154
+ return [common_limits] * len(channels)
155
+
156
+ elif mode == "grouped":
157
+ # Group channels with similar ranges
158
+ # First calculate individual ranges
159
+ individual_limits = [
160
+ calculate_axis_limits(ch, nice_numbers=False, **kwargs) for ch in channels
161
+ ]
162
+
163
+ # Simple grouping: group by order of magnitude
164
+ grouped_limits = []
165
+ for y_min, y_max in individual_limits:
166
+ range_mag = np.log10(max(abs(y_max - y_min), 1e-10))
167
+ # Round to nearest integer magnitude
168
+ group_mag = int(np.round(range_mag))
169
+
170
+ # Use 10^group_mag as the range scale
171
+ scale = 10.0**group_mag
172
+
173
+ # Round to this scale
174
+ grouped_min = np.floor(y_min / scale) * scale
175
+ grouped_max = np.ceil(y_max / scale) * scale
176
+
177
+ if nice_numbers:
178
+ grouped_min = _round_to_nice_number(grouped_min, direction="down")
179
+ grouped_max = _round_to_nice_number(grouped_max, direction="up")
180
+
181
+ grouped_limits.append((float(grouped_min), float(grouped_max)))
182
+
183
+ return grouped_limits
184
+
185
+ else:
186
+ raise ValueError(f"Unknown mode: {mode}")
187
+
188
+
189
+ def _round_to_nice_number(
190
+ value: float,
191
+ *,
192
+ direction: Literal["up", "down", "nearest"] = "nearest",
193
+ ) -> float:
194
+ """Round value to nice number (1, 2, 5 × 10^n). # noqa: RUF002
195
+
196
+ Args:
197
+ value: Value to round.
198
+ direction: Rounding direction ("up", "down", "nearest").
199
+
200
+ Returns:
201
+ Rounded nice number.
202
+
203
+ Example:
204
+ >>> _round_to_nice_number(3.7, direction="up")
205
+ 5.0
206
+ >>> _round_to_nice_number(3.7, direction="down")
207
+ 2.0
208
+ >>> _round_to_nice_number(0.037, direction="up")
209
+ 0.05
210
+ """
211
+ if value == 0:
212
+ return 0.0
213
+
214
+ # Determine sign
215
+ sign = 1 if value >= 0 else -1
216
+ abs_value = abs(value)
217
+
218
+ # Find exponent
219
+ exponent = np.floor(np.log10(abs_value))
220
+ mantissa = abs_value / (10**exponent)
221
+
222
+ # Round mantissa to nice fraction (1, 2, 5)
223
+ nice_fractions = [1.0, 2.0, 5.0, 10.0]
224
+
225
+ if direction == "up":
226
+ # Find smallest nice fraction >= mantissa
227
+ nice_mantissa = next((f for f in nice_fractions if f >= mantissa), 10.0)
228
+ elif direction == "down":
229
+ # Find largest nice fraction <= mantissa
230
+ nice_mantissa = 1.0
231
+ for f in nice_fractions:
232
+ if f <= mantissa:
233
+ nice_mantissa = f
234
+ else:
235
+ break
236
+ else: # nearest
237
+ # Find closest nice fraction
238
+ distances = [abs(f - mantissa) for f in nice_fractions]
239
+ min_idx = np.argmin(distances)
240
+ nice_mantissa = nice_fractions[min_idx]
241
+
242
+ # Handle mantissa = 10 case (move to next exponent)
243
+ if nice_mantissa >= 10.0:
244
+ nice_mantissa = 1.0
245
+ exponent += 1
246
+
247
+ return sign * nice_mantissa * (10**exponent) # type: ignore[no-any-return]
248
+
249
+
250
+ def suggest_tick_spacing(
251
+ y_min: float,
252
+ y_max: float,
253
+ *,
254
+ target_ticks: int = 5,
255
+ minor_ticks: bool = True,
256
+ ) -> tuple[float, float]:
257
+ """Suggest tick spacing for axis.
258
+
259
+ Args:
260
+ y_min: Minimum axis value.
261
+ y_max: Maximum axis value.
262
+ target_ticks: Target number of major ticks.
263
+ minor_ticks: Generate minor tick spacing.
264
+
265
+ Returns:
266
+ Tuple of (major_spacing, minor_spacing).
267
+
268
+ Example:
269
+ >>> major, minor = suggest_tick_spacing(0, 10, target_ticks=5)
270
+ >>> # Returns (2.0, 0.5) for nice tick marks at 0, 2, 4, 6, 8, 10
271
+
272
+ References:
273
+ VIS-019: Grid Auto-Spacing
274
+ """
275
+ axis_range = y_max - y_min
276
+
277
+ if axis_range <= 0:
278
+ return (1.0, 0.2)
279
+
280
+ # Calculate rough spacing
281
+ rough_spacing = axis_range / target_ticks
282
+
283
+ # Round to nice number
284
+ major_spacing = _round_to_nice_number(rough_spacing, direction="nearest")
285
+
286
+ # Minor spacing: 1/5 of major for most cases
287
+ if minor_ticks:
288
+ # Use 1/5 for multiples of 5, 1/4 for multiples of 2, 1/2 otherwise
289
+ if major_spacing % 5 == 0:
290
+ minor_spacing = major_spacing / 5
291
+ elif major_spacing % 2 == 0:
292
+ minor_spacing = major_spacing / 4
293
+ else:
294
+ minor_spacing = major_spacing / 2
295
+ else:
296
+ minor_spacing = major_spacing
297
+
298
+ return (float(major_spacing), float(minor_spacing))
299
+
300
+
301
+ __all__ = [
302
+ "calculate_axis_limits",
303
+ "calculate_multi_channel_limits",
304
+ "suggest_tick_spacing",
305
+ ]