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
oscura/exporters/spice_export.py
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
"""SPICE PWL export functionality for Oscura.
|
|
2
|
-
|
|
3
|
-
This module provides export to SPICE Piece-Wise Linear (PWL) format for
|
|
4
|
-
use in circuit simulation tools like LTspice, ngspice, Cadence, etc.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.exporters.spice_export import export_pwl
|
|
9
|
-
>>> export_pwl(trace, "stimulus.pwl")
|
|
10
|
-
>>> # Use in SPICE: V1 in 0 PWL file=stimulus.pwl
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
from typing import TYPE_CHECKING, Any
|
|
17
|
-
|
|
18
|
-
import numpy as np
|
|
19
|
-
|
|
20
|
-
from oscura.core.types import DigitalTrace, WaveformTrace
|
|
21
|
-
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from numpy.typing import NDArray
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def export_pwl(
|
|
27
|
-
data: WaveformTrace | DigitalTrace | NDArray[Any] | tuple[NDArray[Any], NDArray[Any]],
|
|
28
|
-
path: str | Path,
|
|
29
|
-
*,
|
|
30
|
-
time_scale: float = 1.0,
|
|
31
|
-
amplitude_scale: float = 1.0,
|
|
32
|
-
time_offset: float = 0.0,
|
|
33
|
-
amplitude_offset: float = 0.0,
|
|
34
|
-
precision: int = 12,
|
|
35
|
-
comment: str | None = None,
|
|
36
|
-
downsample: int = 1,
|
|
37
|
-
format_style: str = "ltspice",
|
|
38
|
-
) -> None:
|
|
39
|
-
"""Export data to SPICE PWL (Piece-Wise Linear) format.
|
|
40
|
-
|
|
41
|
-
Creates a PWL file that can be used as a stimulus source in SPICE
|
|
42
|
-
circuit simulators. The format consists of time-value pairs.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
data: Data to export. Can be:
|
|
46
|
-
- WaveformTrace or DigitalTrace
|
|
47
|
-
- NumPy array (uses trace.time_vector or generates index-based time)
|
|
48
|
-
- Tuple of (time_array, value_array)
|
|
49
|
-
path: Output file path.
|
|
50
|
-
time_scale: Scaling factor for time values (e.g., 1e-9 for ns).
|
|
51
|
-
amplitude_scale: Scaling factor for amplitude values.
|
|
52
|
-
time_offset: Offset to add to all time values.
|
|
53
|
-
amplitude_offset: Offset to add to all amplitude values.
|
|
54
|
-
precision: Decimal precision for output values.
|
|
55
|
-
comment: Optional comment to include at top of file.
|
|
56
|
-
downsample: Downsample factor to reduce file size (1 = no downsampling).
|
|
57
|
-
format_style: Output format style:
|
|
58
|
-
- "ltspice": LTspice compatible (time value pairs)
|
|
59
|
-
- "ngspice": ngspice compatible (same as ltspice)
|
|
60
|
-
- "hspice": HSPICE compatible (with header)
|
|
61
|
-
|
|
62
|
-
Raises:
|
|
63
|
-
TypeError: If data type is not supported.
|
|
64
|
-
|
|
65
|
-
Example:
|
|
66
|
-
>>> # Export as stimulus for simulation
|
|
67
|
-
>>> export_pwl(trace, "input.pwl")
|
|
68
|
-
>>> # In LTspice: V1 in 0 PWL file=input.pwl
|
|
69
|
-
|
|
70
|
-
>>> # Scale time to nanoseconds for display
|
|
71
|
-
>>> export_pwl(trace, "input.pwl", time_scale=1e9)
|
|
72
|
-
|
|
73
|
-
References:
|
|
74
|
-
EXP-005
|
|
75
|
-
"""
|
|
76
|
-
path = Path(path)
|
|
77
|
-
|
|
78
|
-
# Extract time and value arrays
|
|
79
|
-
if isinstance(data, WaveformTrace | DigitalTrace):
|
|
80
|
-
time = data.time_vector
|
|
81
|
-
values = data.data
|
|
82
|
-
elif isinstance(data, tuple) and len(data) == 2:
|
|
83
|
-
time, values = data
|
|
84
|
-
elif isinstance(data, np.ndarray):
|
|
85
|
-
# Generate time based on array index (assume 1 unit per sample)
|
|
86
|
-
time = np.arange(len(data), dtype=np.float64)
|
|
87
|
-
values = data
|
|
88
|
-
else:
|
|
89
|
-
raise TypeError(f"Unsupported data type: {type(data)}")
|
|
90
|
-
|
|
91
|
-
# Apply downsampling
|
|
92
|
-
if downsample > 1:
|
|
93
|
-
time = time[::downsample]
|
|
94
|
-
values = values[::downsample]
|
|
95
|
-
|
|
96
|
-
# Apply scaling and offset
|
|
97
|
-
time = time * time_scale + time_offset
|
|
98
|
-
values = values * amplitude_scale + amplitude_offset
|
|
99
|
-
|
|
100
|
-
# Write to file
|
|
101
|
-
with open(path, "w") as f:
|
|
102
|
-
# Write comment/header
|
|
103
|
-
if format_style == "hspice":
|
|
104
|
-
f.write("* HSPICE PWL Data\n")
|
|
105
|
-
if comment:
|
|
106
|
-
f.write(f"* {comment}\n")
|
|
107
|
-
f.write(f"* Points: {len(time)}\n")
|
|
108
|
-
f.write("*\n")
|
|
109
|
-
elif comment:
|
|
110
|
-
f.write(f"; {comment}\n")
|
|
111
|
-
|
|
112
|
-
# Write time-value pairs
|
|
113
|
-
fmt = f"{{:.{precision}g}} {{:.{precision}g}}\n"
|
|
114
|
-
f.writelines(fmt.format(t, v) for t, v in zip(time, values, strict=False))
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def export_pwl_multi(
|
|
118
|
-
traces: dict[str, WaveformTrace | DigitalTrace | NDArray[Any]],
|
|
119
|
-
path: str | Path,
|
|
120
|
-
*,
|
|
121
|
-
time_scale: float = 1.0,
|
|
122
|
-
amplitude_scale: float = 1.0,
|
|
123
|
-
precision: int = 12,
|
|
124
|
-
downsample: int = 1,
|
|
125
|
-
) -> None:
|
|
126
|
-
"""Export multiple traces to individual PWL files.
|
|
127
|
-
|
|
128
|
-
Creates separate PWL files for each trace in the dictionary,
|
|
129
|
-
with filenames based on the dictionary keys.
|
|
130
|
-
|
|
131
|
-
Args:
|
|
132
|
-
traces: Dictionary mapping signal names to trace data.
|
|
133
|
-
path: Output directory path.
|
|
134
|
-
time_scale: Scaling factor for time values.
|
|
135
|
-
amplitude_scale: Scaling factor for amplitude values.
|
|
136
|
-
precision: Decimal precision for output values.
|
|
137
|
-
downsample: Downsample factor to reduce file size.
|
|
138
|
-
|
|
139
|
-
Example:
|
|
140
|
-
>>> traces = {
|
|
141
|
-
... "clk": clock_trace,
|
|
142
|
-
... "data": data_trace,
|
|
143
|
-
... "reset": reset_trace,
|
|
144
|
-
... }
|
|
145
|
-
>>> export_pwl_multi(traces, "stimuli/")
|
|
146
|
-
>>> # Creates: stimuli/clk.pwl, stimuli/data.pwl, stimuli/reset.pwl
|
|
147
|
-
|
|
148
|
-
References:
|
|
149
|
-
EXP-005
|
|
150
|
-
"""
|
|
151
|
-
path = Path(path)
|
|
152
|
-
path.mkdir(parents=True, exist_ok=True)
|
|
153
|
-
|
|
154
|
-
for name, data in traces.items():
|
|
155
|
-
# Sanitize filename
|
|
156
|
-
safe_name = "".join(c if c.isalnum() or c in "_-" else "_" for c in name)
|
|
157
|
-
file_path = path / f"{safe_name}.pwl"
|
|
158
|
-
|
|
159
|
-
export_pwl(
|
|
160
|
-
data,
|
|
161
|
-
file_path,
|
|
162
|
-
time_scale=time_scale,
|
|
163
|
-
amplitude_scale=amplitude_scale,
|
|
164
|
-
precision=precision,
|
|
165
|
-
downsample=downsample,
|
|
166
|
-
comment=f"Signal: {name}",
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def generate_spice_source(
|
|
171
|
-
pwl_path: str | Path,
|
|
172
|
-
node_positive: str = "in",
|
|
173
|
-
node_negative: str = "0",
|
|
174
|
-
source_name: str = "V1",
|
|
175
|
-
source_type: str = "voltage",
|
|
176
|
-
) -> str:
|
|
177
|
-
"""Generate SPICE source definition for a PWL file.
|
|
178
|
-
|
|
179
|
-
Args:
|
|
180
|
-
pwl_path: Path to PWL file.
|
|
181
|
-
node_positive: Positive node name.
|
|
182
|
-
node_negative: Negative node name (usually "0" for ground).
|
|
183
|
-
source_name: Source instance name.
|
|
184
|
-
source_type: Source type ("voltage" or "current").
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
SPICE source definition string.
|
|
188
|
-
|
|
189
|
-
Example:
|
|
190
|
-
>>> line = generate_spice_source("input.pwl", "in", "0", "V1")
|
|
191
|
-
>>> print(line)
|
|
192
|
-
V1 in 0 PWL file=input.pwl
|
|
193
|
-
|
|
194
|
-
References:
|
|
195
|
-
EXP-005
|
|
196
|
-
"""
|
|
197
|
-
prefix = "V" if source_type == "voltage" else "I"
|
|
198
|
-
|
|
199
|
-
# Ensure source name starts with correct prefix
|
|
200
|
-
if not source_name.upper().startswith(prefix):
|
|
201
|
-
source_name = f"{prefix}{source_name}"
|
|
202
|
-
|
|
203
|
-
return f"{source_name} {node_positive} {node_negative} PWL file={pwl_path}"
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
__all__ = [
|
|
207
|
-
"export_pwl",
|
|
208
|
-
"export_pwl_multi",
|
|
209
|
-
"generate_spice_source",
|
|
210
|
-
]
|
oscura/search/context.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
"""Context extraction around points of interest.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
This module provides efficient extraction of signal context around
|
|
5
|
-
events, maintaining original time references for debugging workflows.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from typing import Any
|
|
9
|
-
|
|
10
|
-
import numpy as np
|
|
11
|
-
from numpy.typing import NDArray
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def extract_context(
|
|
15
|
-
trace: NDArray[np.float64],
|
|
16
|
-
index: int | list[int] | NDArray[np.int_],
|
|
17
|
-
*,
|
|
18
|
-
before: int = 100,
|
|
19
|
-
after: int = 100,
|
|
20
|
-
sample_rate: float | None = None,
|
|
21
|
-
include_metadata: bool = True,
|
|
22
|
-
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
23
|
-
"""Extract signal context around a point of interest.
|
|
24
|
-
|
|
25
|
-
: Context extraction with time reference preservation.
|
|
26
|
-
Supports batch extraction for multiple indices and optional protocol data.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
trace: Input signal trace
|
|
30
|
-
index: Sample index or list of indices to extract context around.
|
|
31
|
-
Can be int, list of ints, or numpy array.
|
|
32
|
-
before: Number of samples to include before index (default: 100)
|
|
33
|
-
after: Number of samples to include after index (default: 100)
|
|
34
|
-
sample_rate: Optional sample rate in Hz for time calculations
|
|
35
|
-
include_metadata: Include metadata dict with context info (default: True)
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
If index is scalar: Single context dictionary
|
|
39
|
-
If index is list/array: List of context dictionaries
|
|
40
|
-
|
|
41
|
-
Each context dictionary contains:
|
|
42
|
-
- data: Extracted sub-trace array
|
|
43
|
-
- start_index: Starting index in original trace
|
|
44
|
-
- end_index: Ending index in original trace
|
|
45
|
-
- center_index: Center index (original query index)
|
|
46
|
-
- time_reference: Time offset if sample_rate provided
|
|
47
|
-
- length: Number of samples in context
|
|
48
|
-
|
|
49
|
-
Raises:
|
|
50
|
-
ValueError: If index is out of bounds
|
|
51
|
-
ValueError: If before or after are negative
|
|
52
|
-
|
|
53
|
-
Examples:
|
|
54
|
-
>>> # Extract context around a glitch
|
|
55
|
-
>>> trace = np.random.randn(1000)
|
|
56
|
-
>>> glitch_index = 500
|
|
57
|
-
>>> context = extract_context(
|
|
58
|
-
... trace,
|
|
59
|
-
... glitch_index,
|
|
60
|
-
... before=50,
|
|
61
|
-
... after=50,
|
|
62
|
-
... sample_rate=1e6
|
|
63
|
-
... )
|
|
64
|
-
>>> print(f"Context length: {len(context['data'])}")
|
|
65
|
-
>>> print(f"Time reference: {context['time_reference']*1e6:.2f} µs")
|
|
66
|
-
|
|
67
|
-
>>> # Batch extraction for multiple events
|
|
68
|
-
>>> event_indices = [100, 200, 300]
|
|
69
|
-
>>> contexts = extract_context(
|
|
70
|
-
... trace,
|
|
71
|
-
... event_indices,
|
|
72
|
-
... before=25,
|
|
73
|
-
... after=25
|
|
74
|
-
... )
|
|
75
|
-
>>> print(f"Extracted {len(contexts)} contexts")
|
|
76
|
-
|
|
77
|
-
Notes:
|
|
78
|
-
- Handles edge cases at trace boundaries automatically
|
|
79
|
-
- Context may be shorter than before+after at boundaries
|
|
80
|
-
- Time reference is relative to start of extracted context
|
|
81
|
-
- Original trace is not modified
|
|
82
|
-
|
|
83
|
-
References:
|
|
84
|
-
SRCH-003: Context Extraction
|
|
85
|
-
"""
|
|
86
|
-
if before < 0 or after < 0:
|
|
87
|
-
raise ValueError("before and after must be non-negative")
|
|
88
|
-
|
|
89
|
-
if trace.size == 0:
|
|
90
|
-
raise ValueError("Trace cannot be empty")
|
|
91
|
-
|
|
92
|
-
# Handle single index vs multiple indices
|
|
93
|
-
if isinstance(index, int | np.integer):
|
|
94
|
-
indices = [int(index)]
|
|
95
|
-
return_single = True
|
|
96
|
-
else:
|
|
97
|
-
indices = [int(i) for i in index]
|
|
98
|
-
return_single = False
|
|
99
|
-
|
|
100
|
-
# Validate indices
|
|
101
|
-
for idx in indices:
|
|
102
|
-
if idx < 0 or idx >= len(trace):
|
|
103
|
-
raise ValueError(f"Index {idx} out of bounds for trace of length {len(trace)}")
|
|
104
|
-
|
|
105
|
-
# Extract contexts
|
|
106
|
-
contexts = []
|
|
107
|
-
|
|
108
|
-
for idx in indices:
|
|
109
|
-
# Calculate window bounds with boundary handling
|
|
110
|
-
start_idx = max(0, idx - before)
|
|
111
|
-
end_idx = min(len(trace), idx + after + 1)
|
|
112
|
-
|
|
113
|
-
# Extract data
|
|
114
|
-
data = trace[start_idx:end_idx].copy()
|
|
115
|
-
|
|
116
|
-
# Build context dictionary
|
|
117
|
-
context: dict[str, Any] = {
|
|
118
|
-
"data": data,
|
|
119
|
-
"start_index": start_idx,
|
|
120
|
-
"end_index": end_idx,
|
|
121
|
-
"center_index": idx,
|
|
122
|
-
"length": len(data),
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
# Add time reference if sample rate provided
|
|
126
|
-
if sample_rate is not None:
|
|
127
|
-
time_offset = start_idx / sample_rate
|
|
128
|
-
context["time_reference"] = time_offset
|
|
129
|
-
context["sample_rate"] = sample_rate
|
|
130
|
-
|
|
131
|
-
# Time array for the context
|
|
132
|
-
dt = 1.0 / sample_rate
|
|
133
|
-
context["time_array"] = np.arange(len(data)) * dt + time_offset
|
|
134
|
-
|
|
135
|
-
if include_metadata:
|
|
136
|
-
context["metadata"] = {
|
|
137
|
-
"samples_before": idx - start_idx,
|
|
138
|
-
"samples_after": end_idx - idx - 1,
|
|
139
|
-
"at_start_boundary": start_idx == 0,
|
|
140
|
-
"at_end_boundary": end_idx == len(trace),
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
contexts.append(context)
|
|
144
|
-
|
|
145
|
-
# Return single context or list
|
|
146
|
-
if return_single:
|
|
147
|
-
return contexts[0]
|
|
148
|
-
else:
|
|
149
|
-
return contexts
|
oscura/session/__init__.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
"""Session management for Oscura analysis sessions.
|
|
2
|
-
|
|
3
|
-
This module provides session save/restore, trace annotations, and
|
|
4
|
-
operation history tracking.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> import oscura as osc
|
|
9
|
-
>>> session = osc.Session()
|
|
10
|
-
>>> session.load_trace('capture.wfm')
|
|
11
|
-
>>> session.annotate(time=1.5e-6, text='Glitch here')
|
|
12
|
-
>>> session.save('debug_session.tks')
|
|
13
|
-
>>>
|
|
14
|
-
>>> # Later...
|
|
15
|
-
>>> session = osc.load_session('debug_session.tks')
|
|
16
|
-
>>> print(session.annotations)
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from oscura.session.annotations import Annotation, AnnotationLayer, AnnotationType
|
|
20
|
-
from oscura.session.history import HistoryEntry, OperationHistory
|
|
21
|
-
from oscura.session.session import Session, load_session
|
|
22
|
-
|
|
23
|
-
__all__ = [
|
|
24
|
-
# Annotations (SESS-002)
|
|
25
|
-
"Annotation",
|
|
26
|
-
"AnnotationLayer",
|
|
27
|
-
"AnnotationType",
|
|
28
|
-
# History (SESS-003)
|
|
29
|
-
"HistoryEntry",
|
|
30
|
-
"OperationHistory",
|
|
31
|
-
# Session (SESS-001)
|
|
32
|
-
"Session",
|
|
33
|
-
"load_session",
|
|
34
|
-
]
|
oscura/session/annotations.py
DELETED
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
"""Trace annotation support.
|
|
2
|
-
|
|
3
|
-
This module provides annotation capabilities for marking points of interest
|
|
4
|
-
in signal traces.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> layer = AnnotationLayer("Debug Markers")
|
|
9
|
-
>>> layer.add(Annotation(time=1.5e-6, text="Glitch detected"))
|
|
10
|
-
>>> layer.add(Annotation(time_range=(2e-6, 3e-6), text="Data packet"))
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
from enum import Enum
|
|
18
|
-
from typing import Any
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class AnnotationType(Enum):
|
|
22
|
-
"""Types of annotations."""
|
|
23
|
-
|
|
24
|
-
POINT = "point" # Single time point
|
|
25
|
-
RANGE = "range" # Time range
|
|
26
|
-
VERTICAL = "vertical" # Vertical line
|
|
27
|
-
HORIZONTAL = "horizontal" # Horizontal line
|
|
28
|
-
REGION = "region" # 2D region (time + amplitude)
|
|
29
|
-
TEXT = "text" # Free-floating text
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class Annotation:
|
|
34
|
-
"""Single annotation on a trace.
|
|
35
|
-
|
|
36
|
-
Attributes:
|
|
37
|
-
text: Annotation text/label
|
|
38
|
-
time: Time point (for point annotations)
|
|
39
|
-
time_range: (start, end) time range
|
|
40
|
-
amplitude: Amplitude value (for horizontal lines)
|
|
41
|
-
amplitude_range: (min, max) amplitude range
|
|
42
|
-
annotation_type: Type of annotation
|
|
43
|
-
color: Display color (hex or name)
|
|
44
|
-
style: Line style ('solid', 'dashed', 'dotted')
|
|
45
|
-
visible: Whether annotation is visible
|
|
46
|
-
created_at: Creation timestamp
|
|
47
|
-
metadata: Additional metadata
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
text: str
|
|
51
|
-
time: float | None = None
|
|
52
|
-
time_range: tuple[float, float] | None = None
|
|
53
|
-
amplitude: float | None = None
|
|
54
|
-
amplitude_range: tuple[float, float] | None = None
|
|
55
|
-
annotation_type: AnnotationType = AnnotationType.POINT
|
|
56
|
-
color: str = "#FF6B6B"
|
|
57
|
-
style: str = "solid"
|
|
58
|
-
visible: bool = True
|
|
59
|
-
created_at: datetime = field(default_factory=datetime.now)
|
|
60
|
-
metadata: dict[str, Any] = field(default_factory=dict)
|
|
61
|
-
|
|
62
|
-
def __post_init__(self) -> None:
|
|
63
|
-
"""Infer annotation type from provided parameters."""
|
|
64
|
-
if self.annotation_type == AnnotationType.POINT:
|
|
65
|
-
if self.time_range is not None:
|
|
66
|
-
self.annotation_type = AnnotationType.RANGE
|
|
67
|
-
elif self.amplitude is not None and self.time is None:
|
|
68
|
-
self.annotation_type = AnnotationType.HORIZONTAL
|
|
69
|
-
elif self.amplitude_range is not None and self.time_range is not None:
|
|
70
|
-
self.annotation_type = AnnotationType.REGION # type: ignore[unreachable]
|
|
71
|
-
|
|
72
|
-
@property
|
|
73
|
-
def start_time(self) -> float | None:
|
|
74
|
-
"""Get start time for range annotations."""
|
|
75
|
-
if self.time_range:
|
|
76
|
-
return self.time_range[0]
|
|
77
|
-
return self.time
|
|
78
|
-
|
|
79
|
-
@property
|
|
80
|
-
def end_time(self) -> float | None:
|
|
81
|
-
"""Get end time for range annotations."""
|
|
82
|
-
if self.time_range:
|
|
83
|
-
return self.time_range[1]
|
|
84
|
-
return self.time
|
|
85
|
-
|
|
86
|
-
def to_dict(self) -> dict[str, Any]:
|
|
87
|
-
"""Convert to dictionary for serialization."""
|
|
88
|
-
return {
|
|
89
|
-
"text": self.text,
|
|
90
|
-
"time": self.time,
|
|
91
|
-
"time_range": self.time_range,
|
|
92
|
-
"amplitude": self.amplitude,
|
|
93
|
-
"amplitude_range": self.amplitude_range,
|
|
94
|
-
"annotation_type": self.annotation_type.value,
|
|
95
|
-
"color": self.color,
|
|
96
|
-
"style": self.style,
|
|
97
|
-
"visible": self.visible,
|
|
98
|
-
"created_at": self.created_at.isoformat(),
|
|
99
|
-
"metadata": self.metadata,
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
@classmethod
|
|
103
|
-
def from_dict(cls, data: dict[str, Any]) -> Annotation:
|
|
104
|
-
"""Create from dictionary."""
|
|
105
|
-
data = data.copy()
|
|
106
|
-
data["annotation_type"] = AnnotationType(data.get("annotation_type", "point"))
|
|
107
|
-
if "created_at" in data and isinstance(data["created_at"], str):
|
|
108
|
-
data["created_at"] = datetime.fromisoformat(data["created_at"])
|
|
109
|
-
return cls(**data)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
@dataclass
|
|
113
|
-
class AnnotationLayer:
|
|
114
|
-
"""Collection of related annotations.
|
|
115
|
-
|
|
116
|
-
Attributes:
|
|
117
|
-
name: Layer name
|
|
118
|
-
annotations: List of annotations
|
|
119
|
-
visible: Whether layer is visible
|
|
120
|
-
locked: Whether layer is locked (read-only)
|
|
121
|
-
color: Default color for new annotations
|
|
122
|
-
description: Layer description
|
|
123
|
-
"""
|
|
124
|
-
|
|
125
|
-
name: str
|
|
126
|
-
annotations: list[Annotation] = field(default_factory=list)
|
|
127
|
-
visible: bool = True
|
|
128
|
-
locked: bool = False
|
|
129
|
-
color: str = "#FF6B6B"
|
|
130
|
-
description: str = ""
|
|
131
|
-
|
|
132
|
-
def add(
|
|
133
|
-
self,
|
|
134
|
-
annotation: Annotation | None = None,
|
|
135
|
-
*,
|
|
136
|
-
text: str = "",
|
|
137
|
-
time: float | None = None,
|
|
138
|
-
time_range: tuple[float, float] | None = None,
|
|
139
|
-
**kwargs: Any,
|
|
140
|
-
) -> Annotation:
|
|
141
|
-
"""Add annotation to layer.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
annotation: Pre-built Annotation object.
|
|
145
|
-
text: Annotation text (if not using pre-built).
|
|
146
|
-
time: Time point.
|
|
147
|
-
time_range: Time range.
|
|
148
|
-
**kwargs: Additional Annotation parameters.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
Added annotation.
|
|
152
|
-
|
|
153
|
-
Raises:
|
|
154
|
-
ValueError: If layer is locked.
|
|
155
|
-
"""
|
|
156
|
-
if self.locked:
|
|
157
|
-
raise ValueError(f"Layer '{self.name}' is locked")
|
|
158
|
-
|
|
159
|
-
if annotation is None:
|
|
160
|
-
annotation = Annotation(
|
|
161
|
-
text=text,
|
|
162
|
-
time=time,
|
|
163
|
-
time_range=time_range,
|
|
164
|
-
color=kwargs.pop("color", self.color),
|
|
165
|
-
**kwargs,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
self.annotations.append(annotation)
|
|
169
|
-
return annotation
|
|
170
|
-
|
|
171
|
-
def remove(self, annotation: Annotation) -> bool:
|
|
172
|
-
"""Remove annotation from layer.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
annotation: Annotation to remove.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
True if removed, False if not found.
|
|
179
|
-
|
|
180
|
-
Raises:
|
|
181
|
-
ValueError: If layer is locked.
|
|
182
|
-
"""
|
|
183
|
-
if self.locked:
|
|
184
|
-
raise ValueError(f"Layer '{self.name}' is locked")
|
|
185
|
-
|
|
186
|
-
try:
|
|
187
|
-
self.annotations.remove(annotation)
|
|
188
|
-
return True
|
|
189
|
-
except ValueError:
|
|
190
|
-
return False
|
|
191
|
-
|
|
192
|
-
def find_at_time(
|
|
193
|
-
self,
|
|
194
|
-
time: float,
|
|
195
|
-
tolerance: float = 0.0,
|
|
196
|
-
) -> list[Annotation]:
|
|
197
|
-
"""Find annotations at or near a specific time.
|
|
198
|
-
|
|
199
|
-
Args:
|
|
200
|
-
time: Time to search.
|
|
201
|
-
tolerance: Time tolerance for matching.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
List of matching annotations.
|
|
205
|
-
"""
|
|
206
|
-
matches = []
|
|
207
|
-
for ann in self.annotations:
|
|
208
|
-
if ann.time is not None:
|
|
209
|
-
if abs(ann.time - time) <= tolerance:
|
|
210
|
-
matches.append(ann)
|
|
211
|
-
elif ann.time_range is not None and (
|
|
212
|
-
ann.time_range[0] - tolerance <= time <= ann.time_range[1] + tolerance
|
|
213
|
-
):
|
|
214
|
-
matches.append(ann)
|
|
215
|
-
return matches
|
|
216
|
-
|
|
217
|
-
def find_in_range(
|
|
218
|
-
self,
|
|
219
|
-
start_time: float,
|
|
220
|
-
end_time: float,
|
|
221
|
-
) -> list[Annotation]:
|
|
222
|
-
"""Find annotations within a time range.
|
|
223
|
-
|
|
224
|
-
Args:
|
|
225
|
-
start_time: Range start.
|
|
226
|
-
end_time: Range end.
|
|
227
|
-
|
|
228
|
-
Returns:
|
|
229
|
-
List of annotations within range.
|
|
230
|
-
"""
|
|
231
|
-
matches = []
|
|
232
|
-
for ann in self.annotations:
|
|
233
|
-
ann_start = ann.start_time
|
|
234
|
-
ann_end = ann.end_time
|
|
235
|
-
|
|
236
|
-
if ann_start is not None and (
|
|
237
|
-
start_time <= ann_start <= end_time
|
|
238
|
-
or (ann_end is not None and ann_start <= end_time and ann_end >= start_time)
|
|
239
|
-
):
|
|
240
|
-
matches.append(ann)
|
|
241
|
-
|
|
242
|
-
return matches
|
|
243
|
-
|
|
244
|
-
def clear(self) -> int:
|
|
245
|
-
"""Remove all annotations.
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
Number of annotations removed.
|
|
249
|
-
|
|
250
|
-
Raises:
|
|
251
|
-
ValueError: If layer is locked.
|
|
252
|
-
"""
|
|
253
|
-
if self.locked:
|
|
254
|
-
raise ValueError(f"Layer '{self.name}' is locked")
|
|
255
|
-
|
|
256
|
-
count = len(self.annotations)
|
|
257
|
-
self.annotations.clear()
|
|
258
|
-
return count
|
|
259
|
-
|
|
260
|
-
def to_dict(self) -> dict[str, Any]:
|
|
261
|
-
"""Convert to dictionary for serialization."""
|
|
262
|
-
return {
|
|
263
|
-
"name": self.name,
|
|
264
|
-
"annotations": [a.to_dict() for a in self.annotations],
|
|
265
|
-
"visible": self.visible,
|
|
266
|
-
"locked": self.locked,
|
|
267
|
-
"color": self.color,
|
|
268
|
-
"description": self.description,
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
@classmethod
|
|
272
|
-
def from_dict(cls, data: dict[str, Any]) -> AnnotationLayer:
|
|
273
|
-
"""Create from dictionary."""
|
|
274
|
-
annotations = [Annotation.from_dict(a) for a in data.get("annotations", [])]
|
|
275
|
-
return cls(
|
|
276
|
-
name=data["name"],
|
|
277
|
-
annotations=annotations,
|
|
278
|
-
visible=data.get("visible", True),
|
|
279
|
-
locked=data.get("locked", False),
|
|
280
|
-
color=data.get("color", "#FF6B6B"),
|
|
281
|
-
description=data.get("description", ""),
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
__all__ = [
|
|
286
|
-
"Annotation",
|
|
287
|
-
"AnnotationLayer",
|
|
288
|
-
"AnnotationType",
|
|
289
|
-
]
|