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.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {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
|
+
)
|