oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Machine learning-based signal analysis module.
|
|
2
|
+
|
|
3
|
+
This module provides ML-based signal classification and analysis tools:
|
|
4
|
+
- Automatic protocol detection using supervised learning
|
|
5
|
+
- Feature extraction (statistical, spectral, temporal, entropy)
|
|
6
|
+
- Multiple ML algorithms (Random Forest, SVM, Neural Networks)
|
|
7
|
+
- Model persistence and incremental learning
|
|
8
|
+
|
|
9
|
+
Supported signal types:
|
|
10
|
+
- Digital protocols: UART, SPI, I2C, CAN, Manchester, NRZ, RZ
|
|
11
|
+
- Analog signals: PWM, AM, FM, analog baseband
|
|
12
|
+
- General classifications: digital, analog, mixed-signal
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
>>> from oscura.analyzers.ml import MLSignalClassifier, TrainingDataset
|
|
16
|
+
>>> classifier = MLSignalClassifier(algorithm="random_forest")
|
|
17
|
+
>>> dataset = TrainingDataset(
|
|
18
|
+
... signals=[uart_signal, spi_signal],
|
|
19
|
+
... labels=["uart", "spi"],
|
|
20
|
+
... sample_rates=[1e6, 1e6]
|
|
21
|
+
... )
|
|
22
|
+
>>> metrics = classifier.train(dataset)
|
|
23
|
+
>>> result = classifier.predict(unknown_signal, sample_rate=1e6)
|
|
24
|
+
>>> print(f"Detected: {result.signal_type} ({result.confidence:.2%})")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from oscura.analyzers.ml.features import FeatureExtractor
|
|
28
|
+
from oscura.analyzers.ml.signal_classifier import (
|
|
29
|
+
MLClassificationResult,
|
|
30
|
+
MLSignalClassifier,
|
|
31
|
+
TrainingDataset,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"FeatureExtractor",
|
|
36
|
+
"MLClassificationResult",
|
|
37
|
+
"MLSignalClassifier",
|
|
38
|
+
"TrainingDataset",
|
|
39
|
+
]
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""Feature extraction utilities for ML-based signal classification.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive feature extraction for signal classification:
|
|
4
|
+
- Statistical features (mean, variance, skewness, kurtosis)
|
|
5
|
+
- Spectral features (FFT-based frequency domain analysis)
|
|
6
|
+
- Temporal features (autocorrelation, zero-crossings, peaks)
|
|
7
|
+
- Entropy features (Shannon entropy, permutation entropy)
|
|
8
|
+
- Shape features (rise/fall time, duty cycle for digital signals)
|
|
9
|
+
|
|
10
|
+
Features are extracted in a standardized format suitable for ML algorithms.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
from scipy import signal as sp_signal
|
|
20
|
+
from scipy import stats
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from numpy.typing import NDArray
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FeatureExtractor:
|
|
29
|
+
"""Extract comprehensive features from signals for ML classification.
|
|
30
|
+
|
|
31
|
+
This class provides a unified interface for extracting multiple feature types
|
|
32
|
+
from time-domain signals. Features are returned as dictionaries with consistent
|
|
33
|
+
naming for use in machine learning pipelines.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> extractor = FeatureExtractor()
|
|
37
|
+
>>> features = extractor.extract_all(signal, sample_rate=1e6)
|
|
38
|
+
>>> print(f"Extracted {len(features)} features")
|
|
39
|
+
>>> print(f"Dominant frequency: {features['dominant_frequency']:.1f} Hz")
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def extract_all(self, data: NDArray[np.floating[Any]], sample_rate: float) -> dict[str, float]:
|
|
43
|
+
"""Extract all feature types from signal.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
data: Input signal as 1D numpy array.
|
|
47
|
+
sample_rate: Sampling rate in Hz.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Dictionary of feature name to value mappings. Contains 40+ features
|
|
51
|
+
spanning statistical, spectral, temporal, entropy, and shape categories.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> signal = np.sin(2 * np.pi * 1000 * np.linspace(0, 1, 10000))
|
|
55
|
+
>>> features = extractor.extract_all(signal, 10000)
|
|
56
|
+
>>> features['dominant_frequency'] # Should be ~1000 Hz
|
|
57
|
+
1000.0
|
|
58
|
+
"""
|
|
59
|
+
features: dict[str, float] = {}
|
|
60
|
+
|
|
61
|
+
# Extract each feature category
|
|
62
|
+
features.update(self.extract_statistical(data))
|
|
63
|
+
features.update(self.extract_spectral(data, sample_rate))
|
|
64
|
+
features.update(self.extract_temporal(data))
|
|
65
|
+
features.update(self.extract_entropy(data))
|
|
66
|
+
features.update(self.extract_shape(data, sample_rate))
|
|
67
|
+
|
|
68
|
+
return features
|
|
69
|
+
|
|
70
|
+
def extract_statistical(self, data: NDArray[np.floating[Any]]) -> dict[str, float]:
|
|
71
|
+
"""Extract statistical features from signal.
|
|
72
|
+
|
|
73
|
+
Computes basic statistical moments and distribution properties:
|
|
74
|
+
- Central tendency: mean, median
|
|
75
|
+
- Dispersion: std, variance, range, IQR
|
|
76
|
+
- Shape: skewness, kurtosis
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
data: Input signal as 1D numpy array.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary with 9 statistical features.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> gaussian = np.random.normal(0, 1, 10000)
|
|
86
|
+
>>> features = extractor.extract_statistical(gaussian)
|
|
87
|
+
>>> abs(features['mean']) < 0.1 # Near zero
|
|
88
|
+
True
|
|
89
|
+
>>> 0.9 < features['std'] < 1.1 # Near 1
|
|
90
|
+
True
|
|
91
|
+
"""
|
|
92
|
+
import warnings
|
|
93
|
+
|
|
94
|
+
# For constant signals, skew/kurtosis cause precision warnings
|
|
95
|
+
with warnings.catch_warnings():
|
|
96
|
+
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
|
97
|
+
skewness = float(stats.skew(data))
|
|
98
|
+
kurtosis = float(stats.kurtosis(data))
|
|
99
|
+
|
|
100
|
+
# Handle NaN from constant signals
|
|
101
|
+
if np.isnan(skewness):
|
|
102
|
+
skewness = 0.0
|
|
103
|
+
if np.isnan(kurtosis):
|
|
104
|
+
kurtosis = 0.0
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"mean": float(np.mean(data)),
|
|
108
|
+
"median": float(np.median(data)),
|
|
109
|
+
"std": float(np.std(data)),
|
|
110
|
+
"variance": float(np.var(data)),
|
|
111
|
+
"min": float(np.min(data)),
|
|
112
|
+
"max": float(np.max(data)),
|
|
113
|
+
"range": float(np.ptp(data)),
|
|
114
|
+
"skewness": skewness,
|
|
115
|
+
"kurtosis": kurtosis,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def extract_spectral(
|
|
119
|
+
self, data: NDArray[np.floating[Any]], sample_rate: float
|
|
120
|
+
) -> dict[str, float]:
|
|
121
|
+
"""Extract spectral features via FFT analysis.
|
|
122
|
+
|
|
123
|
+
Analyzes frequency domain properties:
|
|
124
|
+
- Dominant frequency (peak in spectrum)
|
|
125
|
+
- Spectral centroid (center of mass of spectrum)
|
|
126
|
+
- Bandwidth (frequency range containing most energy)
|
|
127
|
+
- Spectral energy (total power)
|
|
128
|
+
- Spectral flatness (measure of tone vs noise)
|
|
129
|
+
- Spectral rolloff (95th percentile frequency)
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
data: Input signal as 1D numpy array.
|
|
133
|
+
sample_rate: Sampling rate in Hz.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary with 8 spectral features.
|
|
137
|
+
|
|
138
|
+
Example:
|
|
139
|
+
>>> # 1 kHz sine wave
|
|
140
|
+
>>> t = np.linspace(0, 1, 10000)
|
|
141
|
+
>>> signal = np.sin(2 * np.pi * 1000 * t)
|
|
142
|
+
>>> features = extractor.extract_spectral(signal, 10000)
|
|
143
|
+
>>> 950 < features['dominant_frequency'] < 1050
|
|
144
|
+
True
|
|
145
|
+
"""
|
|
146
|
+
# Compute FFT (use real FFT for efficiency)
|
|
147
|
+
fft = np.fft.rfft(data)
|
|
148
|
+
freqs = np.fft.rfftfreq(len(data), 1.0 / sample_rate)
|
|
149
|
+
magnitude = np.abs(fft)
|
|
150
|
+
power = magnitude**2
|
|
151
|
+
|
|
152
|
+
# Avoid division by zero
|
|
153
|
+
magnitude_safe = magnitude + 1e-10
|
|
154
|
+
power_safe = power + 1e-10
|
|
155
|
+
|
|
156
|
+
# Dominant frequency (peak in spectrum)
|
|
157
|
+
if len(magnitude) > 1:
|
|
158
|
+
dominant_idx = np.argmax(magnitude[1:]) + 1 # Skip DC component
|
|
159
|
+
dominant_freq = float(freqs[dominant_idx])
|
|
160
|
+
else:
|
|
161
|
+
dominant_freq = 0.0
|
|
162
|
+
|
|
163
|
+
# Spectral centroid (center of mass)
|
|
164
|
+
spectral_centroid = float(np.sum(freqs * magnitude) / np.sum(magnitude_safe))
|
|
165
|
+
|
|
166
|
+
# Bandwidth (frequencies containing >10% of max power)
|
|
167
|
+
threshold = 0.1 * np.max(power)
|
|
168
|
+
bandwidth_freqs = freqs[power > threshold]
|
|
169
|
+
bandwidth = (
|
|
170
|
+
float(bandwidth_freqs[-1] - bandwidth_freqs[0]) if len(bandwidth_freqs) > 1 else 0.0
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Spectral energy
|
|
174
|
+
spectral_energy = float(np.sum(power))
|
|
175
|
+
|
|
176
|
+
# Spectral flatness (geometric mean / arithmetic mean)
|
|
177
|
+
geometric_mean = float(np.exp(np.mean(np.log(magnitude_safe))))
|
|
178
|
+
arithmetic_mean = float(np.mean(magnitude))
|
|
179
|
+
spectral_flatness = geometric_mean / arithmetic_mean if arithmetic_mean > 0 else 0.0
|
|
180
|
+
|
|
181
|
+
# Spectral rolloff (95th percentile frequency)
|
|
182
|
+
cumsum = np.cumsum(power)
|
|
183
|
+
total_energy = cumsum[-1]
|
|
184
|
+
rolloff_threshold = 0.95 * total_energy
|
|
185
|
+
rolloff_idx = np.where(cumsum >= rolloff_threshold)[0]
|
|
186
|
+
spectral_rolloff = float(freqs[rolloff_idx[0]]) if len(rolloff_idx) > 0 else 0.0
|
|
187
|
+
|
|
188
|
+
# Number of spectral peaks
|
|
189
|
+
peaks, _ = sp_signal.find_peaks(magnitude, height=0.1 * np.max(magnitude))
|
|
190
|
+
num_spectral_peaks = float(len(peaks))
|
|
191
|
+
|
|
192
|
+
# Spectral spread (standard deviation around centroid)
|
|
193
|
+
spectral_spread = float(
|
|
194
|
+
np.sqrt(np.sum(((freqs - spectral_centroid) ** 2) * power) / np.sum(power_safe))
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"dominant_frequency": dominant_freq,
|
|
199
|
+
"spectral_centroid": spectral_centroid,
|
|
200
|
+
"bandwidth": bandwidth,
|
|
201
|
+
"spectral_energy": spectral_energy,
|
|
202
|
+
"spectral_flatness": spectral_flatness,
|
|
203
|
+
"spectral_rolloff": spectral_rolloff,
|
|
204
|
+
"num_spectral_peaks": num_spectral_peaks,
|
|
205
|
+
"spectral_spread": spectral_spread,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
def extract_temporal(self, data: NDArray[np.floating[Any]]) -> dict[str, float]:
|
|
209
|
+
"""Extract temporal domain features.
|
|
210
|
+
|
|
211
|
+
Analyzes time-domain signal properties:
|
|
212
|
+
- Zero-crossings (transitions through mean)
|
|
213
|
+
- Autocorrelation (self-similarity measure)
|
|
214
|
+
- Peak count (number of local maxima)
|
|
215
|
+
- Peak prominence (average peak height)
|
|
216
|
+
- Energy (sum of squared values)
|
|
217
|
+
- RMS (root mean square)
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
data: Input signal as 1D numpy array.
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dictionary with 8 temporal features.
|
|
224
|
+
|
|
225
|
+
Example:
|
|
226
|
+
>>> # Square wave has many zero crossings
|
|
227
|
+
>>> square = np.sign(np.sin(2 * np.pi * 10 * np.linspace(0, 1, 1000)))
|
|
228
|
+
>>> features = extractor.extract_temporal(square)
|
|
229
|
+
>>> features['zero_crossing_rate'] > 0.01
|
|
230
|
+
True
|
|
231
|
+
"""
|
|
232
|
+
# Zero-crossings (normalized by length)
|
|
233
|
+
mean_centered = data - np.mean(data)
|
|
234
|
+
zero_crossings = np.sum(np.diff(np.sign(mean_centered)) != 0)
|
|
235
|
+
zero_crossing_rate = float(zero_crossings) / len(data)
|
|
236
|
+
|
|
237
|
+
# Autocorrelation at lag 1
|
|
238
|
+
if len(data) > 1:
|
|
239
|
+
autocorr = float(np.corrcoef(data[:-1], data[1:])[0, 1])
|
|
240
|
+
# Handle NaN from constant signals
|
|
241
|
+
autocorr = 0.0 if np.isnan(autocorr) else autocorr
|
|
242
|
+
else:
|
|
243
|
+
autocorr = 0.0
|
|
244
|
+
|
|
245
|
+
# Peak detection
|
|
246
|
+
peaks, properties = sp_signal.find_peaks(data, prominence=0.1 * np.ptp(data))
|
|
247
|
+
peak_count = float(len(peaks))
|
|
248
|
+
peak_prominence = float(np.mean(properties["prominences"])) if len(peaks) > 0 else 0.0
|
|
249
|
+
|
|
250
|
+
# Energy and RMS
|
|
251
|
+
energy = float(np.sum(data**2))
|
|
252
|
+
rms = float(np.sqrt(np.mean(data**2)))
|
|
253
|
+
|
|
254
|
+
# Signal to noise ratio estimate (robust)
|
|
255
|
+
# Use median absolute deviation for noise estimate
|
|
256
|
+
mad = float(np.median(np.abs(data - np.median(data))))
|
|
257
|
+
signal_power = float(np.mean(data**2))
|
|
258
|
+
noise_power = (1.4826 * mad) ** 2 # Convert MAD to std estimate
|
|
259
|
+
snr_estimate = 10 * np.log10(signal_power / noise_power) if noise_power > 0 else 0.0
|
|
260
|
+
|
|
261
|
+
# Crest factor (peak to RMS ratio)
|
|
262
|
+
crest_factor = float(np.max(np.abs(data)) / rms) if rms > 0 else 0.0
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"zero_crossing_rate": zero_crossing_rate,
|
|
266
|
+
"autocorrelation": autocorr,
|
|
267
|
+
"peak_count": peak_count,
|
|
268
|
+
"peak_prominence": peak_prominence,
|
|
269
|
+
"energy": energy,
|
|
270
|
+
"rms": rms,
|
|
271
|
+
"snr_estimate": snr_estimate,
|
|
272
|
+
"crest_factor": crest_factor,
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def extract_entropy(self, data: NDArray[np.floating[Any]]) -> dict[str, float]:
|
|
276
|
+
"""Extract entropy-based features.
|
|
277
|
+
|
|
278
|
+
Computes information-theoretic measures:
|
|
279
|
+
- Shannon entropy (information content)
|
|
280
|
+
- Approximate entropy (regularity measure)
|
|
281
|
+
- Sample entropy (complexity measure, similar to ApEn)
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
data: Input signal as 1D numpy array.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Dictionary with 3 entropy features.
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
>>> # Random signal has high entropy
|
|
291
|
+
>>> random = np.random.randn(1000)
|
|
292
|
+
>>> features = extractor.extract_entropy(random)
|
|
293
|
+
>>> features['shannon_entropy'] > 5.0
|
|
294
|
+
True
|
|
295
|
+
"""
|
|
296
|
+
# Shannon entropy (discretize signal into bins)
|
|
297
|
+
# Normalize to 0-255 range for byte-like entropy
|
|
298
|
+
data_normalized = ((data - np.min(data)) / (np.ptp(data) + 1e-10) * 255).astype(np.uint8)
|
|
299
|
+
_, counts = np.unique(data_normalized, return_counts=True)
|
|
300
|
+
probabilities = counts / len(data_normalized)
|
|
301
|
+
shannon_entropy = float(-np.sum(probabilities * np.log2(probabilities + 1e-10)))
|
|
302
|
+
|
|
303
|
+
# Approximate entropy (ApEn)
|
|
304
|
+
# Measures regularity - low for regular signals, high for complex
|
|
305
|
+
approx_entropy = self._approximate_entropy(data, m=2, r=0.2 * np.std(data))
|
|
306
|
+
|
|
307
|
+
# Sample entropy (SampEn) - improved version of ApEn
|
|
308
|
+
sample_entropy = self._sample_entropy(data, m=2, r=0.2 * np.std(data))
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
"shannon_entropy": shannon_entropy,
|
|
312
|
+
"approximate_entropy": approx_entropy,
|
|
313
|
+
"sample_entropy": sample_entropy,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def _approximate_entropy(self, data: NDArray[np.floating[Any]], m: int, r: float) -> float:
|
|
317
|
+
"""Calculate approximate entropy (ApEn).
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
data: Input signal.
|
|
321
|
+
m: Embedding dimension.
|
|
322
|
+
r: Tolerance (fraction of std).
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Approximate entropy value.
|
|
326
|
+
"""
|
|
327
|
+
n = len(data)
|
|
328
|
+
|
|
329
|
+
# For very long signals, downsample to avoid O(n^2) cost
|
|
330
|
+
# Use aggressive downsampling to keep performance reasonable
|
|
331
|
+
# Approximate entropy is statistical and works well on smaller samples
|
|
332
|
+
if n > 200:
|
|
333
|
+
downsample_factor = max(n // 100, 1) # Target ~100 samples max
|
|
334
|
+
data = data[::downsample_factor]
|
|
335
|
+
n = len(data)
|
|
336
|
+
|
|
337
|
+
def _phi(m: int) -> float:
|
|
338
|
+
if n - m + 1 <= 0:
|
|
339
|
+
return 0.0
|
|
340
|
+
|
|
341
|
+
patterns = np.array([data[i : i + m] for i in range(n - m + 1)])
|
|
342
|
+
counts = np.zeros(len(patterns))
|
|
343
|
+
|
|
344
|
+
for i, pattern in enumerate(patterns):
|
|
345
|
+
# Count patterns within tolerance r
|
|
346
|
+
distances = np.max(np.abs(patterns - pattern), axis=1)
|
|
347
|
+
counts[i] = np.sum(distances <= r)
|
|
348
|
+
|
|
349
|
+
# Avoid log(0)
|
|
350
|
+
counts = np.maximum(counts, 1)
|
|
351
|
+
phi_value = np.sum(np.log(counts / (n - m + 1))) / (n - m + 1)
|
|
352
|
+
return float(phi_value)
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
return float(_phi(m) - _phi(m + 1))
|
|
356
|
+
except (ValueError, RuntimeWarning):
|
|
357
|
+
return 0.0
|
|
358
|
+
|
|
359
|
+
def _sample_entropy(self, data: NDArray[np.floating[Any]], m: int, r: float) -> float:
|
|
360
|
+
"""Calculate sample entropy (SampEn).
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
data: Input signal.
|
|
364
|
+
m: Embedding dimension.
|
|
365
|
+
r: Tolerance (fraction of std).
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
Sample entropy value.
|
|
369
|
+
"""
|
|
370
|
+
n = len(data)
|
|
371
|
+
|
|
372
|
+
# For very long signals, downsample to avoid O(n^2) cost
|
|
373
|
+
# Use aggressive downsampling to keep performance reasonable
|
|
374
|
+
# Sample entropy is statistical and works well on smaller samples
|
|
375
|
+
if n > 200:
|
|
376
|
+
downsample_factor = max(n // 100, 1) # Target ~100 samples max
|
|
377
|
+
data = data[::downsample_factor]
|
|
378
|
+
n = len(data)
|
|
379
|
+
|
|
380
|
+
def _count_matches(m: int) -> tuple[int, int]:
|
|
381
|
+
if n - m <= 0:
|
|
382
|
+
return 0, 0
|
|
383
|
+
|
|
384
|
+
patterns = np.array([data[i : i + m] for i in range(n - m)])
|
|
385
|
+
count_a = 0
|
|
386
|
+
count_b = 0
|
|
387
|
+
|
|
388
|
+
for i in range(len(patterns)):
|
|
389
|
+
# Don't count self-matches
|
|
390
|
+
for j in range(i + 1, len(patterns)):
|
|
391
|
+
dist = np.max(np.abs(patterns[i] - patterns[j]))
|
|
392
|
+
if dist <= r:
|
|
393
|
+
count_b += 1
|
|
394
|
+
if m > 1:
|
|
395
|
+
# Check if extended pattern also matches
|
|
396
|
+
if i + m < n and j + m < n:
|
|
397
|
+
if abs(data[i + m] - data[j + m]) <= r:
|
|
398
|
+
count_a += 1
|
|
399
|
+
|
|
400
|
+
return count_a, count_b
|
|
401
|
+
|
|
402
|
+
try:
|
|
403
|
+
count_a, count_b = _count_matches(m)
|
|
404
|
+
if count_a == 0 or count_b == 0:
|
|
405
|
+
return 0.0
|
|
406
|
+
return float(-np.log(count_a / count_b))
|
|
407
|
+
except (ValueError, RuntimeWarning):
|
|
408
|
+
return 0.0
|
|
409
|
+
|
|
410
|
+
def _normalize_signal(self, data: NDArray[np.floating[Any]]) -> NDArray[np.floating[Any]]:
|
|
411
|
+
"""Normalize signal to 0-1 range.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
data: Input signal
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Normalized signal
|
|
418
|
+
"""
|
|
419
|
+
normalized: NDArray[np.floating[Any]] = (data - np.min(data)) / (np.ptp(data) + 1e-10)
|
|
420
|
+
return normalized
|
|
421
|
+
|
|
422
|
+
def _find_edges(
|
|
423
|
+
self, data_normalized: NDArray[np.floating[Any]]
|
|
424
|
+
) -> tuple[NDArray[np.intp], NDArray[np.intp]]:
|
|
425
|
+
"""Find rising and falling edges.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
data_normalized: Normalized signal data
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Tuple of (rising edges, falling edges)
|
|
432
|
+
"""
|
|
433
|
+
rising_edges = np.where((data_normalized[:-1] < 0.5) & (data_normalized[1:] >= 0.5))[0]
|
|
434
|
+
falling_edges = np.where((data_normalized[:-1] >= 0.5) & (data_normalized[1:] < 0.5))[0]
|
|
435
|
+
return rising_edges, falling_edges
|
|
436
|
+
|
|
437
|
+
def _calculate_rise_times(
|
|
438
|
+
self,
|
|
439
|
+
rising_edges: NDArray[np.intp],
|
|
440
|
+
data_normalized: NDArray[np.floating[Any]],
|
|
441
|
+
threshold_low: float,
|
|
442
|
+
threshold_high: float,
|
|
443
|
+
) -> list[int]:
|
|
444
|
+
"""Calculate rise times for all rising edges.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
rising_edges: Array of rising edge indices
|
|
448
|
+
data_normalized: Normalized signal data
|
|
449
|
+
threshold_low: Low threshold (10%)
|
|
450
|
+
threshold_high: High threshold (90%)
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
List of rise times in samples
|
|
454
|
+
"""
|
|
455
|
+
rise_times = []
|
|
456
|
+
for edge in rising_edges:
|
|
457
|
+
# Look backward for 10% crossing
|
|
458
|
+
start_idx = edge
|
|
459
|
+
for i in range(max(0, edge - 100), edge):
|
|
460
|
+
if data_normalized[i] < threshold_low:
|
|
461
|
+
start_idx = i
|
|
462
|
+
break
|
|
463
|
+
# Look forward for 90% crossing
|
|
464
|
+
end_idx = edge
|
|
465
|
+
for i in range(edge, min(len(data_normalized), edge + 100)):
|
|
466
|
+
if data_normalized[i] > threshold_high:
|
|
467
|
+
end_idx = i
|
|
468
|
+
break
|
|
469
|
+
if end_idx > start_idx:
|
|
470
|
+
rise_times.append(end_idx - start_idx)
|
|
471
|
+
return rise_times
|
|
472
|
+
|
|
473
|
+
def _calculate_fall_times(
|
|
474
|
+
self,
|
|
475
|
+
falling_edges: NDArray[np.intp],
|
|
476
|
+
data_normalized: NDArray[np.floating[Any]],
|
|
477
|
+
threshold_low: float,
|
|
478
|
+
threshold_high: float,
|
|
479
|
+
) -> list[int]:
|
|
480
|
+
"""Calculate fall times for all falling edges.
|
|
481
|
+
|
|
482
|
+
Args:
|
|
483
|
+
falling_edges: Array of falling edge indices
|
|
484
|
+
data_normalized: Normalized signal data
|
|
485
|
+
threshold_low: Low threshold (10%)
|
|
486
|
+
threshold_high: High threshold (90%)
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
List of fall times in samples
|
|
490
|
+
"""
|
|
491
|
+
fall_times = []
|
|
492
|
+
for edge in falling_edges:
|
|
493
|
+
# Look backward for 90% crossing
|
|
494
|
+
start_idx = edge
|
|
495
|
+
for i in range(max(0, edge - 100), edge):
|
|
496
|
+
if data_normalized[i] > threshold_high:
|
|
497
|
+
start_idx = i
|
|
498
|
+
break
|
|
499
|
+
# Look forward for 10% crossing
|
|
500
|
+
end_idx = edge
|
|
501
|
+
for i in range(edge, min(len(data_normalized), edge + 100)):
|
|
502
|
+
if data_normalized[i] < threshold_low:
|
|
503
|
+
end_idx = i
|
|
504
|
+
break
|
|
505
|
+
if end_idx > start_idx:
|
|
506
|
+
fall_times.append(end_idx - start_idx)
|
|
507
|
+
return fall_times
|
|
508
|
+
|
|
509
|
+
def _calculate_pulse_widths(
|
|
510
|
+
self, rising_edges: NDArray[np.intp], falling_edges: NDArray[np.intp]
|
|
511
|
+
) -> list[int]:
|
|
512
|
+
"""Calculate pulse widths from edges.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
rising_edges: Array of rising edge indices
|
|
516
|
+
falling_edges: Array of falling edge indices
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
List of pulse widths in samples
|
|
520
|
+
"""
|
|
521
|
+
pulse_widths = []
|
|
522
|
+
for rising in rising_edges:
|
|
523
|
+
# Find next falling edge
|
|
524
|
+
next_falling = falling_edges[falling_edges > rising]
|
|
525
|
+
if len(next_falling) > 0:
|
|
526
|
+
pulse_widths.append(int(next_falling[0] - rising))
|
|
527
|
+
return pulse_widths
|
|
528
|
+
|
|
529
|
+
def _calculate_form_factor(self, data: NDArray[np.floating[Any]]) -> float:
|
|
530
|
+
"""Calculate form factor (RMS / mean).
|
|
531
|
+
|
|
532
|
+
Args:
|
|
533
|
+
data: Input signal
|
|
534
|
+
|
|
535
|
+
Returns:
|
|
536
|
+
Form factor
|
|
537
|
+
"""
|
|
538
|
+
rms = float(np.sqrt(np.mean(data**2)))
|
|
539
|
+
mean_abs = float(np.mean(np.abs(data)))
|
|
540
|
+
return rms / mean_abs if mean_abs > 0 else 0.0
|
|
541
|
+
|
|
542
|
+
def extract_shape(
|
|
543
|
+
self, data: NDArray[np.floating[Any]], sample_rate: float
|
|
544
|
+
) -> dict[str, float]:
|
|
545
|
+
"""Extract shape-related features for digital signals.
|
|
546
|
+
|
|
547
|
+
Analyzes waveform shape properties:
|
|
548
|
+
- Rise time (10% to 90% transition)
|
|
549
|
+
- Fall time (90% to 10% transition)
|
|
550
|
+
- Duty cycle (high time / period for digital)
|
|
551
|
+
- Pulse width (average high duration)
|
|
552
|
+
- Form factor (RMS / mean)
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
data: Input signal as 1D numpy array.
|
|
556
|
+
sample_rate: Sampling rate in Hz.
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
Dictionary with 5 shape features.
|
|
560
|
+
|
|
561
|
+
Example:
|
|
562
|
+
>>> # 50% duty cycle square wave
|
|
563
|
+
>>> square = np.tile([1, 1, 1, 1, 1, 0, 0, 0, 0, 0], 100)
|
|
564
|
+
>>> features = extractor.extract_shape(square, 1000)
|
|
565
|
+
>>> 0.4 < features['duty_cycle'] < 0.6
|
|
566
|
+
True
|
|
567
|
+
"""
|
|
568
|
+
# Normalize and extract basic features
|
|
569
|
+
data_normalized = self._normalize_signal(data)
|
|
570
|
+
duty_cycle = float(np.mean(data_normalized > 0.5))
|
|
571
|
+
|
|
572
|
+
# Find edges
|
|
573
|
+
rising_edges, falling_edges = self._find_edges(data_normalized)
|
|
574
|
+
|
|
575
|
+
# Calculate timing features
|
|
576
|
+
threshold_low = 0.1
|
|
577
|
+
threshold_high = 0.9
|
|
578
|
+
|
|
579
|
+
rise_times = self._calculate_rise_times(
|
|
580
|
+
rising_edges, data_normalized, threshold_low, threshold_high
|
|
581
|
+
)
|
|
582
|
+
rise_time = float(np.mean(rise_times)) / sample_rate if rise_times else 0.0
|
|
583
|
+
|
|
584
|
+
fall_times = self._calculate_fall_times(
|
|
585
|
+
falling_edges, data_normalized, threshold_low, threshold_high
|
|
586
|
+
)
|
|
587
|
+
fall_time = float(np.mean(fall_times)) / sample_rate if fall_times else 0.0
|
|
588
|
+
|
|
589
|
+
pulse_widths = self._calculate_pulse_widths(rising_edges, falling_edges)
|
|
590
|
+
pulse_width = float(np.mean(pulse_widths)) / sample_rate if pulse_widths else 0.0
|
|
591
|
+
|
|
592
|
+
form_factor = self._calculate_form_factor(data)
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
"duty_cycle": duty_cycle,
|
|
596
|
+
"rise_time": rise_time,
|
|
597
|
+
"fall_time": fall_time,
|
|
598
|
+
"pulse_width": pulse_width,
|
|
599
|
+
"form_factor": form_factor,
|
|
600
|
+
}
|