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,583 @@
|
|
|
1
|
+
"""Memory-mapped file loader for huge waveform files.
|
|
2
|
+
|
|
3
|
+
This module provides efficient memory-mapped loading for GB+ files that cannot
|
|
4
|
+
fit in RAM. Unlike eager loading, memory-mapped arrays don't load the entire
|
|
5
|
+
file into memory but access it in chunks on-demand via the OS page cache.
|
|
6
|
+
|
|
7
|
+
Key features:
|
|
8
|
+
- Zero-copy data access via numpy.memmap
|
|
9
|
+
- Chunked iteration for processing huge files
|
|
10
|
+
- Integration with existing TraceKit loader infrastructure
|
|
11
|
+
- Support for common binary formats (raw, NPY, structured)
|
|
12
|
+
- Automatic fallback to regular loading for small files
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from oscura.loaders.mmap_loader import load_mmap
|
|
16
|
+
>>> # Load 10 GB file without loading all data to RAM
|
|
17
|
+
>>> trace = load_mmap("huge_trace.npy", sample_rate=1e9)
|
|
18
|
+
>>> print(f"Length: {len(trace)} samples")
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Process in chunks to avoid OOM
|
|
21
|
+
>>> for chunk in trace.iter_chunks(chunk_size=1_000_000):
|
|
22
|
+
... result = analyze_chunk(chunk)
|
|
23
|
+
|
|
24
|
+
References:
|
|
25
|
+
Performance optimization for huge files (>1 GB)
|
|
26
|
+
API-017: Lazy Loading for Huge Files
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from collections.abc import Iterator
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from oscura.core.exceptions import LoaderError
|
|
38
|
+
from oscura.core.types import TraceMetadata
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from os import PathLike
|
|
42
|
+
|
|
43
|
+
from numpy.typing import DTypeLike, NDArray
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# File size threshold for automatic mmap suggestion (1 GB)
|
|
47
|
+
MMAP_THRESHOLD = 1024 * 1024 * 1024
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MmapWaveformTrace:
|
|
51
|
+
"""Memory-mapped waveform trace for huge files.
|
|
52
|
+
|
|
53
|
+
Provides lazy access to waveform data via memory mapping. Data is not
|
|
54
|
+
loaded into RAM but accessed directly from disk through the OS page cache.
|
|
55
|
+
|
|
56
|
+
This allows working with files larger than available RAM without OOM errors.
|
|
57
|
+
|
|
58
|
+
Attributes:
|
|
59
|
+
file_path: Path to the memory-mapped file.
|
|
60
|
+
sample_rate: Sample rate in Hz.
|
|
61
|
+
length: Number of samples in the trace.
|
|
62
|
+
dtype: NumPy dtype of the samples.
|
|
63
|
+
metadata: Additional trace metadata.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> trace = MmapWaveformTrace(
|
|
67
|
+
... file_path="huge_trace.bin",
|
|
68
|
+
... sample_rate=1e9,
|
|
69
|
+
... length=10_000_000_000,
|
|
70
|
+
... dtype=np.float32
|
|
71
|
+
... )
|
|
72
|
+
>>> # Access subset without loading entire file
|
|
73
|
+
>>> subset = trace[1000:2000]
|
|
74
|
+
>>> # Process in chunks
|
|
75
|
+
>>> for chunk in trace.iter_chunks(chunk_size=1_000_000):
|
|
76
|
+
... process(chunk)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
file_path: str | Path,
|
|
82
|
+
sample_rate: float,
|
|
83
|
+
length: int,
|
|
84
|
+
*,
|
|
85
|
+
dtype: DTypeLike = np.float64,
|
|
86
|
+
offset: int = 0,
|
|
87
|
+
metadata: dict[str, Any] | None = None,
|
|
88
|
+
mode: str = "r",
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Initialize memory-mapped trace.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
file_path: Path to binary data file.
|
|
94
|
+
sample_rate: Sample rate in Hz.
|
|
95
|
+
length: Number of samples.
|
|
96
|
+
dtype: Data type of samples.
|
|
97
|
+
offset: Byte offset to start of data in file.
|
|
98
|
+
metadata: Additional metadata dictionary.
|
|
99
|
+
mode: File access mode ('r' for read-only, 'r+' for read-write).
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
LoaderError: If file not found or invalid parameters.
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
>>> trace = MmapWaveformTrace(
|
|
106
|
+
... file_path="trace.f32",
|
|
107
|
+
... sample_rate=1e9,
|
|
108
|
+
... length=1_000_000_000,
|
|
109
|
+
... dtype=np.float32
|
|
110
|
+
... )
|
|
111
|
+
"""
|
|
112
|
+
self._file_path = Path(file_path)
|
|
113
|
+
self._sample_rate = float(sample_rate)
|
|
114
|
+
self._length = int(length)
|
|
115
|
+
self._dtype = np.dtype(dtype)
|
|
116
|
+
self._offset = int(offset)
|
|
117
|
+
self._metadata = metadata or {}
|
|
118
|
+
self._mode = mode
|
|
119
|
+
|
|
120
|
+
# Memory-mapped array - created on first access
|
|
121
|
+
self._memmap: np.memmap[Any, np.dtype[Any]] | None = None
|
|
122
|
+
|
|
123
|
+
# Validate inputs
|
|
124
|
+
if self._sample_rate <= 0:
|
|
125
|
+
raise LoaderError(f"sample_rate must be positive, got {self._sample_rate}")
|
|
126
|
+
if self._length < 0:
|
|
127
|
+
raise LoaderError(f"length must be non-negative, got {self._length}")
|
|
128
|
+
if self._offset < 0:
|
|
129
|
+
raise LoaderError(f"offset must be non-negative, got {self._offset}")
|
|
130
|
+
|
|
131
|
+
# Verify file exists
|
|
132
|
+
if not self._file_path.exists():
|
|
133
|
+
raise LoaderError(f"File not found: {self._file_path}")
|
|
134
|
+
|
|
135
|
+
# Verify file size
|
|
136
|
+
expected_size = self._offset + self._length * self._dtype.itemsize
|
|
137
|
+
actual_size = self._file_path.stat().st_size
|
|
138
|
+
if actual_size < expected_size:
|
|
139
|
+
raise LoaderError(
|
|
140
|
+
f"File too small for requested data. "
|
|
141
|
+
f"Expected at least {expected_size} bytes, got {actual_size} bytes",
|
|
142
|
+
file_path=str(self._file_path),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def sample_rate(self) -> float:
|
|
147
|
+
"""Sample rate in Hz."""
|
|
148
|
+
return self._sample_rate
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def length(self) -> int:
|
|
152
|
+
"""Number of samples."""
|
|
153
|
+
return self._length
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def duration(self) -> float:
|
|
157
|
+
"""Duration in seconds."""
|
|
158
|
+
return self._length / self._sample_rate
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def metadata(self) -> dict[str, Any]:
|
|
162
|
+
"""Metadata dictionary."""
|
|
163
|
+
return self._metadata
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def dtype(self) -> np.dtype[Any]:
|
|
167
|
+
"""Data type of samples."""
|
|
168
|
+
return self._dtype
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def file_path(self) -> Path:
|
|
172
|
+
"""Path to memory-mapped file."""
|
|
173
|
+
return self._file_path
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def data(self) -> np.memmap[Any, np.dtype[Any]]:
|
|
177
|
+
"""Memory-mapped data array.
|
|
178
|
+
|
|
179
|
+
Returns a numpy.memmap object that behaves like a numpy array
|
|
180
|
+
but doesn't load data into memory until accessed.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Memory-mapped numpy array.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> trace = load_mmap("huge.npy", sample_rate=1e9)
|
|
187
|
+
>>> data = trace.data # No data loaded yet
|
|
188
|
+
>>> subset = data[1000:2000] # Only this range loaded
|
|
189
|
+
"""
|
|
190
|
+
if self._memmap is None:
|
|
191
|
+
self._memmap = np.memmap( # type: ignore[call-overload]
|
|
192
|
+
str(self._file_path),
|
|
193
|
+
dtype=self._dtype,
|
|
194
|
+
mode=self._mode,
|
|
195
|
+
offset=self._offset,
|
|
196
|
+
shape=(self._length,),
|
|
197
|
+
)
|
|
198
|
+
return self._memmap
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def time_vector(self) -> NDArray[np.float64]:
|
|
202
|
+
"""Time vector in seconds.
|
|
203
|
+
|
|
204
|
+
Note: For huge traces, this can consume significant memory.
|
|
205
|
+
Consider using time values on-demand instead.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Array of time values corresponding to samples.
|
|
209
|
+
|
|
210
|
+
Example:
|
|
211
|
+
>>> # For huge traces, avoid materializing full time vector
|
|
212
|
+
>>> # Instead compute on-demand:
|
|
213
|
+
>>> t_start = 0
|
|
214
|
+
>>> t_end = trace.length / trace.sample_rate
|
|
215
|
+
"""
|
|
216
|
+
return np.arange(self._length, dtype=np.float64) / self._sample_rate
|
|
217
|
+
|
|
218
|
+
def __getitem__(self, key: int | slice) -> float | NDArray[np.float64]:
|
|
219
|
+
"""Slice the memory-mapped trace.
|
|
220
|
+
|
|
221
|
+
Supports both integer indexing and slicing. Only the requested
|
|
222
|
+
portion is loaded from disk.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
key: Index or slice.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Single sample (float) or array slice.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
TypeError: If key is not int or slice.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> sample = trace[1000] # Load single sample
|
|
235
|
+
>>> chunk = trace[1000:2000] # Load 1000 samples
|
|
236
|
+
>>> every_10th = trace[::10] # Load decimated data
|
|
237
|
+
"""
|
|
238
|
+
if isinstance(key, (int, slice)):
|
|
239
|
+
return self.data[key]
|
|
240
|
+
else:
|
|
241
|
+
raise TypeError(f"Indices must be int or slice, not {type(key).__name__}")
|
|
242
|
+
|
|
243
|
+
def __len__(self) -> int:
|
|
244
|
+
"""Number of samples."""
|
|
245
|
+
return self._length
|
|
246
|
+
|
|
247
|
+
def iter_chunks(
|
|
248
|
+
self, chunk_size: int = 1_000_000, overlap: int = 0
|
|
249
|
+
) -> Iterator[NDArray[np.float64]]:
|
|
250
|
+
"""Iterate over trace in chunks.
|
|
251
|
+
|
|
252
|
+
Yields consecutive chunks of data, optionally with overlap between
|
|
253
|
+
chunks. This is efficient for processing huge files that don't fit
|
|
254
|
+
in memory.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
chunk_size: Number of samples per chunk.
|
|
258
|
+
overlap: Number of samples to overlap between chunks.
|
|
259
|
+
|
|
260
|
+
Yields:
|
|
261
|
+
Numpy arrays of chunk_size (or smaller for last chunk).
|
|
262
|
+
|
|
263
|
+
Raises:
|
|
264
|
+
ValueError: If chunk_size or overlap invalid.
|
|
265
|
+
|
|
266
|
+
Example:
|
|
267
|
+
>>> # Process 10 GB file in 1M sample chunks
|
|
268
|
+
>>> for chunk in trace.iter_chunks(chunk_size=1_000_000):
|
|
269
|
+
... result = compute_fft(chunk)
|
|
270
|
+
...
|
|
271
|
+
>>> # With 50% overlap for windowed processing
|
|
272
|
+
>>> for chunk in trace.iter_chunks(chunk_size=2048, overlap=1024):
|
|
273
|
+
... spectrum = analyze_spectrum(chunk)
|
|
274
|
+
"""
|
|
275
|
+
if chunk_size <= 0:
|
|
276
|
+
raise ValueError(f"chunk_size must be positive, got {chunk_size}")
|
|
277
|
+
if overlap < 0:
|
|
278
|
+
raise ValueError(f"overlap must be non-negative, got {overlap}")
|
|
279
|
+
if overlap >= chunk_size:
|
|
280
|
+
raise ValueError(f"overlap ({overlap}) must be less than chunk_size ({chunk_size})")
|
|
281
|
+
|
|
282
|
+
data = self.data
|
|
283
|
+
step = chunk_size - overlap
|
|
284
|
+
|
|
285
|
+
for start in range(0, self._length, step):
|
|
286
|
+
end = min(start + chunk_size, self._length)
|
|
287
|
+
# Convert memmap slice to regular array to avoid keeping file handle open
|
|
288
|
+
yield np.asarray(data[start:end], dtype=np.float64)
|
|
289
|
+
|
|
290
|
+
def to_eager(self) -> Any:
|
|
291
|
+
"""Convert to eager WaveformTrace by loading all data.
|
|
292
|
+
|
|
293
|
+
WARNING: This loads the entire file into memory. Only use this
|
|
294
|
+
if you're sure the data fits in RAM.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
WaveformTrace with data loaded in memory.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> # Only convert to eager if file is small enough
|
|
301
|
+
>>> if trace.length < 10_000_000:
|
|
302
|
+
... eager_trace = trace.to_eager()
|
|
303
|
+
"""
|
|
304
|
+
from oscura.core.types import WaveformTrace
|
|
305
|
+
|
|
306
|
+
# Load all data into memory
|
|
307
|
+
data = np.asarray(self.data, dtype=np.float64)
|
|
308
|
+
|
|
309
|
+
metadata = TraceMetadata(
|
|
310
|
+
sample_rate=self._sample_rate,
|
|
311
|
+
source_file=str(self._file_path),
|
|
312
|
+
**self._metadata, # type: ignore[arg-type]
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
316
|
+
|
|
317
|
+
def close(self) -> None:
|
|
318
|
+
"""Close memory-mapped file handle.
|
|
319
|
+
|
|
320
|
+
Should be called when done with the trace to free resources.
|
|
321
|
+
The trace cannot be used after closing.
|
|
322
|
+
|
|
323
|
+
Example:
|
|
324
|
+
>>> trace = load_mmap("huge.npy", sample_rate=1e9)
|
|
325
|
+
>>> # ... use trace ...
|
|
326
|
+
>>> trace.close()
|
|
327
|
+
"""
|
|
328
|
+
if self._memmap is not None:
|
|
329
|
+
# Delete reference to allow garbage collection
|
|
330
|
+
del self._memmap
|
|
331
|
+
self._memmap = None
|
|
332
|
+
|
|
333
|
+
def __del__(self) -> None:
|
|
334
|
+
"""Cleanup memory map on deletion."""
|
|
335
|
+
self.close()
|
|
336
|
+
|
|
337
|
+
def __repr__(self) -> str:
|
|
338
|
+
"""String representation."""
|
|
339
|
+
size_mb = (self._length * self._dtype.itemsize) / (1024 * 1024)
|
|
340
|
+
return (
|
|
341
|
+
f"MmapWaveformTrace("
|
|
342
|
+
f"file={self._file_path.name}, "
|
|
343
|
+
f"sample_rate={self._sample_rate:.2e} Hz, "
|
|
344
|
+
f"length={self._length:,} samples, "
|
|
345
|
+
f"size={size_mb:.1f} MB, "
|
|
346
|
+
f"dtype={self._dtype})"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def __enter__(self) -> MmapWaveformTrace:
|
|
350
|
+
"""Context manager entry."""
|
|
351
|
+
return self
|
|
352
|
+
|
|
353
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
354
|
+
"""Context manager exit - close the file."""
|
|
355
|
+
self.close()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def load_mmap(
|
|
359
|
+
file_path: str | PathLike[str],
|
|
360
|
+
sample_rate: float | None = None,
|
|
361
|
+
*,
|
|
362
|
+
dtype: DTypeLike | None = None,
|
|
363
|
+
offset: int = 0,
|
|
364
|
+
length: int | None = None,
|
|
365
|
+
mode: str = "r",
|
|
366
|
+
**metadata: Any,
|
|
367
|
+
) -> MmapWaveformTrace:
|
|
368
|
+
"""Load waveform file with memory mapping.
|
|
369
|
+
|
|
370
|
+
Creates a memory-mapped trace that doesn't load data into RAM.
|
|
371
|
+
Supports .npy files (auto-detects format) and raw binary files.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
file_path: Path to waveform file (.npy or raw binary).
|
|
375
|
+
sample_rate: Sample rate in Hz (required for raw files, optional for .npy).
|
|
376
|
+
dtype: Data type (required for raw files, auto-detected for .npy).
|
|
377
|
+
offset: Byte offset to data start (auto-computed for .npy).
|
|
378
|
+
length: Number of samples (auto-computed if possible).
|
|
379
|
+
mode: File access mode ('r' for read-only, 'r+' for read-write).
|
|
380
|
+
**metadata: Additional metadata to store.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
MmapWaveformTrace for memory-mapped access.
|
|
384
|
+
|
|
385
|
+
Raises:
|
|
386
|
+
LoaderError: If file not found or parameters invalid.
|
|
387
|
+
|
|
388
|
+
Example:
|
|
389
|
+
>>> # Load NumPy file (auto-detects format)
|
|
390
|
+
>>> trace = load_mmap("huge_trace.npy", sample_rate=1e9)
|
|
391
|
+
>>>
|
|
392
|
+
>>> # Load raw binary file
|
|
393
|
+
>>> trace = load_mmap(
|
|
394
|
+
... "data.f32",
|
|
395
|
+
... sample_rate=1e9,
|
|
396
|
+
... dtype=np.float32,
|
|
397
|
+
... length=1_000_000_000
|
|
398
|
+
... )
|
|
399
|
+
>>>
|
|
400
|
+
>>> # Use context manager
|
|
401
|
+
>>> with load_mmap("huge.npy", sample_rate=1e9) as trace:
|
|
402
|
+
... for chunk in trace.iter_chunks(chunk_size=1_000_000):
|
|
403
|
+
... process(chunk)
|
|
404
|
+
|
|
405
|
+
References:
|
|
406
|
+
API-017: Lazy Loading for Huge Files
|
|
407
|
+
"""
|
|
408
|
+
file_path = Path(file_path)
|
|
409
|
+
|
|
410
|
+
if not file_path.exists():
|
|
411
|
+
raise LoaderError(f"File not found: {file_path}")
|
|
412
|
+
|
|
413
|
+
suffix = file_path.suffix.lower()
|
|
414
|
+
|
|
415
|
+
# Handle .npy files with automatic format detection
|
|
416
|
+
if suffix == ".npy":
|
|
417
|
+
return _load_npy_mmap(file_path, sample_rate, mode, metadata)
|
|
418
|
+
|
|
419
|
+
# Handle .npz files (not directly memory-mappable, but can extract)
|
|
420
|
+
elif suffix == ".npz":
|
|
421
|
+
raise LoaderError(
|
|
422
|
+
"NPZ files cannot be directly memory-mapped. "
|
|
423
|
+
"Extract the array first using np.load() and save as .npy",
|
|
424
|
+
file_path=str(file_path),
|
|
425
|
+
fix_hint="Use: np.save('array.npy', np.load('file.npz')['array'])",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Handle raw binary files
|
|
429
|
+
else:
|
|
430
|
+
if dtype is None:
|
|
431
|
+
raise LoaderError(
|
|
432
|
+
"dtype is required for raw binary files",
|
|
433
|
+
file_path=str(file_path),
|
|
434
|
+
fix_hint="Specify dtype, e.g., dtype=np.float32",
|
|
435
|
+
)
|
|
436
|
+
if sample_rate is None:
|
|
437
|
+
raise LoaderError(
|
|
438
|
+
"sample_rate is required for raw binary files",
|
|
439
|
+
file_path=str(file_path),
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Compute length from file size if not provided
|
|
443
|
+
dtype_np = np.dtype(dtype)
|
|
444
|
+
if length is None:
|
|
445
|
+
file_size = file_path.stat().st_size - offset
|
|
446
|
+
length = file_size // dtype_np.itemsize
|
|
447
|
+
|
|
448
|
+
return MmapWaveformTrace(
|
|
449
|
+
file_path=file_path,
|
|
450
|
+
sample_rate=sample_rate,
|
|
451
|
+
length=length,
|
|
452
|
+
dtype=dtype_np,
|
|
453
|
+
offset=offset,
|
|
454
|
+
metadata=metadata,
|
|
455
|
+
mode=mode,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _load_npy_mmap(
|
|
460
|
+
file_path: Path,
|
|
461
|
+
sample_rate: float | None,
|
|
462
|
+
mode: str,
|
|
463
|
+
metadata: dict[str, Any],
|
|
464
|
+
) -> MmapWaveformTrace:
|
|
465
|
+
"""Load NumPy .npy file with memory mapping.
|
|
466
|
+
|
|
467
|
+
Reads the .npy header to extract dtype, shape, and data offset,
|
|
468
|
+
then creates a memory-mapped array.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
file_path: Path to .npy file.
|
|
472
|
+
sample_rate: Sample rate in Hz (required).
|
|
473
|
+
mode: File access mode.
|
|
474
|
+
metadata: Additional metadata.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
MmapWaveformTrace for the .npy file.
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
LoaderError: If sample_rate not provided or file invalid.
|
|
481
|
+
"""
|
|
482
|
+
if sample_rate is None:
|
|
483
|
+
raise LoaderError(
|
|
484
|
+
"sample_rate is required for .npy files",
|
|
485
|
+
file_path=str(file_path),
|
|
486
|
+
fix_hint="Specify sample_rate, e.g., sample_rate=1e9",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
# Read NumPy header without loading data
|
|
491
|
+
with open(file_path, "rb") as f:
|
|
492
|
+
import numpy.lib.format as npf
|
|
493
|
+
|
|
494
|
+
# Read header
|
|
495
|
+
version = npf.read_magic(f) # type: ignore[no-untyped-call]
|
|
496
|
+
|
|
497
|
+
if version == (1, 0):
|
|
498
|
+
shape, fortran_order, dtype = npf.read_array_header_1_0(f) # type: ignore[no-untyped-call]
|
|
499
|
+
elif version == (2, 0):
|
|
500
|
+
shape, fortran_order, dtype = npf.read_array_header_2_0(f) # type: ignore[no-untyped-call]
|
|
501
|
+
else:
|
|
502
|
+
raise LoaderError(
|
|
503
|
+
f"Unsupported NPY version: {version}",
|
|
504
|
+
file_path=str(file_path),
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Get data offset
|
|
508
|
+
offset = f.tell()
|
|
509
|
+
|
|
510
|
+
# Validate shape
|
|
511
|
+
if not isinstance(shape, tuple):
|
|
512
|
+
raise LoaderError(
|
|
513
|
+
f"Invalid .npy shape: {shape}",
|
|
514
|
+
file_path=str(file_path),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if len(shape) != 1:
|
|
518
|
+
raise LoaderError(
|
|
519
|
+
f"Expected 1D array, got shape {shape}",
|
|
520
|
+
file_path=str(file_path),
|
|
521
|
+
fix_hint="Reshape to 1D or extract specific column",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
length = shape[0]
|
|
525
|
+
|
|
526
|
+
if fortran_order:
|
|
527
|
+
raise LoaderError(
|
|
528
|
+
"Fortran-ordered arrays not supported for memory mapping",
|
|
529
|
+
file_path=str(file_path),
|
|
530
|
+
fix_hint="Resave array in C order: np.save('file.npy', arr, allow_pickle=False)",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return MmapWaveformTrace(
|
|
534
|
+
file_path=file_path,
|
|
535
|
+
sample_rate=sample_rate,
|
|
536
|
+
length=length,
|
|
537
|
+
dtype=dtype,
|
|
538
|
+
offset=offset,
|
|
539
|
+
metadata=metadata,
|
|
540
|
+
mode=mode,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
except Exception as e:
|
|
544
|
+
if isinstance(e, LoaderError):
|
|
545
|
+
raise
|
|
546
|
+
raise LoaderError(
|
|
547
|
+
f"Failed to load .npy file: {e}",
|
|
548
|
+
file_path=str(file_path),
|
|
549
|
+
) from e
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def should_use_mmap(file_path: str | PathLike[str], threshold: int = MMAP_THRESHOLD) -> bool:
|
|
553
|
+
"""Check if file should use memory mapping.
|
|
554
|
+
|
|
555
|
+
Recommends memory mapping for files larger than threshold (default 1 GB).
|
|
556
|
+
|
|
557
|
+
Args:
|
|
558
|
+
file_path: Path to file.
|
|
559
|
+
threshold: Size threshold in bytes (default: 1 GB).
|
|
560
|
+
|
|
561
|
+
Returns:
|
|
562
|
+
True if file size >= threshold, False otherwise.
|
|
563
|
+
|
|
564
|
+
Example:
|
|
565
|
+
>>> if should_use_mmap("huge_trace.npy"):
|
|
566
|
+
... trace = load_mmap("huge_trace.npy", sample_rate=1e9)
|
|
567
|
+
... else:
|
|
568
|
+
... trace = load("huge_trace.npy", sample_rate=1e9)
|
|
569
|
+
"""
|
|
570
|
+
file_path = Path(file_path)
|
|
571
|
+
if not file_path.exists():
|
|
572
|
+
return False
|
|
573
|
+
|
|
574
|
+
file_size = file_path.stat().st_size
|
|
575
|
+
return file_size >= threshold
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
__all__ = [
|
|
579
|
+
"MMAP_THRESHOLD",
|
|
580
|
+
"MmapWaveformTrace",
|
|
581
|
+
"load_mmap",
|
|
582
|
+
"should_use_mmap",
|
|
583
|
+
]
|