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
oscura/loaders/csv.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Alias module for CSV loader - provides expected import path.
|
|
2
|
+
|
|
3
|
+
Why two CSV loader files exist:
|
|
4
|
+
- `oscura/loaders/csv_loader.py`: Canonical implementation with full CSV
|
|
5
|
+
parsing logic, format detection, and validation
|
|
6
|
+
- `oscura/loaders/csv.py` (this file): Convenience re-export to support
|
|
7
|
+
natural import syntax like `from oscura.loaders.csv import load_csv`
|
|
8
|
+
|
|
9
|
+
The name `csv_loader.py` avoids shadowing Python's built-in `csv` module
|
|
10
|
+
within the implementation file. This alias module provides a cleaner import
|
|
11
|
+
path for external users.
|
|
12
|
+
|
|
13
|
+
Usage patterns:
|
|
14
|
+
# Recommended - explicit module name
|
|
15
|
+
from oscura.loaders.csv_loader import load_csv
|
|
16
|
+
|
|
17
|
+
# Also supported - natural import path via this alias
|
|
18
|
+
from oscura.loaders.csv import load_csv
|
|
19
|
+
|
|
20
|
+
# Via loaders package __init__
|
|
21
|
+
from oscura.loaders import load_csv
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from oscura.loaders.csv_loader import load_csv
|
|
25
|
+
|
|
26
|
+
__all__ = ["load_csv"]
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""CSV file loader for waveform data.
|
|
2
|
+
|
|
3
|
+
This module provides loading of waveform data from CSV files with
|
|
4
|
+
automatic header detection and column mapping.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.loaders.csv_loader import load_csv
|
|
9
|
+
>>> trace = load_csv("oscilloscope_export.csv")
|
|
10
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import csv
|
|
16
|
+
from io import StringIO
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from oscura.core.exceptions import FormatError, LoaderError
|
|
23
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from os import PathLike
|
|
27
|
+
|
|
28
|
+
# Try to import pandas for better CSV handling
|
|
29
|
+
try:
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
PANDAS_AVAILABLE = True
|
|
33
|
+
except ImportError:
|
|
34
|
+
PANDAS_AVAILABLE = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Common column names for time data
|
|
38
|
+
TIME_COLUMN_NAMES = [
|
|
39
|
+
"time",
|
|
40
|
+
"t",
|
|
41
|
+
"time_s",
|
|
42
|
+
"time_sec",
|
|
43
|
+
"seconds",
|
|
44
|
+
"timestamp",
|
|
45
|
+
"x",
|
|
46
|
+
"Time",
|
|
47
|
+
"TIME",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Common column names for voltage data
|
|
51
|
+
VOLTAGE_COLUMN_NAMES = [
|
|
52
|
+
"voltage",
|
|
53
|
+
"v",
|
|
54
|
+
"volt",
|
|
55
|
+
"volts",
|
|
56
|
+
"amplitude",
|
|
57
|
+
"signal",
|
|
58
|
+
"y",
|
|
59
|
+
"value",
|
|
60
|
+
"data",
|
|
61
|
+
"ch1",
|
|
62
|
+
"ch2",
|
|
63
|
+
"ch3",
|
|
64
|
+
"ch4",
|
|
65
|
+
"channel1",
|
|
66
|
+
"channel2",
|
|
67
|
+
"Voltage",
|
|
68
|
+
"VOLTAGE",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_csv(
|
|
73
|
+
path: str | PathLike[str],
|
|
74
|
+
*,
|
|
75
|
+
time_column: str | int | None = None,
|
|
76
|
+
voltage_column: str | int | None = None,
|
|
77
|
+
sample_rate: float | None = None,
|
|
78
|
+
delimiter: str | None = None,
|
|
79
|
+
skip_rows: int = 0,
|
|
80
|
+
encoding: str = "utf-8",
|
|
81
|
+
mmap: bool = False,
|
|
82
|
+
) -> WaveformTrace | Any:
|
|
83
|
+
"""Load waveform data from a CSV file.
|
|
84
|
+
|
|
85
|
+
Parses CSV files exported from oscilloscopes or other data sources.
|
|
86
|
+
Automatically detects header rows and maps columns for time and
|
|
87
|
+
voltage data.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path: Path to the CSV file.
|
|
91
|
+
time_column: Name or index of time column. If None, auto-detects.
|
|
92
|
+
voltage_column: Name or index of voltage column. If None, auto-detects.
|
|
93
|
+
sample_rate: Override sample rate. If None, computed from time column.
|
|
94
|
+
delimiter: Column delimiter. If None, auto-detects.
|
|
95
|
+
skip_rows: Number of rows to skip before header.
|
|
96
|
+
encoding: File encoding (default: utf-8).
|
|
97
|
+
mmap: If True, return memory-mapped trace for large files.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
WaveformTrace containing the waveform data and metadata.
|
|
101
|
+
If mmap=True, returns MmapWaveformTrace instead.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
LoaderError: If the file cannot be loaded.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> trace = load_csv("oscilloscope.csv")
|
|
108
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
109
|
+
|
|
110
|
+
>>> # Specify columns explicitly
|
|
111
|
+
>>> trace = load_csv("data.csv", time_column="Time", voltage_column="CH1")
|
|
112
|
+
|
|
113
|
+
>>> # Load as memory-mapped for large files
|
|
114
|
+
>>> trace = load_csv("huge_capture.csv", mmap=True)
|
|
115
|
+
"""
|
|
116
|
+
path = Path(path)
|
|
117
|
+
|
|
118
|
+
if not path.exists():
|
|
119
|
+
raise LoaderError(
|
|
120
|
+
"File not found",
|
|
121
|
+
file_path=str(path),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if PANDAS_AVAILABLE:
|
|
125
|
+
trace = _load_with_pandas(
|
|
126
|
+
path,
|
|
127
|
+
time_column=time_column,
|
|
128
|
+
voltage_column=voltage_column,
|
|
129
|
+
sample_rate=sample_rate,
|
|
130
|
+
delimiter=delimiter,
|
|
131
|
+
skip_rows=skip_rows,
|
|
132
|
+
encoding=encoding,
|
|
133
|
+
)
|
|
134
|
+
else:
|
|
135
|
+
trace = _load_basic(
|
|
136
|
+
path,
|
|
137
|
+
time_column=time_column,
|
|
138
|
+
voltage_column=voltage_column,
|
|
139
|
+
sample_rate=sample_rate,
|
|
140
|
+
delimiter=delimiter,
|
|
141
|
+
skip_rows=skip_rows,
|
|
142
|
+
encoding=encoding,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Convert to memory-mapped if requested
|
|
146
|
+
if mmap:
|
|
147
|
+
import tempfile
|
|
148
|
+
|
|
149
|
+
from oscura.loaders.mmap_loader import load_mmap
|
|
150
|
+
|
|
151
|
+
# Save data to temporary .npy file for memory mapping
|
|
152
|
+
with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as tmp:
|
|
153
|
+
tmp_path = Path(tmp.name)
|
|
154
|
+
|
|
155
|
+
np.save(tmp_path, trace.data)
|
|
156
|
+
|
|
157
|
+
# Load as memory-mapped trace
|
|
158
|
+
return load_mmap(
|
|
159
|
+
tmp_path,
|
|
160
|
+
sample_rate=trace.metadata.sample_rate,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return trace
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _load_with_pandas(
|
|
167
|
+
path: Path,
|
|
168
|
+
*,
|
|
169
|
+
time_column: str | int | None,
|
|
170
|
+
voltage_column: str | int | None,
|
|
171
|
+
sample_rate: float | None,
|
|
172
|
+
delimiter: str | None,
|
|
173
|
+
skip_rows: int,
|
|
174
|
+
encoding: str,
|
|
175
|
+
) -> WaveformTrace:
|
|
176
|
+
"""Load CSV using pandas for better parsing."""
|
|
177
|
+
try:
|
|
178
|
+
# Auto-detect delimiter if not specified
|
|
179
|
+
if delimiter is None:
|
|
180
|
+
delimiter = _detect_delimiter(path, encoding)
|
|
181
|
+
|
|
182
|
+
# Read CSV with pandas
|
|
183
|
+
df = pd.read_csv(
|
|
184
|
+
path,
|
|
185
|
+
delimiter=delimiter,
|
|
186
|
+
skiprows=skip_rows,
|
|
187
|
+
encoding=encoding,
|
|
188
|
+
engine="python", # More flexible parsing
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if df.empty:
|
|
192
|
+
raise FormatError(
|
|
193
|
+
"CSV file is empty",
|
|
194
|
+
file_path=str(path),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Find time column
|
|
198
|
+
time_data = None
|
|
199
|
+
time_col_name = None
|
|
200
|
+
|
|
201
|
+
if time_column is not None:
|
|
202
|
+
if isinstance(time_column, int):
|
|
203
|
+
if time_column < len(df.columns):
|
|
204
|
+
time_col_name = df.columns[time_column]
|
|
205
|
+
time_data = df.iloc[:, time_column].values
|
|
206
|
+
elif time_column in df.columns:
|
|
207
|
+
time_col_name = time_column
|
|
208
|
+
time_data = df[time_column].values
|
|
209
|
+
else:
|
|
210
|
+
# Auto-detect time column
|
|
211
|
+
for col in df.columns:
|
|
212
|
+
col_lower = col.lower().strip()
|
|
213
|
+
if col_lower in [n.lower() for n in TIME_COLUMN_NAMES]:
|
|
214
|
+
time_col_name = col
|
|
215
|
+
time_data = df[col].values
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
# Find voltage column
|
|
219
|
+
voltage_data = None
|
|
220
|
+
voltage_col_name = None
|
|
221
|
+
|
|
222
|
+
if voltage_column is not None:
|
|
223
|
+
if isinstance(voltage_column, int):
|
|
224
|
+
if voltage_column < len(df.columns):
|
|
225
|
+
voltage_col_name = df.columns[voltage_column]
|
|
226
|
+
voltage_data = df.iloc[:, voltage_column].values
|
|
227
|
+
elif voltage_column in df.columns:
|
|
228
|
+
voltage_col_name = voltage_column
|
|
229
|
+
voltage_data = df[voltage_column].values
|
|
230
|
+
else:
|
|
231
|
+
# Auto-detect voltage column (first non-time numeric column)
|
|
232
|
+
for col in df.columns:
|
|
233
|
+
if col == time_col_name:
|
|
234
|
+
continue
|
|
235
|
+
col_lower = col.lower().strip()
|
|
236
|
+
# Check if numeric
|
|
237
|
+
if pd.api.types.is_numeric_dtype(df[col]):
|
|
238
|
+
# Prefer columns with voltage-like names
|
|
239
|
+
if col_lower in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
|
|
240
|
+
voltage_col_name = col
|
|
241
|
+
voltage_data = df[col].values
|
|
242
|
+
break
|
|
243
|
+
elif voltage_data is None:
|
|
244
|
+
voltage_col_name = col
|
|
245
|
+
voltage_data = df[col].values
|
|
246
|
+
|
|
247
|
+
if voltage_data is None:
|
|
248
|
+
raise FormatError(
|
|
249
|
+
"No voltage data found in CSV",
|
|
250
|
+
file_path=str(path),
|
|
251
|
+
expected="Numeric column for voltage data",
|
|
252
|
+
got=f"Columns: {', '.join(df.columns)}",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Convert to float64
|
|
256
|
+
data = np.asarray(voltage_data, dtype=np.float64)
|
|
257
|
+
|
|
258
|
+
# Determine sample rate
|
|
259
|
+
detected_sample_rate = sample_rate
|
|
260
|
+
if detected_sample_rate is None and time_data is not None:
|
|
261
|
+
time_data = np.asarray(time_data, dtype=np.float64)
|
|
262
|
+
if len(time_data) > 1:
|
|
263
|
+
# Calculate sample rate from time intervals
|
|
264
|
+
dt = np.median(np.diff(time_data))
|
|
265
|
+
if dt > 0:
|
|
266
|
+
detected_sample_rate = 1.0 / dt
|
|
267
|
+
|
|
268
|
+
if detected_sample_rate is None:
|
|
269
|
+
detected_sample_rate = 1e6 # Default to 1 MSa/s
|
|
270
|
+
|
|
271
|
+
# Build metadata
|
|
272
|
+
metadata = TraceMetadata(
|
|
273
|
+
sample_rate=detected_sample_rate,
|
|
274
|
+
source_file=str(path),
|
|
275
|
+
channel_name=voltage_col_name or "CH1",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
279
|
+
|
|
280
|
+
except pd.errors.ParserError as e:
|
|
281
|
+
raise FormatError(
|
|
282
|
+
"Failed to parse CSV file",
|
|
283
|
+
file_path=str(path),
|
|
284
|
+
details=str(e),
|
|
285
|
+
) from e
|
|
286
|
+
except Exception as e:
|
|
287
|
+
if isinstance(e, LoaderError | FormatError):
|
|
288
|
+
raise
|
|
289
|
+
raise LoaderError(
|
|
290
|
+
"Failed to load CSV file",
|
|
291
|
+
file_path=str(path),
|
|
292
|
+
details=str(e),
|
|
293
|
+
) from e
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _load_basic(
|
|
297
|
+
path: Path,
|
|
298
|
+
*,
|
|
299
|
+
time_column: str | int | None,
|
|
300
|
+
voltage_column: str | int | None,
|
|
301
|
+
sample_rate: float | None,
|
|
302
|
+
delimiter: str | None,
|
|
303
|
+
skip_rows: int,
|
|
304
|
+
encoding: str,
|
|
305
|
+
) -> WaveformTrace:
|
|
306
|
+
"""Basic CSV loader without pandas."""
|
|
307
|
+
try:
|
|
308
|
+
with open(path, encoding=encoding) as f:
|
|
309
|
+
# Skip rows
|
|
310
|
+
for _ in range(skip_rows):
|
|
311
|
+
next(f)
|
|
312
|
+
|
|
313
|
+
content = f.read()
|
|
314
|
+
|
|
315
|
+
# Auto-detect delimiter
|
|
316
|
+
if delimiter is None:
|
|
317
|
+
delimiter = _detect_delimiter_from_content(content)
|
|
318
|
+
|
|
319
|
+
# Parse CSV
|
|
320
|
+
reader = csv.reader(StringIO(content), delimiter=delimiter)
|
|
321
|
+
rows = list(reader)
|
|
322
|
+
|
|
323
|
+
if not rows:
|
|
324
|
+
raise FormatError("CSV file is empty", file_path=str(path))
|
|
325
|
+
|
|
326
|
+
# Detect header
|
|
327
|
+
header = None
|
|
328
|
+
data_start = 0
|
|
329
|
+
first_row = rows[0]
|
|
330
|
+
|
|
331
|
+
# Check if first row is a header (contains non-numeric values)
|
|
332
|
+
is_header = False
|
|
333
|
+
for cell in first_row:
|
|
334
|
+
try:
|
|
335
|
+
float(cell)
|
|
336
|
+
except ValueError:
|
|
337
|
+
if cell.strip(): # Non-empty, non-numeric
|
|
338
|
+
is_header = True
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
if is_header:
|
|
342
|
+
header = [cell.strip() for cell in first_row]
|
|
343
|
+
data_start = 1
|
|
344
|
+
|
|
345
|
+
# Determine column indices
|
|
346
|
+
time_idx = None
|
|
347
|
+
voltage_idx = None
|
|
348
|
+
|
|
349
|
+
if header:
|
|
350
|
+
# Find columns by name
|
|
351
|
+
if time_column is not None:
|
|
352
|
+
if isinstance(time_column, int):
|
|
353
|
+
time_idx = time_column
|
|
354
|
+
elif time_column in header:
|
|
355
|
+
time_idx = header.index(time_column)
|
|
356
|
+
else:
|
|
357
|
+
# Auto-detect
|
|
358
|
+
for i, col in enumerate(header):
|
|
359
|
+
if col.lower() in [n.lower() for n in TIME_COLUMN_NAMES]:
|
|
360
|
+
time_idx = i
|
|
361
|
+
break
|
|
362
|
+
|
|
363
|
+
if voltage_column is not None:
|
|
364
|
+
if isinstance(voltage_column, int):
|
|
365
|
+
voltage_idx = voltage_column
|
|
366
|
+
elif voltage_column in header:
|
|
367
|
+
voltage_idx = header.index(voltage_column)
|
|
368
|
+
else:
|
|
369
|
+
# Auto-detect (first column that's not time)
|
|
370
|
+
for i, col in enumerate(header):
|
|
371
|
+
if i == time_idx:
|
|
372
|
+
continue
|
|
373
|
+
if col.lower() in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
|
|
374
|
+
voltage_idx = i
|
|
375
|
+
break
|
|
376
|
+
if voltage_idx is None:
|
|
377
|
+
voltage_idx = 1 if time_idx == 0 else 0
|
|
378
|
+
else:
|
|
379
|
+
# No header - use indices
|
|
380
|
+
if isinstance(time_column, int):
|
|
381
|
+
time_idx = time_column
|
|
382
|
+
else:
|
|
383
|
+
time_idx = 0 # Assume first column is time
|
|
384
|
+
|
|
385
|
+
if isinstance(voltage_column, int):
|
|
386
|
+
voltage_idx = voltage_column
|
|
387
|
+
else:
|
|
388
|
+
voltage_idx = 1 # Assume second column is voltage
|
|
389
|
+
|
|
390
|
+
# Extract data
|
|
391
|
+
time_data = []
|
|
392
|
+
voltage_data = []
|
|
393
|
+
|
|
394
|
+
for row in rows[data_start:]:
|
|
395
|
+
if not row:
|
|
396
|
+
continue
|
|
397
|
+
try:
|
|
398
|
+
if voltage_idx is not None and voltage_idx < len(row):
|
|
399
|
+
voltage_data.append(float(row[voltage_idx]))
|
|
400
|
+
if time_idx is not None and time_idx < len(row):
|
|
401
|
+
time_data.append(float(row[time_idx]))
|
|
402
|
+
except (ValueError, IndexError):
|
|
403
|
+
continue # Skip malformed rows
|
|
404
|
+
|
|
405
|
+
if not voltage_data:
|
|
406
|
+
raise FormatError(
|
|
407
|
+
"No valid voltage data found in CSV",
|
|
408
|
+
file_path=str(path),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
data = np.array(voltage_data, dtype=np.float64)
|
|
412
|
+
|
|
413
|
+
# Determine sample rate
|
|
414
|
+
detected_sample_rate = sample_rate
|
|
415
|
+
if detected_sample_rate is None and time_data:
|
|
416
|
+
time_arr = np.array(time_data, dtype=np.float64)
|
|
417
|
+
if len(time_arr) > 1:
|
|
418
|
+
dt = np.median(np.diff(time_arr))
|
|
419
|
+
if dt > 0:
|
|
420
|
+
detected_sample_rate = 1.0 / dt
|
|
421
|
+
|
|
422
|
+
if detected_sample_rate is None:
|
|
423
|
+
detected_sample_rate = 1e6
|
|
424
|
+
|
|
425
|
+
# Channel name
|
|
426
|
+
channel_name = "CH1"
|
|
427
|
+
if header and voltage_idx is not None and voltage_idx < len(header):
|
|
428
|
+
channel_name = header[voltage_idx]
|
|
429
|
+
|
|
430
|
+
metadata = TraceMetadata(
|
|
431
|
+
sample_rate=detected_sample_rate,
|
|
432
|
+
source_file=str(path),
|
|
433
|
+
channel_name=channel_name,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
return WaveformTrace(data=data, metadata=metadata)
|
|
437
|
+
|
|
438
|
+
except Exception as e:
|
|
439
|
+
if isinstance(e, LoaderError | FormatError):
|
|
440
|
+
raise
|
|
441
|
+
raise LoaderError(
|
|
442
|
+
"Failed to load CSV file",
|
|
443
|
+
file_path=str(path),
|
|
444
|
+
details=str(e),
|
|
445
|
+
) from e
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _detect_delimiter(path: Path, encoding: str) -> str:
|
|
449
|
+
"""Detect the delimiter used in a CSV file."""
|
|
450
|
+
try:
|
|
451
|
+
with open(path, encoding=encoding) as f:
|
|
452
|
+
sample = f.read(4096)
|
|
453
|
+
return _detect_delimiter_from_content(sample)
|
|
454
|
+
except Exception:
|
|
455
|
+
return ","
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _detect_delimiter_from_content(content: str) -> str:
|
|
459
|
+
"""Detect delimiter from CSV content."""
|
|
460
|
+
# Try common delimiters and count occurrences
|
|
461
|
+
delimiters = [",", "\t", ";", "|", " "]
|
|
462
|
+
counts: dict[str, int] = {}
|
|
463
|
+
|
|
464
|
+
for delim in delimiters:
|
|
465
|
+
counts[delim] = content.count(delim)
|
|
466
|
+
|
|
467
|
+
# Return the most common delimiter
|
|
468
|
+
if counts:
|
|
469
|
+
return max(counts, key=lambda d: counts[d])
|
|
470
|
+
return ","
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
__all__ = ["load_csv"]
|
oscura/loaders/hdf5.py
ADDED