oscura 0.0.1__py3-none-any.whl → 0.1.1__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.1.dist-info/METADATA +300 -0
  460. oscura-0.1.1.dist-info/RECORD +463 -0
  461. oscura-0.1.1.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.1.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.1.dist-info}/WHEEL +0 -0
oscura/utils/memory.py ADDED
@@ -0,0 +1,836 @@
1
+ """Memory management utilities for Oscura.
2
+
3
+ This module provides memory estimation, availability checking, and
4
+ OOM prevention for large signal processing operations.
5
+
6
+
7
+ Example:
8
+ >>> from oscura.utils.memory import estimate_memory, check_memory_available
9
+ >>> estimate = estimate_memory('fft', samples=1e9)
10
+ >>> check = check_memory_available('spectrogram', samples=1e9, nperseg=4096)
11
+
12
+ References:
13
+ Python psutil documentation
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import gc
19
+ import os
20
+ import platform
21
+ from dataclasses import dataclass
22
+ from typing import Any
23
+
24
+ import numpy as np
25
+
26
+
27
+ @dataclass
28
+ class MemoryEstimate:
29
+ """Memory requirement estimate for an operation.
30
+
31
+ Attributes:
32
+ data: Memory for input data (bytes).
33
+ intermediate: Memory for intermediate buffers (bytes).
34
+ output: Memory for output data (bytes).
35
+ total: Total memory required (bytes).
36
+ operation: Operation name.
37
+ parameters: Parameters used for estimate.
38
+ """
39
+
40
+ data: int
41
+ intermediate: int
42
+ output: int
43
+ total: int
44
+ operation: str
45
+ parameters: dict # type: ignore[type-arg]
46
+
47
+ def __repr__(self) -> str:
48
+ return (
49
+ f"MemoryEstimate({self.operation}: "
50
+ f"total={self.total / 1e9:.2f} GB, "
51
+ f"data={self.data / 1e9:.2f} GB, "
52
+ f"intermediate={self.intermediate / 1e9:.2f} GB, "
53
+ f"output={self.output / 1e9:.2f} GB)"
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class MemoryCheck:
59
+ """Result of memory availability check.
60
+
61
+ Attributes:
62
+ sufficient: True if enough memory is available.
63
+ available: Available memory (bytes).
64
+ required: Required memory (bytes).
65
+ recommendation: Suggested action if insufficient.
66
+ """
67
+
68
+ sufficient: bool
69
+ available: int
70
+ required: int
71
+ recommendation: str
72
+
73
+
74
+ class MemoryCheckError(Exception):
75
+ """Exception raised when memory check fails.
76
+
77
+ Attributes:
78
+ required: Required memory in bytes.
79
+ available: Available memory in bytes.
80
+ recommendation: Suggested action.
81
+ """
82
+
83
+ def __init__(self, message: str, required: int, available: int, recommendation: str):
84
+ super().__init__(message)
85
+ self.required = required
86
+ self.available = available
87
+ self.recommendation = recommendation
88
+
89
+
90
+ def detect_wsl() -> bool:
91
+ """Detect if running in Windows Subsystem for Linux.
92
+
93
+ Returns:
94
+ True if running in WSL.
95
+ """
96
+ try:
97
+ with open("/proc/version") as f:
98
+ version = f.read().lower()
99
+ return "microsoft" in version or "wsl" in version
100
+ except (FileNotFoundError, PermissionError):
101
+ return False
102
+
103
+
104
+ def get_total_memory() -> int:
105
+ """Get total system memory in bytes.
106
+
107
+ Returns:
108
+ Total physical memory in bytes.
109
+ """
110
+ try:
111
+ import psutil
112
+
113
+ return psutil.virtual_memory().total # type: ignore[no-any-return]
114
+ except ImportError:
115
+ # Fallback without psutil
116
+ if platform.system() == "Linux":
117
+ try:
118
+ with open("/proc/meminfo") as f:
119
+ for line in f:
120
+ if line.startswith("MemTotal:"):
121
+ # Format: "MemTotal: 16384 kB"
122
+ return int(line.split()[1]) * 1024
123
+ except (FileNotFoundError, PermissionError):
124
+ pass
125
+ # Default fallback: assume 8 GB
126
+ return 8 * 1024 * 1024 * 1024
127
+
128
+
129
+ def get_available_memory() -> int:
130
+ """Get available memory in bytes.
131
+
132
+ Accounts for OS overhead and applies WSL conservative factor.
133
+
134
+ Returns:
135
+ Available memory in bytes.
136
+ """
137
+ # Get memory reserve from environment
138
+ reserve_str = os.environ.get("TK_MEMORY_RESERVE", "0")
139
+ try:
140
+ if reserve_str.upper().endswith("GB"):
141
+ reserve = int(float(reserve_str[:-2]) * 1e9)
142
+ elif reserve_str.upper().endswith("MB"):
143
+ reserve = int(float(reserve_str[:-2]) * 1e6)
144
+ else:
145
+ reserve = int(float(reserve_str))
146
+ except ValueError:
147
+ reserve = 0
148
+
149
+ try:
150
+ import psutil
151
+
152
+ available = psutil.virtual_memory().available
153
+ except ImportError:
154
+ # Fallback without psutil
155
+ if platform.system() == "Linux":
156
+ try:
157
+ with open("/proc/meminfo") as f:
158
+ for line in f:
159
+ if line.startswith("MemAvailable:"):
160
+ available = int(line.split()[1]) * 1024
161
+ break
162
+ else:
163
+ available = get_total_memory() // 2
164
+ except (FileNotFoundError, PermissionError):
165
+ available = get_total_memory() // 2
166
+ else:
167
+ available = get_total_memory() // 2
168
+
169
+ # Apply WSL conservative factor
170
+ if detect_wsl():
171
+ available = int(available * 0.5)
172
+
173
+ # Apply reserve
174
+ available = max(0, available - reserve)
175
+
176
+ return available # type: ignore[no-any-return]
177
+
178
+
179
+ def get_swap_available() -> int:
180
+ """Get available swap space in bytes.
181
+
182
+ Returns:
183
+ Available swap in bytes.
184
+ """
185
+ try:
186
+ import psutil
187
+
188
+ return psutil.swap_memory().free # type: ignore[no-any-return]
189
+ except ImportError:
190
+ # Fallback
191
+ if platform.system() == "Linux":
192
+ try:
193
+ with open("/proc/meminfo") as f:
194
+ for line in f:
195
+ if line.startswith("SwapFree:"):
196
+ return int(line.split()[1]) * 1024
197
+ except (FileNotFoundError, PermissionError):
198
+ pass
199
+ return 0
200
+
201
+
202
+ def get_memory_pressure() -> float:
203
+ """Get current memory utilization (0.0 to 1.0).
204
+
205
+ Returns:
206
+ Memory pressure as fraction of total memory used.
207
+ """
208
+ try:
209
+ import psutil
210
+
211
+ return psutil.virtual_memory().percent / 100.0 # type: ignore[no-any-return]
212
+ except ImportError:
213
+ total = get_total_memory()
214
+ available = get_available_memory()
215
+ return 1.0 - (available / total) if total > 0 else 0.5
216
+
217
+
218
+ def estimate_memory(
219
+ operation: str,
220
+ samples: int | float | None = None,
221
+ *,
222
+ nfft: int | None = None,
223
+ nperseg: int | None = None,
224
+ noverlap: int | None = None,
225
+ dtype: str = "float64",
226
+ channels: int = 1,
227
+ **kwargs: Any,
228
+ ) -> MemoryEstimate:
229
+ """Estimate memory requirements for an operation.
230
+
231
+ Args:
232
+ operation: Operation name (fft, psd, spectrogram, eye_diagram, correlate, filter).
233
+ samples: Number of samples (can be float for large values).
234
+ nfft: FFT length (for fft, psd, spectrogram).
235
+ nperseg: Segment length (for spectrogram, psd).
236
+ noverlap: Overlap samples (for spectrogram).
237
+ dtype: Data type (float32 or float64).
238
+ channels: Number of channels.
239
+ **kwargs: Additional operation-specific parameters.
240
+
241
+ Returns:
242
+ MemoryEstimate with memory requirements.
243
+
244
+ Example:
245
+ >>> estimate = estimate_memory('fft', samples=1e9, nfft=8192)
246
+ >>> print(f"Required: {estimate.total / 1e9:.2f} GB")
247
+ """
248
+ # Bytes per element
249
+ bytes_per_sample = 4 if dtype == "float32" else 8
250
+
251
+ samples = int(samples or 0)
252
+
253
+ # Calculate based on operation
254
+ if operation == "fft":
255
+ nfft = nfft or _next_power_of_2(samples)
256
+ data_mem = samples * bytes_per_sample * channels
257
+ # FFT needs complex output (2x) plus work buffer
258
+ intermediate_mem = nfft * bytes_per_sample * 2 * 2 # complex, work buffer
259
+ output_mem = (nfft // 2 + 1) * bytes_per_sample * 2 * channels # complex output
260
+
261
+ elif operation == "psd":
262
+ nperseg = nperseg or 256
263
+ nfft = nfft or nperseg
264
+ data_mem = samples * bytes_per_sample * channels
265
+ # Welch needs segment buffer plus FFT work
266
+ intermediate_mem = nperseg * bytes_per_sample * 2 + nfft * bytes_per_sample * 2
267
+ output_mem = (nfft // 2 + 1) * bytes_per_sample * channels
268
+
269
+ elif operation == "spectrogram":
270
+ nperseg = nperseg or 256
271
+ noverlap = noverlap or nperseg // 2
272
+ nfft = nfft or nperseg
273
+ hop = nperseg - noverlap
274
+ num_segments = max(1, (samples - noverlap) // hop)
275
+
276
+ data_mem = samples * bytes_per_sample * channels
277
+ # STFT needs segment buffer
278
+ intermediate_mem = nperseg * bytes_per_sample * 2 + nfft * bytes_per_sample * 2
279
+ # Output: (nfft//2+1) frequencies x num_segments times
280
+ output_mem = (nfft // 2 + 1) * num_segments * bytes_per_sample * 2 * channels
281
+
282
+ elif operation == "eye_diagram":
283
+ samples_per_ui = kwargs.get("samples_per_ui", 100)
284
+ num_uis = kwargs.get("num_uis", 1000)
285
+ data_mem = samples * bytes_per_sample * channels
286
+ # Eye diagram accumulates traces
287
+ intermediate_mem = samples_per_ui * num_uis * bytes_per_sample
288
+ output_mem = samples_per_ui * num_uis * bytes_per_sample
289
+
290
+ elif operation == "correlate":
291
+ data_mem = samples * bytes_per_sample * 2 * channels # Two signals
292
+ # FFT-based correlation
293
+ nfft = _next_power_of_2(samples * 2)
294
+ intermediate_mem = nfft * bytes_per_sample * 2 * 2 # Two FFTs
295
+ output_mem = (samples * 2 - 1) * bytes_per_sample * channels
296
+
297
+ elif operation == "filter":
298
+ filter_order = kwargs.get("filter_order", 8)
299
+ data_mem = samples * bytes_per_sample * channels
300
+ # Filter state and buffer
301
+ intermediate_mem = (filter_order + samples) * bytes_per_sample
302
+ output_mem = samples * bytes_per_sample * channels
303
+
304
+ else:
305
+ # Generic estimate
306
+ data_mem = samples * bytes_per_sample * channels
307
+ intermediate_mem = samples * bytes_per_sample
308
+ output_mem = samples * bytes_per_sample * channels
309
+
310
+ total_mem = data_mem + intermediate_mem + output_mem
311
+
312
+ return MemoryEstimate(
313
+ data=data_mem,
314
+ intermediate=intermediate_mem,
315
+ output=output_mem,
316
+ total=total_mem,
317
+ operation=operation,
318
+ parameters={
319
+ "samples": samples,
320
+ "nfft": nfft,
321
+ "nperseg": nperseg,
322
+ "noverlap": noverlap,
323
+ "dtype": dtype,
324
+ "channels": channels,
325
+ **kwargs,
326
+ },
327
+ )
328
+
329
+
330
+ def check_memory_available(
331
+ operation: str,
332
+ samples: int | float | None = None,
333
+ **kwargs: Any,
334
+ ) -> MemoryCheck:
335
+ """Check if sufficient memory is available for an operation.
336
+
337
+ Args:
338
+ operation: Operation name.
339
+ samples: Number of samples.
340
+ **kwargs: Additional parameters for estimate_memory.
341
+
342
+ Returns:
343
+ MemoryCheck with sufficiency status and recommendation.
344
+
345
+ Example:
346
+ >>> check = check_memory_available('spectrogram', samples=1e9, nperseg=4096)
347
+ >>> if not check.sufficient:
348
+ ... print(check.recommendation)
349
+ """
350
+ estimate = estimate_memory(operation, samples, **kwargs)
351
+ available = get_available_memory()
352
+
353
+ sufficient = estimate.total <= available
354
+
355
+ if sufficient:
356
+ recommendation = "Memory sufficient for operation."
357
+ else:
358
+ # Generate recommendations
359
+ ratio = estimate.total / available
360
+ if ratio < 2:
361
+ recommendation = (
362
+ f"Need {estimate.total / 1e9:.1f} GB, have {available / 1e9:.1f} GB. "
363
+ "Consider closing other applications or using chunked processing."
364
+ )
365
+ elif ratio < 10:
366
+ recommendation = (
367
+ f"Need {estimate.total / 1e9:.1f} GB, have {available / 1e9:.1f} GB. "
368
+ f"Use chunked processing or downsample by {int(ratio)}x."
369
+ )
370
+ else:
371
+ recommendation = (
372
+ f"Need {estimate.total / 1e9:.1f} GB, have {available / 1e9:.1f} GB. "
373
+ "Data too large for available memory. Use streaming/chunked processing "
374
+ "or process a subset of the data."
375
+ )
376
+
377
+ return MemoryCheck(
378
+ sufficient=sufficient,
379
+ available=available,
380
+ required=estimate.total,
381
+ recommendation=recommendation,
382
+ )
383
+
384
+
385
+ def require_memory(
386
+ operation: str,
387
+ samples: int | float | None = None,
388
+ **kwargs: Any,
389
+ ) -> None:
390
+ """Raise exception if insufficient memory for operation.
391
+
392
+ Args:
393
+ operation: Operation name.
394
+ samples: Number of samples.
395
+ **kwargs: Additional parameters.
396
+
397
+ Raises:
398
+ MemoryCheckError: If insufficient memory.
399
+ """
400
+ check = check_memory_available(operation, samples, **kwargs)
401
+ if not check.sufficient:
402
+ raise MemoryCheckError(
403
+ f"Insufficient memory for {operation}",
404
+ required=check.required,
405
+ available=check.available,
406
+ recommendation=check.recommendation,
407
+ )
408
+
409
+
410
+ def _next_power_of_2(n: int) -> int:
411
+ """Return next power of 2 >= n."""
412
+ if n <= 0:
413
+ return 1
414
+ return 1 << (n - 1).bit_length()
415
+
416
+
417
+ # Memory configuration
418
+ _max_memory: int | None = None
419
+
420
+
421
+ def set_max_memory(limit: int | str | None) -> None:
422
+ """Set global memory limit for Oscura operations.
423
+
424
+ Args:
425
+ limit: Maximum memory in bytes, or string like "4GB", "512MB".
426
+
427
+ Example:
428
+ >>> set_max_memory("4GB")
429
+ >>> set_max_memory(4 * 1024 * 1024 * 1024)
430
+ """
431
+ global _max_memory
432
+
433
+ if limit is None:
434
+ _max_memory = None
435
+ return
436
+
437
+ if isinstance(limit, str):
438
+ limit = limit.upper().strip()
439
+ if limit.endswith("GB"):
440
+ _max_memory = int(float(limit[:-2]) * 1e9)
441
+ elif limit.endswith("MB"):
442
+ _max_memory = int(float(limit[:-2]) * 1e6)
443
+ elif limit.endswith("KB"):
444
+ _max_memory = int(float(limit[:-2]) * 1e3)
445
+ else:
446
+ _max_memory = int(float(limit))
447
+ else:
448
+ _max_memory = int(limit)
449
+
450
+
451
+ def get_max_memory() -> int:
452
+ """Get the current memory limit.
453
+
454
+ Returns:
455
+ Memory limit in bytes (default: 80% of available).
456
+ """
457
+ if _max_memory is not None:
458
+ return _max_memory
459
+
460
+ # Check environment variable
461
+ env_limit = os.environ.get("TK_MAX_MEMORY")
462
+ if env_limit:
463
+ set_max_memory(env_limit)
464
+ if _max_memory is not None:
465
+ return _max_memory # type: ignore[unreachable]
466
+
467
+ # Default: 80% of available
468
+ return int(get_available_memory() * 0.8)
469
+
470
+
471
+ def gc_collect() -> int:
472
+ """Force garbage collection.
473
+
474
+ Returns:
475
+ Number of unreachable objects collected.
476
+ """
477
+ return gc.collect()
478
+
479
+
480
+ def get_memory_info() -> dict[str, int]:
481
+ """Get comprehensive memory information.
482
+
483
+ Returns:
484
+ Dictionary with memory statistics.
485
+ """
486
+ return {
487
+ "total": get_total_memory(),
488
+ "available": get_available_memory(),
489
+ "swap_available": get_swap_available(),
490
+ "max_memory": get_max_memory(),
491
+ "pressure_pct": int(get_memory_pressure() * 100),
492
+ "wsl": detect_wsl(),
493
+ }
494
+
495
+
496
+ # ==========================================================================
497
+ # MEM-009, MEM-010, MEM-011: Memory Configuration & Limits
498
+ # ==========================================================================
499
+
500
+
501
+ @dataclass
502
+ class MemoryConfig:
503
+ """Global memory configuration for Oscura operations.
504
+
505
+
506
+ Attributes:
507
+ max_memory: Global memory limit in bytes (None = 80% of available).
508
+ warn_threshold: Warning threshold (0.0-1.0, default 0.7).
509
+ critical_threshold: Critical threshold (0.0-1.0, default 0.9).
510
+ auto_degrade: Automatically downsample if memory exceeded.
511
+ """
512
+
513
+ max_memory: int | None = None
514
+ warn_threshold: float = 0.7
515
+ critical_threshold: float = 0.9
516
+ auto_degrade: bool = False
517
+
518
+ def __post_init__(self) -> None:
519
+ """Validate thresholds."""
520
+ if not 0.0 <= self.warn_threshold <= 1.0:
521
+ raise ValueError(f"warn_threshold must be 0.0-1.0, got {self.warn_threshold}")
522
+ if not 0.0 <= self.critical_threshold <= 1.0:
523
+ raise ValueError(f"critical_threshold must be 0.0-1.0, got {self.critical_threshold}")
524
+ if self.warn_threshold >= self.critical_threshold:
525
+ raise ValueError(
526
+ f"warn_threshold ({self.warn_threshold}) must be < critical_threshold "
527
+ f"({self.critical_threshold})"
528
+ )
529
+
530
+
531
+ # Global memory configuration instance
532
+ _memory_config = MemoryConfig()
533
+
534
+
535
+ def configure_memory(
536
+ *,
537
+ max_memory: int | str | None = None,
538
+ warn_threshold: float | None = None,
539
+ critical_threshold: float | None = None,
540
+ auto_degrade: bool | None = None,
541
+ ) -> None:
542
+ """Configure global memory limits and thresholds.
543
+
544
+
545
+ Args:
546
+ max_memory: Maximum memory in bytes or string ("4GB", "512MB").
547
+ warn_threshold: Warning threshold (0.0-1.0).
548
+ critical_threshold: Critical threshold (0.0-1.0).
549
+ auto_degrade: Enable automatic downsampling.
550
+
551
+ Example:
552
+ >>> configure_memory(max_memory="4GB", warn_threshold=0.7, critical_threshold=0.9)
553
+ >>> configure_memory(auto_degrade=True)
554
+ """
555
+ global _memory_config # noqa: PLW0602
556
+
557
+ if max_memory is not None:
558
+ if isinstance(max_memory, str):
559
+ # Parse string format
560
+ limit_upper = max_memory.upper().strip()
561
+ if limit_upper.endswith("GB"):
562
+ _memory_config.max_memory = int(float(limit_upper[:-2]) * 1e9)
563
+ elif limit_upper.endswith("MB"):
564
+ _memory_config.max_memory = int(float(limit_upper[:-2]) * 1e6)
565
+ elif limit_upper.endswith("KB"):
566
+ _memory_config.max_memory = int(float(limit_upper[:-2]) * 1e3)
567
+ else:
568
+ _memory_config.max_memory = int(float(limit_upper))
569
+ else:
570
+ _memory_config.max_memory = int(max_memory)
571
+
572
+ if warn_threshold is not None:
573
+ _memory_config.warn_threshold = warn_threshold
574
+ if critical_threshold is not None:
575
+ _memory_config.critical_threshold = critical_threshold
576
+ if auto_degrade is not None:
577
+ _memory_config.auto_degrade = auto_degrade
578
+
579
+ # Validate after updates
580
+ _memory_config.__post_init__()
581
+
582
+
583
+ def get_memory_config() -> MemoryConfig:
584
+ """Get current memory configuration.
585
+
586
+ Returns:
587
+ Current MemoryConfig instance.
588
+ """
589
+ return _memory_config
590
+
591
+
592
+ # ==========================================================================
593
+ # ==========================================================================
594
+
595
+
596
+ @dataclass
597
+ class DownsamplingRecommendation:
598
+ """Recommendation for downsampling to fit memory constraints.
599
+
600
+ Attributes:
601
+ factor: Suggested downsampling factor (2, 4, 8, 16, etc.).
602
+ required_memory: Memory required without downsampling (bytes).
603
+ available_memory: Available memory (bytes).
604
+ new_sample_rate: Effective sample rate after downsampling (Hz).
605
+ message: Human-readable recommendation message.
606
+ """
607
+
608
+ factor: int
609
+ required_memory: int
610
+ available_memory: int
611
+ new_sample_rate: float
612
+ message: str
613
+
614
+
615
+ def suggest_downsampling(
616
+ operation: str,
617
+ samples: int | float,
618
+ sample_rate: float,
619
+ **kwargs: Any,
620
+ ) -> DownsamplingRecommendation | None:
621
+ """Suggest downsampling factor if operation would exceed memory limits.
622
+
623
+
624
+ Args:
625
+ operation: Operation name.
626
+ samples: Number of samples.
627
+ sample_rate: Current sample rate in Hz.
628
+ **kwargs: Additional parameters for memory estimation.
629
+
630
+ Returns:
631
+ DownsamplingRecommendation if downsampling needed, None if sufficient memory.
632
+
633
+ Example:
634
+ >>> rec = suggest_downsampling('spectrogram', samples=1e9, sample_rate=1e9, nperseg=4096)
635
+ >>> if rec:
636
+ ... print(f"Downsample by {rec.factor}x to {rec.new_sample_rate/1e6:.1f} MSa/s")
637
+ """
638
+ estimate = estimate_memory(operation, samples, **kwargs)
639
+ available = get_available_memory()
640
+
641
+ if estimate.total <= available:
642
+ return None # Sufficient memory
643
+
644
+ # Calculate required downsampling factor
645
+ ratio = estimate.total / available
646
+ # Round up to next power of 2
647
+ factor = 2 ** int(np.ceil(np.log2(ratio)))
648
+ # Limit to reasonable factors
649
+ factor = min(factor, 16)
650
+
651
+ new_sample_rate = sample_rate / factor
652
+ new_samples = int(samples) // factor
653
+
654
+ # Re-estimate with downsampled size
655
+ new_estimate = estimate_memory(operation, new_samples, **kwargs)
656
+
657
+ message = (
658
+ f"Insufficient memory for {operation}. "
659
+ f"Need {estimate.total / 1e9:.1f} GB, have {available / 1e9:.1f} GB. "
660
+ f"Recommend downsampling by {factor}x (new rate: {new_sample_rate / 1e6:.1f} MSa/s). "
661
+ f"Estimated memory after downsampling: {new_estimate.total / 1e9:.2f} GB."
662
+ )
663
+
664
+ return DownsamplingRecommendation(
665
+ factor=factor,
666
+ required_memory=estimate.total,
667
+ available_memory=available,
668
+ new_sample_rate=new_sample_rate,
669
+ message=message,
670
+ )
671
+
672
+
673
+ # ==========================================================================
674
+ # ==========================================================================
675
+
676
+
677
+ class MemoryMonitor:
678
+ """Context manager for monitoring memory usage and preventing OOM crashes.
679
+
680
+
681
+ Attributes:
682
+ operation: Name of the operation being monitored.
683
+ max_memory: Maximum allowed memory (None = use global config).
684
+ check_interval: How often to check memory (number of iterations).
685
+
686
+ Example:
687
+ >>> with MemoryMonitor('spectrogram', max_memory=4e9) as monitor:
688
+ ... for i in range(1000):
689
+ ... # Perform work
690
+ ... monitor.check(i) # Check memory periodically
691
+ """
692
+
693
+ def __init__(
694
+ self,
695
+ operation: str,
696
+ *,
697
+ max_memory: int | str | None = None,
698
+ check_interval: int = 100,
699
+ ):
700
+ self.operation = operation
701
+ self.check_interval = check_interval
702
+ self.start_memory = 0
703
+ self.peak_memory = 0
704
+ self.current_memory = 0
705
+ self._iteration = 0
706
+
707
+ # Parse max_memory
708
+ if max_memory is None:
709
+ self.max_memory = get_max_memory()
710
+ elif isinstance(max_memory, str):
711
+ limit_upper = max_memory.upper().strip()
712
+ if limit_upper.endswith("GB"):
713
+ self.max_memory = int(float(limit_upper[:-2]) * 1e9)
714
+ elif limit_upper.endswith("MB"):
715
+ self.max_memory = int(float(limit_upper[:-2]) * 1e6)
716
+ else:
717
+ self.max_memory = int(float(limit_upper))
718
+ else:
719
+ self.max_memory = int(max_memory)
720
+
721
+ def __enter__(self) -> MemoryMonitor:
722
+ """Enter context and record starting memory."""
723
+ self.start_memory = self._get_process_memory()
724
+ self.peak_memory = self.start_memory
725
+ self.current_memory = self.start_memory
726
+ return self
727
+
728
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
729
+ """Exit context."""
730
+ # Note: exc_val and exc_tb intentionally unused but required for Python 3.11+ compatibility
731
+
732
+ def check(self, iteration: int | None = None) -> None:
733
+ """Check memory usage and raise error if limit approached.
734
+
735
+ Args:
736
+ iteration: Current iteration number (for periodic checking).
737
+
738
+ Raises:
739
+ MemoryError: If memory usage exceeds 95% of max_memory.
740
+ """
741
+ self._iteration += 1
742
+
743
+ # Only check periodically
744
+ if iteration is not None and iteration % self.check_interval != 0:
745
+ return
746
+
747
+ self.current_memory = self._get_process_memory()
748
+ self.peak_memory = max(self.peak_memory, self.current_memory)
749
+
750
+ # Check against available memory
751
+ available = get_available_memory()
752
+ critical_threshold = _memory_config.critical_threshold
753
+
754
+ if available < self.max_memory * (1 - critical_threshold):
755
+ raise MemoryError(
756
+ f"Memory limit approached during {self.operation}. "
757
+ f"Available: {available / 1e9:.2f} GB, "
758
+ f"Limit: {self.max_memory / 1e9:.2f} GB. "
759
+ f"Operation aborted to prevent system crash."
760
+ )
761
+
762
+ def _get_process_memory(self) -> int:
763
+ """Get current process memory usage in bytes."""
764
+ try:
765
+ import psutil
766
+
767
+ process = psutil.Process()
768
+ return process.memory_info().rss # type: ignore[no-any-return]
769
+ except ImportError:
770
+ # Fallback: use system available memory
771
+ return get_total_memory() - get_available_memory()
772
+
773
+ def get_stats(self) -> dict[str, int]:
774
+ """Get memory statistics for this monitoring session.
775
+
776
+ Returns:
777
+ Dictionary with start, current, and peak memory usage.
778
+
779
+ Example:
780
+ >>> with MemoryMonitor('fft') as monitor:
781
+ ... # ... do work ...
782
+ ... stats = monitor.get_stats()
783
+ >>> print(f"Peak memory: {stats['peak'] / 1e6:.1f} MB")
784
+ """
785
+ return {
786
+ "start": self.start_memory,
787
+ "current": self.current_memory,
788
+ "peak": self.peak_memory,
789
+ "delta": self.peak_memory - self.start_memory,
790
+ }
791
+
792
+
793
+ # ==========================================================================
794
+ # ==========================================================================
795
+
796
+
797
+ @dataclass
798
+ class ProgressInfo:
799
+ """Progress information with memory metrics.
800
+
801
+
802
+ Attributes:
803
+ current: Current progress value.
804
+ total: Total progress value.
805
+ eta_seconds: Estimated time to completion in seconds.
806
+ memory_used: Current memory usage in bytes.
807
+ memory_peak: Peak memory usage since start in bytes.
808
+ operation: Name of the operation.
809
+ """
810
+
811
+ current: int
812
+ total: int
813
+ eta_seconds: float
814
+ memory_used: int
815
+ memory_peak: int
816
+ operation: str
817
+
818
+ @property
819
+ def percent(self) -> float:
820
+ """Progress percentage (0.0-100.0)."""
821
+ if self.total == 0:
822
+ return 100.0
823
+ return (self.current / self.total) * 100.0
824
+
825
+ def format_progress(self) -> str:
826
+ """Format progress as human-readable string.
827
+
828
+ Returns:
829
+ Formatted string like "42.5% | 1.2 GB used | 2.1 GB peak | ETA 5s"
830
+ """
831
+ return (
832
+ f"{self.percent:.1f}% | "
833
+ f"{self.memory_used / 1e9:.2f} GB used | "
834
+ f"{self.memory_peak / 1e9:.2f} GB peak | "
835
+ f"ETA {self.eta_seconds:.0f}s"
836
+ )