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/visualization/layout.py
CHANGED
|
@@ -21,6 +21,8 @@ from typing import TYPE_CHECKING
|
|
|
21
21
|
|
|
22
22
|
import numpy as np
|
|
23
23
|
|
|
24
|
+
from oscura.utils.geometry import generate_leader_line
|
|
25
|
+
|
|
24
26
|
if TYPE_CHECKING:
|
|
25
27
|
from numpy.typing import NDArray
|
|
26
28
|
|
|
@@ -165,53 +167,19 @@ def layout_stacked_channels(
|
|
|
165
167
|
)
|
|
166
168
|
|
|
167
169
|
|
|
168
|
-
def
|
|
170
|
+
def _initialize_placed_annotations(
|
|
169
171
|
annotations: list[Annotation],
|
|
170
|
-
*,
|
|
171
|
-
display_width: float = 800.0,
|
|
172
|
-
display_height: float = 600.0,
|
|
173
|
-
max_iterations: int = 100,
|
|
174
|
-
repulsion_strength: float = 10.0,
|
|
175
|
-
min_spacing: float = 5.0,
|
|
176
172
|
) -> list[PlacedAnnotation]:
|
|
177
|
-
"""
|
|
178
|
-
|
|
179
|
-
Uses force-directed layout algorithm to separate overlapping labels
|
|
180
|
-
with repulsive forces. Generates leader lines when labels must be
|
|
181
|
-
displaced from anchor points.
|
|
173
|
+
"""Initialize placed annotations at anchor points.
|
|
182
174
|
|
|
183
175
|
Args:
|
|
184
176
|
annotations: List of annotations to place.
|
|
185
|
-
display_width: Display area width in pixels.
|
|
186
|
-
display_height: Display area height in pixels.
|
|
187
|
-
max_iterations: Maximum iterations for force-directed layout.
|
|
188
|
-
repulsion_strength: Strength of repulsive force between overlapping labels.
|
|
189
|
-
min_spacing: Minimum spacing between annotations in pixels.
|
|
190
177
|
|
|
191
178
|
Returns:
|
|
192
|
-
List of PlacedAnnotation
|
|
193
|
-
|
|
194
|
-
Raises:
|
|
195
|
-
ValueError: If annotations list is empty.
|
|
196
|
-
|
|
197
|
-
Example:
|
|
198
|
-
>>> annots = [Annotation("Peak", 0.5, 1.0, priority=0.9)]
|
|
199
|
-
>>> placed = optimize_annotation_placement(annots)
|
|
200
|
-
>>> print(f"Needs leader: {placed[0].needs_leader}")
|
|
201
|
-
|
|
202
|
-
References:
|
|
203
|
-
VIS-016: Annotation Placement Intelligence
|
|
204
|
-
Force-directed graph layout (Fruchterman-Reingold)
|
|
179
|
+
List of PlacedAnnotation initially at anchor points.
|
|
205
180
|
"""
|
|
206
|
-
if len(annotations) == 0:
|
|
207
|
-
raise ValueError("annotations list cannot be empty")
|
|
208
|
-
|
|
209
|
-
# Convert annotations to display coordinates
|
|
210
|
-
# For now, assume data coordinates are normalized to display units
|
|
211
181
|
placed = []
|
|
212
|
-
|
|
213
182
|
for annot in annotations:
|
|
214
|
-
# Initial placement at anchor point
|
|
215
183
|
placed.append(
|
|
216
184
|
PlacedAnnotation(
|
|
217
185
|
annotation=annot,
|
|
@@ -221,84 +189,126 @@ def optimize_annotation_placement(
|
|
|
221
189
|
leader_points=None,
|
|
222
190
|
)
|
|
223
191
|
)
|
|
192
|
+
return placed
|
|
224
193
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
194
|
+
|
|
195
|
+
def _calculate_repulsive_force(
|
|
196
|
+
placed_i: PlacedAnnotation,
|
|
197
|
+
placed_j: PlacedAnnotation,
|
|
198
|
+
min_spacing: float,
|
|
199
|
+
repulsion_strength: float,
|
|
200
|
+
) -> tuple[float, float]:
|
|
201
|
+
"""Calculate repulsive force between two annotations.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
placed_i: First annotation.
|
|
205
|
+
placed_j: Second annotation.
|
|
206
|
+
min_spacing: Minimum spacing in pixels.
|
|
207
|
+
repulsion_strength: Repulsive force strength.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Tuple of (fx, fy) force components.
|
|
211
|
+
"""
|
|
212
|
+
# Check for bounding box overlap
|
|
213
|
+
dx = placed_j.display_x - placed_i.display_x
|
|
214
|
+
dy = placed_j.display_y - placed_i.display_y
|
|
215
|
+
|
|
216
|
+
# Bounding box sizes
|
|
217
|
+
w1 = placed_i.annotation.bbox_width
|
|
218
|
+
h1 = placed_i.annotation.bbox_height
|
|
219
|
+
w2 = placed_j.annotation.bbox_width
|
|
220
|
+
h2 = placed_j.annotation.bbox_height
|
|
221
|
+
|
|
222
|
+
# Minimum separation (sum of half-widths + spacing)
|
|
223
|
+
min_dx = (w1 + w2) / 2 + min_spacing
|
|
224
|
+
min_dy = (h1 + h2) / 2 + min_spacing
|
|
225
|
+
|
|
226
|
+
# Check if overlapping
|
|
227
|
+
if abs(dx) < min_dx and abs(dy) < min_dy:
|
|
228
|
+
# Calculate repulsive force
|
|
229
|
+
distance = np.sqrt(dx**2 + dy**2)
|
|
230
|
+
if distance < 1e-6:
|
|
231
|
+
# Avoid division by zero
|
|
232
|
+
distance = 1e-6
|
|
233
|
+
dx = np.random.randn() * 0.1
|
|
234
|
+
dy = np.random.randn() * 0.1
|
|
235
|
+
|
|
236
|
+
# Repulsion inversely proportional to distance
|
|
237
|
+
force = repulsion_strength / distance
|
|
238
|
+
|
|
239
|
+
# Return force in direction away from overlap
|
|
240
|
+
return -force * dx / distance, -force * dy / distance
|
|
241
|
+
|
|
242
|
+
return 0.0, 0.0
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _apply_force_iteration(
|
|
246
|
+
placed: list[PlacedAnnotation],
|
|
247
|
+
display_width: float,
|
|
248
|
+
display_height: float,
|
|
249
|
+
min_spacing: float,
|
|
250
|
+
repulsion_strength: float,
|
|
251
|
+
) -> bool:
|
|
252
|
+
"""Apply one iteration of force-directed layout.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
placed: List of placed annotations to update.
|
|
256
|
+
display_width: Display width for clamping.
|
|
257
|
+
display_height: Display height for clamping.
|
|
258
|
+
min_spacing: Minimum spacing in pixels.
|
|
259
|
+
repulsion_strength: Repulsive force strength.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
True if any annotation moved significantly.
|
|
263
|
+
"""
|
|
264
|
+
moved = False
|
|
265
|
+
|
|
266
|
+
for i in range(len(placed)):
|
|
267
|
+
fx = 0.0
|
|
268
|
+
fy = 0.0
|
|
269
|
+
|
|
270
|
+
# Calculate forces from all other annotations
|
|
271
|
+
for j in range(len(placed)):
|
|
272
|
+
if i != j:
|
|
273
|
+
force_x, force_y = _calculate_repulsive_force(
|
|
274
|
+
placed[i], placed[j], min_spacing, repulsion_strength
|
|
291
275
|
)
|
|
292
|
-
|
|
276
|
+
fx += force_x
|
|
277
|
+
fy += force_y
|
|
293
278
|
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
279
|
+
# Apply forces with damping (priority affects inertia)
|
|
280
|
+
damping = 0.5
|
|
281
|
+
priority_factor = 1.0 - placed[i].annotation.priority
|
|
282
|
+
step_size = damping * priority_factor
|
|
297
283
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
leader_threshold = 20.0 # pixels
|
|
284
|
+
new_x = placed[i].display_x + fx * step_size
|
|
285
|
+
new_y = placed[i].display_y + fy * step_size
|
|
301
286
|
|
|
287
|
+
# Clamp to display bounds
|
|
288
|
+
new_x = np.clip(new_x, 0, display_width)
|
|
289
|
+
new_y = np.clip(new_y, 0, display_height)
|
|
290
|
+
|
|
291
|
+
# Update if moved significantly
|
|
292
|
+
if abs(new_x - placed[i].display_x) > 0.1 or abs(new_y - placed[i].display_y) > 0.1:
|
|
293
|
+
placed[i] = PlacedAnnotation(
|
|
294
|
+
annotation=placed[i].annotation,
|
|
295
|
+
display_x=new_x,
|
|
296
|
+
display_y=new_y,
|
|
297
|
+
needs_leader=False,
|
|
298
|
+
leader_points=None,
|
|
299
|
+
)
|
|
300
|
+
moved = True
|
|
301
|
+
|
|
302
|
+
return moved
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _add_leader_lines(placed: list[PlacedAnnotation], leader_threshold: float = 20.0) -> None:
|
|
306
|
+
"""Add leader lines to annotations displaced from anchor points.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
placed: List of placed annotations to update in-place.
|
|
310
|
+
leader_threshold: Displacement threshold for leader line in pixels.
|
|
311
|
+
"""
|
|
302
312
|
for i, p in enumerate(placed):
|
|
303
313
|
anchor_x = p.annotation.x
|
|
304
314
|
anchor_y = p.annotation.y
|
|
@@ -307,7 +317,7 @@ def optimize_annotation_placement(
|
|
|
307
317
|
|
|
308
318
|
if displacement > leader_threshold:
|
|
309
319
|
# Generate simple orthogonal leader line
|
|
310
|
-
leader_points =
|
|
320
|
+
leader_points = generate_leader_line(
|
|
311
321
|
(anchor_x, anchor_y),
|
|
312
322
|
(p.display_x, p.display_y),
|
|
313
323
|
)
|
|
@@ -320,40 +330,65 @@ def optimize_annotation_placement(
|
|
|
320
330
|
leader_points=leader_points,
|
|
321
331
|
)
|
|
322
332
|
|
|
323
|
-
return placed
|
|
324
333
|
|
|
334
|
+
def optimize_annotation_placement(
|
|
335
|
+
annotations: list[Annotation],
|
|
336
|
+
*,
|
|
337
|
+
display_width: float = 800.0,
|
|
338
|
+
display_height: float = 600.0,
|
|
339
|
+
max_iterations: int = 100,
|
|
340
|
+
repulsion_strength: float = 10.0,
|
|
341
|
+
min_spacing: float = 5.0,
|
|
342
|
+
) -> list[PlacedAnnotation]:
|
|
343
|
+
"""Optimize annotation placement with collision avoidance.
|
|
325
344
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
) -> list[tuple[float, float]]:
|
|
330
|
-
"""Generate orthogonal leader line from anchor to label.
|
|
345
|
+
Uses force-directed layout algorithm to separate overlapping labels
|
|
346
|
+
with repulsive forces. Generates leader lines when labels must be
|
|
347
|
+
displaced from anchor points.
|
|
331
348
|
|
|
332
349
|
Args:
|
|
333
|
-
|
|
334
|
-
|
|
350
|
+
annotations: List of annotations to place.
|
|
351
|
+
display_width: Display area width in pixels.
|
|
352
|
+
display_height: Display area height in pixels.
|
|
353
|
+
max_iterations: Maximum iterations for force-directed layout.
|
|
354
|
+
repulsion_strength: Strength of repulsive force between overlapping labels.
|
|
355
|
+
min_spacing: Minimum spacing between annotations in pixels.
|
|
335
356
|
|
|
336
357
|
Returns:
|
|
337
|
-
List of
|
|
358
|
+
List of PlacedAnnotation with optimized positions.
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
ValueError: If annotations list is empty.
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> annots = [Annotation("Peak", 0.5, 1.0, priority=0.9)]
|
|
365
|
+
>>> placed = optimize_annotation_placement(annots)
|
|
366
|
+
>>> print(f"Needs leader: {placed[0].needs_leader}")
|
|
367
|
+
|
|
368
|
+
References:
|
|
369
|
+
VIS-016: Annotation Placement Intelligence
|
|
370
|
+
Force-directed graph layout (Fruchterman-Reingold)
|
|
338
371
|
"""
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
if len(annotations) == 0:
|
|
373
|
+
raise ValueError("annotations list cannot be empty")
|
|
341
374
|
|
|
342
|
-
#
|
|
343
|
-
|
|
344
|
-
# based on which dimension has larger displacement
|
|
375
|
+
# Data preparation - initialize at anchor points
|
|
376
|
+
placed = _initialize_placed_annotations(annotations)
|
|
345
377
|
|
|
346
|
-
|
|
347
|
-
|
|
378
|
+
# Force-directed layout iterations
|
|
379
|
+
for _iteration in range(max_iterations):
|
|
380
|
+
moved = _apply_force_iteration(
|
|
381
|
+
placed, display_width, display_height, min_spacing, repulsion_strength
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Converged if nothing moved
|
|
385
|
+
if not moved:
|
|
386
|
+
break
|
|
348
387
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
mid = (lx, ay)
|
|
352
|
-
else:
|
|
353
|
-
# Vertical-first
|
|
354
|
-
mid = (ax, ly)
|
|
388
|
+
# Annotation - add leader lines for displaced annotations
|
|
389
|
+
_add_leader_lines(placed, leader_threshold=20.0)
|
|
355
390
|
|
|
356
|
-
return
|
|
391
|
+
return placed
|
|
357
392
|
|
|
358
393
|
|
|
359
394
|
__all__ = [
|
|
@@ -65,37 +65,64 @@ def calculate_optimal_y_range(
|
|
|
65
65
|
References:
|
|
66
66
|
VIS-013: Auto Y-Axis Range Optimization
|
|
67
67
|
"""
|
|
68
|
+
clean_data = _validate_and_clean_data(data)
|
|
69
|
+
filtered_data = _filter_outliers(clean_data, outlier_threshold)
|
|
70
|
+
_check_clipping(clean_data, filtered_data, clip_warning_threshold)
|
|
71
|
+
|
|
72
|
+
# Calculate data range
|
|
73
|
+
data_min, data_max = float(np.min(filtered_data)), float(np.max(filtered_data))
|
|
74
|
+
margin = _select_smart_margin(len(filtered_data), margin_percent)
|
|
75
|
+
|
|
76
|
+
# Apply range mode
|
|
77
|
+
if symmetric:
|
|
78
|
+
return _symmetric_range(data_min, data_max, margin)
|
|
79
|
+
else:
|
|
80
|
+
return _asymmetric_range(data_min, data_max, margin)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _validate_and_clean_data(data: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
84
|
+
"""Validate data and remove NaN values."""
|
|
68
85
|
if len(data) == 0:
|
|
69
86
|
raise ValueError("Data array is empty")
|
|
70
87
|
|
|
71
|
-
# Remove NaN values
|
|
72
88
|
clean_data = data[~np.isnan(data)]
|
|
73
|
-
|
|
74
89
|
if len(clean_data) == 0:
|
|
75
90
|
raise ValueError("Data contains only NaN values")
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
# This corresponds to approximately 3-sigma for normal distributions
|
|
79
|
-
lower_percentile = 0.5
|
|
80
|
-
upper_percentile = 99.5
|
|
92
|
+
return clean_data
|
|
81
93
|
|
|
82
|
-
# Calculate robust statistics using percentiles
|
|
83
|
-
np.percentile(clean_data, lower_percentile)
|
|
84
|
-
np.percentile(clean_data, upper_percentile)
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
def _filter_outliers(data: NDArray[np.float64], outlier_threshold: float) -> NDArray[np.float64]:
|
|
96
|
+
"""Filter outliers using robust MAD-based z-scores.
|
|
97
|
+
|
|
98
|
+
Falls back to standard deviation when MAD = 0 (highly concentrated data).
|
|
99
|
+
"""
|
|
100
|
+
median = np.median(data)
|
|
101
|
+
mad = np.median(np.abs(data - median))
|
|
89
102
|
robust_std = 1.4826 * mad # MAD to std conversion
|
|
90
103
|
|
|
91
104
|
if robust_std > 0:
|
|
92
|
-
z_scores = np.abs(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
z_scores = np.abs(data - median) / robust_std
|
|
106
|
+
filtered: NDArray[np.float64] = data[z_scores <= outlier_threshold]
|
|
107
|
+
return filtered
|
|
108
|
+
|
|
109
|
+
# Fallback to standard deviation when MAD = 0
|
|
110
|
+
mean = np.mean(data)
|
|
111
|
+
std = np.std(data)
|
|
112
|
+
if std > 0:
|
|
113
|
+
z_scores = np.abs(data - mean) / std
|
|
114
|
+
filtered = data[z_scores <= outlier_threshold]
|
|
115
|
+
return filtered
|
|
116
|
+
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _check_clipping(
|
|
121
|
+
clean_data: NDArray[np.float64],
|
|
122
|
+
filtered_data: NDArray[np.float64],
|
|
123
|
+
clip_warning_threshold: float,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Check and warn if too many samples are clipped."""
|
|
99
126
|
clipped_fraction = 1.0 - (len(filtered_data) / len(clean_data))
|
|
100
127
|
if clipped_fraction > clip_warning_threshold:
|
|
101
128
|
import warnings
|
|
@@ -107,34 +134,49 @@ def calculate_optimal_y_range(
|
|
|
107
134
|
stacklevel=2,
|
|
108
135
|
)
|
|
109
136
|
|
|
110
|
-
# Calculate data range
|
|
111
|
-
data_min = np.min(filtered_data)
|
|
112
|
-
data_max = np.max(filtered_data)
|
|
113
|
-
data_range = data_max - data_min
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# Dense data: smaller margin (2%)
|
|
118
|
-
margin = 0.02
|
|
119
|
-
elif len(filtered_data) < 100:
|
|
120
|
-
# Sparse data: larger margin (10%)
|
|
121
|
-
margin = 0.10
|
|
122
|
-
else:
|
|
123
|
-
# Default margin (5%)
|
|
124
|
-
margin = margin_percent / 100.0
|
|
138
|
+
def _select_smart_margin(n_samples: int, margin_percent: float) -> float:
|
|
139
|
+
"""Select margin based on data density.
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
Only applies smart margin when using default value (5.0%).
|
|
142
|
+
Otherwise respects user's explicit margin_percent.
|
|
143
|
+
"""
|
|
144
|
+
# Always respect explicit user values (non-default)
|
|
145
|
+
if margin_percent != 5.0:
|
|
146
|
+
return margin_percent / 100.0
|
|
147
|
+
|
|
148
|
+
# Apply smart margin only for default value
|
|
149
|
+
if n_samples > 10000:
|
|
150
|
+
return 0.02 # Dense data: smaller margin
|
|
151
|
+
elif n_samples < 100:
|
|
152
|
+
return 0.10 # Sparse data: larger margin
|
|
153
|
+
return margin_percent / 100.0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _symmetric_range(data_min: float, data_max: float, margin: float) -> tuple[float, float]:
|
|
157
|
+
"""Calculate symmetric range for bipolar signals."""
|
|
158
|
+
max_abs = max(abs(data_min), abs(data_max))
|
|
159
|
+
|
|
160
|
+
# Handle constant data
|
|
161
|
+
if max_abs == 0:
|
|
162
|
+
return (-0.5, 0.5) # Default range for constant zero
|
|
136
163
|
|
|
137
|
-
|
|
164
|
+
margin_value = max_abs * margin
|
|
165
|
+
return (-(max_abs + margin_value), max_abs + margin_value)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _asymmetric_range(data_min: float, data_max: float, margin: float) -> tuple[float, float]:
|
|
169
|
+
"""Calculate asymmetric range."""
|
|
170
|
+
data_range = data_max - data_min
|
|
171
|
+
|
|
172
|
+
# Handle constant data (range = 0)
|
|
173
|
+
if data_range == 0:
|
|
174
|
+
# Add fixed margin for constant data
|
|
175
|
+
default_margin = 0.5 if data_min == 0 else abs(data_min) * 0.1
|
|
176
|
+
return (data_min - default_margin, data_max + default_margin)
|
|
177
|
+
|
|
178
|
+
margin_value = data_range * margin
|
|
179
|
+
return (data_min - margin_value, data_max + margin_value)
|
|
138
180
|
|
|
139
181
|
|
|
140
182
|
def calculate_optimal_x_window(
|
|
@@ -188,8 +230,10 @@ def calculate_optimal_x_window(
|
|
|
188
230
|
active_regions = rms > rms_threshold
|
|
189
231
|
|
|
190
232
|
if not np.any(active_regions):
|
|
191
|
-
# No significant activity, return full range
|
|
192
|
-
|
|
233
|
+
# No significant activity, return padded full range
|
|
234
|
+
time_range = time[-1] - time[0]
|
|
235
|
+
padding = time_range * 0.05 # 5% padding on each side
|
|
236
|
+
return (float(time[0] - padding), float(time[-1] + padding))
|
|
193
237
|
|
|
194
238
|
# Find first active region
|
|
195
239
|
active_indices = np.where(active_regions)[0]
|
|
@@ -215,21 +259,29 @@ def calculate_optimal_x_window(
|
|
|
215
259
|
|
|
216
260
|
if len(crossings) >= 4:
|
|
217
261
|
# Estimate period from crossings (two crossings per cycle)
|
|
262
|
+
# crossings[::2] already gives full periods (every other crossing)
|
|
218
263
|
periods = np.diff(crossings[::2])
|
|
219
264
|
if len(periods) > 0:
|
|
220
265
|
median_period = np.median(periods)
|
|
221
|
-
samples_per_feature = int(median_period
|
|
266
|
+
samples_per_feature = int(median_period) # Already full cycle from [::2]
|
|
222
267
|
|
|
223
268
|
# Calculate window to show target_features
|
|
224
269
|
total_samples = samples_per_feature * target_features
|
|
270
|
+
|
|
271
|
+
# Respect decimation constraint
|
|
272
|
+
max_window_samples = int(screen_width * samples_per_pixel)
|
|
273
|
+
total_samples = min(total_samples, max_window_samples)
|
|
274
|
+
|
|
225
275
|
window_start = first_active
|
|
226
276
|
window_end = min(window_start + total_samples, len(time) - 1)
|
|
227
277
|
|
|
228
278
|
return (float(time[window_start]), float(time[window_end]))
|
|
229
279
|
|
|
230
|
-
# Fallback: zoom to
|
|
280
|
+
# Fallback: zoom to respect decimation threshold
|
|
281
|
+
# Limit window to screen_width * samples_per_pixel samples
|
|
282
|
+
max_window_samples = int(screen_width * samples_per_pixel)
|
|
231
283
|
active_duration = len(active_indices)
|
|
232
|
-
zoom_samples = min(active_duration,
|
|
284
|
+
zoom_samples = min(active_duration, max_window_samples)
|
|
233
285
|
window_end = min(first_active + zoom_samples, len(time) - 1)
|
|
234
286
|
|
|
235
287
|
return (float(time[first_active]), float(time[window_end]))
|
|
@@ -650,7 +702,7 @@ def detect_interesting_regions(
|
|
|
650
702
|
edge_threshold: float | None = None,
|
|
651
703
|
glitch_sigma: float = 3.0,
|
|
652
704
|
anomaly_threshold: float = 3.0,
|
|
653
|
-
min_region_samples: int =
|
|
705
|
+
min_region_samples: int = 1,
|
|
654
706
|
max_regions: int = 10,
|
|
655
707
|
) -> list[InterestingRegion]:
|
|
656
708
|
"""Detect interesting regions in a signal for automatic zoom/focus.
|
|
@@ -664,7 +716,7 @@ def detect_interesting_regions(
|
|
|
664
716
|
edge_threshold: Edge detection threshold (default: auto from signal stddev)
|
|
665
717
|
glitch_sigma: Sigma threshold for glitch detection (default: 3.0)
|
|
666
718
|
anomaly_threshold: Threshold for anomaly detection in sigma (default: 3.0)
|
|
667
|
-
min_region_samples: Minimum samples per region (default:
|
|
719
|
+
min_region_samples: Minimum samples per region (default: 1)
|
|
668
720
|
max_regions: Maximum number of regions to return (default: 10)
|
|
669
721
|
|
|
670
722
|
Returns:
|
|
@@ -691,7 +743,6 @@ def detect_interesting_regions(
|
|
|
691
743
|
raise ValueError("min_region_samples must be >= 1")
|
|
692
744
|
|
|
693
745
|
regions: list[InterestingRegion] = []
|
|
694
|
-
1.0 / sample_rate
|
|
695
746
|
|
|
696
747
|
# 1. Edge detection using first derivative
|
|
697
748
|
edges = _detect_edges(signal, sample_rate, edge_threshold)
|
oscura/visualization/palettes.py
CHANGED
|
@@ -303,7 +303,7 @@ def show_palette(
|
|
|
303
303
|
ax.set_xticklabels(["0", "0.25", "0.5", "0.75", "1.0"])
|
|
304
304
|
ax.set_title(f"Colormap: {name}")
|
|
305
305
|
except ValueError:
|
|
306
|
-
raise ValueError(f"Unknown palette or colormap: {name}")
|
|
306
|
+
raise ValueError(f"Unknown palette or colormap: {name}")
|
|
307
307
|
|
|
308
308
|
plt.tight_layout()
|
|
309
309
|
|