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
|
@@ -11,7 +11,7 @@ Author: Oscura Development Team
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
|
-
from typing import TYPE_CHECKING, Literal
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
@@ -150,8 +150,6 @@ def detect_periods_fft(
|
|
|
150
150
|
) -> list[PeriodResult]:
|
|
151
151
|
"""Detect periods using FFT spectral analysis.
|
|
152
152
|
|
|
153
|
-
: Periodic Pattern Detection via FFT
|
|
154
|
-
|
|
155
153
|
Uses power spectral density to identify dominant frequencies and their
|
|
156
154
|
harmonics. More efficient than autocorrelation for long signals.
|
|
157
155
|
|
|
@@ -170,42 +168,99 @@ def detect_periods_fft(
|
|
|
170
168
|
>>> periods = detect_periods_fft(signal, sample_rate=1000.0, num_peaks=3)
|
|
171
169
|
"""
|
|
172
170
|
trace = np.asarray(trace).flatten()
|
|
173
|
-
|
|
174
171
|
if trace.size == 0:
|
|
175
172
|
return []
|
|
176
173
|
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
power, freqs = _compute_power_spectrum(trace, sample_rate)
|
|
175
|
+
peak_indices = _find_valid_peaks(power, freqs, min_freq, max_freq, num_peaks)
|
|
176
|
+
|
|
177
|
+
if len(peak_indices) == 0:
|
|
178
|
+
return []
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
return _build_period_results(peak_indices, freqs, power, sample_rate)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _compute_power_spectrum(
|
|
184
|
+
trace: NDArray[np.float64], sample_rate: float
|
|
185
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
186
|
+
"""Compute power spectrum from FFT.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
trace: Input signal array.
|
|
190
|
+
sample_rate: Sampling rate in Hz.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Tuple of (power spectrum, frequencies).
|
|
194
|
+
"""
|
|
195
|
+
trace_centered = trace - np.mean(trace)
|
|
181
196
|
n = len(trace_centered)
|
|
182
|
-
|
|
197
|
+
|
|
198
|
+
# Apply Hanning window to reduce spectral leakage (improves noise robustness)
|
|
199
|
+
window = np.hanning(n)
|
|
200
|
+
trace_windowed = trace_centered * window
|
|
201
|
+
|
|
202
|
+
fft_result = np.fft.rfft(trace_windowed)
|
|
183
203
|
power = np.asarray(np.abs(fft_result) ** 2, dtype=np.float64)
|
|
184
204
|
freqs = np.fft.rfftfreq(n, 1.0 / sample_rate)
|
|
205
|
+
return power, freqs
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _find_valid_peaks(
|
|
209
|
+
power: NDArray[np.float64],
|
|
210
|
+
freqs: NDArray[np.float64],
|
|
211
|
+
min_freq: float | None,
|
|
212
|
+
max_freq: float | None,
|
|
213
|
+
num_peaks: int,
|
|
214
|
+
) -> NDArray[np.intp]:
|
|
215
|
+
"""Find valid spectral peaks within frequency range.
|
|
185
216
|
|
|
186
|
-
|
|
217
|
+
Args:
|
|
218
|
+
power: Power spectrum array.
|
|
219
|
+
freqs: Frequency array.
|
|
220
|
+
min_freq: Minimum frequency.
|
|
221
|
+
max_freq: Maximum frequency.
|
|
222
|
+
num_peaks: Maximum peaks to return.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Array of peak indices sorted by power.
|
|
226
|
+
"""
|
|
187
227
|
valid_mask = np.ones(len(freqs), dtype=bool)
|
|
188
228
|
if min_freq is not None:
|
|
189
229
|
valid_mask &= freqs >= min_freq
|
|
190
230
|
if max_freq is not None:
|
|
191
231
|
valid_mask &= freqs <= max_freq
|
|
232
|
+
valid_mask[0] = False # Exclude DC
|
|
192
233
|
|
|
193
|
-
# Exclude DC component
|
|
194
|
-
valid_mask[0] = False
|
|
195
|
-
|
|
196
|
-
# Find peaks in power spectrum
|
|
197
234
|
peak_indices = _find_spectral_peaks(power, min_distance=1)
|
|
198
235
|
peak_indices = peak_indices[valid_mask[peak_indices]]
|
|
199
236
|
|
|
200
237
|
if len(peak_indices) == 0:
|
|
201
|
-
|
|
238
|
+
empty_result: NDArray[np.signedinteger[Any]] = peak_indices
|
|
239
|
+
return empty_result
|
|
202
240
|
|
|
203
|
-
# Sort by power
|
|
204
241
|
peak_powers = power[peak_indices]
|
|
205
242
|
sorted_indices = np.argsort(peak_powers)[::-1][:num_peaks]
|
|
206
|
-
|
|
243
|
+
result: NDArray[np.signedinteger[Any]] = peak_indices[sorted_indices]
|
|
244
|
+
return result
|
|
207
245
|
|
|
208
|
-
|
|
246
|
+
|
|
247
|
+
def _build_period_results(
|
|
248
|
+
peak_indices: NDArray[np.intp],
|
|
249
|
+
freqs: NDArray[np.float64],
|
|
250
|
+
power: NDArray[np.float64],
|
|
251
|
+
sample_rate: float,
|
|
252
|
+
) -> list[PeriodResult]:
|
|
253
|
+
"""Build PeriodResult objects from spectral peaks.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
peak_indices: Array of peak indices.
|
|
257
|
+
freqs: Frequency array.
|
|
258
|
+
power: Power spectrum array.
|
|
259
|
+
sample_rate: Sampling rate in Hz.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
List of PeriodResult objects.
|
|
263
|
+
"""
|
|
209
264
|
results = []
|
|
210
265
|
max_power = np.max(power[peak_indices]) if len(peak_indices) > 0 else 1.0
|
|
211
266
|
|
|
@@ -214,28 +269,14 @@ def detect_periods_fft(
|
|
|
214
269
|
if freq == 0:
|
|
215
270
|
continue
|
|
216
271
|
|
|
217
|
-
|
|
218
|
-
period_samples = sample_rate / freq
|
|
219
|
-
|
|
220
|
-
# Confidence based on relative power
|
|
221
|
-
confidence = float(power[idx] / max_power)
|
|
222
|
-
|
|
223
|
-
# Detect harmonics (simple approach: look for integer multiples)
|
|
224
|
-
harmonics = []
|
|
225
|
-
for mult in range(2, 6):
|
|
226
|
-
harmonic_freq = freq * mult
|
|
227
|
-
if harmonic_freq < freqs[-1]:
|
|
228
|
-
# Find closest frequency bin
|
|
229
|
-
harmonic_idx = np.argmin(np.abs(freqs - harmonic_freq))
|
|
230
|
-
if power[harmonic_idx] > 0.1 * power[idx]:
|
|
231
|
-
harmonics.append(harmonic_freq)
|
|
272
|
+
harmonics = _detect_harmonics(freq, freqs, power, power[idx])
|
|
232
273
|
|
|
233
274
|
results.append(
|
|
234
275
|
PeriodResult(
|
|
235
|
-
period_samples=
|
|
236
|
-
period_seconds=
|
|
276
|
+
period_samples=sample_rate / freq,
|
|
277
|
+
period_seconds=1.0 / freq,
|
|
237
278
|
frequency_hz=freq,
|
|
238
|
-
confidence=min(
|
|
279
|
+
confidence=min(float(power[idx] / max_power), 1.0),
|
|
239
280
|
method="fft",
|
|
240
281
|
harmonics=harmonics if harmonics else None,
|
|
241
282
|
)
|
|
@@ -244,6 +285,30 @@ def detect_periods_fft(
|
|
|
244
285
|
return results
|
|
245
286
|
|
|
246
287
|
|
|
288
|
+
def _detect_harmonics(
|
|
289
|
+
freq: float, freqs: NDArray[np.float64], power: NDArray[np.float64], base_power: float
|
|
290
|
+
) -> list[float]:
|
|
291
|
+
"""Detect harmonic frequencies.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
freq: Base frequency.
|
|
295
|
+
freqs: Frequency array.
|
|
296
|
+
power: Power spectrum.
|
|
297
|
+
base_power: Power at base frequency.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
List of harmonic frequencies.
|
|
301
|
+
"""
|
|
302
|
+
harmonics = []
|
|
303
|
+
for mult in range(2, 6):
|
|
304
|
+
harmonic_freq = freq * mult
|
|
305
|
+
if harmonic_freq < freqs[-1]:
|
|
306
|
+
harmonic_idx = np.argmin(np.abs(freqs - harmonic_freq))
|
|
307
|
+
if power[harmonic_idx] > 0.1 * base_power:
|
|
308
|
+
harmonics.append(harmonic_freq)
|
|
309
|
+
return harmonics
|
|
310
|
+
|
|
311
|
+
|
|
247
312
|
def detect_periods_autocorr(
|
|
248
313
|
trace: NDArray[np.float64],
|
|
249
314
|
sample_rate: float = 1.0,
|
|
@@ -429,6 +494,8 @@ def _find_spectral_peaks(data: NDArray[np.float64], min_distance: int = 1) -> ND
|
|
|
429
494
|
"""Find peaks in 1D array.
|
|
430
495
|
|
|
431
496
|
Simple peak detection: point is peak if higher than neighbors.
|
|
497
|
+
Includes noise threshold to filter out spurious peaks.
|
|
498
|
+
Handles boundary conditions at edges of the array.
|
|
432
499
|
|
|
433
500
|
Args:
|
|
434
501
|
data: 1D array
|
|
@@ -437,15 +504,31 @@ def _find_spectral_peaks(data: NDArray[np.float64], min_distance: int = 1) -> ND
|
|
|
437
504
|
Returns:
|
|
438
505
|
Array of peak indices
|
|
439
506
|
"""
|
|
440
|
-
if len(data) <
|
|
507
|
+
if len(data) < 2:
|
|
441
508
|
return np.array([], dtype=np.intp)
|
|
442
509
|
|
|
443
|
-
#
|
|
510
|
+
# Calculate noise threshold (5% of max value to filter noise peaks)
|
|
511
|
+
threshold = 0.05 * np.max(data)
|
|
512
|
+
|
|
513
|
+
# Find local maxima above threshold
|
|
444
514
|
peaks_list: list[int] = []
|
|
515
|
+
|
|
516
|
+
# Check interior points (only elements with both neighbors)
|
|
445
517
|
for i in range(1, len(data) - 1):
|
|
446
|
-
if data[i] > data[i - 1] and data[i] > data[i + 1]:
|
|
518
|
+
if data[i] > data[i - 1] and data[i] > data[i + 1] and data[i] > threshold:
|
|
447
519
|
peaks_list.append(i)
|
|
448
520
|
|
|
521
|
+
# Check boundary elements (for Nyquist frequency peaks)
|
|
522
|
+
# Only consider boundary as peak if it's a TRUE local maximum (not just monotonic increase)
|
|
523
|
+
# Require it to be significantly higher than neighbor (at least 2x threshold)
|
|
524
|
+
if len(data) >= 3:
|
|
525
|
+
# Check first element
|
|
526
|
+
if data[0] > data[1] and data[0] > max(threshold * 2, data[1] * 1.5):
|
|
527
|
+
peaks_list.insert(0, 0)
|
|
528
|
+
# Check last element (important for Nyquist frequency in FFT)
|
|
529
|
+
if data[-1] > data[-2] and data[-1] > max(threshold * 2, data[-2] * 1.5):
|
|
530
|
+
peaks_list.append(len(data) - 1)
|
|
531
|
+
|
|
449
532
|
peaks: NDArray[np.intp] = np.array(peaks_list, dtype=np.intp)
|
|
450
533
|
|
|
451
534
|
# Apply minimum distance constraint
|
|
@@ -279,6 +279,155 @@ def find_longest_repeat(data: bytes | NDArray[np.uint8]) -> RepeatingSequence |
|
|
|
279
279
|
)
|
|
280
280
|
|
|
281
281
|
|
|
282
|
+
def _extract_substrings(data_bytes: bytes, min_length: int) -> list[tuple[bytes, int]]:
|
|
283
|
+
"""Extract all substrings of given length with their positions.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
data_bytes: Input byte string
|
|
287
|
+
min_length: Length of substrings to extract
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
List of (pattern, position) tuples
|
|
291
|
+
"""
|
|
292
|
+
n = len(data_bytes)
|
|
293
|
+
substrings = []
|
|
294
|
+
for i in range(n - min_length + 1):
|
|
295
|
+
substrings.append((data_bytes[i : i + min_length], i))
|
|
296
|
+
return substrings
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _build_fuzzy_hash_buckets(
|
|
300
|
+
substrings: list[tuple[bytes, int]], min_length: int
|
|
301
|
+
) -> dict[tuple[bytes, bytes], list[tuple[bytes, int]]]:
|
|
302
|
+
"""Group substrings by fuzzy hash for efficient approximate matching.
|
|
303
|
+
|
|
304
|
+
Uses locality-sensitive hashing: hash of first few bytes + last few bytes.
|
|
305
|
+
Sequences with same prefix/suffix are likely similar.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
substrings: List of (pattern, position) tuples
|
|
309
|
+
min_length: Minimum pattern length
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Dictionary mapping fuzzy hash to list of patterns
|
|
313
|
+
"""
|
|
314
|
+
hash_buckets: dict[tuple[bytes, bytes], list[tuple[bytes, int]]] = defaultdict(list)
|
|
315
|
+
prefix_len = min(3, min_length // 3) # First 3 bytes or ~1/3 of length
|
|
316
|
+
suffix_len = min(3, min_length // 3) # Last 3 bytes
|
|
317
|
+
|
|
318
|
+
for pattern, pos in substrings:
|
|
319
|
+
prefix = pattern[:prefix_len]
|
|
320
|
+
suffix = pattern[-suffix_len:] if len(pattern) > suffix_len else pattern
|
|
321
|
+
fuzzy_hash = (prefix, suffix)
|
|
322
|
+
hash_buckets[fuzzy_hash].append((pattern, pos))
|
|
323
|
+
|
|
324
|
+
return hash_buckets
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _is_pattern_compatible(pattern: bytes, other_pattern: bytes, max_distance: int) -> bool:
|
|
328
|
+
"""Check if two patterns can be within edit distance threshold.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
pattern: First pattern
|
|
332
|
+
other_pattern: Second pattern
|
|
333
|
+
max_distance: Maximum allowed edit distance
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
True if patterns might be within threshold (quick check)
|
|
337
|
+
"""
|
|
338
|
+
return abs(len(pattern) - len(other_pattern)) <= max_distance
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _try_add_to_cluster(
|
|
342
|
+
pattern: bytes,
|
|
343
|
+
other_pattern: bytes,
|
|
344
|
+
other_pos: int,
|
|
345
|
+
max_distance: int,
|
|
346
|
+
cluster_patterns: list[bytes],
|
|
347
|
+
cluster_positions: list[int],
|
|
348
|
+
) -> bool:
|
|
349
|
+
"""Try to add a pattern to existing cluster if within distance threshold.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
pattern: Representative pattern of cluster
|
|
353
|
+
other_pattern: Pattern to potentially add
|
|
354
|
+
other_pos: Position of other pattern
|
|
355
|
+
max_distance: Maximum edit distance allowed
|
|
356
|
+
cluster_patterns: Current cluster patterns (modified in place)
|
|
357
|
+
cluster_positions: Current cluster positions (modified in place)
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
True if pattern was added to cluster
|
|
361
|
+
"""
|
|
362
|
+
if not _is_pattern_compatible(pattern, other_pattern, max_distance):
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
distance = _edit_distance_optimized(pattern, other_pattern, max_distance)
|
|
366
|
+
if distance <= max_distance:
|
|
367
|
+
cluster_patterns.append(other_pattern)
|
|
368
|
+
cluster_positions.append(other_pos)
|
|
369
|
+
return True
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _cluster_bucket_patterns(
|
|
374
|
+
bucket_patterns: list[tuple[bytes, int]],
|
|
375
|
+
substrings: list[tuple[bytes, int]],
|
|
376
|
+
max_distance: int,
|
|
377
|
+
min_count: int,
|
|
378
|
+
global_used: set[int],
|
|
379
|
+
) -> list[tuple[list[bytes], list[int]]]:
|
|
380
|
+
"""Cluster patterns within a single hash bucket.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
bucket_patterns: Patterns in this bucket
|
|
384
|
+
substrings: All substrings (for index lookup)
|
|
385
|
+
max_distance: Maximum edit distance
|
|
386
|
+
min_count: Minimum cluster size
|
|
387
|
+
global_used: Set of globally used indices (modified in place)
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
List of (cluster_patterns, cluster_positions) tuples
|
|
391
|
+
"""
|
|
392
|
+
clusters = []
|
|
393
|
+
bucket_used: set[int] = set()
|
|
394
|
+
|
|
395
|
+
for i, (pattern, pos) in enumerate(bucket_patterns):
|
|
396
|
+
# Check if already used globally
|
|
397
|
+
actual_idx = substrings.index((pattern, pos))
|
|
398
|
+
if actual_idx in global_used:
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
# Start new cluster
|
|
402
|
+
cluster_patterns = [pattern]
|
|
403
|
+
cluster_positions = [pos]
|
|
404
|
+
bucket_used.add(i)
|
|
405
|
+
global_used.add(actual_idx)
|
|
406
|
+
|
|
407
|
+
# Compare within same bucket
|
|
408
|
+
for j in range(i + 1, len(bucket_patterns)):
|
|
409
|
+
if j in bucket_used:
|
|
410
|
+
continue
|
|
411
|
+
|
|
412
|
+
other_pattern, other_pos = bucket_patterns[j]
|
|
413
|
+
other_idx = substrings.index((other_pattern, other_pos))
|
|
414
|
+
if other_idx in global_used:
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
# Try to add to cluster
|
|
418
|
+
if _try_add_to_cluster(
|
|
419
|
+
pattern, other_pattern, other_pos, max_distance, cluster_patterns, cluster_positions
|
|
420
|
+
):
|
|
421
|
+
bucket_used.add(j)
|
|
422
|
+
global_used.add(other_idx)
|
|
423
|
+
|
|
424
|
+
# Add cluster if large enough
|
|
425
|
+
if len(cluster_patterns) >= min_count:
|
|
426
|
+
clusters.append((cluster_patterns, cluster_positions))
|
|
427
|
+
|
|
428
|
+
return clusters
|
|
429
|
+
|
|
430
|
+
|
|
282
431
|
@memoize_analysis(maxsize=16)
|
|
283
432
|
def find_approximate_repeats(
|
|
284
433
|
data: bytes | NDArray[np.uint8],
|
|
@@ -328,98 +477,46 @@ def find_approximate_repeats(
|
|
|
328
477
|
if n < min_length:
|
|
329
478
|
return []
|
|
330
479
|
|
|
331
|
-
#
|
|
332
|
-
substrings =
|
|
333
|
-
for i in range(n - min_length + 1):
|
|
334
|
-
substrings.append((data_bytes[i : i + min_length], i))
|
|
335
|
-
|
|
336
|
-
# OPTIMIZATION 2: Hash-based pre-grouping
|
|
337
|
-
# Group sequences by fuzzy hash to reduce comparisons
|
|
338
|
-
# Use a locality-sensitive hash: hash of first few bytes + last few bytes
|
|
339
|
-
hash_buckets: dict[tuple[bytes, bytes], list[tuple[bytes, int]]] = defaultdict(list)
|
|
340
|
-
prefix_len = min(3, min_length // 3) # First 3 bytes or ~1/3 of length
|
|
341
|
-
suffix_len = min(3, min_length // 3) # Last 3 bytes
|
|
480
|
+
# Extract all substrings
|
|
481
|
+
substrings = _extract_substrings(data_bytes, min_length)
|
|
342
482
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
# Sequences with same prefix/suffix are likely similar
|
|
346
|
-
prefix = pattern[:prefix_len]
|
|
347
|
-
suffix = pattern[-suffix_len:] if len(pattern) > suffix_len else pattern
|
|
348
|
-
fuzzy_hash = (prefix, suffix)
|
|
349
|
-
hash_buckets[fuzzy_hash].append((pattern, pos))
|
|
483
|
+
# Group by fuzzy hash to reduce comparisons
|
|
484
|
+
hash_buckets = _build_fuzzy_hash_buckets(substrings, min_length)
|
|
350
485
|
|
|
351
|
-
#
|
|
352
|
-
|
|
353
|
-
# and m is average bucket size (m << n)
|
|
354
|
-
clusters = []
|
|
486
|
+
# Cluster within hash buckets
|
|
487
|
+
results = []
|
|
355
488
|
global_used: set[int] = set()
|
|
356
489
|
|
|
357
490
|
for bucket_patterns in hash_buckets.values():
|
|
358
491
|
# Skip small buckets that can't form clusters
|
|
359
492
|
if len(bucket_patterns) < min_count:
|
|
360
|
-
# Still need to check if they can join other buckets
|
|
361
|
-
# For now, skip - could optimize further by cross-bucket matching
|
|
362
493
|
continue
|
|
363
494
|
|
|
364
|
-
# Cluster
|
|
365
|
-
|
|
495
|
+
# Cluster patterns in this bucket
|
|
496
|
+
bucket_clusters = _cluster_bucket_patterns(
|
|
497
|
+
bucket_patterns, substrings, max_distance, min_count, global_used
|
|
498
|
+
)
|
|
366
499
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
500
|
+
# Convert clusters to RepeatingSequence objects
|
|
501
|
+
for cluster_patterns, cluster_positions in bucket_clusters:
|
|
502
|
+
# Use most common pattern as representative
|
|
503
|
+
pattern_counter = Counter(cluster_patterns)
|
|
504
|
+
representative = pattern_counter.most_common(1)[0][0]
|
|
372
505
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
for j in range(i + 1, len(bucket_patterns)):
|
|
381
|
-
if j in bucket_used:
|
|
382
|
-
continue
|
|
383
|
-
|
|
384
|
-
other_pattern, other_pos = bucket_patterns[j]
|
|
385
|
-
other_idx = substrings.index((other_pattern, other_pos))
|
|
386
|
-
if other_idx in global_used:
|
|
387
|
-
continue
|
|
388
|
-
|
|
389
|
-
# OPTIMIZATION 5: Early termination with quick checks
|
|
390
|
-
# Check if lengths are compatible
|
|
391
|
-
if abs(len(pattern) - len(other_pattern)) > max_distance:
|
|
392
|
-
continue
|
|
393
|
-
|
|
394
|
-
# OPTIMIZATION 6: Use optimized edit distance
|
|
395
|
-
distance = _edit_distance_optimized(pattern, other_pattern, max_distance)
|
|
396
|
-
|
|
397
|
-
if distance <= max_distance:
|
|
398
|
-
cluster_patterns.append(other_pattern)
|
|
399
|
-
cluster_positions.append(other_pos)
|
|
400
|
-
bucket_used.add(j)
|
|
401
|
-
global_used.add(other_idx)
|
|
402
|
-
|
|
403
|
-
# Add cluster if large enough
|
|
404
|
-
if len(cluster_patterns) >= min_count:
|
|
405
|
-
# Use most common pattern as representative
|
|
406
|
-
pattern_counter = Counter(cluster_patterns)
|
|
407
|
-
representative = pattern_counter.most_common(1)[0][0]
|
|
408
|
-
|
|
409
|
-
clusters.append(
|
|
410
|
-
RepeatingSequence(
|
|
411
|
-
pattern=representative,
|
|
412
|
-
length=len(representative),
|
|
413
|
-
count=len(cluster_patterns),
|
|
414
|
-
positions=sorted(cluster_positions),
|
|
415
|
-
frequency=len(cluster_patterns) / (n - min_length + 1),
|
|
416
|
-
)
|
|
506
|
+
results.append(
|
|
507
|
+
RepeatingSequence(
|
|
508
|
+
pattern=representative,
|
|
509
|
+
length=len(representative),
|
|
510
|
+
count=len(cluster_patterns),
|
|
511
|
+
positions=sorted(cluster_positions),
|
|
512
|
+
frequency=len(cluster_patterns) / (n - min_length + 1),
|
|
417
513
|
)
|
|
514
|
+
)
|
|
418
515
|
|
|
419
516
|
# Sort by count (descending)
|
|
420
|
-
|
|
517
|
+
results.sort(key=lambda x: x.count, reverse=True)
|
|
421
518
|
|
|
422
|
-
return
|
|
519
|
+
return results
|
|
423
520
|
|
|
424
521
|
|
|
425
522
|
def _to_bytes(data: bytes | NDArray[np.uint8] | memoryview | bytearray) -> bytes:
|
|
@@ -439,7 +536,7 @@ def _to_bytes(data: bytes | NDArray[np.uint8] | memoryview | bytearray) -> bytes
|
|
|
439
536
|
elif isinstance(data, bytearray | memoryview):
|
|
440
537
|
return bytes(data)
|
|
441
538
|
elif isinstance(data, np.ndarray):
|
|
442
|
-
return data.astype(np.uint8).tobytes()
|
|
539
|
+
return data.astype(np.uint8).tobytes()
|
|
443
540
|
else:
|
|
444
541
|
raise TypeError(f"Unsupported data type: {type(data)}")
|
|
445
542
|
|
oscura/analyzers/power/soa.py
CHANGED
|
@@ -111,13 +111,13 @@ def soa_analysis(
|
|
|
111
111
|
if pulse_width is None:
|
|
112
112
|
pulse_width = np.inf
|
|
113
113
|
|
|
114
|
-
applicable_limits = [l for l in limits if l.pulse_width >= pulse_width]
|
|
114
|
+
applicable_limits = [l for l in limits if l.pulse_width >= pulse_width]
|
|
115
115
|
if not applicable_limits:
|
|
116
116
|
applicable_limits = limits # Use all if none match
|
|
117
117
|
|
|
118
118
|
# Build SOA boundary (interpolate between limit points)
|
|
119
119
|
# Sort by voltage
|
|
120
|
-
sorted_limits = sorted(applicable_limits, key=lambda l: l.v_max)
|
|
120
|
+
sorted_limits = sorted(applicable_limits, key=lambda l: l.v_max)
|
|
121
121
|
|
|
122
122
|
violations: list[SOAViolation] = []
|
|
123
123
|
margins: list[float] = []
|
|
@@ -171,7 +171,7 @@ def _interpolate_soa_limit(voltage: float, limits: list[SOALimit]) -> float:
|
|
|
171
171
|
Interpolated current limit in Amps
|
|
172
172
|
"""
|
|
173
173
|
if len(limits) == 0:
|
|
174
|
-
return np.inf
|
|
174
|
+
return np.inf
|
|
175
175
|
|
|
176
176
|
if len(limits) == 1:
|
|
177
177
|
if voltage <= limits[0].v_max:
|
|
@@ -259,9 +259,9 @@ def plot_soa(
|
|
|
259
259
|
fig, ax = plt.subplots(figsize=figsize)
|
|
260
260
|
|
|
261
261
|
# Plot SOA boundary
|
|
262
|
-
sorted_limits = sorted(limits, key=lambda l: l.v_max)
|
|
263
|
-
v_boundary = [l.v_max for l in sorted_limits]
|
|
264
|
-
i_boundary = [l.i_max for l in sorted_limits]
|
|
262
|
+
sorted_limits = sorted(limits, key=lambda l: l.v_max)
|
|
263
|
+
v_boundary = [l.v_max for l in sorted_limits]
|
|
264
|
+
i_boundary = [l.i_max for l in sorted_limits]
|
|
265
265
|
|
|
266
266
|
# Add corner points for closed boundary
|
|
267
267
|
v_boundary = [0, *v_boundary, v_boundary[-1], 0]
|