oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -10,7 +10,7 @@ and protocols, including binary field detection and pattern analysis.
|
|
|
10
10
|
- UNKNOWN-005: Reverse Engineering Workflow
|
|
11
11
|
|
|
12
12
|
Example:
|
|
13
|
-
>>> from oscura.exploratory.unknown import characterize_unknown_signal
|
|
13
|
+
>>> from oscura.jupyter.exploratory.unknown import characterize_unknown_signal
|
|
14
14
|
>>> result = characterize_unknown_signal(trace)
|
|
15
15
|
>>> print(f"Signal type: {result.signal_type}")
|
|
16
16
|
>>> print(f"Suggested protocols: {result.suggested_protocols}")
|
|
@@ -75,35 +75,66 @@ def detect_binary_fields(
|
|
|
75
75
|
References:
|
|
76
76
|
UNKNOWN-001: Binary Field Detection
|
|
77
77
|
"""
|
|
78
|
+
# Convert analog to digital
|
|
79
|
+
digital, sample_rate = _convert_to_digital(trace)
|
|
80
|
+
|
|
81
|
+
# Find edges and estimate bit period
|
|
82
|
+
edges = np.where(np.diff(digital) != 0)[0]
|
|
83
|
+
if len(edges) < 2:
|
|
84
|
+
return _create_empty_field_result()
|
|
85
|
+
|
|
86
|
+
bit_period = np.median(np.diff(edges))
|
|
87
|
+
|
|
88
|
+
# Extract fields from edges
|
|
89
|
+
fields = _extract_binary_fields(
|
|
90
|
+
edges, digital, bit_period, max_gap_ratio, min_field_bits, sample_rate
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Calculate bit rate
|
|
94
|
+
bit_rate = sample_rate / bit_period if bit_period > 0 else None
|
|
95
|
+
|
|
96
|
+
# Detect encoding
|
|
97
|
+
encoding = _detect_encoding(digital, edges, bit_period)
|
|
98
|
+
|
|
99
|
+
# Calculate confidence
|
|
100
|
+
confidence = min(1.0, len(fields) / 10.0) * 0.8 + (0.2 if bit_rate else 0)
|
|
101
|
+
|
|
102
|
+
return BinaryFieldResult(
|
|
103
|
+
fields=fields,
|
|
104
|
+
field_count=len(fields),
|
|
105
|
+
bit_rate=bit_rate,
|
|
106
|
+
encoding=encoding,
|
|
107
|
+
confidence=confidence,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _convert_to_digital(trace: WaveformTrace) -> tuple[NDArray[np.int_], float]:
|
|
112
|
+
"""Convert analog signal to digital representation."""
|
|
78
113
|
data = trace.data
|
|
79
114
|
sample_rate = trace.metadata.sample_rate
|
|
80
|
-
|
|
81
|
-
# Threshold for digital conversion
|
|
82
115
|
v_min = np.percentile(data, 5)
|
|
83
116
|
v_max = np.percentile(data, 95)
|
|
84
117
|
threshold = (v_min + v_max) / 2
|
|
85
|
-
|
|
86
|
-
# Convert to digital
|
|
87
118
|
digital = (data > threshold).astype(int)
|
|
119
|
+
return digital, sample_rate
|
|
88
120
|
|
|
89
|
-
# Find edges
|
|
90
|
-
edges = np.where(np.diff(digital) != 0)[0]
|
|
91
121
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
encoding="unknown",
|
|
98
|
-
confidence=0.0,
|
|
99
|
-
)
|
|
122
|
+
def _create_empty_field_result() -> BinaryFieldResult:
|
|
123
|
+
"""Create empty result for insufficient edges."""
|
|
124
|
+
return BinaryFieldResult(
|
|
125
|
+
fields=[], field_count=0, bit_rate=None, encoding="unknown", confidence=0.0
|
|
126
|
+
)
|
|
100
127
|
|
|
101
|
-
# Estimate bit period from edge spacing
|
|
102
|
-
edge_gaps = np.diff(edges)
|
|
103
|
-
median_gap = np.median(edge_gaps)
|
|
104
|
-
bit_period = median_gap
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
def _extract_binary_fields(
|
|
130
|
+
edges: NDArray[np.int_],
|
|
131
|
+
digital: NDArray[np.int_],
|
|
132
|
+
bit_period: float,
|
|
133
|
+
max_gap_ratio: float,
|
|
134
|
+
min_field_bits: int,
|
|
135
|
+
sample_rate: float,
|
|
136
|
+
) -> list[dict[str, Any]]:
|
|
137
|
+
"""Extract binary fields from edges."""
|
|
107
138
|
fields = []
|
|
108
139
|
current_field_start = edges[0]
|
|
109
140
|
current_field_edges = [edges[0]]
|
|
@@ -114,26 +145,10 @@ def detect_binary_fields(
|
|
|
114
145
|
if gap > max_gap_ratio * bit_period:
|
|
115
146
|
# End current field
|
|
116
147
|
if len(current_field_edges) >= min_field_bits:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
# Extract bit pattern
|
|
121
|
-
bits = []
|
|
122
|
-
for j in range(len(current_field_edges) - 1):
|
|
123
|
-
start = current_field_edges[j]
|
|
124
|
-
end = current_field_edges[j + 1]
|
|
125
|
-
mid = (start + end) // 2
|
|
126
|
-
bits.append(digital[mid])
|
|
127
|
-
|
|
128
|
-
fields.append(
|
|
129
|
-
{
|
|
130
|
-
"start_sample": int(current_field_start),
|
|
131
|
-
"end_sample": int(current_field_edges[-1]),
|
|
132
|
-
"length": n_bits,
|
|
133
|
-
"bits": bits,
|
|
134
|
-
"timestamp": current_field_start / sample_rate,
|
|
135
|
-
}
|
|
148
|
+
field = _create_field_from_edges(
|
|
149
|
+
current_field_start, current_field_edges, digital, sample_rate
|
|
136
150
|
)
|
|
151
|
+
fields.append(field)
|
|
137
152
|
|
|
138
153
|
# Start new field
|
|
139
154
|
current_field_start = edges[i]
|
|
@@ -143,43 +158,36 @@ def detect_binary_fields(
|
|
|
143
158
|
|
|
144
159
|
# Handle last field
|
|
145
160
|
if len(current_field_edges) >= min_field_bits:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
bits = []
|
|
149
|
-
for j in range(len(current_field_edges) - 1):
|
|
150
|
-
start = current_field_edges[j]
|
|
151
|
-
end = current_field_edges[j + 1]
|
|
152
|
-
mid = (start + end) // 2
|
|
153
|
-
bits.append(digital[mid])
|
|
154
|
-
|
|
155
|
-
fields.append(
|
|
156
|
-
{
|
|
157
|
-
"start_sample": int(current_field_start),
|
|
158
|
-
"end_sample": int(current_field_edges[-1]),
|
|
159
|
-
"length": n_bits,
|
|
160
|
-
"bits": bits,
|
|
161
|
-
"timestamp": current_field_start / sample_rate,
|
|
162
|
-
}
|
|
161
|
+
field = _create_field_from_edges(
|
|
162
|
+
current_field_start, current_field_edges, digital, sample_rate
|
|
163
163
|
)
|
|
164
|
+
fields.append(field)
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
bit_rate = sample_rate / bit_period if bit_period > 0 else None
|
|
167
|
-
|
|
168
|
-
# Detect encoding
|
|
169
|
-
encoding = _detect_encoding(digital, edges, bit_period)
|
|
166
|
+
return fields
|
|
170
167
|
|
|
171
|
-
# Calculate confidence
|
|
172
|
-
confidence = min(1.0, len(fields) / 10.0) * 0.8
|
|
173
|
-
if bit_rate is not None:
|
|
174
|
-
confidence += 0.2
|
|
175
168
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
def _create_field_from_edges(
|
|
170
|
+
field_start: int,
|
|
171
|
+
field_edges: list[int],
|
|
172
|
+
digital: NDArray[np.int_],
|
|
173
|
+
sample_rate: float,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
"""Create field dictionary from edge list."""
|
|
176
|
+
n_bits = len(field_edges) - 1
|
|
177
|
+
bits = []
|
|
178
|
+
for j in range(len(field_edges) - 1):
|
|
179
|
+
start = field_edges[j]
|
|
180
|
+
end = field_edges[j + 1]
|
|
181
|
+
mid = (start + end) // 2
|
|
182
|
+
bits.append(digital[mid])
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"start_sample": int(field_start),
|
|
186
|
+
"end_sample": int(field_edges[-1]),
|
|
187
|
+
"length": n_bits,
|
|
188
|
+
"bits": bits,
|
|
189
|
+
"timestamp": field_start / sample_rate,
|
|
190
|
+
}
|
|
183
191
|
|
|
184
192
|
|
|
185
193
|
def _detect_encoding(
|
|
@@ -253,156 +261,216 @@ class UnknownSignalCharacterization:
|
|
|
253
261
|
features: dict[str, Any] = field(default_factory=dict)
|
|
254
262
|
|
|
255
263
|
|
|
256
|
-
def
|
|
257
|
-
|
|
258
|
-
) -> UnknownSignalCharacterization:
|
|
259
|
-
"""Comprehensive characterization of unknown signal per UNKNOWN-003.
|
|
260
|
-
|
|
261
|
-
Analyzes signal characteristics to determine type, periodicity,
|
|
262
|
-
and suggest possible protocols.
|
|
264
|
+
def _handle_short_trace(data: NDArray[np.float64]) -> UnknownSignalCharacterization:
|
|
265
|
+
"""Handle edge case of very short traces.
|
|
263
266
|
|
|
264
267
|
Args:
|
|
265
|
-
|
|
268
|
+
data: Signal data array
|
|
266
269
|
|
|
267
270
|
Returns:
|
|
268
|
-
UnknownSignalCharacterization with
|
|
271
|
+
UnknownSignalCharacterization with minimal info
|
|
272
|
+
"""
|
|
273
|
+
return UnknownSignalCharacterization(
|
|
274
|
+
signal_type="analog",
|
|
275
|
+
is_periodic=False,
|
|
276
|
+
fundamental_frequency=None,
|
|
277
|
+
dc_offset=float(data[0]) if len(data) > 0 else 0.0,
|
|
278
|
+
amplitude=0.0,
|
|
279
|
+
rise_time=None,
|
|
280
|
+
fall_time=None,
|
|
281
|
+
suggested_protocols=[],
|
|
282
|
+
noise_floor=0.0,
|
|
283
|
+
snr_db=float("inf"),
|
|
284
|
+
features={},
|
|
285
|
+
)
|
|
269
286
|
|
|
270
|
-
Example:
|
|
271
|
-
>>> result = characterize_unknown_signal(trace)
|
|
272
|
-
>>> print(f"Signal type: {result.signal_type}")
|
|
273
|
-
>>> print(f"Periodic: {result.is_periodic}")
|
|
274
|
-
>>> for protocol, confidence in result.suggested_protocols:
|
|
275
|
-
... print(f" {protocol}: {confidence:.1%}")
|
|
276
287
|
|
|
277
|
-
|
|
278
|
-
|
|
288
|
+
def _compute_basic_statistics(
|
|
289
|
+
data: NDArray[np.float64],
|
|
290
|
+
) -> tuple[float, float, float, float, float, float]:
|
|
291
|
+
"""Compute basic signal statistics.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
data: Signal data array
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Tuple of (v_min, v_max, v_mean, v_std, dc_offset, amplitude)
|
|
279
298
|
"""
|
|
280
|
-
|
|
281
|
-
|
|
299
|
+
v_min: float = float(np.min(data))
|
|
300
|
+
v_max: float = float(np.max(data))
|
|
301
|
+
v_mean: float = float(np.mean(data))
|
|
302
|
+
v_std: float = float(np.std(data))
|
|
303
|
+
dc_offset: float = v_mean
|
|
304
|
+
amplitude: float = (v_max - v_min) / 2
|
|
305
|
+
return v_min, v_max, v_mean, v_std, dc_offset, amplitude
|
|
282
306
|
|
|
283
|
-
# Handle edge case of very short traces
|
|
284
|
-
if len(data) < 2:
|
|
285
|
-
return UnknownSignalCharacterization(
|
|
286
|
-
signal_type="analog",
|
|
287
|
-
is_periodic=False,
|
|
288
|
-
fundamental_frequency=None,
|
|
289
|
-
dc_offset=float(data[0]) if len(data) > 0 else 0.0,
|
|
290
|
-
amplitude=0.0,
|
|
291
|
-
rise_time=None,
|
|
292
|
-
fall_time=None,
|
|
293
|
-
suggested_protocols=[],
|
|
294
|
-
noise_floor=0.0,
|
|
295
|
-
snr_db=float("inf"),
|
|
296
|
-
features={},
|
|
297
|
-
)
|
|
298
307
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
def _find_histogram_peaks(
|
|
309
|
+
data: NDArray[np.float64],
|
|
310
|
+
v_min: float,
|
|
311
|
+
v_max: float,
|
|
312
|
+
) -> list[tuple[float, float]]:
|
|
313
|
+
"""Find peaks in signal histogram for distribution analysis.
|
|
304
314
|
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
Args:
|
|
316
|
+
data: Signal data array
|
|
317
|
+
v_min: Minimum voltage
|
|
318
|
+
v_max: Maximum voltage
|
|
307
319
|
|
|
308
|
-
|
|
309
|
-
|
|
320
|
+
Returns:
|
|
321
|
+
List of (peak_position, peak_height) tuples
|
|
322
|
+
"""
|
|
310
323
|
hist, bin_edges = np.histogram(data, bins=50)
|
|
311
324
|
centers = (bin_edges[:-1] + bin_edges[1:]) / 2
|
|
312
325
|
|
|
313
|
-
# Find peaks in histogram
|
|
314
326
|
peaks = []
|
|
315
327
|
for i in range(1, len(hist) - 1):
|
|
316
328
|
if hist[i] > hist[i - 1] and hist[i] > hist[i + 1] and hist[i] > 0.1 * np.max(hist):
|
|
317
329
|
peaks.append((centers[i], hist[i]))
|
|
318
330
|
|
|
331
|
+
return peaks
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _classify_signal_type(
|
|
335
|
+
peaks: list[tuple[float, float]],
|
|
336
|
+
v_min: float,
|
|
337
|
+
v_max: float,
|
|
338
|
+
v_std: float,
|
|
339
|
+
amplitude: float,
|
|
340
|
+
) -> Literal["digital", "analog", "mixed"]:
|
|
341
|
+
"""Classify signal type based on histogram peaks.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
peaks: List of histogram peaks
|
|
345
|
+
v_min: Minimum voltage
|
|
346
|
+
v_max: Maximum voltage
|
|
347
|
+
v_std: Standard deviation
|
|
348
|
+
amplitude: Signal amplitude
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Signal type classification
|
|
352
|
+
"""
|
|
319
353
|
if len(peaks) >= 4:
|
|
320
|
-
# Many peaks suggest analog signal
|
|
321
|
-
|
|
354
|
+
# Many peaks suggest analog signal
|
|
355
|
+
return "analog"
|
|
322
356
|
elif len(peaks) == 2 or len(peaks) == 3:
|
|
323
|
-
#
|
|
357
|
+
# Check if peaks are well-separated (digital bimodal)
|
|
324
358
|
peak_positions = [p[0] for p in peaks]
|
|
325
|
-
# Normalize peak positions to 0-1 range
|
|
326
359
|
normalized_peaks = [(p - v_min) / (v_max - v_min) for p in peak_positions]
|
|
327
360
|
|
|
328
|
-
# If peaks are well-separated (one < 0.4, one > 0.6), likely digital
|
|
329
361
|
has_low_peak = any(p < 0.4 for p in normalized_peaks)
|
|
330
362
|
has_high_peak = any(p > 0.6 for p in normalized_peaks)
|
|
331
363
|
|
|
332
364
|
if has_low_peak and has_high_peak:
|
|
333
|
-
|
|
365
|
+
return "digital"
|
|
334
366
|
else:
|
|
335
|
-
|
|
336
|
-
signal_type = "analog"
|
|
367
|
+
return "analog"
|
|
337
368
|
elif len(peaks) == 1:
|
|
338
369
|
# Check for modulated signal
|
|
339
|
-
|
|
370
|
+
return "mixed" if v_std > 0.2 * amplitude else "analog"
|
|
340
371
|
else:
|
|
341
|
-
|
|
372
|
+
return "analog"
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _analyze_periodicity(
|
|
376
|
+
data: NDArray[np.float64],
|
|
377
|
+
sample_rate: float,
|
|
378
|
+
) -> tuple[bool, float | None, float, float]:
|
|
379
|
+
"""Analyze signal periodicity using FFT.
|
|
342
380
|
|
|
343
|
-
|
|
381
|
+
Args:
|
|
382
|
+
data: Signal data array
|
|
383
|
+
sample_rate: Sample rate in Hz
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Tuple of (is_periodic, fundamental_frequency, noise_floor, snr_db)
|
|
387
|
+
"""
|
|
344
388
|
from scipy import signal as sp_signal
|
|
345
389
|
|
|
346
390
|
n = len(data)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
f, psd = sp_signal.welch(data, fs=sample_rate, nperseg=min(4096, n))
|
|
350
|
-
|
|
351
|
-
# Find dominant frequency (excluding DC)
|
|
352
|
-
psd_no_dc = psd.copy()
|
|
353
|
-
psd_no_dc[0] = 0
|
|
354
|
-
|
|
355
|
-
if len(psd_no_dc) > 0 and np.any(psd_no_dc > 0):
|
|
356
|
-
peak_idx = np.argmax(psd_no_dc)
|
|
357
|
-
mean_psd = np.mean(psd_no_dc[psd_no_dc > 0]) if np.any(psd_no_dc > 0) else 0
|
|
358
|
-
fundamental_frequency = f[peak_idx] if psd_no_dc[peak_idx] > 10 * mean_psd else None
|
|
359
|
-
else:
|
|
360
|
-
fundamental_frequency = None
|
|
391
|
+
if n < 4:
|
|
392
|
+
return False, None, 0.0, 0.0
|
|
361
393
|
|
|
362
|
-
|
|
394
|
+
f, psd = sp_signal.welch(data, fs=sample_rate, nperseg=min(4096, n))
|
|
395
|
+
|
|
396
|
+
# Find dominant frequency (excluding DC)
|
|
397
|
+
psd_no_dc = psd.copy()
|
|
398
|
+
psd_no_dc[0] = 0
|
|
399
|
+
|
|
400
|
+
if len(psd_no_dc) > 0 and np.any(psd_no_dc > 0):
|
|
401
|
+
peak_idx = np.argmax(psd_no_dc)
|
|
402
|
+
mean_psd = np.mean(psd_no_dc[psd_no_dc > 0]) if np.any(psd_no_dc > 0) else 0
|
|
403
|
+
fundamental_frequency = f[peak_idx] if psd_no_dc[peak_idx] > 10 * mean_psd else None
|
|
363
404
|
else:
|
|
364
405
|
fundamental_frequency = None
|
|
365
|
-
|
|
406
|
+
|
|
407
|
+
is_periodic = fundamental_frequency is not None
|
|
366
408
|
|
|
367
409
|
# Estimate noise floor
|
|
368
|
-
if
|
|
369
|
-
|
|
370
|
-
signal_power = np.max(psd) - noise_floor if len(psd) > 0 else 0.0
|
|
371
|
-
else:
|
|
372
|
-
noise_floor = 0.0
|
|
373
|
-
signal_power = 0.0
|
|
410
|
+
noise_floor = np.median(np.sort(psd)[: len(psd) // 4]) if len(psd) > 0 else 0.0
|
|
411
|
+
signal_power = np.max(psd) - noise_floor if len(psd) > 0 else 0.0
|
|
374
412
|
snr_db = 10 * np.log10(signal_power / noise_floor) if noise_floor > 0 else 0
|
|
375
413
|
|
|
376
|
-
|
|
377
|
-
rise_time = None
|
|
378
|
-
fall_time = None
|
|
414
|
+
return is_periodic, fundamental_frequency, noise_floor, snr_db
|
|
379
415
|
|
|
380
|
-
if signal_type == "digital":
|
|
381
|
-
threshold_low = v_min + 0.1 * (v_max - v_min)
|
|
382
|
-
threshold_high = v_min + 0.9 * (v_max - v_min)
|
|
383
|
-
|
|
384
|
-
# Find rising edges
|
|
385
|
-
rising_times = []
|
|
386
|
-
falling_times = []
|
|
387
|
-
|
|
388
|
-
for i in range(1, len(data) - 1):
|
|
389
|
-
if data[i - 1] < threshold_low and data[i + 1] > threshold_high:
|
|
390
|
-
# Rising edge
|
|
391
|
-
rising_times.append(1 / sample_rate)
|
|
392
|
-
elif data[i - 1] > threshold_high and data[i + 1] < threshold_low:
|
|
393
|
-
# Falling edge
|
|
394
|
-
falling_times.append(1 / sample_rate)
|
|
395
|
-
|
|
396
|
-
if rising_times:
|
|
397
|
-
rise_time = float(np.median(rising_times))
|
|
398
|
-
if falling_times:
|
|
399
|
-
fall_time = float(np.median(falling_times))
|
|
400
416
|
|
|
401
|
-
|
|
402
|
-
|
|
417
|
+
def _estimate_rise_fall_times(
|
|
418
|
+
data: NDArray[np.float64],
|
|
419
|
+
v_min: float,
|
|
420
|
+
v_max: float,
|
|
421
|
+
sample_rate: float,
|
|
422
|
+
) -> tuple[float | None, float | None]:
|
|
423
|
+
"""Estimate rise and fall times for digital signals.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
data: Signal data array
|
|
427
|
+
v_min: Minimum voltage
|
|
428
|
+
v_max: Maximum voltage
|
|
429
|
+
sample_rate: Sample rate in Hz
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
Tuple of (rise_time, fall_time) in seconds
|
|
433
|
+
"""
|
|
434
|
+
threshold_low = v_min + 0.1 * (v_max - v_min)
|
|
435
|
+
threshold_high = v_min + 0.9 * (v_max - v_min)
|
|
436
|
+
|
|
437
|
+
rising_times = []
|
|
438
|
+
falling_times = []
|
|
403
439
|
|
|
404
|
-
|
|
405
|
-
|
|
440
|
+
for i in range(1, len(data) - 1):
|
|
441
|
+
if data[i - 1] < threshold_low and data[i + 1] > threshold_high:
|
|
442
|
+
rising_times.append(1 / sample_rate)
|
|
443
|
+
elif data[i - 1] > threshold_high and data[i + 1] < threshold_low:
|
|
444
|
+
falling_times.append(1 / sample_rate)
|
|
445
|
+
|
|
446
|
+
rise_time = float(np.median(rising_times)) if rising_times else None
|
|
447
|
+
fall_time = float(np.median(falling_times)) if falling_times else None
|
|
448
|
+
|
|
449
|
+
return rise_time, fall_time
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _build_features_dict(
|
|
453
|
+
v_min: float,
|
|
454
|
+
v_max: float,
|
|
455
|
+
v_mean: float,
|
|
456
|
+
v_std: float,
|
|
457
|
+
data: NDArray[np.float64],
|
|
458
|
+
peaks: list[tuple[float, float]],
|
|
459
|
+
) -> dict[str, Any]:
|
|
460
|
+
"""Build features dictionary for characterization.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
v_min: Minimum voltage
|
|
464
|
+
v_max: Maximum voltage
|
|
465
|
+
v_mean: Mean voltage
|
|
466
|
+
v_std: Standard deviation
|
|
467
|
+
data: Signal data array
|
|
468
|
+
peaks: Histogram peaks
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Dictionary of extracted features
|
|
472
|
+
"""
|
|
473
|
+
return {
|
|
406
474
|
"v_min": v_min,
|
|
407
475
|
"v_max": v_max,
|
|
408
476
|
"v_mean": v_mean,
|
|
@@ -412,6 +480,61 @@ def characterize_unknown_signal(
|
|
|
412
480
|
"peak_positions": [p[0] for p in peaks],
|
|
413
481
|
}
|
|
414
482
|
|
|
483
|
+
|
|
484
|
+
def characterize_unknown_signal(
|
|
485
|
+
trace: WaveformTrace,
|
|
486
|
+
) -> UnknownSignalCharacterization:
|
|
487
|
+
"""Comprehensive characterization of unknown signal per UNKNOWN-003.
|
|
488
|
+
|
|
489
|
+
Analyzes signal characteristics to determine type, periodicity,
|
|
490
|
+
and suggest possible protocols.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
trace: Signal trace to characterize.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
UnknownSignalCharacterization with all extracted features.
|
|
497
|
+
|
|
498
|
+
Example:
|
|
499
|
+
>>> result = characterize_unknown_signal(trace)
|
|
500
|
+
>>> print(f"Signal type: {result.signal_type}")
|
|
501
|
+
>>> print(f"Periodic: {result.is_periodic}")
|
|
502
|
+
>>> for protocol, confidence in result.suggested_protocols:
|
|
503
|
+
... print(f" {protocol}: {confidence:.1%}")
|
|
504
|
+
|
|
505
|
+
References:
|
|
506
|
+
UNKNOWN-003: Unknown Signal Characterization
|
|
507
|
+
"""
|
|
508
|
+
data = trace.data
|
|
509
|
+
sample_rate = trace.metadata.sample_rate
|
|
510
|
+
|
|
511
|
+
# Handle edge case of very short traces
|
|
512
|
+
if len(data) < 2:
|
|
513
|
+
return _handle_short_trace(data)
|
|
514
|
+
|
|
515
|
+
# Compute basic statistics
|
|
516
|
+
v_min, v_max, v_mean, v_std, dc_offset, amplitude = _compute_basic_statistics(data)
|
|
517
|
+
|
|
518
|
+
# Find histogram peaks and classify signal type
|
|
519
|
+
peaks = _find_histogram_peaks(data, v_min, v_max)
|
|
520
|
+
signal_type = _classify_signal_type(peaks, v_min, v_max, v_std, amplitude)
|
|
521
|
+
|
|
522
|
+
# Analyze periodicity
|
|
523
|
+
is_periodic, fundamental_frequency, noise_floor, snr_db = _analyze_periodicity(
|
|
524
|
+
data, sample_rate
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Estimate rise/fall times for digital signals
|
|
528
|
+
rise_time, fall_time = None, None
|
|
529
|
+
if signal_type == "digital":
|
|
530
|
+
rise_time, fall_time = _estimate_rise_fall_times(data, v_min, v_max, sample_rate)
|
|
531
|
+
|
|
532
|
+
# Suggest protocols
|
|
533
|
+
suggested_protocols = _suggest_protocols(signal_type, fundamental_frequency, sample_rate, data)
|
|
534
|
+
|
|
535
|
+
# Build features dictionary
|
|
536
|
+
features = _build_features_dict(v_min, v_max, v_mean, v_std, data, peaks)
|
|
537
|
+
|
|
415
538
|
return UnknownSignalCharacterization(
|
|
416
539
|
signal_type=signal_type,
|
|
417
540
|
is_periodic=is_periodic,
|
oscura/jupyter/magic.py
CHANGED
|
@@ -68,7 +68,7 @@ except ImportError:
|
|
|
68
68
|
class Magics: # type: ignore[no-redef]
|
|
69
69
|
"""Fallback Magics class when IPython not available."""
|
|
70
70
|
|
|
71
|
-
def magics_class(cls:
|
|
71
|
+
def magics_class(cls: Any) -> Any:
|
|
72
72
|
"""Dummy decorator when IPython not available."""
|
|
73
73
|
return cls
|
|
74
74
|
|
|
@@ -82,14 +82,14 @@ except ImportError:
|
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
@magics_class
|
|
85
|
-
class OscuraMagics(Magics):
|
|
85
|
+
class OscuraMagics(Magics):
|
|
86
86
|
"""IPython magics for Oscura analysis.
|
|
87
87
|
|
|
88
88
|
Provides convenient shortcuts for loading traces, running measurements,
|
|
89
89
|
and displaying results in Jupyter notebooks.
|
|
90
90
|
"""
|
|
91
91
|
|
|
92
|
-
@line_magic # type: ignore[
|
|
92
|
+
@line_magic # type: ignore[untyped-decorator]
|
|
93
93
|
def oscura(self, line: str) -> Any:
|
|
94
94
|
"""Oscura line magic for quick operations.
|
|
95
95
|
|
|
@@ -263,7 +263,7 @@ Example:
|
|
|
263
263
|
"""
|
|
264
264
|
print(help_text)
|
|
265
265
|
|
|
266
|
-
@cell_magic # type: ignore[
|
|
266
|
+
@cell_magic # type: ignore[untyped-decorator]
|
|
267
267
|
def analyze(self, line: str, cell: str) -> Any:
|
|
268
268
|
"""Cell magic for multi-line analysis.
|
|
269
269
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Provides user interface patterns for progressive disclosure and formatting utilities.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from oscura.ui.formatters import (
|
|
6
|
+
from oscura.jupyter.ui.formatters import (
|
|
7
7
|
Color,
|
|
8
8
|
FormattedText,
|
|
9
9
|
TextAlignment,
|
|
@@ -20,7 +20,7 @@ from oscura.ui.formatters import (
|
|
|
20
20
|
format_text,
|
|
21
21
|
truncate,
|
|
22
22
|
)
|
|
23
|
-
from oscura.ui.progressive_display import (
|
|
23
|
+
from oscura.jupyter.ui.progressive_display import (
|
|
24
24
|
ProgressiveDisplay,
|
|
25
25
|
ProgressiveOutput,
|
|
26
26
|
Section,
|
|
@@ -6,7 +6,7 @@ including text alignment, truncation, color codes, and structured output.
|
|
|
6
6
|
- UI formatting for terminal and web outputs
|
|
7
7
|
|
|
8
8
|
Example:
|
|
9
|
-
>>> from oscura.ui.formatters import format_text, truncate, colorize
|
|
9
|
+
>>> from oscura.jupyter.ui.formatters import format_text, truncate, colorize
|
|
10
10
|
>>> format_text("Status", "active", align="left", width=20)
|
|
11
11
|
' Status: active'
|
|
12
12
|
>>> truncate("Very long text here", max_length=10)
|
|
@@ -294,7 +294,7 @@ def format_status(
|
|
|
294
294
|
"pass": "✓",
|
|
295
295
|
"fail": "✗",
|
|
296
296
|
"warning": "⚠",
|
|
297
|
-
"info": "ℹ",
|
|
297
|
+
"info": "ℹ",
|
|
298
298
|
"pending": "⏳",
|
|
299
299
|
}
|
|
300
300
|
|
|
@@ -307,7 +307,7 @@ def format_status(
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
# Get color with default, cast needed because dict.get default is str
|
|
310
|
-
status_color: ColorName = colors.get(status, "white")
|
|
310
|
+
status_color: ColorName = colors.get(status, "white")
|
|
311
311
|
|
|
312
312
|
if use_symbols:
|
|
313
313
|
symbol = symbols.get(status, "•")
|