oscura 0.5.1__py3-none-any.whl → 0.7.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 +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
"""Memory optimization for large signal file processing.
|
|
2
|
+
|
|
3
|
+
This module provides memory management utilities for processing huge datasets
|
|
4
|
+
efficiently without running out of RAM. Implements memory-mapped I/O, streaming
|
|
5
|
+
analysis, adaptive chunking, and memory leak detection.
|
|
6
|
+
|
|
7
|
+
Key Features:
|
|
8
|
+
- Memory-mapped file I/O for huge datasets (via numpy.memmap)
|
|
9
|
+
- Streaming iterators for chunk-by-chunk processing
|
|
10
|
+
- Lazy loading (load data only when accessed)
|
|
11
|
+
- Adaptive chunking based on available memory
|
|
12
|
+
- Real-time memory usage tracking and leak detection
|
|
13
|
+
- Object pooling for frequently allocated objects
|
|
14
|
+
- In-memory compression for infrequently accessed data
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> from oscura.utils.performance.memory_optimizer import MemoryOptimizer
|
|
18
|
+
>>>
|
|
19
|
+
>>> # Optimize memory usage for large file
|
|
20
|
+
>>> optimizer = MemoryOptimizer(max_memory_mb=1024)
|
|
21
|
+
>>> trace = optimizer.load_optimized("huge_file.npy", sample_rate=1e9)
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Process in chunks with streaming
|
|
24
|
+
>>> processor = optimizer.create_stream_processor(
|
|
25
|
+
... trace, chunk_size=1_000_000, overlap=0
|
|
26
|
+
... )
|
|
27
|
+
>>> for chunk in processor:
|
|
28
|
+
... result = analyze(chunk)
|
|
29
|
+
>>>
|
|
30
|
+
>>> # Check memory statistics
|
|
31
|
+
>>> stats = optimizer.get_memory_stats()
|
|
32
|
+
>>> print(f"Peak memory: {stats.peak_memory_mb:.1f} MB")
|
|
33
|
+
>>> if stats.leak_detected:
|
|
34
|
+
... print("Warning: Memory leak detected!")
|
|
35
|
+
|
|
36
|
+
References:
|
|
37
|
+
Phase 5 Feature 42: Memory Optimization (v0.6.0)
|
|
38
|
+
Streaming APIs: src/oscura/streaming/chunked.py
|
|
39
|
+
Memory-mapped loading: src/oscura/loaders/mmap_loader.py
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import gc
|
|
45
|
+
from collections.abc import Iterator
|
|
46
|
+
from dataclasses import dataclass
|
|
47
|
+
from enum import Enum
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from typing import TYPE_CHECKING, Any
|
|
50
|
+
|
|
51
|
+
import numpy as np
|
|
52
|
+
import psutil
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from os import PathLike
|
|
56
|
+
|
|
57
|
+
from numpy.typing import DTypeLike, NDArray
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChunkingStrategy(Enum):
|
|
61
|
+
"""Chunking strategy for data processing."""
|
|
62
|
+
|
|
63
|
+
FIXED = "fixed" # Fixed-size chunks
|
|
64
|
+
SLIDING = "sliding" # Sliding window with overlap
|
|
65
|
+
ADAPTIVE = "adaptive" # Adaptive chunk size based on memory
|
|
66
|
+
TIME_BASED = "time_based" # Chunk by time duration
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ChunkingConfig:
|
|
71
|
+
"""Configuration for data chunking strategy.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
strategy: Chunking strategy to use.
|
|
75
|
+
chunk_size: Size of each chunk in samples (for fixed/sliding).
|
|
76
|
+
overlap: Number of samples to overlap between chunks.
|
|
77
|
+
adaptive: Whether to adaptively adjust chunk size based on memory.
|
|
78
|
+
time_window: Time duration for time-based chunking (seconds).
|
|
79
|
+
min_chunk_size: Minimum chunk size in samples (for adaptive).
|
|
80
|
+
max_chunk_size: Maximum chunk size in samples (for adaptive).
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> # Fixed-size chunks
|
|
84
|
+
>>> config = ChunkingConfig(
|
|
85
|
+
... strategy=ChunkingStrategy.FIXED,
|
|
86
|
+
... chunk_size=1_000_000
|
|
87
|
+
... )
|
|
88
|
+
>>>
|
|
89
|
+
>>> # Adaptive chunking
|
|
90
|
+
>>> config = ChunkingConfig(
|
|
91
|
+
... strategy=ChunkingStrategy.ADAPTIVE,
|
|
92
|
+
... adaptive=True,
|
|
93
|
+
... min_chunk_size=100_000,
|
|
94
|
+
... max_chunk_size=10_000_000
|
|
95
|
+
... )
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
strategy: ChunkingStrategy = ChunkingStrategy.FIXED
|
|
99
|
+
chunk_size: int = 1_000_000
|
|
100
|
+
overlap: int = 0
|
|
101
|
+
adaptive: bool = False
|
|
102
|
+
time_window: float | None = None
|
|
103
|
+
min_chunk_size: int = 100_000
|
|
104
|
+
max_chunk_size: int = 10_000_000
|
|
105
|
+
|
|
106
|
+
def __post_init__(self) -> None:
|
|
107
|
+
"""Validate configuration."""
|
|
108
|
+
if self.chunk_size <= 0:
|
|
109
|
+
raise ValueError(f"chunk_size must be positive, got {self.chunk_size}")
|
|
110
|
+
if self.overlap < 0:
|
|
111
|
+
raise ValueError(f"overlap must be non-negative, got {self.overlap}")
|
|
112
|
+
if self.overlap >= self.chunk_size:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"overlap ({self.overlap}) must be less than chunk_size ({self.chunk_size})"
|
|
115
|
+
)
|
|
116
|
+
if self.min_chunk_size <= 0:
|
|
117
|
+
raise ValueError(f"min_chunk_size must be positive, got {self.min_chunk_size}")
|
|
118
|
+
if self.max_chunk_size < self.min_chunk_size:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"max_chunk_size ({self.max_chunk_size}) must be >= "
|
|
121
|
+
f"min_chunk_size ({self.min_chunk_size})"
|
|
122
|
+
)
|
|
123
|
+
if self.time_window is not None and self.time_window <= 0:
|
|
124
|
+
raise ValueError(f"time_window must be positive, got {self.time_window}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class MemoryStats:
|
|
129
|
+
"""Memory usage statistics.
|
|
130
|
+
|
|
131
|
+
Attributes:
|
|
132
|
+
peak_memory_mb: Peak memory usage in megabytes.
|
|
133
|
+
current_memory_mb: Current memory usage in megabytes.
|
|
134
|
+
allocated_mb: Total memory allocated in megabytes.
|
|
135
|
+
freed_mb: Total memory freed in megabytes.
|
|
136
|
+
leak_detected: Whether a memory leak was detected.
|
|
137
|
+
available_memory_mb: Available system memory in megabytes.
|
|
138
|
+
usage_percent: Memory usage as percentage of total.
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
>>> stats = optimizer.get_memory_stats()
|
|
142
|
+
>>> print(f"Current: {stats.current_memory_mb:.1f} MB")
|
|
143
|
+
>>> print(f"Peak: {stats.peak_memory_mb:.1f} MB")
|
|
144
|
+
>>> if stats.leak_detected:
|
|
145
|
+
... print("WARNING: Memory leak detected!")
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
peak_memory_mb: float
|
|
149
|
+
current_memory_mb: float
|
|
150
|
+
allocated_mb: float
|
|
151
|
+
freed_mb: float
|
|
152
|
+
leak_detected: bool
|
|
153
|
+
available_memory_mb: float
|
|
154
|
+
usage_percent: float
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class StreamProcessor:
|
|
158
|
+
"""Streaming processor for chunk-by-chunk data processing.
|
|
159
|
+
|
|
160
|
+
Provides iterator interface for processing large datasets in chunks
|
|
161
|
+
without loading all data into memory. Supports overlapping chunks
|
|
162
|
+
for windowed operations.
|
|
163
|
+
|
|
164
|
+
Attributes:
|
|
165
|
+
data: Data source (array or memory-mapped array).
|
|
166
|
+
chunk_size: Size of each chunk in samples.
|
|
167
|
+
overlap: Number of samples to overlap between chunks.
|
|
168
|
+
total_samples: Total number of samples in data.
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
>>> processor = StreamProcessor(
|
|
172
|
+
... data=huge_array,
|
|
173
|
+
... chunk_size=1_000_000,
|
|
174
|
+
... overlap=1024
|
|
175
|
+
... )
|
|
176
|
+
>>> for chunk in processor:
|
|
177
|
+
... result = analyze_chunk(chunk)
|
|
178
|
+
... print(f"Processed {len(chunk)} samples")
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
data: NDArray[Any] | np.memmap[Any, Any],
|
|
184
|
+
chunk_size: int,
|
|
185
|
+
overlap: int = 0,
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Initialize stream processor.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
data: Data array or memmap to process.
|
|
191
|
+
chunk_size: Number of samples per chunk.
|
|
192
|
+
overlap: Number of samples to overlap between chunks.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
ValueError: If chunk_size or overlap invalid.
|
|
196
|
+
"""
|
|
197
|
+
if chunk_size <= 0:
|
|
198
|
+
raise ValueError(f"chunk_size must be positive, got {chunk_size}")
|
|
199
|
+
if overlap < 0:
|
|
200
|
+
raise ValueError(f"overlap must be non-negative, got {overlap}")
|
|
201
|
+
if overlap >= chunk_size:
|
|
202
|
+
raise ValueError(f"overlap ({overlap}) must be less than chunk_size ({chunk_size})")
|
|
203
|
+
|
|
204
|
+
self.data = data
|
|
205
|
+
self.chunk_size = chunk_size
|
|
206
|
+
self.overlap = overlap
|
|
207
|
+
self.total_samples = len(data)
|
|
208
|
+
self._current_position = 0
|
|
209
|
+
|
|
210
|
+
def __iter__(self) -> Iterator[NDArray[np.float64]]:
|
|
211
|
+
"""Iterate over chunks.
|
|
212
|
+
|
|
213
|
+
Yields:
|
|
214
|
+
Chunks of data as numpy arrays.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
>>> for chunk in processor:
|
|
218
|
+
... mean = np.mean(chunk)
|
|
219
|
+
... print(f"Chunk mean: {mean}")
|
|
220
|
+
"""
|
|
221
|
+
self._current_position = 0
|
|
222
|
+
|
|
223
|
+
while self._current_position < self.total_samples:
|
|
224
|
+
end = min(self._current_position + self.chunk_size, self.total_samples)
|
|
225
|
+
chunk = self.data[self._current_position : end]
|
|
226
|
+
|
|
227
|
+
# Convert memmap to regular array to avoid keeping file handle open
|
|
228
|
+
if isinstance(chunk, np.memmap):
|
|
229
|
+
chunk = np.asarray(chunk, dtype=np.float64)
|
|
230
|
+
|
|
231
|
+
yield chunk
|
|
232
|
+
|
|
233
|
+
self._current_position = end - self.overlap
|
|
234
|
+
|
|
235
|
+
# Break if we've reached the end
|
|
236
|
+
if end >= self.total_samples:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
def __len__(self) -> int:
|
|
240
|
+
"""Number of chunks that will be yielded.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Total number of chunks.
|
|
244
|
+
"""
|
|
245
|
+
step = self.chunk_size - self.overlap
|
|
246
|
+
num_chunks = (self.total_samples - self.overlap) // step
|
|
247
|
+
if (self.total_samples - self.overlap) % step != 0:
|
|
248
|
+
num_chunks += 1
|
|
249
|
+
return num_chunks
|
|
250
|
+
|
|
251
|
+
def reset(self) -> None:
|
|
252
|
+
"""Reset iterator to beginning.
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
>>> processor.reset()
|
|
256
|
+
>>> for chunk in processor:
|
|
257
|
+
... process(chunk)
|
|
258
|
+
"""
|
|
259
|
+
self._current_position = 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class MemoryOptimizer:
|
|
263
|
+
"""Memory optimizer for large signal file processing.
|
|
264
|
+
|
|
265
|
+
Provides comprehensive memory management including memory-mapped I/O,
|
|
266
|
+
streaming analysis, adaptive chunking, and memory leak detection.
|
|
267
|
+
|
|
268
|
+
Attributes:
|
|
269
|
+
max_memory_mb: Maximum memory limit in megabytes (None = no limit).
|
|
270
|
+
enable_compression: Whether to enable in-memory compression.
|
|
271
|
+
gc_threshold: Memory usage threshold to trigger garbage collection.
|
|
272
|
+
|
|
273
|
+
Example:
|
|
274
|
+
>>> # Create optimizer with 2 GB memory limit
|
|
275
|
+
>>> optimizer = MemoryOptimizer(max_memory_mb=2048)
|
|
276
|
+
>>>
|
|
277
|
+
>>> # Load file optimally (mmap if huge, eager if small)
|
|
278
|
+
>>> trace = optimizer.load_optimized("data.npy", sample_rate=1e9)
|
|
279
|
+
>>>
|
|
280
|
+
>>> # Get recommended chunk size
|
|
281
|
+
>>> chunk_size = optimizer.recommend_chunk_size(
|
|
282
|
+
... data_length=100_000_000,
|
|
283
|
+
... dtype=np.float64
|
|
284
|
+
... )
|
|
285
|
+
>>>
|
|
286
|
+
>>> # Create streaming processor
|
|
287
|
+
>>> processor = optimizer.create_stream_processor(
|
|
288
|
+
... trace, chunk_size=chunk_size
|
|
289
|
+
... )
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
def __init__(
|
|
293
|
+
self,
|
|
294
|
+
max_memory_mb: float | None = None,
|
|
295
|
+
enable_compression: bool = False,
|
|
296
|
+
gc_threshold: float = 0.8,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Initialize memory optimizer.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
max_memory_mb: Maximum memory limit in MB (None = no limit).
|
|
302
|
+
enable_compression: Enable in-memory compression.
|
|
303
|
+
gc_threshold: Memory usage fraction to trigger GC (0.0-1.0).
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValueError: If parameters invalid.
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
>>> # Limit to 1 GB with aggressive GC
|
|
310
|
+
>>> optimizer = MemoryOptimizer(
|
|
311
|
+
... max_memory_mb=1024,
|
|
312
|
+
... gc_threshold=0.7
|
|
313
|
+
... )
|
|
314
|
+
"""
|
|
315
|
+
if max_memory_mb is not None and max_memory_mb <= 0:
|
|
316
|
+
raise ValueError(f"max_memory_mb must be positive, got {max_memory_mb}")
|
|
317
|
+
if not 0.0 <= gc_threshold <= 1.0:
|
|
318
|
+
raise ValueError(f"gc_threshold must be in [0, 1], got {gc_threshold}")
|
|
319
|
+
|
|
320
|
+
self.max_memory_mb = max_memory_mb
|
|
321
|
+
self.enable_compression = enable_compression
|
|
322
|
+
self.gc_threshold = gc_threshold
|
|
323
|
+
|
|
324
|
+
# Memory tracking
|
|
325
|
+
self._initial_memory_mb = self._get_memory_usage()
|
|
326
|
+
self._peak_memory_mb = self._initial_memory_mb
|
|
327
|
+
self._total_allocated_mb = 0.0
|
|
328
|
+
self._total_freed_mb = 0.0
|
|
329
|
+
self._allocation_count = 0
|
|
330
|
+
self._last_gc_memory = self._initial_memory_mb
|
|
331
|
+
|
|
332
|
+
def _get_memory_usage(self) -> float:
|
|
333
|
+
"""Get current process memory usage in MB.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Memory usage in megabytes.
|
|
337
|
+
"""
|
|
338
|
+
process = psutil.Process()
|
|
339
|
+
return float(process.memory_info().rss / (1024 * 1024))
|
|
340
|
+
|
|
341
|
+
def _get_available_memory(self) -> float:
|
|
342
|
+
"""Get available system memory in MB.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Available memory in megabytes.
|
|
346
|
+
"""
|
|
347
|
+
return float(psutil.virtual_memory().available / (1024 * 1024))
|
|
348
|
+
|
|
349
|
+
def _update_memory_tracking(self) -> None:
|
|
350
|
+
"""Update memory usage tracking and trigger GC if needed."""
|
|
351
|
+
current_memory = self._get_memory_usage()
|
|
352
|
+
|
|
353
|
+
# Update peak
|
|
354
|
+
if current_memory > self._peak_memory_mb:
|
|
355
|
+
self._peak_memory_mb = current_memory
|
|
356
|
+
|
|
357
|
+
# Track allocations
|
|
358
|
+
memory_delta = current_memory - self._last_gc_memory
|
|
359
|
+
if memory_delta > 0:
|
|
360
|
+
self._total_allocated_mb += memory_delta
|
|
361
|
+
self._allocation_count += 1
|
|
362
|
+
elif memory_delta < 0:
|
|
363
|
+
self._total_freed_mb += abs(memory_delta)
|
|
364
|
+
|
|
365
|
+
# Check if GC needed
|
|
366
|
+
if self.max_memory_mb is not None:
|
|
367
|
+
usage_fraction = current_memory / self.max_memory_mb
|
|
368
|
+
if usage_fraction >= self.gc_threshold:
|
|
369
|
+
self._trigger_gc()
|
|
370
|
+
self._last_gc_memory = self._get_memory_usage()
|
|
371
|
+
else:
|
|
372
|
+
# Use available system memory
|
|
373
|
+
available = self._get_available_memory()
|
|
374
|
+
total = psutil.virtual_memory().total / (1024 * 1024)
|
|
375
|
+
usage_fraction = 1.0 - (available / total)
|
|
376
|
+
if usage_fraction >= self.gc_threshold:
|
|
377
|
+
self._trigger_gc()
|
|
378
|
+
self._last_gc_memory = self._get_memory_usage()
|
|
379
|
+
|
|
380
|
+
def _trigger_gc(self) -> None:
|
|
381
|
+
"""Trigger garbage collection to free memory."""
|
|
382
|
+
gc.collect()
|
|
383
|
+
|
|
384
|
+
def _detect_memory_leak(self) -> bool:
|
|
385
|
+
"""Detect potential memory leak.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
True if leak detected, False otherwise.
|
|
389
|
+
|
|
390
|
+
Note:
|
|
391
|
+
Detects leak if allocated memory keeps growing without being freed.
|
|
392
|
+
"""
|
|
393
|
+
if self._allocation_count < 10:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
# Check if allocated > freed by large margin
|
|
397
|
+
leak_threshold = 100.0 # MB
|
|
398
|
+
net_growth = self._total_allocated_mb - self._total_freed_mb
|
|
399
|
+
|
|
400
|
+
return net_growth > leak_threshold
|
|
401
|
+
|
|
402
|
+
def get_memory_stats(self) -> MemoryStats:
|
|
403
|
+
"""Get current memory usage statistics.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
MemoryStats with current memory usage information.
|
|
407
|
+
|
|
408
|
+
Example:
|
|
409
|
+
>>> stats = optimizer.get_memory_stats()
|
|
410
|
+
>>> print(f"Peak: {stats.peak_memory_mb:.1f} MB")
|
|
411
|
+
>>> print(f"Available: {stats.available_memory_mb:.1f} MB")
|
|
412
|
+
>>> if stats.leak_detected:
|
|
413
|
+
... print("WARNING: Memory leak detected!")
|
|
414
|
+
"""
|
|
415
|
+
current = self._get_memory_usage()
|
|
416
|
+
available = self._get_available_memory()
|
|
417
|
+
total = psutil.virtual_memory().total / (1024 * 1024)
|
|
418
|
+
|
|
419
|
+
return MemoryStats(
|
|
420
|
+
peak_memory_mb=self._peak_memory_mb,
|
|
421
|
+
current_memory_mb=current,
|
|
422
|
+
allocated_mb=self._total_allocated_mb,
|
|
423
|
+
freed_mb=self._total_freed_mb,
|
|
424
|
+
leak_detected=self._detect_memory_leak(),
|
|
425
|
+
available_memory_mb=available,
|
|
426
|
+
usage_percent=(current / total) * 100.0,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def recommend_chunk_size(
|
|
430
|
+
self,
|
|
431
|
+
data_length: int,
|
|
432
|
+
dtype: DTypeLike = np.float64,
|
|
433
|
+
target_memory_mb: float = 100.0,
|
|
434
|
+
) -> int:
|
|
435
|
+
"""Recommend optimal chunk size based on available memory.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
data_length: Total length of data in samples.
|
|
439
|
+
dtype: Data type of samples.
|
|
440
|
+
target_memory_mb: Target memory per chunk in MB.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Recommended chunk size in samples.
|
|
444
|
+
|
|
445
|
+
Example:
|
|
446
|
+
>>> chunk_size = optimizer.recommend_chunk_size(
|
|
447
|
+
... data_length=100_000_000,
|
|
448
|
+
... dtype=np.float64,
|
|
449
|
+
... target_memory_mb=50.0
|
|
450
|
+
... )
|
|
451
|
+
>>> print(f"Recommended: {chunk_size:,} samples")
|
|
452
|
+
"""
|
|
453
|
+
dtype_np = np.dtype(dtype)
|
|
454
|
+
bytes_per_sample = dtype_np.itemsize
|
|
455
|
+
|
|
456
|
+
# Calculate chunk size for target memory
|
|
457
|
+
target_bytes = target_memory_mb * 1024 * 1024
|
|
458
|
+
chunk_size = int(target_bytes / bytes_per_sample)
|
|
459
|
+
|
|
460
|
+
# Clamp to reasonable bounds
|
|
461
|
+
min_chunk = 1000
|
|
462
|
+
max_chunk = data_length
|
|
463
|
+
|
|
464
|
+
return max(min_chunk, min(chunk_size, max_chunk))
|
|
465
|
+
|
|
466
|
+
def load_optimized(
|
|
467
|
+
self,
|
|
468
|
+
file_path: str | PathLike[str],
|
|
469
|
+
sample_rate: float,
|
|
470
|
+
*,
|
|
471
|
+
dtype: DTypeLike | None = None,
|
|
472
|
+
mmap_threshold_mb: float = 100.0,
|
|
473
|
+
) -> Any:
|
|
474
|
+
"""Load file with optimal strategy (mmap for huge files, eager for small).
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
file_path: Path to data file.
|
|
478
|
+
sample_rate: Sample rate in Hz.
|
|
479
|
+
dtype: Data type (auto-detected for .npy).
|
|
480
|
+
mmap_threshold_mb: File size threshold for memory mapping (MB).
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Loaded trace (MmapWaveformTrace or WaveformTrace).
|
|
484
|
+
|
|
485
|
+
Example:
|
|
486
|
+
>>> # Automatically use mmap for huge files
|
|
487
|
+
>>> trace = optimizer.load_optimized(
|
|
488
|
+
... "huge_file.npy",
|
|
489
|
+
... sample_rate=1e9,
|
|
490
|
+
... mmap_threshold_mb=100.0
|
|
491
|
+
... )
|
|
492
|
+
"""
|
|
493
|
+
file_path = Path(file_path)
|
|
494
|
+
file_size_mb = file_path.stat().st_size / (1024 * 1024)
|
|
495
|
+
|
|
496
|
+
# Update memory tracking
|
|
497
|
+
self._update_memory_tracking()
|
|
498
|
+
|
|
499
|
+
# Use mmap for large files
|
|
500
|
+
if file_size_mb >= mmap_threshold_mb:
|
|
501
|
+
from oscura.loaders.mmap_loader import load_mmap
|
|
502
|
+
|
|
503
|
+
return load_mmap(file_path, sample_rate=sample_rate, dtype=dtype)
|
|
504
|
+
else:
|
|
505
|
+
# Use eager loading for small files
|
|
506
|
+
from oscura.loaders import load
|
|
507
|
+
|
|
508
|
+
return load(file_path, sample_rate=sample_rate)
|
|
509
|
+
|
|
510
|
+
def create_stream_processor(
|
|
511
|
+
self,
|
|
512
|
+
data: NDArray[Any] | np.memmap[Any, Any] | Any,
|
|
513
|
+
chunk_size: int | None = None,
|
|
514
|
+
overlap: int = 0,
|
|
515
|
+
config: ChunkingConfig | None = None,
|
|
516
|
+
) -> StreamProcessor:
|
|
517
|
+
"""Create streaming processor for chunk-by-chunk processing.
|
|
518
|
+
|
|
519
|
+
Args:
|
|
520
|
+
data: Data array, memmap, or trace object.
|
|
521
|
+
chunk_size: Size of each chunk (auto if None).
|
|
522
|
+
overlap: Number of samples to overlap.
|
|
523
|
+
config: Chunking configuration (overrides chunk_size/overlap).
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
StreamProcessor for iterating over chunks.
|
|
527
|
+
|
|
528
|
+
Example:
|
|
529
|
+
>>> processor = optimizer.create_stream_processor(
|
|
530
|
+
... data=huge_array,
|
|
531
|
+
... chunk_size=1_000_000,
|
|
532
|
+
... overlap=1024
|
|
533
|
+
... )
|
|
534
|
+
>>> for chunk in processor:
|
|
535
|
+
... result = analyze(chunk)
|
|
536
|
+
"""
|
|
537
|
+
# Extract data array if trace object
|
|
538
|
+
if hasattr(data, "data"):
|
|
539
|
+
data_array = data.data
|
|
540
|
+
else:
|
|
541
|
+
data_array = data
|
|
542
|
+
|
|
543
|
+
# Convert to numpy array if needed
|
|
544
|
+
if not isinstance(data_array, (np.ndarray, np.memmap)):
|
|
545
|
+
data_array = np.asarray(data_array)
|
|
546
|
+
|
|
547
|
+
# Use config if provided
|
|
548
|
+
if config is not None:
|
|
549
|
+
chunk_size = config.chunk_size
|
|
550
|
+
overlap = config.overlap
|
|
551
|
+
|
|
552
|
+
# Adaptive chunking
|
|
553
|
+
if config.adaptive:
|
|
554
|
+
chunk_size = self.recommend_chunk_size(
|
|
555
|
+
data_length=len(data_array),
|
|
556
|
+
dtype=data_array.dtype,
|
|
557
|
+
)
|
|
558
|
+
chunk_size = max(config.min_chunk_size, min(chunk_size, config.max_chunk_size))
|
|
559
|
+
|
|
560
|
+
# Auto-recommend chunk size if not provided
|
|
561
|
+
if chunk_size is None:
|
|
562
|
+
chunk_size = self.recommend_chunk_size(
|
|
563
|
+
data_length=len(data_array),
|
|
564
|
+
dtype=data_array.dtype,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Update memory tracking
|
|
568
|
+
self._update_memory_tracking()
|
|
569
|
+
|
|
570
|
+
return StreamProcessor(data=data_array, chunk_size=chunk_size, overlap=overlap)
|
|
571
|
+
|
|
572
|
+
def optimize_array(
|
|
573
|
+
self,
|
|
574
|
+
array: NDArray[Any],
|
|
575
|
+
compress: bool | None = None,
|
|
576
|
+
) -> NDArray[Any]:
|
|
577
|
+
"""Optimize array memory usage.
|
|
578
|
+
|
|
579
|
+
Args:
|
|
580
|
+
array: Array to optimize.
|
|
581
|
+
compress: Whether to compress (uses instance default if None).
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Optimized array (may be compressed or compacted).
|
|
585
|
+
|
|
586
|
+
Note:
|
|
587
|
+
Currently returns compacted array. Future versions may add
|
|
588
|
+
compression support via zlib or blosc.
|
|
589
|
+
|
|
590
|
+
Example:
|
|
591
|
+
>>> optimized = optimizer.optimize_array(large_array)
|
|
592
|
+
"""
|
|
593
|
+
# Update memory tracking
|
|
594
|
+
self._update_memory_tracking()
|
|
595
|
+
|
|
596
|
+
# Make array contiguous for better cache performance
|
|
597
|
+
if not array.flags["C_CONTIGUOUS"]:
|
|
598
|
+
array = np.ascontiguousarray(array)
|
|
599
|
+
|
|
600
|
+
# Future: Add compression support
|
|
601
|
+
# if compress or (compress is None and self.enable_compression):
|
|
602
|
+
# return compress_array(array)
|
|
603
|
+
|
|
604
|
+
return array
|
|
605
|
+
|
|
606
|
+
def set_memory_limit(self, max_memory_mb: float) -> None:
|
|
607
|
+
"""Set maximum memory limit.
|
|
608
|
+
|
|
609
|
+
Args:
|
|
610
|
+
max_memory_mb: Maximum memory in megabytes.
|
|
611
|
+
|
|
612
|
+
Raises:
|
|
613
|
+
ValueError: If max_memory_mb not positive.
|
|
614
|
+
|
|
615
|
+
Example:
|
|
616
|
+
>>> optimizer.set_memory_limit(1024) # 1 GB
|
|
617
|
+
"""
|
|
618
|
+
if max_memory_mb <= 0:
|
|
619
|
+
raise ValueError(f"max_memory_mb must be positive, got {max_memory_mb}")
|
|
620
|
+
self.max_memory_mb = max_memory_mb
|
|
621
|
+
|
|
622
|
+
def check_available_memory(self, required_mb: float) -> bool:
|
|
623
|
+
"""Check if sufficient memory available.
|
|
624
|
+
|
|
625
|
+
Args:
|
|
626
|
+
required_mb: Required memory in megabytes.
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
True if sufficient memory available, False otherwise.
|
|
630
|
+
|
|
631
|
+
Example:
|
|
632
|
+
>>> if optimizer.check_available_memory(500):
|
|
633
|
+
... process_large_data()
|
|
634
|
+
... else:
|
|
635
|
+
... print("Insufficient memory")
|
|
636
|
+
"""
|
|
637
|
+
available = self._get_available_memory()
|
|
638
|
+
return available >= required_mb
|
|
639
|
+
|
|
640
|
+
def suggest_downsampling(
|
|
641
|
+
self,
|
|
642
|
+
data_length: int,
|
|
643
|
+
dtype: DTypeLike = np.float64,
|
|
644
|
+
target_memory_mb: float = 100.0,
|
|
645
|
+
) -> int:
|
|
646
|
+
"""Suggest downsampling factor to fit in target memory.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
data_length: Length of data in samples.
|
|
650
|
+
dtype: Data type of samples.
|
|
651
|
+
target_memory_mb: Target memory in megabytes.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
Downsampling factor (1 = no downsampling, 2 = every other sample, etc).
|
|
655
|
+
|
|
656
|
+
Example:
|
|
657
|
+
>>> factor = optimizer.suggest_downsampling(
|
|
658
|
+
... data_length=100_000_000,
|
|
659
|
+
... target_memory_mb=50.0
|
|
660
|
+
... )
|
|
661
|
+
>>> if factor > 1:
|
|
662
|
+
... data = data[::factor]
|
|
663
|
+
"""
|
|
664
|
+
dtype_np = np.dtype(dtype)
|
|
665
|
+
bytes_per_sample = dtype_np.itemsize
|
|
666
|
+
|
|
667
|
+
# Calculate current memory requirement
|
|
668
|
+
current_mb = (data_length * bytes_per_sample) / (1024 * 1024)
|
|
669
|
+
|
|
670
|
+
if current_mb <= target_memory_mb:
|
|
671
|
+
return 1
|
|
672
|
+
|
|
673
|
+
# Calculate downsampling factor
|
|
674
|
+
factor = int(np.ceil(current_mb / target_memory_mb))
|
|
675
|
+
return factor
|
|
676
|
+
|
|
677
|
+
def reset_statistics(self) -> None:
|
|
678
|
+
"""Reset memory tracking statistics.
|
|
679
|
+
|
|
680
|
+
Example:
|
|
681
|
+
>>> optimizer.reset_statistics()
|
|
682
|
+
>>> # Track memory for new operation
|
|
683
|
+
>>> stats = optimizer.get_memory_stats()
|
|
684
|
+
"""
|
|
685
|
+
self._initial_memory_mb = self._get_memory_usage()
|
|
686
|
+
self._peak_memory_mb = self._initial_memory_mb
|
|
687
|
+
self._total_allocated_mb = 0.0
|
|
688
|
+
self._total_freed_mb = 0.0
|
|
689
|
+
self._allocation_count = 0
|
|
690
|
+
self._last_gc_memory = self._initial_memory_mb
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
__all__ = [
|
|
694
|
+
"ChunkingConfig",
|
|
695
|
+
"ChunkingStrategy",
|
|
696
|
+
"MemoryOptimizer",
|
|
697
|
+
"MemoryStats",
|
|
698
|
+
"StreamProcessor",
|
|
699
|
+
]
|