oscura 0.5.0__py3-none-any.whl → 0.6.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/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- 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/__init__.py +1 -22
- 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 +2763 -0
- 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/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- 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 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- 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/README.md +7 -7
- 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 +171 -63
- 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/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- 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/reporting/templates/index.md +13 -13
- 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/autodetect.py +1 -5
- 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 +11 -3
- 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.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- 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/export/wavedrom.py +0 -430
- 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 -338
- 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/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- 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/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /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/{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.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
oscura/loaders/csv_loader.py
CHANGED
|
@@ -173,109 +173,46 @@ def _load_with_pandas(
|
|
|
173
173
|
skip_rows: int,
|
|
174
174
|
encoding: str,
|
|
175
175
|
) -> WaveformTrace:
|
|
176
|
-
"""Load CSV using pandas for better parsing.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
"""Load CSV using pandas for better parsing.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: Path to CSV file.
|
|
180
|
+
time_column: Name or index of time column (None for auto-detect).
|
|
181
|
+
voltage_column: Name or index of voltage column (None for auto-detect).
|
|
182
|
+
sample_rate: Override sample rate (None to compute from time column).
|
|
183
|
+
delimiter: Column delimiter (None for auto-detect).
|
|
184
|
+
skip_rows: Number of rows to skip before header.
|
|
185
|
+
encoding: File encoding.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
WaveformTrace containing waveform data and metadata.
|
|
181
189
|
|
|
190
|
+
Raises:
|
|
191
|
+
FormatError: If CSV format is invalid or missing data.
|
|
192
|
+
LoaderError: If file cannot be loaded.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
182
195
|
# Read CSV with pandas
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
196
|
+
delimiter = delimiter or _detect_delimiter(path, encoding)
|
|
197
|
+
df = _read_csv_with_pandas(path, delimiter, skip_rows, encoding)
|
|
198
|
+
|
|
199
|
+
# Find time and voltage columns
|
|
200
|
+
time_data, time_col_name = _find_pandas_time_column(df, time_column)
|
|
201
|
+
voltage_data, voltage_col_name = _find_pandas_voltage_column(
|
|
202
|
+
df, voltage_column, time_col_name, path
|
|
189
203
|
)
|
|
190
204
|
|
|
191
|
-
if
|
|
192
|
-
|
|
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
|
|
205
|
+
# Compute sample rate from time data if not provided
|
|
206
|
+
detected_sample_rate = _compute_sample_rate_from_array(sample_rate, time_data)
|
|
217
207
|
|
|
218
|
-
#
|
|
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
|
|
208
|
+
# Create metadata and trace
|
|
272
209
|
metadata = TraceMetadata(
|
|
273
210
|
sample_rate=detected_sample_rate,
|
|
274
211
|
source_file=str(path),
|
|
275
212
|
channel_name=voltage_col_name or "CH1",
|
|
276
213
|
)
|
|
277
214
|
|
|
278
|
-
return WaveformTrace(data=
|
|
215
|
+
return WaveformTrace(data=np.asarray(voltage_data, dtype=np.float64), metadata=metadata)
|
|
279
216
|
|
|
280
217
|
except pd.errors.ParserError as e:
|
|
281
218
|
raise FormatError(
|
|
@@ -293,6 +230,151 @@ def _load_with_pandas(
|
|
|
293
230
|
) from e
|
|
294
231
|
|
|
295
232
|
|
|
233
|
+
def _read_csv_with_pandas(path: Path, delimiter: str, skip_rows: int, encoding: str) -> Any:
|
|
234
|
+
"""Read CSV file using pandas.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
path: Path to CSV file.
|
|
238
|
+
delimiter: Column delimiter.
|
|
239
|
+
skip_rows: Number of rows to skip before header.
|
|
240
|
+
encoding: File encoding.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Pandas DataFrame.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
FormatError: If CSV is empty.
|
|
247
|
+
"""
|
|
248
|
+
df = pd.read_csv(
|
|
249
|
+
path,
|
|
250
|
+
delimiter=delimiter,
|
|
251
|
+
skiprows=skip_rows,
|
|
252
|
+
encoding=encoding,
|
|
253
|
+
engine="python", # More flexible parsing
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if df.empty:
|
|
257
|
+
raise FormatError("CSV file is empty", file_path=str(path))
|
|
258
|
+
|
|
259
|
+
return df
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _find_pandas_time_column(
|
|
263
|
+
df: Any, time_column: str | int | None
|
|
264
|
+
) -> tuple[Any | None, str | None]:
|
|
265
|
+
"""Find time column in pandas DataFrame.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
df: Pandas DataFrame.
|
|
269
|
+
time_column: User-specified time column name or index.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Tuple of (time_data, time_column_name).
|
|
273
|
+
"""
|
|
274
|
+
time_data = None
|
|
275
|
+
time_col_name = None
|
|
276
|
+
|
|
277
|
+
if time_column is not None:
|
|
278
|
+
if isinstance(time_column, int):
|
|
279
|
+
if time_column < len(df.columns):
|
|
280
|
+
time_col_name = df.columns[time_column]
|
|
281
|
+
time_data = df.iloc[:, time_column].values
|
|
282
|
+
elif time_column in df.columns:
|
|
283
|
+
time_col_name = time_column
|
|
284
|
+
time_data = df[time_column].values
|
|
285
|
+
else:
|
|
286
|
+
# Auto-detect time column
|
|
287
|
+
for col in df.columns:
|
|
288
|
+
col_lower = col.lower().strip()
|
|
289
|
+
if col_lower in [n.lower() for n in TIME_COLUMN_NAMES]:
|
|
290
|
+
time_col_name = col
|
|
291
|
+
time_data = df[col].values
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
return time_data, time_col_name
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _find_pandas_voltage_column(
|
|
298
|
+
df: Any, voltage_column: str | int | None, time_col_name: str | None, path: Path
|
|
299
|
+
) -> tuple[Any, str | None]:
|
|
300
|
+
"""Find voltage column in pandas DataFrame.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
df: Pandas DataFrame.
|
|
304
|
+
voltage_column: User-specified voltage column name or index.
|
|
305
|
+
time_col_name: Name of time column (to exclude from voltage search).
|
|
306
|
+
path: Path to CSV file (for error reporting).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Tuple of (voltage_data, voltage_column_name).
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
FormatError: If no voltage data found.
|
|
313
|
+
"""
|
|
314
|
+
voltage_data = None
|
|
315
|
+
voltage_col_name = None
|
|
316
|
+
|
|
317
|
+
if voltage_column is not None:
|
|
318
|
+
if isinstance(voltage_column, int):
|
|
319
|
+
if voltage_column < len(df.columns):
|
|
320
|
+
voltage_col_name = df.columns[voltage_column]
|
|
321
|
+
voltage_data = df.iloc[:, voltage_column].values
|
|
322
|
+
elif voltage_column in df.columns:
|
|
323
|
+
voltage_col_name = voltage_column
|
|
324
|
+
voltage_data = df[voltage_column].values
|
|
325
|
+
else:
|
|
326
|
+
# Auto-detect voltage column (first non-time numeric column)
|
|
327
|
+
for col in df.columns:
|
|
328
|
+
if col == time_col_name:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
col_lower = col.lower().strip()
|
|
332
|
+
|
|
333
|
+
# Check if numeric
|
|
334
|
+
if pd.api.types.is_numeric_dtype(df[col]):
|
|
335
|
+
# Prefer columns with voltage-like names
|
|
336
|
+
if col_lower in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
|
|
337
|
+
voltage_col_name = col
|
|
338
|
+
voltage_data = df[col].values
|
|
339
|
+
break
|
|
340
|
+
elif voltage_data is None:
|
|
341
|
+
voltage_col_name = col
|
|
342
|
+
voltage_data = df[col].values
|
|
343
|
+
|
|
344
|
+
if voltage_data is None:
|
|
345
|
+
raise FormatError(
|
|
346
|
+
"No voltage data found in CSV",
|
|
347
|
+
file_path=str(path),
|
|
348
|
+
expected="Numeric column for voltage data",
|
|
349
|
+
got=f"Columns: {', '.join(df.columns)}",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return voltage_data, voltage_col_name
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _compute_sample_rate_from_array(sample_rate: float | None, time_data: Any | None) -> float:
|
|
356
|
+
"""Compute sample rate from numpy array or use override.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
sample_rate: User-specified sample rate (None to compute).
|
|
360
|
+
time_data: Numpy array of time values.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Sample rate in Hz. Defaults to 1 MHz if cannot be computed.
|
|
364
|
+
"""
|
|
365
|
+
if sample_rate is not None:
|
|
366
|
+
return sample_rate
|
|
367
|
+
|
|
368
|
+
if time_data is not None:
|
|
369
|
+
time_arr = np.asarray(time_data, dtype=np.float64)
|
|
370
|
+
if len(time_arr) > 1:
|
|
371
|
+
dt = float(np.median(np.diff(time_arr)))
|
|
372
|
+
if dt > 0:
|
|
373
|
+
return 1.0 / dt
|
|
374
|
+
|
|
375
|
+
return 1e6 # Default to 1 MSa/s
|
|
376
|
+
|
|
377
|
+
|
|
296
378
|
def _load_basic(
|
|
297
379
|
path: Path,
|
|
298
380
|
*,
|
|
@@ -303,137 +385,55 @@ def _load_basic(
|
|
|
303
385
|
skip_rows: int,
|
|
304
386
|
encoding: str,
|
|
305
387
|
) -> WaveformTrace:
|
|
306
|
-
"""Basic CSV loader without pandas.
|
|
388
|
+
"""Basic CSV loader without pandas.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
path: Path to CSV file.
|
|
392
|
+
time_column: Name or index of time column (None for auto-detect).
|
|
393
|
+
voltage_column: Name or index of voltage column (None for auto-detect).
|
|
394
|
+
sample_rate: Override sample rate (None to compute from time column).
|
|
395
|
+
delimiter: Column delimiter (None for auto-detect).
|
|
396
|
+
skip_rows: Number of rows to skip before header.
|
|
397
|
+
encoding: File encoding.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
WaveformTrace containing waveform data and metadata.
|
|
401
|
+
|
|
402
|
+
Raises:
|
|
403
|
+
FormatError: If CSV format is invalid or missing data.
|
|
404
|
+
LoaderError: If file cannot be loaded.
|
|
405
|
+
"""
|
|
307
406
|
try:
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
407
|
+
# Read and parse CSV
|
|
408
|
+
content = _read_file_content(path, skip_rows, encoding)
|
|
409
|
+
delimiter = delimiter or _detect_delimiter_from_content(content)
|
|
410
|
+
rows = _parse_csv_rows(content, delimiter, path)
|
|
340
411
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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]
|
|
412
|
+
# Detect header and determine data start position
|
|
413
|
+
header, data_start = _detect_header(rows)
|
|
414
|
+
|
|
415
|
+
# Find column indices for time and voltage data
|
|
416
|
+
time_idx, voltage_idx = _determine_column_indices(header, time_column, voltage_column)
|
|
417
|
+
|
|
418
|
+
# Extract numeric data from rows
|
|
419
|
+
time_data, voltage_data = _extract_data_from_rows(
|
|
420
|
+
rows, data_start, time_idx, voltage_idx, path
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
# Calculate sample rate from time data if not provided
|
|
424
|
+
detected_sample_rate = _compute_sample_rate(sample_rate, time_data)
|
|
429
425
|
|
|
426
|
+
# Build channel name from header if available
|
|
427
|
+
channel_name = _get_channel_name(header, voltage_idx)
|
|
428
|
+
|
|
429
|
+
# Create metadata and trace
|
|
430
430
|
metadata = TraceMetadata(
|
|
431
431
|
sample_rate=detected_sample_rate,
|
|
432
432
|
source_file=str(path),
|
|
433
433
|
channel_name=channel_name,
|
|
434
434
|
)
|
|
435
435
|
|
|
436
|
-
return WaveformTrace(data=
|
|
436
|
+
return WaveformTrace(data=np.array(voltage_data, dtype=np.float64), metadata=metadata)
|
|
437
437
|
|
|
438
438
|
except Exception as e:
|
|
439
439
|
if isinstance(e, LoaderError | FormatError):
|
|
@@ -445,10 +445,253 @@ def _load_basic(
|
|
|
445
445
|
) from e
|
|
446
446
|
|
|
447
447
|
|
|
448
|
+
def _read_file_content(path: Path, skip_rows: int, encoding: str) -> str:
|
|
449
|
+
"""Read file content after skipping specified rows.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
path: Path to CSV file.
|
|
453
|
+
skip_rows: Number of rows to skip.
|
|
454
|
+
encoding: File encoding.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
File content as string.
|
|
458
|
+
"""
|
|
459
|
+
with open(path, encoding=encoding, buffering=65536) as f:
|
|
460
|
+
for _ in range(skip_rows):
|
|
461
|
+
next(f)
|
|
462
|
+
return f.read()
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _parse_csv_rows(content: str, delimiter: str, path: Path) -> list[list[str]]:
|
|
466
|
+
"""Parse CSV content into rows.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
content: CSV file content.
|
|
470
|
+
delimiter: Column delimiter.
|
|
471
|
+
path: Path to CSV file (for error reporting).
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
List of rows, where each row is a list of cell values.
|
|
475
|
+
|
|
476
|
+
Raises:
|
|
477
|
+
FormatError: If CSV is empty.
|
|
478
|
+
"""
|
|
479
|
+
reader = csv.reader(StringIO(content), delimiter=delimiter)
|
|
480
|
+
rows = list(reader)
|
|
481
|
+
|
|
482
|
+
if not rows:
|
|
483
|
+
raise FormatError("CSV file is empty", file_path=str(path))
|
|
484
|
+
|
|
485
|
+
return rows
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _detect_header(rows: list[list[str]]) -> tuple[list[str] | None, int]:
|
|
489
|
+
"""Detect if first row is a header row.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
rows: Parsed CSV rows.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Tuple of (header row, data start index).
|
|
496
|
+
If no header detected, returns (None, 0).
|
|
497
|
+
"""
|
|
498
|
+
first_row = rows[0]
|
|
499
|
+
|
|
500
|
+
# Check if first row contains non-numeric values (indicates header)
|
|
501
|
+
for cell in first_row:
|
|
502
|
+
try:
|
|
503
|
+
float(cell)
|
|
504
|
+
except ValueError:
|
|
505
|
+
if cell.strip(): # Non-empty, non-numeric
|
|
506
|
+
return [cell.strip() for cell in first_row], 1
|
|
507
|
+
|
|
508
|
+
return None, 0
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _determine_column_indices(
|
|
512
|
+
header: list[str] | None,
|
|
513
|
+
time_column: str | int | None,
|
|
514
|
+
voltage_column: str | int | None,
|
|
515
|
+
) -> tuple[int | None, int | None]:
|
|
516
|
+
"""Determine column indices for time and voltage data.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
header: Header row if detected, None otherwise.
|
|
520
|
+
time_column: User-specified time column name or index.
|
|
521
|
+
voltage_column: User-specified voltage column name or index.
|
|
522
|
+
|
|
523
|
+
Returns:
|
|
524
|
+
Tuple of (time_index, voltage_index).
|
|
525
|
+
"""
|
|
526
|
+
if header:
|
|
527
|
+
time_idx = _find_time_column_index(header, time_column)
|
|
528
|
+
voltage_idx = _find_voltage_column_index(header, voltage_column, time_idx)
|
|
529
|
+
else:
|
|
530
|
+
time_idx = _get_index_or_default(time_column, 0)
|
|
531
|
+
voltage_idx = _get_index_or_default(voltage_column, 1)
|
|
532
|
+
|
|
533
|
+
return time_idx, voltage_idx
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _find_time_column_index(header: list[str], time_column: str | int | None) -> int | None:
|
|
537
|
+
"""Find time column index from header.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
header: Header row.
|
|
541
|
+
time_column: User-specified time column name or index.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Time column index, or None if not found.
|
|
545
|
+
"""
|
|
546
|
+
if time_column is not None:
|
|
547
|
+
if isinstance(time_column, int):
|
|
548
|
+
return time_column
|
|
549
|
+
if time_column in header:
|
|
550
|
+
return header.index(time_column)
|
|
551
|
+
else:
|
|
552
|
+
# Auto-detect
|
|
553
|
+
for i, col in enumerate(header):
|
|
554
|
+
if col.lower() in [n.lower() for n in TIME_COLUMN_NAMES]:
|
|
555
|
+
return i
|
|
556
|
+
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _find_voltage_column_index(
|
|
561
|
+
header: list[str], voltage_column: str | int | None, time_idx: int | None
|
|
562
|
+
) -> int | None:
|
|
563
|
+
"""Find voltage column index from header.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
header: Header row.
|
|
567
|
+
voltage_column: User-specified voltage column name or index.
|
|
568
|
+
time_idx: Time column index (to exclude from voltage search).
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
Voltage column index.
|
|
572
|
+
"""
|
|
573
|
+
if voltage_column is not None:
|
|
574
|
+
if isinstance(voltage_column, int):
|
|
575
|
+
return voltage_column
|
|
576
|
+
if voltage_column in header:
|
|
577
|
+
return header.index(voltage_column)
|
|
578
|
+
else:
|
|
579
|
+
# Auto-detect (first column that's not time)
|
|
580
|
+
for i, col in enumerate(header):
|
|
581
|
+
if i == time_idx:
|
|
582
|
+
continue
|
|
583
|
+
if col.lower() in [n.lower() for n in VOLTAGE_COLUMN_NAMES]:
|
|
584
|
+
return i
|
|
585
|
+
|
|
586
|
+
# Default: column 1 if time is 0, otherwise column 0
|
|
587
|
+
return 1 if time_idx == 0 else 0
|
|
588
|
+
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _get_index_or_default(column: str | int | None, default: int) -> int:
|
|
593
|
+
"""Get column index or return default.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
column: User-specified column (int or string).
|
|
597
|
+
default: Default index if column is not an int.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Column index.
|
|
601
|
+
"""
|
|
602
|
+
if isinstance(column, int):
|
|
603
|
+
return column
|
|
604
|
+
return default
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _extract_data_from_rows(
|
|
608
|
+
rows: list[list[str]],
|
|
609
|
+
data_start: int,
|
|
610
|
+
time_idx: int | None,
|
|
611
|
+
voltage_idx: int | None,
|
|
612
|
+
path: Path,
|
|
613
|
+
) -> tuple[list[float], list[float]]:
|
|
614
|
+
"""Extract numeric data from CSV rows.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
rows: Parsed CSV rows.
|
|
618
|
+
data_start: Index of first data row.
|
|
619
|
+
time_idx: Time column index.
|
|
620
|
+
voltage_idx: Voltage column index.
|
|
621
|
+
path: Path to CSV file (for error reporting).
|
|
622
|
+
|
|
623
|
+
Returns:
|
|
624
|
+
Tuple of (time_data, voltage_data) lists.
|
|
625
|
+
|
|
626
|
+
Raises:
|
|
627
|
+
FormatError: If no valid voltage data found.
|
|
628
|
+
"""
|
|
629
|
+
time_data: list[float] = []
|
|
630
|
+
voltage_data: list[float] = []
|
|
631
|
+
|
|
632
|
+
for row in rows[data_start:]:
|
|
633
|
+
if not row:
|
|
634
|
+
continue
|
|
635
|
+
|
|
636
|
+
try:
|
|
637
|
+
if voltage_idx is not None and voltage_idx < len(row):
|
|
638
|
+
voltage_data.append(float(row[voltage_idx]))
|
|
639
|
+
if time_idx is not None and time_idx < len(row):
|
|
640
|
+
time_data.append(float(row[time_idx]))
|
|
641
|
+
except (ValueError, IndexError):
|
|
642
|
+
continue # Skip malformed rows
|
|
643
|
+
|
|
644
|
+
if not voltage_data:
|
|
645
|
+
raise FormatError(
|
|
646
|
+
"No valid voltage data found in CSV",
|
|
647
|
+
file_path=str(path),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return time_data, voltage_data
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def _compute_sample_rate(sample_rate: float | None, time_data: list[float]) -> float:
|
|
654
|
+
"""Compute sample rate from time data or use override.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
sample_rate: User-specified sample rate (None to compute).
|
|
658
|
+
time_data: List of time values.
|
|
659
|
+
|
|
660
|
+
Returns:
|
|
661
|
+
Sample rate in Hz. Defaults to 1 MHz if cannot be computed.
|
|
662
|
+
"""
|
|
663
|
+
if sample_rate is not None:
|
|
664
|
+
return sample_rate
|
|
665
|
+
|
|
666
|
+
if time_data:
|
|
667
|
+
time_arr = np.array(time_data, dtype=np.float64)
|
|
668
|
+
if len(time_arr) > 1:
|
|
669
|
+
dt = float(np.median(np.diff(time_arr)))
|
|
670
|
+
if dt > 0:
|
|
671
|
+
return 1.0 / dt
|
|
672
|
+
|
|
673
|
+
return 1e6 # Default to 1 MSa/s
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def _get_channel_name(header: list[str] | None, voltage_idx: int | None) -> str:
|
|
677
|
+
"""Get channel name from header or use default.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
header: Header row if available.
|
|
681
|
+
voltage_idx: Voltage column index.
|
|
682
|
+
|
|
683
|
+
Returns:
|
|
684
|
+
Channel name string.
|
|
685
|
+
"""
|
|
686
|
+
if header and voltage_idx is not None and voltage_idx < len(header):
|
|
687
|
+
return header[voltage_idx]
|
|
688
|
+
return "CH1"
|
|
689
|
+
|
|
690
|
+
|
|
448
691
|
def _detect_delimiter(path: Path, encoding: str) -> str:
|
|
449
692
|
"""Detect the delimiter used in a CSV file."""
|
|
450
693
|
try:
|
|
451
|
-
with open(path, encoding=encoding) as f:
|
|
694
|
+
with open(path, encoding=encoding, buffering=65536) as f:
|
|
452
695
|
sample = f.read(4096)
|
|
453
696
|
return _detect_delimiter_from_content(sample)
|
|
454
697
|
except Exception:
|