oscura 0.0.1__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.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.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Memory-safe guards for TraceKit analysis.
|
|
2
|
+
|
|
3
|
+
This module provides memory guards and resource limiting utilities to prevent
|
|
4
|
+
out-of-memory conditions during analysis operations.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.core.memory_guard import MemoryGuard, check_memory_available
|
|
9
|
+
>>> if check_memory_available(500): # Need at least 500MB
|
|
10
|
+
... with MemoryGuard(max_mb=1000, name="fft") as guard:
|
|
11
|
+
... result = compute_fft(data)
|
|
12
|
+
... if not guard.check():
|
|
13
|
+
... raise MemoryError("Exceeded memory limit")
|
|
14
|
+
|
|
15
|
+
References:
|
|
16
|
+
See oscura.core.memory_monitor for runtime monitoring.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_memory_usage_mb() -> float:
|
|
30
|
+
"""Get current process memory usage in MB.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Current resident set size (RSS) in megabytes.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> mem_mb = get_memory_usage_mb()
|
|
37
|
+
>>> print(f"Current process using {mem_mb:.1f} MB")
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
import psutil
|
|
41
|
+
|
|
42
|
+
process = psutil.Process(os.getpid())
|
|
43
|
+
return float(process.memory_info().rss / (1024 * 1024))
|
|
44
|
+
except ImportError:
|
|
45
|
+
# Fallback for systems without psutil
|
|
46
|
+
logger.debug("psutil not available, memory usage tracking disabled")
|
|
47
|
+
return 0.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_memory_available(required_mb: float = 100) -> bool:
|
|
51
|
+
"""Check if sufficient memory is available.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
required_mb: Required available memory in megabytes.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if sufficient memory is available.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> if not check_memory_available(1000):
|
|
61
|
+
... print("Warning: Less than 1GB available")
|
|
62
|
+
... # Reduce batch size or chunk operations
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
import psutil
|
|
66
|
+
|
|
67
|
+
available = float(psutil.virtual_memory().available / (1024 * 1024))
|
|
68
|
+
return bool(available > required_mb)
|
|
69
|
+
except ImportError:
|
|
70
|
+
# Assume OK if we can't check
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MemoryGuard:
|
|
75
|
+
"""Context manager for memory-safe operations.
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
Monitors memory usage within a context and raises warnings/errors
|
|
79
|
+
if limits are exceeded.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
max_mb: Maximum memory limit in megabytes.
|
|
83
|
+
name: Operation name for logging.
|
|
84
|
+
start_mem: Starting memory usage (MB).
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> with MemoryGuard(max_mb=2000, name="spectrogram") as guard:
|
|
88
|
+
... # Perform memory-intensive operation
|
|
89
|
+
... for chunk in data_chunks:
|
|
90
|
+
... process_chunk(chunk)
|
|
91
|
+
... if not guard.check():
|
|
92
|
+
... break # Stop before exceeding limit
|
|
93
|
+
>>> stats = guard.get_stats()
|
|
94
|
+
>>> print(f"Peak: {stats['peak_mb']:.1f} MB, Delta: {stats['delta_mb']:.1f} MB")
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, max_mb: float = 1000, name: str = "operation"):
|
|
98
|
+
"""Initialize memory guard.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
max_mb: Maximum memory increase allowed in megabytes.
|
|
102
|
+
name: Operation name for logging and error messages.
|
|
103
|
+
"""
|
|
104
|
+
self.max_mb = max_mb
|
|
105
|
+
self.name = name
|
|
106
|
+
self.start_mem = 0.0
|
|
107
|
+
self._peak_mem = 0.0
|
|
108
|
+
|
|
109
|
+
def __enter__(self) -> MemoryGuard:
|
|
110
|
+
"""Enter context and record starting memory."""
|
|
111
|
+
self.start_mem = get_memory_usage_mb()
|
|
112
|
+
self._peak_mem = self.start_mem
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def __exit__(
|
|
116
|
+
self,
|
|
117
|
+
exc_type: type[BaseException] | None,
|
|
118
|
+
exc_val: BaseException | None,
|
|
119
|
+
exc_tb: Any,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Exit context and report memory usage."""
|
|
122
|
+
# Note: exc_val and exc_tb intentionally unused but required for Python 3.11+ compatibility
|
|
123
|
+
end_mem = get_memory_usage_mb()
|
|
124
|
+
delta = end_mem - self.start_mem
|
|
125
|
+
|
|
126
|
+
if delta > self.max_mb:
|
|
127
|
+
logger.warning(
|
|
128
|
+
f"{self.name} used {delta:.1f} MB (limit: {self.max_mb:.1f} MB). "
|
|
129
|
+
f"Consider reducing batch size or enabling chunked processing."
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Update peak
|
|
133
|
+
self._peak_mem = max(self._peak_mem, end_mem)
|
|
134
|
+
|
|
135
|
+
def check(self) -> bool:
|
|
136
|
+
"""Check if within memory limit.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if within limit, False if limit exceeded.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> with MemoryGuard(max_mb=500) as guard:
|
|
143
|
+
... for i in range(1000):
|
|
144
|
+
... # Do work
|
|
145
|
+
... if i % 100 == 0 and not guard.check():
|
|
146
|
+
... raise MemoryError("Memory limit exceeded")
|
|
147
|
+
"""
|
|
148
|
+
current = get_memory_usage_mb()
|
|
149
|
+
self._peak_mem = max(self._peak_mem, current)
|
|
150
|
+
delta = current - self.start_mem
|
|
151
|
+
|
|
152
|
+
if delta > self.max_mb:
|
|
153
|
+
logger.warning(
|
|
154
|
+
f"{self.name}: Memory usage {delta:.1f} MB exceeds limit {self.max_mb:.1f} MB"
|
|
155
|
+
)
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
return True
|
|
159
|
+
|
|
160
|
+
def get_stats(self) -> dict[str, float]:
|
|
161
|
+
"""Get memory statistics for this guard.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dictionary with keys:
|
|
165
|
+
- start_mb: Starting memory
|
|
166
|
+
- current_mb: Current memory
|
|
167
|
+
- peak_mb: Peak memory
|
|
168
|
+
- delta_mb: Memory increase since start
|
|
169
|
+
- limit_mb: Configured limit
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
>>> with MemoryGuard(max_mb=1000, name="test") as guard:
|
|
173
|
+
... # ... work ...
|
|
174
|
+
... pass
|
|
175
|
+
>>> stats = guard.get_stats()
|
|
176
|
+
>>> print(f"Used {stats['delta_mb']:.1f} / {stats['limit_mb']:.1f} MB")
|
|
177
|
+
"""
|
|
178
|
+
current = get_memory_usage_mb()
|
|
179
|
+
return {
|
|
180
|
+
"start_mb": self.start_mem,
|
|
181
|
+
"current_mb": current,
|
|
182
|
+
"peak_mb": self._peak_mem,
|
|
183
|
+
"delta_mb": current - self.start_mem,
|
|
184
|
+
"limit_mb": self.max_mb,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def safe_array_size(shape: tuple[int, ...], dtype_bytes: int = 8) -> int:
|
|
189
|
+
"""Calculate array size in bytes, checking for overflow.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
shape: Array shape tuple.
|
|
194
|
+
dtype_bytes: Bytes per element (default: 8 for float64).
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Total array size in bytes.
|
|
198
|
+
|
|
199
|
+
Raises:
|
|
200
|
+
OverflowError: If array size would overflow.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
>>> size = safe_array_size((1000, 1000, 8), dtype_bytes=8)
|
|
204
|
+
>>> print(f"Array would use {size / 1e6:.1f} MB")
|
|
205
|
+
>>> # Check if safe to allocate
|
|
206
|
+
>>> if can_allocate(size):
|
|
207
|
+
... arr = np.zeros((1000, 1000, 8))
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
import numpy as np
|
|
211
|
+
|
|
212
|
+
total_elements = np.prod(shape)
|
|
213
|
+
|
|
214
|
+
# Check for overflow in element count
|
|
215
|
+
if total_elements > sys.maxsize // dtype_bytes:
|
|
216
|
+
raise OverflowError(f"Array size too large: {shape}")
|
|
217
|
+
|
|
218
|
+
size = int(total_elements) * dtype_bytes
|
|
219
|
+
return size
|
|
220
|
+
|
|
221
|
+
except (OverflowError, ValueError) as e:
|
|
222
|
+
raise OverflowError(f"Array dimensions {shape} would cause overflow") from e
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def can_allocate(size_bytes: int) -> bool:
|
|
226
|
+
"""Check if allocation is safe given available memory.
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
size_bytes: Requested allocation size in bytes.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
True if allocation is safe (with 2x safety margin).
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
>>> import numpy as np
|
|
237
|
+
>>> shape = (10000, 10000)
|
|
238
|
+
>>> size = safe_array_size(shape, dtype_bytes=8)
|
|
239
|
+
>>> if can_allocate(size):
|
|
240
|
+
... arr = np.zeros(shape)
|
|
241
|
+
... else:
|
|
242
|
+
... print("Not enough memory, use chunked processing")
|
|
243
|
+
"""
|
|
244
|
+
size_mb = size_bytes / (1024 * 1024)
|
|
245
|
+
|
|
246
|
+
# Check with 2x safety margin
|
|
247
|
+
return check_memory_available(size_mb * 2)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def get_safe_chunk_size(
|
|
251
|
+
total_samples: int,
|
|
252
|
+
dtype_bytes: int = 8,
|
|
253
|
+
max_chunk_mb: float = 100,
|
|
254
|
+
) -> int:
|
|
255
|
+
"""Calculate safe chunk size for processing large datasets.
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
total_samples: Total number of samples to process.
|
|
260
|
+
dtype_bytes: Bytes per sample (default: 8 for float64).
|
|
261
|
+
max_chunk_mb: Maximum chunk size in megabytes.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Chunk size in samples that fits within memory limit.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
>>> total = 1_000_000_000 # 1 billion samples
|
|
268
|
+
>>> chunk_size = get_safe_chunk_size(total, max_chunk_mb=100)
|
|
269
|
+
>>> print(f"Process in chunks of {chunk_size:,} samples")
|
|
270
|
+
>>> for i in range(0, total, chunk_size):
|
|
271
|
+
... chunk = data[i:i+chunk_size]
|
|
272
|
+
... process(chunk)
|
|
273
|
+
"""
|
|
274
|
+
max_bytes = max_chunk_mb * 1024 * 1024
|
|
275
|
+
max_samples = max_bytes // dtype_bytes
|
|
276
|
+
|
|
277
|
+
# Ensure at least 1000 samples per chunk, but not more than total
|
|
278
|
+
chunk_size = max(1000, min(max_samples, total_samples))
|
|
279
|
+
|
|
280
|
+
return int(chunk_size)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
__all__ = [
|
|
284
|
+
"MemoryGuard",
|
|
285
|
+
"can_allocate",
|
|
286
|
+
"check_memory_available",
|
|
287
|
+
"get_memory_usage_mb",
|
|
288
|
+
"get_safe_chunk_size",
|
|
289
|
+
"safe_array_size",
|
|
290
|
+
]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Per-operation memory limits for TraceKit.
|
|
2
|
+
|
|
3
|
+
This module provides fine-grained memory control for individual operations
|
|
4
|
+
with automatic parameter adjustment to fit memory constraints.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.core.memory_limits import apply_memory_limit
|
|
9
|
+
>>> params = apply_memory_limit('spectrogram', samples=1e9, max_memory='512MB')
|
|
10
|
+
>>> print(f"Adjusted nperseg: {params['nperseg']}")
|
|
11
|
+
|
|
12
|
+
References:
|
|
13
|
+
See oscura.config.memory for global memory configuration.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import warnings
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from oscura.config.memory import get_memory_config
|
|
22
|
+
from oscura.utils.memory import estimate_memory
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def parse_memory_limit(limit: int | str | None) -> int | None:
|
|
26
|
+
"""Parse memory limit from various formats.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
limit: Memory limit as bytes (int), string ("4GB", "512MB"), or None.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Memory limit in bytes, or None for no limit.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If format is invalid.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> parse_memory_limit("4GB")
|
|
39
|
+
4000000000
|
|
40
|
+
>>> parse_memory_limit(512 * 1024**2)
|
|
41
|
+
536870912
|
|
42
|
+
>>> parse_memory_limit(None) is None
|
|
43
|
+
True
|
|
44
|
+
"""
|
|
45
|
+
if limit is None:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
if isinstance(limit, str):
|
|
49
|
+
limit_upper = limit.upper().strip()
|
|
50
|
+
try:
|
|
51
|
+
if limit_upper.endswith("GB"):
|
|
52
|
+
return int(float(limit_upper[:-2]) * 1e9)
|
|
53
|
+
elif limit_upper.endswith("MB"):
|
|
54
|
+
return int(float(limit_upper[:-2]) * 1e6)
|
|
55
|
+
elif limit_upper.endswith("KB"):
|
|
56
|
+
return int(float(limit_upper[:-2]) * 1e3)
|
|
57
|
+
elif limit_upper.endswith("GIB"):
|
|
58
|
+
return int(float(limit_upper[:-3]) * 1024**3)
|
|
59
|
+
elif limit_upper.endswith("MIB"):
|
|
60
|
+
return int(float(limit_upper[:-3]) * 1024**2)
|
|
61
|
+
elif limit_upper.endswith("KIB"):
|
|
62
|
+
return int(float(limit_upper[:-3]) * 1024)
|
|
63
|
+
else:
|
|
64
|
+
return int(float(limit_upper))
|
|
65
|
+
except ValueError as e:
|
|
66
|
+
raise ValueError(f"Invalid memory limit format: {limit}") from e
|
|
67
|
+
|
|
68
|
+
return int(limit)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def apply_memory_limit(
|
|
72
|
+
operation: str,
|
|
73
|
+
samples: int | float,
|
|
74
|
+
*,
|
|
75
|
+
max_memory: int | str | None = None,
|
|
76
|
+
**params: Any,
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Apply memory limit and adjust parameters to fit.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
operation: Operation name (fft, psd, spectrogram, etc.).
|
|
83
|
+
samples: Number of samples to process.
|
|
84
|
+
max_memory: Maximum memory limit (overrides global config if provided).
|
|
85
|
+
**params: Operation parameters to adjust.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Adjusted parameters dictionary that fits within memory limit.
|
|
89
|
+
|
|
90
|
+
Example:
|
|
91
|
+
>>> params = apply_memory_limit('spectrogram', samples=1e9, max_memory='512MB', nperseg=8192)
|
|
92
|
+
>>> print(f"Adjusted to nperseg={params['nperseg']} to fit 512MB")
|
|
93
|
+
|
|
94
|
+
Note:
|
|
95
|
+
If parameters cannot be adjusted to fit memory, a warning is issued
|
|
96
|
+
and the original parameters are returned.
|
|
97
|
+
"""
|
|
98
|
+
# Parse memory limit
|
|
99
|
+
limit_bytes = parse_memory_limit(max_memory)
|
|
100
|
+
if limit_bytes is None:
|
|
101
|
+
# Use global config
|
|
102
|
+
config = get_memory_config()
|
|
103
|
+
limit_bytes = config.max_memory
|
|
104
|
+
if limit_bytes is None:
|
|
105
|
+
# No limit, return params unchanged
|
|
106
|
+
return params
|
|
107
|
+
|
|
108
|
+
samples = int(samples)
|
|
109
|
+
|
|
110
|
+
# Estimate with current parameters
|
|
111
|
+
current_estimate = estimate_memory(operation, samples, **params)
|
|
112
|
+
|
|
113
|
+
if current_estimate.total <= limit_bytes:
|
|
114
|
+
# Already within limit
|
|
115
|
+
return params
|
|
116
|
+
|
|
117
|
+
# Need to adjust parameters
|
|
118
|
+
adjusted_params = params.copy()
|
|
119
|
+
|
|
120
|
+
if operation in ("fft", "psd"):
|
|
121
|
+
# Reduce nfft if specified
|
|
122
|
+
if "nfft" in adjusted_params:
|
|
123
|
+
# Try reducing nfft
|
|
124
|
+
original_nfft = adjusted_params["nfft"]
|
|
125
|
+
# Binary search for suitable nfft
|
|
126
|
+
nfft = _find_max_nfft(operation, samples, limit_bytes, **adjusted_params)
|
|
127
|
+
if nfft < original_nfft:
|
|
128
|
+
adjusted_params["nfft"] = nfft
|
|
129
|
+
warnings.warn(
|
|
130
|
+
f"Reduced nfft from {original_nfft} to {nfft} to fit {limit_bytes / 1e6:.1f} MB limit",
|
|
131
|
+
UserWarning,
|
|
132
|
+
stacklevel=2,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
elif operation == "spectrogram":
|
|
136
|
+
# Adjust nperseg and/or nfft
|
|
137
|
+
original_nperseg = adjusted_params.get("nperseg", 256)
|
|
138
|
+
original_nfft = adjusted_params.get("nfft", original_nperseg)
|
|
139
|
+
|
|
140
|
+
# Try reducing nperseg first
|
|
141
|
+
nperseg = _find_max_nperseg(samples, limit_bytes, noverlap=adjusted_params.get("noverlap"))
|
|
142
|
+
if nperseg < original_nperseg:
|
|
143
|
+
adjusted_params["nperseg"] = nperseg
|
|
144
|
+
# Adjust noverlap proportionally
|
|
145
|
+
if "noverlap" in adjusted_params:
|
|
146
|
+
overlap_ratio = adjusted_params["noverlap"] / original_nperseg
|
|
147
|
+
adjusted_params["noverlap"] = int(nperseg * overlap_ratio)
|
|
148
|
+
warnings.warn(
|
|
149
|
+
f"Reduced nperseg from {original_nperseg} to {nperseg} to fit {limit_bytes / 1e6:.1f} MB limit",
|
|
150
|
+
UserWarning,
|
|
151
|
+
stacklevel=2,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Also reduce nfft if needed
|
|
155
|
+
if "nfft" in adjusted_params and adjusted_params["nfft"] > nperseg:
|
|
156
|
+
adjusted_params["nfft"] = nperseg
|
|
157
|
+
|
|
158
|
+
elif operation == "eye_diagram":
|
|
159
|
+
# Reduce samples_per_ui or num_uis
|
|
160
|
+
if "num_uis" in adjusted_params:
|
|
161
|
+
original_num_uis = adjusted_params["num_uis"]
|
|
162
|
+
# Calculate max num_uis that fits
|
|
163
|
+
samples_per_ui = adjusted_params.get("samples_per_ui", 100)
|
|
164
|
+
max_num_uis = _find_max_num_uis(limit_bytes, samples_per_ui)
|
|
165
|
+
if max_num_uis < original_num_uis:
|
|
166
|
+
adjusted_params["num_uis"] = max_num_uis
|
|
167
|
+
warnings.warn(
|
|
168
|
+
f"Reduced num_uis from {original_num_uis} to {max_num_uis} to fit {limit_bytes / 1e6:.1f} MB limit",
|
|
169
|
+
UserWarning,
|
|
170
|
+
stacklevel=2,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Verify final estimate
|
|
174
|
+
final_estimate = estimate_memory(operation, samples, **adjusted_params)
|
|
175
|
+
if final_estimate.total > limit_bytes:
|
|
176
|
+
warnings.warn(
|
|
177
|
+
f"Could not adjust parameters to fit {limit_bytes / 1e6:.1f} MB limit. "
|
|
178
|
+
f"Operation requires {final_estimate.total / 1e6:.1f} MB. "
|
|
179
|
+
"Consider using chunked processing or increasing memory limit.",
|
|
180
|
+
UserWarning,
|
|
181
|
+
stacklevel=2,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return adjusted_params
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _find_max_nfft(operation: str, samples: int, limit_bytes: int, **params: Any) -> int:
|
|
188
|
+
"""Binary search for maximum nfft that fits memory limit.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
operation: Operation name.
|
|
192
|
+
samples: Number of samples.
|
|
193
|
+
limit_bytes: Memory limit in bytes.
|
|
194
|
+
**params: Additional parameters.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Maximum nfft that fits within limit.
|
|
198
|
+
"""
|
|
199
|
+
min_nfft = 64
|
|
200
|
+
max_nfft = params.get("nfft", 8192)
|
|
201
|
+
|
|
202
|
+
# Binary search
|
|
203
|
+
while min_nfft < max_nfft:
|
|
204
|
+
mid_nfft = (min_nfft + max_nfft + 1) // 2
|
|
205
|
+
test_params = {**params, "nfft": mid_nfft}
|
|
206
|
+
estimate = estimate_memory(operation, samples, **test_params)
|
|
207
|
+
|
|
208
|
+
if estimate.total <= limit_bytes:
|
|
209
|
+
min_nfft = mid_nfft
|
|
210
|
+
else:
|
|
211
|
+
max_nfft = mid_nfft - 1
|
|
212
|
+
|
|
213
|
+
return min_nfft
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _find_max_nperseg(samples: int, limit_bytes: int, noverlap: int | None = None) -> int:
|
|
217
|
+
"""Binary search for maximum nperseg that fits memory limit.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
samples: Number of samples.
|
|
221
|
+
limit_bytes: Memory limit in bytes.
|
|
222
|
+
noverlap: Overlap samples (if specified).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Maximum nperseg that fits within limit.
|
|
226
|
+
"""
|
|
227
|
+
min_nperseg = 64
|
|
228
|
+
max_nperseg = min(8192, samples // 4)
|
|
229
|
+
|
|
230
|
+
# Binary search
|
|
231
|
+
while min_nperseg < max_nperseg:
|
|
232
|
+
mid_nperseg = (min_nperseg + max_nperseg + 1) // 2
|
|
233
|
+
|
|
234
|
+
# Calculate memory for this nperseg
|
|
235
|
+
hop = mid_nperseg - (noverlap or mid_nperseg // 2)
|
|
236
|
+
num_segments = max(1, (samples - (noverlap or mid_nperseg // 2)) // hop)
|
|
237
|
+
|
|
238
|
+
# Estimate memory
|
|
239
|
+
bytes_per_sample = 8 # float64
|
|
240
|
+
data_mem = samples * bytes_per_sample
|
|
241
|
+
intermediate_mem = mid_nperseg * bytes_per_sample * 2 + mid_nperseg * bytes_per_sample * 2
|
|
242
|
+
output_mem = (mid_nperseg // 2 + 1) * num_segments * bytes_per_sample * 2
|
|
243
|
+
|
|
244
|
+
total_mem = data_mem + intermediate_mem + output_mem
|
|
245
|
+
|
|
246
|
+
if total_mem <= limit_bytes:
|
|
247
|
+
min_nperseg = mid_nperseg
|
|
248
|
+
else:
|
|
249
|
+
max_nperseg = mid_nperseg - 1
|
|
250
|
+
|
|
251
|
+
return min_nperseg
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _find_max_num_uis(limit_bytes: int, samples_per_ui: int) -> int:
|
|
255
|
+
"""Find maximum num_uis that fits memory limit for eye diagrams.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
limit_bytes: Memory limit in bytes.
|
|
259
|
+
samples_per_ui: Samples per unit interval.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Maximum num_uis that fits.
|
|
263
|
+
"""
|
|
264
|
+
bytes_per_sample = 8 # float64
|
|
265
|
+
# Eye diagram memory: samples_per_ui * num_uis * bytes_per_sample
|
|
266
|
+
max_num_uis = limit_bytes // (samples_per_ui * bytes_per_sample * 2)
|
|
267
|
+
return max(100, int(max_num_uis)) # At least 100 UIs
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def get_operation_memory_limit(
|
|
271
|
+
operation: str,
|
|
272
|
+
max_memory: int | str | None = None,
|
|
273
|
+
) -> int:
|
|
274
|
+
"""Get effective memory limit for an operation.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
operation: Operation name.
|
|
278
|
+
max_memory: Override limit (or None for global config).
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Memory limit in bytes.
|
|
282
|
+
|
|
283
|
+
Example:
|
|
284
|
+
>>> limit = get_operation_memory_limit('spectrogram', max_memory='512MB')
|
|
285
|
+
>>> print(f"Limit: {limit / 1e6:.1f} MB")
|
|
286
|
+
"""
|
|
287
|
+
# Parse override
|
|
288
|
+
limit_bytes = parse_memory_limit(max_memory)
|
|
289
|
+
if limit_bytes is not None:
|
|
290
|
+
return limit_bytes
|
|
291
|
+
|
|
292
|
+
# Use global config
|
|
293
|
+
config = get_memory_config()
|
|
294
|
+
if config.max_memory is not None:
|
|
295
|
+
return config.max_memory
|
|
296
|
+
|
|
297
|
+
# Default: 80% of available
|
|
298
|
+
from oscura.utils.memory import get_available_memory
|
|
299
|
+
|
|
300
|
+
return int(get_available_memory() * 0.8)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def check_operation_fits(
|
|
304
|
+
operation: str,
|
|
305
|
+
samples: int | float,
|
|
306
|
+
*,
|
|
307
|
+
max_memory: int | str | None = None,
|
|
308
|
+
**params: Any,
|
|
309
|
+
) -> bool:
|
|
310
|
+
"""Check if operation with given parameters fits within memory limit.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
operation: Operation name.
|
|
314
|
+
samples: Number of samples.
|
|
315
|
+
max_memory: Memory limit (or None for global config).
|
|
316
|
+
**params: Operation parameters.
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
True if operation fits within limit.
|
|
320
|
+
|
|
321
|
+
Example:
|
|
322
|
+
>>> fits = check_operation_fits('fft', samples=1e9, max_memory='4GB', nfft=8192)
|
|
323
|
+
>>> if not fits:
|
|
324
|
+
... print("FFT too large for 4GB limit")
|
|
325
|
+
"""
|
|
326
|
+
limit_bytes = get_operation_memory_limit(operation, max_memory)
|
|
327
|
+
estimate = estimate_memory(operation, samples, **params)
|
|
328
|
+
return estimate.total <= limit_bytes
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
__all__ = [
|
|
332
|
+
"apply_memory_limit",
|
|
333
|
+
"check_operation_fits",
|
|
334
|
+
"get_operation_memory_limit",
|
|
335
|
+
"parse_memory_limit",
|
|
336
|
+
]
|