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
|
@@ -6,7 +6,7 @@ and integration.
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Example:
|
|
9
|
-
>>> from oscura.math import add, differentiate
|
|
9
|
+
>>> from oscura.utils.math import add, differentiate
|
|
10
10
|
>>> combined = add(trace1, trace2)
|
|
11
11
|
>>> derivative = differentiate(trace)
|
|
12
12
|
|
|
@@ -716,48 +716,21 @@ class _SafeExpressionEvaluator(ast.NodeVisitor):
|
|
|
716
716
|
raise AnalysisError(f"AST node type {node.__class__.__name__} not allowed")
|
|
717
717
|
|
|
718
718
|
|
|
719
|
-
def
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
channel_name: str | None = None,
|
|
724
|
-
) -> WaveformTrace:
|
|
725
|
-
"""Evaluate a mathematical expression on traces.
|
|
726
|
-
|
|
727
|
-
Evaluates an expression string using named traces as variables.
|
|
728
|
-
Supports standard mathematical operations and numpy functions.
|
|
719
|
+
def _validate_trace_compatibility(
|
|
720
|
+
traces: dict[str, WaveformTrace], ref_trace: WaveformTrace
|
|
721
|
+
) -> None:
|
|
722
|
+
"""Validate all traces have same length and sample rate.
|
|
729
723
|
|
|
730
724
|
Args:
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
channel_name: Name for the result trace (optional).
|
|
734
|
-
|
|
735
|
-
Returns:
|
|
736
|
-
Result WaveformTrace.
|
|
725
|
+
traces: Dictionary of traces to validate.
|
|
726
|
+
ref_trace: Reference trace for comparison.
|
|
737
727
|
|
|
738
728
|
Raises:
|
|
739
|
-
AnalysisError: If
|
|
740
|
-
|
|
741
|
-
Example:
|
|
742
|
-
>>> power = math_expression(
|
|
743
|
-
... "voltage * current",
|
|
744
|
-
... {"voltage": v_trace, "current": i_trace}
|
|
745
|
-
... )
|
|
746
|
-
|
|
747
|
-
Security:
|
|
748
|
-
Uses AST-based safe evaluation (not eval()). Only whitelisted
|
|
749
|
-
operations are permitted: arithmetic, comparisons, and whitelisted
|
|
750
|
-
numpy functions. No arbitrary code execution is possible.
|
|
729
|
+
AnalysisError: If traces have incompatible dimensions.
|
|
751
730
|
"""
|
|
752
|
-
|
|
753
|
-
raise AnalysisError("No traces provided for expression evaluation")
|
|
754
|
-
|
|
755
|
-
# Get a reference trace for metadata
|
|
756
|
-
ref_trace = next(iter(traces.values()))
|
|
731
|
+
ref_len = len(ref_trace.data)
|
|
757
732
|
sample_rate = ref_trace.metadata.sample_rate
|
|
758
733
|
|
|
759
|
-
# Validate all traces have same length and sample rate
|
|
760
|
-
ref_len = len(ref_trace.data)
|
|
761
734
|
for name, trace in traces.items():
|
|
762
735
|
if len(trace.data) != ref_len:
|
|
763
736
|
raise AnalysisError(
|
|
@@ -771,8 +744,17 @@ def math_expression(
|
|
|
771
744
|
details={"expected": sample_rate, "got": trace.metadata.sample_rate}, # type: ignore[arg-type]
|
|
772
745
|
)
|
|
773
746
|
|
|
774
|
-
|
|
775
|
-
|
|
747
|
+
|
|
748
|
+
def _build_safe_namespace(traces: dict[str, WaveformTrace]) -> dict[str, Any]:
|
|
749
|
+
"""Build safe namespace with trace data and whitelisted functions.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
traces: Dictionary of traces.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
Namespace dictionary with safe functions and trace data.
|
|
756
|
+
"""
|
|
757
|
+
safe_namespace: dict[str, Any] = {
|
|
776
758
|
"np": np,
|
|
777
759
|
"abs": np.abs,
|
|
778
760
|
"sqrt": np.sqrt,
|
|
@@ -789,28 +771,67 @@ def math_expression(
|
|
|
789
771
|
"pi": np.pi,
|
|
790
772
|
}
|
|
791
773
|
|
|
792
|
-
# Add trace data to namespace
|
|
793
774
|
for name, trace in traces.items():
|
|
794
775
|
safe_namespace[name] = trace.data.astype(np.float64)
|
|
795
776
|
|
|
796
|
-
|
|
797
|
-
|
|
777
|
+
return safe_namespace
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def _evaluate_expression(expression: str, namespace: dict[str, Any]) -> Any:
|
|
781
|
+
"""Evaluate expression using safe AST-based evaluator.
|
|
782
|
+
|
|
783
|
+
Args:
|
|
784
|
+
expression: Mathematical expression string.
|
|
785
|
+
namespace: Safe namespace with available functions and variables.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
Evaluated result.
|
|
789
|
+
|
|
790
|
+
Raises:
|
|
791
|
+
AnalysisError: If evaluation fails.
|
|
792
|
+
"""
|
|
793
|
+
evaluator = _SafeExpressionEvaluator(namespace)
|
|
798
794
|
try:
|
|
799
|
-
|
|
795
|
+
return evaluator.eval(expression)
|
|
800
796
|
except AnalysisError:
|
|
801
|
-
raise
|
|
797
|
+
raise
|
|
802
798
|
except Exception as e:
|
|
803
799
|
raise AnalysisError(
|
|
804
800
|
f"Failed to evaluate expression: {e}",
|
|
805
801
|
details={"expression": expression}, # type: ignore[arg-type]
|
|
806
802
|
) from e
|
|
807
803
|
|
|
804
|
+
|
|
805
|
+
def _ensure_array_result(result: Any, expected_len: int) -> NDArray[np.float64]:
|
|
806
|
+
"""Ensure result is array of expected length.
|
|
807
|
+
|
|
808
|
+
Args:
|
|
809
|
+
result: Evaluation result.
|
|
810
|
+
expected_len: Expected array length.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Result as float64 array.
|
|
814
|
+
"""
|
|
808
815
|
if not isinstance(result, np.ndarray):
|
|
809
|
-
|
|
810
|
-
|
|
816
|
+
return np.full(expected_len, result, dtype=np.float64)
|
|
817
|
+
return result
|
|
811
818
|
|
|
812
|
-
|
|
813
|
-
|
|
819
|
+
|
|
820
|
+
def _build_expression_metadata(
|
|
821
|
+
ref_trace: WaveformTrace, expression: str, channel_name: str | None
|
|
822
|
+
) -> TraceMetadata:
|
|
823
|
+
"""Build metadata for expression result trace.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
ref_trace: Reference trace for metadata.
|
|
827
|
+
expression: Expression string (for default naming).
|
|
828
|
+
channel_name: Optional channel name override.
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
Metadata for result trace.
|
|
832
|
+
"""
|
|
833
|
+
return TraceMetadata(
|
|
834
|
+
sample_rate=ref_trace.metadata.sample_rate,
|
|
814
835
|
vertical_scale=None,
|
|
815
836
|
vertical_offset=None,
|
|
816
837
|
acquisition_time=ref_trace.metadata.acquisition_time,
|
|
@@ -819,4 +840,49 @@ def math_expression(
|
|
|
819
840
|
channel_name=channel_name or f"expr({expression[:20]})",
|
|
820
841
|
)
|
|
821
842
|
|
|
822
|
-
|
|
843
|
+
|
|
844
|
+
def math_expression(
|
|
845
|
+
expression: str,
|
|
846
|
+
traces: dict[str, WaveformTrace],
|
|
847
|
+
*,
|
|
848
|
+
channel_name: str | None = None,
|
|
849
|
+
) -> WaveformTrace:
|
|
850
|
+
"""Evaluate a mathematical expression on traces.
|
|
851
|
+
|
|
852
|
+
Evaluates an expression string using named traces as variables.
|
|
853
|
+
Supports standard mathematical operations and numpy functions.
|
|
854
|
+
|
|
855
|
+
Args:
|
|
856
|
+
expression: Math expression (e.g., "CH1 + CH2", "abs(CH1 - CH2)").
|
|
857
|
+
traces: Dictionary mapping variable names to traces.
|
|
858
|
+
channel_name: Name for the result trace (optional).
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
Result WaveformTrace.
|
|
862
|
+
|
|
863
|
+
Raises:
|
|
864
|
+
AnalysisError: If expression is invalid or traces are incompatible.
|
|
865
|
+
|
|
866
|
+
Example:
|
|
867
|
+
>>> power = math_expression(
|
|
868
|
+
... "voltage * current",
|
|
869
|
+
... {"voltage": v_trace, "current": i_trace}
|
|
870
|
+
... )
|
|
871
|
+
|
|
872
|
+
Security:
|
|
873
|
+
Uses AST-based safe evaluation (not eval()). Only whitelisted
|
|
874
|
+
operations are permitted: arithmetic, comparisons, and whitelisted
|
|
875
|
+
numpy functions. No arbitrary code execution is possible.
|
|
876
|
+
"""
|
|
877
|
+
if not traces:
|
|
878
|
+
raise AnalysisError("No traces provided for expression evaluation")
|
|
879
|
+
|
|
880
|
+
ref_trace = next(iter(traces.values()))
|
|
881
|
+
_validate_trace_compatibility(traces, ref_trace)
|
|
882
|
+
|
|
883
|
+
safe_namespace = _build_safe_namespace(traces)
|
|
884
|
+
result = _evaluate_expression(expression, safe_namespace)
|
|
885
|
+
result = _ensure_array_result(result, len(ref_trace.data))
|
|
886
|
+
|
|
887
|
+
metadata = _build_expression_metadata(ref_trace, expression, channel_name)
|
|
888
|
+
return WaveformTrace(data=result.astype(np.float64), metadata=metadata)
|
|
@@ -5,7 +5,7 @@ functions for waveform data.
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Example:
|
|
8
|
-
>>> from oscura.math import resample, align_traces
|
|
8
|
+
>>> from oscura.utils.math import resample, align_traces
|
|
9
9
|
>>> resampled = resample(trace, new_sample_rate=1e6)
|
|
10
10
|
>>> aligned = align_traces(trace1, trace2)
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ References:
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import warnings
|
|
19
|
-
from typing import TYPE_CHECKING, Literal
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
from scipy import interpolate as sp_interp
|
|
@@ -45,13 +45,8 @@ def interpolate(
|
|
|
45
45
|
Args:
|
|
46
46
|
trace: Input trace.
|
|
47
47
|
new_time: New time points in seconds.
|
|
48
|
-
method: Interpolation method
|
|
49
|
-
- "linear": Linear interpolation (default)
|
|
50
|
-
- "cubic": Cubic spline interpolation
|
|
51
|
-
- "nearest": Nearest neighbor
|
|
52
|
-
- "zero": Zero-order hold (step function)
|
|
48
|
+
method: Interpolation method ("linear", "cubic", "nearest", "zero").
|
|
53
49
|
fill_value: Value for points outside original range.
|
|
54
|
-
Can be a single value or (below, above) tuple.
|
|
55
50
|
channel_name: Name for the result trace (optional).
|
|
56
51
|
|
|
57
52
|
Returns:
|
|
@@ -73,55 +68,49 @@ def interpolate(
|
|
|
73
68
|
analysis_type="interpolate",
|
|
74
69
|
)
|
|
75
70
|
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
# Create interpolator and interpolate
|
|
72
|
+
interp_func = _create_interpolator(
|
|
73
|
+
trace.time_vector, trace.data.astype(np.float64), method, fill_value
|
|
74
|
+
)
|
|
75
|
+
result_data = interp_func(new_time)
|
|
78
76
|
|
|
79
|
-
#
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
elif method == "nearest":
|
|
97
|
-
interp_func = sp_interp.interp1d(
|
|
98
|
-
original_time,
|
|
99
|
-
data,
|
|
100
|
-
kind="nearest",
|
|
101
|
-
bounds_error=False,
|
|
102
|
-
fill_value=fill_value,
|
|
103
|
-
)
|
|
104
|
-
elif method == "zero":
|
|
105
|
-
interp_func = sp_interp.interp1d(
|
|
106
|
-
original_time,
|
|
107
|
-
data,
|
|
108
|
-
kind="zero",
|
|
109
|
-
bounds_error=False,
|
|
110
|
-
fill_value=fill_value,
|
|
111
|
-
)
|
|
112
|
-
else:
|
|
77
|
+
# Build result trace
|
|
78
|
+
new_sample_rate = _calculate_new_sample_rate(new_time, trace.metadata.sample_rate)
|
|
79
|
+
new_metadata = _create_interpolated_metadata(trace, new_sample_rate, channel_name)
|
|
80
|
+
|
|
81
|
+
return WaveformTrace(data=result_data.astype(np.float64), metadata=new_metadata)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _create_interpolator(
|
|
85
|
+
original_time: NDArray[np.float64],
|
|
86
|
+
data: NDArray[np.float64],
|
|
87
|
+
method: str,
|
|
88
|
+
fill_value: float | tuple[float, float],
|
|
89
|
+
) -> Any:
|
|
90
|
+
"""Create scipy interpolation function."""
|
|
91
|
+
valid_methods = {"linear", "cubic", "nearest", "zero"}
|
|
92
|
+
if method not in valid_methods:
|
|
113
93
|
raise ValueError(f"Unknown interpolation method: {method}")
|
|
114
94
|
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
return sp_interp.interp1d(
|
|
96
|
+
original_time, data, kind=method, bounds_error=False, fill_value=fill_value
|
|
97
|
+
)
|
|
98
|
+
|
|
117
99
|
|
|
118
|
-
|
|
100
|
+
def _calculate_new_sample_rate(new_time: NDArray[np.float64], original_sample_rate: float) -> float:
|
|
101
|
+
"""Calculate new sample rate from time points."""
|
|
119
102
|
if len(new_time) > 1:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
mean_diff: np.floating[Any] = np.mean(np.diff(new_time))
|
|
104
|
+
new_rate: float = float(1.0 / mean_diff)
|
|
105
|
+
return new_rate
|
|
106
|
+
return original_sample_rate
|
|
123
107
|
|
|
124
|
-
|
|
108
|
+
|
|
109
|
+
def _create_interpolated_metadata(
|
|
110
|
+
trace: WaveformTrace, new_sample_rate: float, channel_name: str | None
|
|
111
|
+
) -> TraceMetadata:
|
|
112
|
+
"""Create metadata for interpolated trace."""
|
|
113
|
+
return TraceMetadata(
|
|
125
114
|
sample_rate=new_sample_rate,
|
|
126
115
|
vertical_scale=trace.metadata.vertical_scale,
|
|
127
116
|
vertical_offset=trace.metadata.vertical_offset,
|
|
@@ -131,7 +120,91 @@ def interpolate(
|
|
|
131
120
|
channel_name=channel_name or f"{trace.metadata.channel_name or 'trace'}_interp",
|
|
132
121
|
)
|
|
133
122
|
|
|
134
|
-
|
|
123
|
+
|
|
124
|
+
def _calculate_target_params(
|
|
125
|
+
new_sample_rate: float | None,
|
|
126
|
+
num_samples: int | None,
|
|
127
|
+
original_rate: float,
|
|
128
|
+
original_samples: int,
|
|
129
|
+
) -> tuple[float, int]:
|
|
130
|
+
"""Calculate target sample rate and sample count for resampling."""
|
|
131
|
+
if new_sample_rate is not None:
|
|
132
|
+
target_rate = new_sample_rate
|
|
133
|
+
target_samples = round(original_samples * target_rate / original_rate)
|
|
134
|
+
else:
|
|
135
|
+
target_samples = num_samples # type: ignore[assignment]
|
|
136
|
+
target_rate = original_rate * target_samples / original_samples
|
|
137
|
+
return target_rate, target_samples
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _check_nyquist_violation(
|
|
141
|
+
data: NDArray[np.float64], original_rate: float, target_rate: float
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Validate Nyquist criterion when downsampling and warn if violated."""
|
|
144
|
+
fft_data = np.fft.rfft(data)
|
|
145
|
+
fft_freqs = np.fft.rfftfreq(len(data), 1 / original_rate)
|
|
146
|
+
power = np.abs(fft_data) ** 2
|
|
147
|
+
power_threshold = 0.01 * np.max(power)
|
|
148
|
+
significant_freqs = fft_freqs[power > power_threshold]
|
|
149
|
+
|
|
150
|
+
if len(significant_freqs) > 0:
|
|
151
|
+
max_frequency = np.max(significant_freqs)
|
|
152
|
+
nyquist_required = 2 * max_frequency
|
|
153
|
+
if target_rate < nyquist_required:
|
|
154
|
+
warnings.warn(
|
|
155
|
+
f"Downsampling to {target_rate:.2e} Hz violates Nyquist criterion. "
|
|
156
|
+
f"Maximum signal frequency is ~{max_frequency:.2e} Hz, "
|
|
157
|
+
f"requiring ≥{nyquist_required:.2e} Hz sample rate. "
|
|
158
|
+
f"Aliasing may occur.",
|
|
159
|
+
UserWarning,
|
|
160
|
+
stacklevel=3,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _apply_anti_alias_filter(
|
|
165
|
+
data: NDArray[np.float64], target_rate: float, original_rate: float
|
|
166
|
+
) -> NDArray[np.float64]:
|
|
167
|
+
"""Apply lowpass anti-aliasing filter before downsampling."""
|
|
168
|
+
nyquist = target_rate / 2
|
|
169
|
+
cutoff = nyquist / original_rate * 2 # Normalized frequency
|
|
170
|
+
if cutoff < 1.0:
|
|
171
|
+
b, a = sp_signal.butter(8, min(cutoff * 0.9, 0.99), btype="low")
|
|
172
|
+
filtered: NDArray[np.float64] = np.asarray(sp_signal.filtfilt(b, a, data), dtype=np.float64)
|
|
173
|
+
return filtered
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _perform_resampling(
|
|
178
|
+
data: NDArray[np.float64],
|
|
179
|
+
method: Literal["fft", "polyphase", "interp"],
|
|
180
|
+
target_samples: int,
|
|
181
|
+
original_samples: int,
|
|
182
|
+
original_rate: float,
|
|
183
|
+
target_rate: float,
|
|
184
|
+
) -> NDArray[np.float64]:
|
|
185
|
+
"""Perform the actual resampling based on selected method."""
|
|
186
|
+
if method == "fft":
|
|
187
|
+
resampled: NDArray[np.float64] = np.asarray(
|
|
188
|
+
sp_signal.resample(data, target_samples), dtype=np.float64
|
|
189
|
+
)
|
|
190
|
+
return resampled
|
|
191
|
+
elif method == "polyphase":
|
|
192
|
+
from fractions import Fraction
|
|
193
|
+
|
|
194
|
+
ratio = Fraction(target_samples, original_samples).limit_denominator(1000)
|
|
195
|
+
up, down = ratio.numerator, ratio.denominator
|
|
196
|
+
result = sp_signal.resample_poly(data, up, down)
|
|
197
|
+
truncated: NDArray[np.float64] = np.asarray(result[:target_samples], dtype=np.float64)
|
|
198
|
+
return truncated
|
|
199
|
+
elif method == "interp":
|
|
200
|
+
old_time = np.arange(original_samples) / original_rate
|
|
201
|
+
new_time = np.arange(target_samples) / target_rate
|
|
202
|
+
interpolated: NDArray[np.float64] = np.asarray(
|
|
203
|
+
np.interp(new_time, old_time, data), dtype=np.float64
|
|
204
|
+
)
|
|
205
|
+
return interpolated
|
|
206
|
+
else:
|
|
207
|
+
raise ValueError(f"Unknown resampling method: {method}")
|
|
135
208
|
|
|
136
209
|
|
|
137
210
|
def resample(
|
|
@@ -175,9 +248,9 @@ def resample(
|
|
|
175
248
|
References:
|
|
176
249
|
MEM-012 (downsampling for memory management)
|
|
177
250
|
"""
|
|
251
|
+
# Validate inputs
|
|
178
252
|
if (new_sample_rate is None) == (num_samples is None):
|
|
179
253
|
raise ValueError("Specify exactly one of new_sample_rate or num_samples")
|
|
180
|
-
|
|
181
254
|
if len(trace.data) < 2:
|
|
182
255
|
raise InsufficientDataError(
|
|
183
256
|
"Need at least 2 samples for resampling",
|
|
@@ -186,73 +259,33 @@ def resample(
|
|
|
186
259
|
analysis_type="resample",
|
|
187
260
|
)
|
|
188
261
|
|
|
262
|
+
# Setup
|
|
189
263
|
data = trace.data.astype(np.float64)
|
|
190
264
|
original_rate = trace.metadata.sample_rate
|
|
191
265
|
original_samples = len(data)
|
|
192
266
|
|
|
193
267
|
# Calculate target parameters
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
else:
|
|
198
|
-
target_samples = num_samples # type: ignore[assignment]
|
|
199
|
-
target_rate = original_rate * target_samples / original_samples
|
|
268
|
+
target_rate, target_samples = _calculate_target_params(
|
|
269
|
+
new_sample_rate, num_samples, original_rate, original_samples
|
|
270
|
+
)
|
|
200
271
|
|
|
201
272
|
if target_samples < 1:
|
|
202
273
|
raise ValueError("Target number of samples must be at least 1")
|
|
203
274
|
|
|
204
275
|
# REQ: API-019 - Validate Nyquist criterion when downsampling
|
|
205
276
|
if target_rate < original_rate:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
# Find frequency with 90% of max power as max frequency
|
|
210
|
-
power = np.abs(fft_data) ** 2
|
|
211
|
-
power_threshold = 0.01 * np.max(power) # 1% of max power
|
|
212
|
-
significant_freqs = fft_freqs[power > power_threshold]
|
|
213
|
-
if len(significant_freqs) > 0:
|
|
214
|
-
max_frequency = np.max(significant_freqs)
|
|
215
|
-
nyquist_required = 2 * max_frequency
|
|
216
|
-
if target_rate < nyquist_required:
|
|
217
|
-
warnings.warn(
|
|
218
|
-
f"Downsampling to {target_rate:.2e} Hz violates Nyquist criterion. "
|
|
219
|
-
f"Maximum signal frequency is ~{max_frequency:.2e} Hz, "
|
|
220
|
-
f"requiring ≥{nyquist_required:.2e} Hz sample rate. "
|
|
221
|
-
f"Aliasing may occur.",
|
|
222
|
-
UserWarning,
|
|
223
|
-
stacklevel=2,
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
# Check if downsampling and apply anti-alias filter
|
|
277
|
+
_check_nyquist_violation(data, original_rate, target_rate)
|
|
278
|
+
|
|
279
|
+
# Apply anti-aliasing filter if downsampling
|
|
227
280
|
if anti_alias and target_samples < original_samples:
|
|
228
|
-
|
|
229
|
-
nyquist = target_rate / 2
|
|
230
|
-
cutoff = nyquist / original_rate * 2 # Normalized frequency
|
|
231
|
-
if cutoff < 1.0:
|
|
232
|
-
# Design lowpass filter
|
|
233
|
-
b, a = sp_signal.butter(8, min(cutoff * 0.9, 0.99), btype="low")
|
|
234
|
-
data = sp_signal.filtfilt(b, a, data)
|
|
235
|
-
|
|
236
|
-
# Resample
|
|
237
|
-
if method == "fft":
|
|
238
|
-
result_data = sp_signal.resample(data, target_samples)
|
|
239
|
-
elif method == "polyphase":
|
|
240
|
-
# Find rational approximation for polyphase resampling
|
|
241
|
-
from fractions import Fraction
|
|
281
|
+
data = _apply_anti_alias_filter(data, target_rate, original_rate)
|
|
242
282
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
result_data = result_data[:target_samples]
|
|
248
|
-
elif method == "interp":
|
|
249
|
-
# Simple interpolation
|
|
250
|
-
old_time = np.arange(original_samples) / original_rate
|
|
251
|
-
new_time = np.arange(target_samples) / target_rate
|
|
252
|
-
result_data = np.interp(new_time, old_time, data)
|
|
253
|
-
else:
|
|
254
|
-
raise ValueError(f"Unknown resampling method: {method}")
|
|
283
|
+
# Perform resampling
|
|
284
|
+
result_data = _perform_resampling(
|
|
285
|
+
data, method, target_samples, original_samples, original_rate, target_rate
|
|
286
|
+
)
|
|
255
287
|
|
|
288
|
+
# Build output trace
|
|
256
289
|
new_metadata = TraceMetadata(
|
|
257
290
|
sample_rate=target_rate,
|
|
258
291
|
vertical_scale=trace.metadata.vertical_scale,
|