oscura 0.0.1__py3-none-any.whl → 0.1.1__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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
oscura/cli/compare.py
ADDED
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
"""Oscura Compare Command implementing CLI-005.
|
|
2
|
+
|
|
3
|
+
Provides CLI for comparing two signal captures with timing, noise, and
|
|
4
|
+
spectral difference analysis.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
$ oscura compare before.wfm after.wfm
|
|
9
|
+
$ oscura compare golden.wfm measured.wfm --threshold 5 --save-report diff.html
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
import numpy as np
|
|
20
|
+
from numpy.typing import NDArray
|
|
21
|
+
from scipy import fft, signal
|
|
22
|
+
|
|
23
|
+
from oscura.cli.main import format_output
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from oscura.core.types import WaveformTrace
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("oscura.cli.compare")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command() # type: ignore[misc]
|
|
32
|
+
@click.argument("file1", type=click.Path(exists=True)) # type: ignore[misc]
|
|
33
|
+
@click.argument("file2", type=click.Path(exists=True)) # type: ignore[misc]
|
|
34
|
+
@click.option( # type: ignore[misc]
|
|
35
|
+
"--threshold",
|
|
36
|
+
type=float,
|
|
37
|
+
default=5.0,
|
|
38
|
+
help="Report differences greater than this percentage (default: 5%).",
|
|
39
|
+
)
|
|
40
|
+
@click.option( # type: ignore[misc]
|
|
41
|
+
"--output",
|
|
42
|
+
type=click.Choice(["json", "csv", "html", "table"], case_sensitive=False),
|
|
43
|
+
default="table",
|
|
44
|
+
help="Output format (default: table).",
|
|
45
|
+
)
|
|
46
|
+
@click.option( # type: ignore[misc]
|
|
47
|
+
"--save-report",
|
|
48
|
+
type=click.Path(),
|
|
49
|
+
default=None,
|
|
50
|
+
help="Save detailed HTML comparison report.",
|
|
51
|
+
)
|
|
52
|
+
@click.option( # type: ignore[misc]
|
|
53
|
+
"--align",
|
|
54
|
+
is_flag=True,
|
|
55
|
+
help="Align signals using cross-correlation before comparison.",
|
|
56
|
+
)
|
|
57
|
+
@click.pass_context # type: ignore[misc]
|
|
58
|
+
def compare(
|
|
59
|
+
ctx: click.Context,
|
|
60
|
+
file1: str,
|
|
61
|
+
file2: str,
|
|
62
|
+
threshold: float,
|
|
63
|
+
output: str,
|
|
64
|
+
save_report: str | None,
|
|
65
|
+
align: bool,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Compare two signal captures.
|
|
68
|
+
|
|
69
|
+
Analyzes differences between two waveforms including timing drift,
|
|
70
|
+
amplitude changes, noise variations, and spectral differences.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
ctx: Click context object.
|
|
74
|
+
file1: Path to first waveform file.
|
|
75
|
+
file2: Path to second waveform file.
|
|
76
|
+
threshold: Percentage threshold for reporting differences.
|
|
77
|
+
output: Output format (json, csv, html, table).
|
|
78
|
+
save_report: Path to save HTML comparison report.
|
|
79
|
+
align: Align signals using cross-correlation before comparison.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
Exception: If comparison fails or files cannot be loaded.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
|
|
86
|
+
\b
|
|
87
|
+
# Simple comparison
|
|
88
|
+
$ oscura compare before.wfm after.wfm
|
|
89
|
+
|
|
90
|
+
\b
|
|
91
|
+
# Report only significant differences (>10%)
|
|
92
|
+
$ oscura compare golden.wfm measured.wfm --threshold 10
|
|
93
|
+
|
|
94
|
+
\b
|
|
95
|
+
# Full comparison with alignment and HTML report
|
|
96
|
+
$ oscura compare reference.wfm test.wfm \\
|
|
97
|
+
--align \\
|
|
98
|
+
--save-report comparison.html
|
|
99
|
+
|
|
100
|
+
\b
|
|
101
|
+
# JSON output for automation
|
|
102
|
+
$ oscura compare before.wfm after.wfm --output json
|
|
103
|
+
"""
|
|
104
|
+
verbose = ctx.obj.get("verbose", 0)
|
|
105
|
+
|
|
106
|
+
if verbose:
|
|
107
|
+
logger.info(f"Comparing: {file1} vs {file2}")
|
|
108
|
+
logger.info(f"Threshold: {threshold}%")
|
|
109
|
+
logger.info(f"Align signals: {align}")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
# Import here to avoid circular imports
|
|
113
|
+
from oscura.loaders import load
|
|
114
|
+
|
|
115
|
+
# Load both traces
|
|
116
|
+
logger.debug(f"Loading first trace from {file1}")
|
|
117
|
+
trace1 = load(file1)
|
|
118
|
+
|
|
119
|
+
logger.debug(f"Loading second trace from {file2}")
|
|
120
|
+
trace2 = load(file2)
|
|
121
|
+
|
|
122
|
+
# Perform comparison
|
|
123
|
+
results = _perform_comparison(
|
|
124
|
+
trace1=trace1, # type: ignore[arg-type]
|
|
125
|
+
trace2=trace2, # type: ignore[arg-type]
|
|
126
|
+
threshold=threshold,
|
|
127
|
+
align_signals=align,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Add metadata
|
|
131
|
+
results["file1"] = str(Path(file1).name)
|
|
132
|
+
results["file2"] = str(Path(file2).name)
|
|
133
|
+
|
|
134
|
+
# Generate HTML report if requested
|
|
135
|
+
if save_report:
|
|
136
|
+
html_content = _generate_html_report(results, file1, file2)
|
|
137
|
+
with open(save_report, "w") as f:
|
|
138
|
+
f.write(html_content)
|
|
139
|
+
logger.info(f"Comparison report saved to {save_report}")
|
|
140
|
+
results["report_saved"] = str(save_report)
|
|
141
|
+
|
|
142
|
+
# Output results
|
|
143
|
+
formatted = format_output(results, output)
|
|
144
|
+
click.echo(formatted)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Comparison failed: {e}")
|
|
148
|
+
if verbose > 1:
|
|
149
|
+
raise
|
|
150
|
+
click.echo(f"Error: {e}", err=True)
|
|
151
|
+
ctx.exit(1)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _align_signals(
|
|
155
|
+
data1: NDArray[np.float64],
|
|
156
|
+
data2: NDArray[np.float64],
|
|
157
|
+
sample_rate: float,
|
|
158
|
+
) -> tuple[NDArray[np.float64], NDArray[np.float64], dict[str, Any]]:
|
|
159
|
+
"""Align two signals using cross-correlation.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
data1: Reference signal.
|
|
163
|
+
data2: Signal to align.
|
|
164
|
+
sample_rate: Sample rate in Hz.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Tuple of (aligned_data1, aligned_data2, alignment_info).
|
|
168
|
+
"""
|
|
169
|
+
# Use cross-correlation to find optimal alignment
|
|
170
|
+
# For efficiency, use FFT-based correlation
|
|
171
|
+
n = len(data1) + len(data2) - 1
|
|
172
|
+
n_fft = 2 ** int(np.ceil(np.log2(n))) # Next power of 2
|
|
173
|
+
|
|
174
|
+
# Compute cross-correlation using FFT
|
|
175
|
+
fft1 = fft.fft(data1, n=n_fft)
|
|
176
|
+
fft2 = fft.fft(data2, n=n_fft)
|
|
177
|
+
cross_corr = fft.ifft(fft1 * np.conj(fft2)).real
|
|
178
|
+
|
|
179
|
+
# Find peak
|
|
180
|
+
peak_idx = np.argmax(np.abs(cross_corr))
|
|
181
|
+
offset = peak_idx - n_fft if peak_idx > n_fft // 2 else peak_idx
|
|
182
|
+
|
|
183
|
+
# Compute correlation coefficient at peak
|
|
184
|
+
corr_peak = cross_corr[peak_idx] / np.sqrt(np.sum(data1**2) * np.sum(data2**2))
|
|
185
|
+
|
|
186
|
+
# Apply offset
|
|
187
|
+
if offset > 0:
|
|
188
|
+
aligned1 = data1[offset:]
|
|
189
|
+
aligned2 = data2[: len(aligned1)]
|
|
190
|
+
elif offset < 0:
|
|
191
|
+
aligned2 = data2[-offset:]
|
|
192
|
+
aligned1 = data1[: len(aligned2)]
|
|
193
|
+
else:
|
|
194
|
+
min_len = min(len(data1), len(data2))
|
|
195
|
+
aligned1 = data1[:min_len]
|
|
196
|
+
aligned2 = data2[:min_len]
|
|
197
|
+
|
|
198
|
+
# Ensure equal length
|
|
199
|
+
min_len = min(len(aligned1), len(aligned2))
|
|
200
|
+
aligned1 = aligned1[:min_len]
|
|
201
|
+
aligned2 = aligned2[:min_len]
|
|
202
|
+
|
|
203
|
+
# Calculate timing offset in ns
|
|
204
|
+
offset_time_ns = offset / sample_rate * 1e9
|
|
205
|
+
|
|
206
|
+
alignment_info = {
|
|
207
|
+
"offset_samples": int(offset),
|
|
208
|
+
"offset_time_ns": f"{offset_time_ns:.2f}",
|
|
209
|
+
"correlation_peak": f"{corr_peak:.6f}",
|
|
210
|
+
"quality": "excellent"
|
|
211
|
+
if abs(corr_peak) > 0.95
|
|
212
|
+
else "good"
|
|
213
|
+
if abs(corr_peak) > 0.8
|
|
214
|
+
else "poor",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return aligned1, aligned2, alignment_info
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _compute_timing_drift(
|
|
221
|
+
data1: NDArray[np.float64],
|
|
222
|
+
data2: NDArray[np.float64],
|
|
223
|
+
sample_rate: float,
|
|
224
|
+
) -> dict[str, Any]:
|
|
225
|
+
"""Compute timing drift between two signals using edge detection.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
data1: Reference signal.
|
|
229
|
+
data2: Comparison signal.
|
|
230
|
+
sample_rate: Sample rate in Hz.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Dictionary with timing drift metrics.
|
|
234
|
+
"""
|
|
235
|
+
# Find edges using threshold crossing
|
|
236
|
+
threshold1 = (np.max(data1) + np.min(data1)) / 2
|
|
237
|
+
threshold2 = (np.max(data2) + np.min(data2)) / 2
|
|
238
|
+
|
|
239
|
+
# Rising edges
|
|
240
|
+
edges1 = np.where(np.diff(data1 > threshold1).astype(int) > 0)[0]
|
|
241
|
+
edges2 = np.where(np.diff(data2 > threshold2).astype(int) > 0)[0]
|
|
242
|
+
|
|
243
|
+
if len(edges1) < 2 or len(edges2) < 2:
|
|
244
|
+
return {
|
|
245
|
+
"value_ns": "N/A",
|
|
246
|
+
"percentage": "N/A",
|
|
247
|
+
"significant": False,
|
|
248
|
+
"note": "Insufficient edges for timing analysis",
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Match edges and compute timing differences
|
|
252
|
+
# Use nearest-neighbor matching
|
|
253
|
+
timing_diffs = []
|
|
254
|
+
for e1 in edges1[: min(100, len(edges1))]: # Limit to first 100 edges
|
|
255
|
+
nearest_idx = np.argmin(np.abs(edges2 - e1))
|
|
256
|
+
if abs(edges2[nearest_idx] - e1) < sample_rate * 0.1: # Within 100ms
|
|
257
|
+
timing_diffs.append((edges2[nearest_idx] - e1) / sample_rate)
|
|
258
|
+
|
|
259
|
+
if len(timing_diffs) < 3:
|
|
260
|
+
return {
|
|
261
|
+
"value_ns": "N/A",
|
|
262
|
+
"percentage": "N/A",
|
|
263
|
+
"significant": False,
|
|
264
|
+
"note": "Could not match sufficient edges",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
timing_diffs_arr = np.array(timing_diffs)
|
|
268
|
+
mean_drift_ns = float(np.mean(timing_diffs_arr)) * 1e9
|
|
269
|
+
std_drift_ns = float(np.std(timing_diffs_arr)) * 1e9
|
|
270
|
+
|
|
271
|
+
# Calculate period for percentage
|
|
272
|
+
periods1 = np.diff(edges1) / sample_rate
|
|
273
|
+
mean_period = float(np.mean(periods1)) if len(periods1) > 0 else 1.0
|
|
274
|
+
mean_diff = float(np.mean(timing_diffs_arr))
|
|
275
|
+
drift_percent = abs(mean_diff / mean_period * 100) if mean_period > 0 else 0.0
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"value_ns": f"{mean_drift_ns:.2f}",
|
|
279
|
+
"std_ns": f"{std_drift_ns:.2f}",
|
|
280
|
+
"percentage": f"{drift_percent:.4f}%",
|
|
281
|
+
"edges_analyzed": len(timing_diffs_arr),
|
|
282
|
+
"significant": bool(drift_percent > 0.1), # >0.1% is significant
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _compute_spectral_difference(
|
|
287
|
+
data1: NDArray[np.float64],
|
|
288
|
+
data2: NDArray[np.float64],
|
|
289
|
+
sample_rate: float,
|
|
290
|
+
threshold: float,
|
|
291
|
+
) -> dict[str, Any]:
|
|
292
|
+
"""Compute spectral differences between two signals.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
data1: Reference signal.
|
|
296
|
+
data2: Comparison signal.
|
|
297
|
+
sample_rate: Sample rate in Hz.
|
|
298
|
+
threshold: Percentage threshold for significance.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Dictionary with spectral comparison metrics.
|
|
302
|
+
"""
|
|
303
|
+
# Compute FFT for both signals
|
|
304
|
+
n = len(data1)
|
|
305
|
+
# Use zero-padding for better frequency resolution
|
|
306
|
+
# Pad to at least 10x the original length for good interpolation
|
|
307
|
+
n_fft = 2 ** int(np.ceil(np.log2(n * 10)))
|
|
308
|
+
|
|
309
|
+
# Apply window to reduce spectral leakage
|
|
310
|
+
window = signal.windows.hann(n)
|
|
311
|
+
windowed1 = data1 * window
|
|
312
|
+
windowed2 = data2 * window
|
|
313
|
+
|
|
314
|
+
# Compute magnitude spectra
|
|
315
|
+
fft1 = np.abs(fft.rfft(windowed1, n=n_fft))
|
|
316
|
+
fft2 = np.abs(fft.rfft(windowed2, n=n_fft))
|
|
317
|
+
freqs = fft.rfftfreq(n_fft, d=1 / sample_rate)
|
|
318
|
+
|
|
319
|
+
# Avoid division by zero
|
|
320
|
+
fft1 = np.maximum(fft1, 1e-12)
|
|
321
|
+
fft2 = np.maximum(fft2, 1e-12)
|
|
322
|
+
|
|
323
|
+
# Find dominant frequencies
|
|
324
|
+
peak1_idx = np.argmax(fft1[1:]) + 1 # Skip DC
|
|
325
|
+
peak2_idx = np.argmax(fft2[1:]) + 1
|
|
326
|
+
dominant_freq1 = freqs[peak1_idx]
|
|
327
|
+
dominant_freq2 = freqs[peak2_idx]
|
|
328
|
+
freq_diff = abs(dominant_freq2 - dominant_freq1)
|
|
329
|
+
freq_diff_percent = freq_diff / dominant_freq1 * 100 if dominant_freq1 > 0 else 0
|
|
330
|
+
|
|
331
|
+
# Compute magnitude differences in dB
|
|
332
|
+
db_diff = 20 * np.log10(fft2 / fft1)
|
|
333
|
+
max_db_diff = np.max(np.abs(db_diff))
|
|
334
|
+
mean_db_diff = np.mean(np.abs(db_diff))
|
|
335
|
+
|
|
336
|
+
# Check for harmonic changes
|
|
337
|
+
# Find first 5 harmonics of dominant frequency
|
|
338
|
+
harmonic_changes = []
|
|
339
|
+
for h in range(1, 6):
|
|
340
|
+
harm_freq = dominant_freq1 * h
|
|
341
|
+
harm_idx = int(harm_freq / (sample_rate / n_fft))
|
|
342
|
+
if harm_idx < len(fft1):
|
|
343
|
+
harm_db_diff = 20 * np.log10(fft2[harm_idx] / fft1[harm_idx])
|
|
344
|
+
harmonic_changes.append(
|
|
345
|
+
{
|
|
346
|
+
"harmonic": h,
|
|
347
|
+
"frequency_hz": f"{harm_freq:.1f}",
|
|
348
|
+
"change_db": f"{harm_db_diff:.2f}",
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
"dominant_freq1_hz": f"{dominant_freq1:.1f}",
|
|
354
|
+
"dominant_freq2_hz": f"{dominant_freq2:.1f}",
|
|
355
|
+
"freq_diff_hz": f"{freq_diff:.2f}",
|
|
356
|
+
"freq_diff_percent": f"{freq_diff_percent:.4f}%",
|
|
357
|
+
"max_magnitude_diff_db": f"{max_db_diff:.2f}",
|
|
358
|
+
"mean_magnitude_diff_db": f"{mean_db_diff:.2f}",
|
|
359
|
+
"harmonic_changes": harmonic_changes[:3], # First 3 harmonics
|
|
360
|
+
"significant": bool(freq_diff_percent > threshold or max_db_diff > 6.0), # 6dB = 2x power
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _perform_comparison(
|
|
365
|
+
trace1: WaveformTrace,
|
|
366
|
+
trace2: WaveformTrace,
|
|
367
|
+
threshold: float,
|
|
368
|
+
align_signals: bool,
|
|
369
|
+
) -> dict[str, Any]:
|
|
370
|
+
"""Perform comprehensive signal comparison analysis.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
trace1: First trace (reference).
|
|
374
|
+
trace2: Second trace (comparison).
|
|
375
|
+
threshold: Percentage threshold for reporting differences.
|
|
376
|
+
align_signals: Whether to align signals using cross-correlation.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Dictionary of comparison results.
|
|
380
|
+
"""
|
|
381
|
+
sample_rate1 = trace1.metadata.sample_rate
|
|
382
|
+
sample_rate2 = trace2.metadata.sample_rate
|
|
383
|
+
|
|
384
|
+
# Check sample rate compatibility
|
|
385
|
+
rate_mismatch = False
|
|
386
|
+
if sample_rate1 != sample_rate2:
|
|
387
|
+
logger.warning(f"Sample rates differ: {sample_rate1:.2e} vs {sample_rate2:.2e} Hz")
|
|
388
|
+
rate_mismatch = True
|
|
389
|
+
|
|
390
|
+
# Initialize results
|
|
391
|
+
results: dict[str, Any] = {
|
|
392
|
+
"threshold_percent": threshold,
|
|
393
|
+
"aligned": align_signals,
|
|
394
|
+
"sample_rate_mismatch": rate_mismatch,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
# Basic statistics for each trace
|
|
398
|
+
results["trace1_stats"] = {
|
|
399
|
+
"samples": len(trace1.data),
|
|
400
|
+
"sample_rate": f"{sample_rate1 / 1e6:.2f} MHz",
|
|
401
|
+
"duration_ms": f"{len(trace1.data) / sample_rate1 * 1e3:.3f} ms",
|
|
402
|
+
"mean": f"{float(trace1.data.mean()):.6f} V",
|
|
403
|
+
"rms": f"{float(np.sqrt((trace1.data**2).mean())):.6f} V",
|
|
404
|
+
"peak_to_peak": f"{float(trace1.data.max() - trace1.data.min()):.6f} V",
|
|
405
|
+
"min": f"{float(trace1.data.min()):.6f} V",
|
|
406
|
+
"max": f"{float(trace1.data.max()):.6f} V",
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
results["trace2_stats"] = {
|
|
410
|
+
"samples": len(trace2.data),
|
|
411
|
+
"sample_rate": f"{sample_rate2 / 1e6:.2f} MHz",
|
|
412
|
+
"duration_ms": f"{len(trace2.data) / sample_rate2 * 1e3:.3f} ms",
|
|
413
|
+
"mean": f"{float(trace2.data.mean()):.6f} V",
|
|
414
|
+
"rms": f"{float(np.sqrt((trace2.data**2).mean())):.6f} V",
|
|
415
|
+
"peak_to_peak": f"{float(trace2.data.max() - trace2.data.min()):.6f} V",
|
|
416
|
+
"min": f"{float(trace2.data.min()):.6f} V",
|
|
417
|
+
"max": f"{float(trace2.data.max()):.6f} V",
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Prepare data for comparison
|
|
421
|
+
data1 = trace1.data.astype(np.float64)
|
|
422
|
+
data2 = trace2.data.astype(np.float64)
|
|
423
|
+
|
|
424
|
+
# Signal alignment using cross-correlation
|
|
425
|
+
if align_signals:
|
|
426
|
+
data1, data2, alignment_info = _align_signals(data1, data2, sample_rate1)
|
|
427
|
+
results["alignment"] = alignment_info
|
|
428
|
+
else:
|
|
429
|
+
# Ensure equal length
|
|
430
|
+
min_len = min(len(data1), len(data2))
|
|
431
|
+
data1 = data1[:min_len]
|
|
432
|
+
data2 = data2[:min_len]
|
|
433
|
+
|
|
434
|
+
# Timing drift analysis
|
|
435
|
+
results["timing_drift"] = _compute_timing_drift(data1, data2, sample_rate1)
|
|
436
|
+
|
|
437
|
+
# Amplitude difference analysis
|
|
438
|
+
diff = data2 - data1
|
|
439
|
+
abs_diff = np.abs(diff)
|
|
440
|
+
|
|
441
|
+
mean1 = data1.mean()
|
|
442
|
+
mean_diff = float(diff.mean())
|
|
443
|
+
mean_diff_percent = abs(mean_diff / mean1 * 100) if mean1 != 0 else 0
|
|
444
|
+
max_diff = float(abs_diff.max())
|
|
445
|
+
rms_diff = float(np.sqrt((diff**2).mean()))
|
|
446
|
+
|
|
447
|
+
# Compare to reference RMS
|
|
448
|
+
rms1 = float(np.sqrt((data1**2).mean()))
|
|
449
|
+
rms_diff_percent = rms_diff / rms1 * 100 if rms1 > 0 else 0
|
|
450
|
+
|
|
451
|
+
results["amplitude_difference"] = {
|
|
452
|
+
"mean_diff_v": f"{mean_diff:.6f}",
|
|
453
|
+
"mean_diff_percent": f"{mean_diff_percent:.2f}%",
|
|
454
|
+
"max_diff_v": f"{max_diff:.6f}",
|
|
455
|
+
"rms_diff_v": f"{rms_diff:.6f}",
|
|
456
|
+
"rms_diff_percent": f"{rms_diff_percent:.2f}%",
|
|
457
|
+
"significant": bool(mean_diff_percent > threshold),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# Noise analysis
|
|
461
|
+
# Use high-pass filter to extract noise component
|
|
462
|
+
nyquist = sample_rate1 / 2
|
|
463
|
+
cutoff = min(1000, nyquist * 0.9) # 1kHz or 90% of Nyquist
|
|
464
|
+
b, a = signal.butter(4, cutoff / nyquist, btype="high")
|
|
465
|
+
|
|
466
|
+
try:
|
|
467
|
+
noise1 = signal.filtfilt(b, a, data1)
|
|
468
|
+
noise2 = signal.filtfilt(b, a, data2)
|
|
469
|
+
noise_std1 = float(np.std(noise1))
|
|
470
|
+
noise_std2 = float(np.std(noise2))
|
|
471
|
+
except Exception:
|
|
472
|
+
# Fallback to simple std if filter fails
|
|
473
|
+
noise_std1 = float(np.std(data1))
|
|
474
|
+
noise_std2 = float(np.std(data2))
|
|
475
|
+
|
|
476
|
+
noise_change = ((noise_std2 - noise_std1) / noise_std1 * 100) if noise_std1 != 0 else 0
|
|
477
|
+
|
|
478
|
+
results["noise_change"] = {
|
|
479
|
+
"noise1_v": f"{noise_std1:.6f}",
|
|
480
|
+
"noise2_v": f"{noise_std2:.6f}",
|
|
481
|
+
"change_percent": f"{noise_change:.2f}%",
|
|
482
|
+
"significant": bool(abs(noise_change) > threshold),
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Correlation coefficient
|
|
486
|
+
if len(data1) > 1 and len(data2) > 1:
|
|
487
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
488
|
+
correlation = float(np.corrcoef(data1, data2)[0, 1])
|
|
489
|
+
results["correlation"] = {
|
|
490
|
+
"coefficient": f"{correlation:.6f}",
|
|
491
|
+
"quality": "excellent"
|
|
492
|
+
if correlation > 0.99
|
|
493
|
+
else "good"
|
|
494
|
+
if correlation > 0.95
|
|
495
|
+
else "fair"
|
|
496
|
+
if correlation > 0.8
|
|
497
|
+
else "poor",
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
# Spectral differences
|
|
501
|
+
results["spectral_difference"] = _compute_spectral_difference(
|
|
502
|
+
data1, data2, sample_rate1, threshold
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Overall assessment
|
|
506
|
+
significant_count = sum(
|
|
507
|
+
[
|
|
508
|
+
results.get("amplitude_difference", {}).get("significant", False),
|
|
509
|
+
results.get("noise_change", {}).get("significant", False),
|
|
510
|
+
results.get("timing_drift", {}).get("significant", False),
|
|
511
|
+
results.get("spectral_difference", {}).get("significant", False),
|
|
512
|
+
]
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if significant_count == 0:
|
|
516
|
+
match_quality = "excellent"
|
|
517
|
+
elif significant_count == 1:
|
|
518
|
+
match_quality = "good"
|
|
519
|
+
elif significant_count == 2:
|
|
520
|
+
match_quality = "fair"
|
|
521
|
+
else:
|
|
522
|
+
match_quality = "poor"
|
|
523
|
+
|
|
524
|
+
results["summary"] = {
|
|
525
|
+
"significant_differences": significant_count,
|
|
526
|
+
"overall_match": match_quality,
|
|
527
|
+
"categories_with_differences": [
|
|
528
|
+
cat
|
|
529
|
+
for cat in [
|
|
530
|
+
"amplitude_difference",
|
|
531
|
+
"noise_change",
|
|
532
|
+
"timing_drift",
|
|
533
|
+
"spectral_difference",
|
|
534
|
+
]
|
|
535
|
+
if results.get(cat, {}).get("significant", False)
|
|
536
|
+
],
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return results
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _generate_html_report(
|
|
543
|
+
results: dict[str, Any],
|
|
544
|
+
file1: str,
|
|
545
|
+
file2: str,
|
|
546
|
+
) -> str:
|
|
547
|
+
"""Generate HTML comparison report.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
results: Comparison results dictionary.
|
|
551
|
+
file1: First file path.
|
|
552
|
+
file2: Second file path.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
HTML content as string.
|
|
556
|
+
"""
|
|
557
|
+
# Get summary info
|
|
558
|
+
summary = results.get("summary", {})
|
|
559
|
+
match_quality = summary.get("overall_match", "unknown")
|
|
560
|
+
significant_diffs = summary.get("significant_differences", 0)
|
|
561
|
+
|
|
562
|
+
# Color based on quality
|
|
563
|
+
quality_colors = {
|
|
564
|
+
"excellent": "#28a745",
|
|
565
|
+
"good": "#17a2b8",
|
|
566
|
+
"fair": "#ffc107",
|
|
567
|
+
"poor": "#dc3545",
|
|
568
|
+
}
|
|
569
|
+
quality_color = quality_colors.get(match_quality, "#6c757d")
|
|
570
|
+
|
|
571
|
+
html = f"""<!DOCTYPE html>
|
|
572
|
+
<html lang="en">
|
|
573
|
+
<head>
|
|
574
|
+
<meta charset="UTF-8">
|
|
575
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
576
|
+
<title>Oscura Signal Comparison Report</title>
|
|
577
|
+
<style>
|
|
578
|
+
body {{
|
|
579
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
580
|
+
max-width: 1200px;
|
|
581
|
+
margin: 0 auto;
|
|
582
|
+
padding: 20px;
|
|
583
|
+
background: #f5f5f5;
|
|
584
|
+
}}
|
|
585
|
+
.header {{
|
|
586
|
+
background: #2c3e50;
|
|
587
|
+
color: white;
|
|
588
|
+
padding: 20px;
|
|
589
|
+
border-radius: 8px 8px 0 0;
|
|
590
|
+
}}
|
|
591
|
+
.content {{
|
|
592
|
+
background: white;
|
|
593
|
+
padding: 20px;
|
|
594
|
+
border-radius: 0 0 8px 8px;
|
|
595
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
596
|
+
}}
|
|
597
|
+
.summary {{
|
|
598
|
+
background: {quality_color};
|
|
599
|
+
color: white;
|
|
600
|
+
padding: 15px;
|
|
601
|
+
border-radius: 8px;
|
|
602
|
+
margin: 20px 0;
|
|
603
|
+
}}
|
|
604
|
+
.section {{
|
|
605
|
+
margin: 20px 0;
|
|
606
|
+
padding: 15px;
|
|
607
|
+
border: 1px solid #e0e0e0;
|
|
608
|
+
border-radius: 8px;
|
|
609
|
+
}}
|
|
610
|
+
.section h3 {{
|
|
611
|
+
margin-top: 0;
|
|
612
|
+
color: #2c3e50;
|
|
613
|
+
}}
|
|
614
|
+
table {{
|
|
615
|
+
width: 100%;
|
|
616
|
+
border-collapse: collapse;
|
|
617
|
+
}}
|
|
618
|
+
th, td {{
|
|
619
|
+
padding: 10px;
|
|
620
|
+
text-align: left;
|
|
621
|
+
border-bottom: 1px solid #e0e0e0;
|
|
622
|
+
}}
|
|
623
|
+
th {{
|
|
624
|
+
background: #f8f9fa;
|
|
625
|
+
}}
|
|
626
|
+
.significant {{
|
|
627
|
+
color: #dc3545;
|
|
628
|
+
font-weight: bold;
|
|
629
|
+
}}
|
|
630
|
+
.ok {{
|
|
631
|
+
color: #28a745;
|
|
632
|
+
}}
|
|
633
|
+
</style>
|
|
634
|
+
</head>
|
|
635
|
+
<body>
|
|
636
|
+
<div class="header">
|
|
637
|
+
<h1>Oscura Signal Comparison Report</h1>
|
|
638
|
+
<p>File 1: {Path(file1).name}</p>
|
|
639
|
+
<p>File 2: {Path(file2).name}</p>
|
|
640
|
+
</div>
|
|
641
|
+
|
|
642
|
+
<div class="content">
|
|
643
|
+
<div class="summary">
|
|
644
|
+
<h2>Overall Match: {match_quality.upper()}</h2>
|
|
645
|
+
<p>{significant_diffs} significant difference(s) detected</p>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div class="section">
|
|
649
|
+
<h3>Trace Statistics</h3>
|
|
650
|
+
<table>
|
|
651
|
+
<tr>
|
|
652
|
+
<th>Metric</th>
|
|
653
|
+
<th>Trace 1</th>
|
|
654
|
+
<th>Trace 2</th>
|
|
655
|
+
</tr>
|
|
656
|
+
<tr>
|
|
657
|
+
<td>Samples</td>
|
|
658
|
+
<td>{results.get("trace1_stats", {}).get("samples", "N/A")}</td>
|
|
659
|
+
<td>{results.get("trace2_stats", {}).get("samples", "N/A")}</td>
|
|
660
|
+
</tr>
|
|
661
|
+
<tr>
|
|
662
|
+
<td>Sample Rate</td>
|
|
663
|
+
<td>{results.get("trace1_stats", {}).get("sample_rate", "N/A")}</td>
|
|
664
|
+
<td>{results.get("trace2_stats", {}).get("sample_rate", "N/A")}</td>
|
|
665
|
+
</tr>
|
|
666
|
+
<tr>
|
|
667
|
+
<td>Mean</td>
|
|
668
|
+
<td>{results.get("trace1_stats", {}).get("mean", "N/A")}</td>
|
|
669
|
+
<td>{results.get("trace2_stats", {}).get("mean", "N/A")}</td>
|
|
670
|
+
</tr>
|
|
671
|
+
<tr>
|
|
672
|
+
<td>RMS</td>
|
|
673
|
+
<td>{results.get("trace1_stats", {}).get("rms", "N/A")}</td>
|
|
674
|
+
<td>{results.get("trace2_stats", {}).get("rms", "N/A")}</td>
|
|
675
|
+
</tr>
|
|
676
|
+
<tr>
|
|
677
|
+
<td>Peak-to-Peak</td>
|
|
678
|
+
<td>{results.get("trace1_stats", {}).get("peak_to_peak", "N/A")}</td>
|
|
679
|
+
<td>{results.get("trace2_stats", {}).get("peak_to_peak", "N/A")}</td>
|
|
680
|
+
</tr>
|
|
681
|
+
</table>
|
|
682
|
+
</div>
|
|
683
|
+
|
|
684
|
+
<div class="section">
|
|
685
|
+
<h3>Amplitude Difference</h3>
|
|
686
|
+
<table>
|
|
687
|
+
<tr>
|
|
688
|
+
<td>Mean Difference</td>
|
|
689
|
+
<td>{results.get("amplitude_difference", {}).get("mean_diff_v", "N/A")}</td>
|
|
690
|
+
<td class="{"significant" if results.get("amplitude_difference", {}).get("significant") else "ok"}">
|
|
691
|
+
{results.get("amplitude_difference", {}).get("mean_diff_percent", "N/A")}
|
|
692
|
+
</td>
|
|
693
|
+
</tr>
|
|
694
|
+
<tr>
|
|
695
|
+
<td>RMS Difference</td>
|
|
696
|
+
<td>{results.get("amplitude_difference", {}).get("rms_diff_v", "N/A")}</td>
|
|
697
|
+
<td>{results.get("amplitude_difference", {}).get("rms_diff_percent", "N/A")}</td>
|
|
698
|
+
</tr>
|
|
699
|
+
<tr>
|
|
700
|
+
<td>Max Difference</td>
|
|
701
|
+
<td colspan="2">{results.get("amplitude_difference", {}).get("max_diff_v", "N/A")}</td>
|
|
702
|
+
</tr>
|
|
703
|
+
</table>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<div class="section">
|
|
707
|
+
<h3>Timing Drift</h3>
|
|
708
|
+
<table>
|
|
709
|
+
<tr>
|
|
710
|
+
<td>Mean Drift</td>
|
|
711
|
+
<td>{results.get("timing_drift", {}).get("value_ns", "N/A")} ns</td>
|
|
712
|
+
<td class="{"significant" if results.get("timing_drift", {}).get("significant") else "ok"}">
|
|
713
|
+
{results.get("timing_drift", {}).get("percentage", "N/A")}
|
|
714
|
+
</td>
|
|
715
|
+
</tr>
|
|
716
|
+
</table>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<div class="section">
|
|
720
|
+
<h3>Noise Change</h3>
|
|
721
|
+
<table>
|
|
722
|
+
<tr>
|
|
723
|
+
<td>Trace 1 Noise</td>
|
|
724
|
+
<td>{results.get("noise_change", {}).get("noise1_v", "N/A")}</td>
|
|
725
|
+
</tr>
|
|
726
|
+
<tr>
|
|
727
|
+
<td>Trace 2 Noise</td>
|
|
728
|
+
<td>{results.get("noise_change", {}).get("noise2_v", "N/A")}</td>
|
|
729
|
+
</tr>
|
|
730
|
+
<tr>
|
|
731
|
+
<td>Change</td>
|
|
732
|
+
<td class="{"significant" if results.get("noise_change", {}).get("significant") else "ok"}">
|
|
733
|
+
{results.get("noise_change", {}).get("change_percent", "N/A")}
|
|
734
|
+
</td>
|
|
735
|
+
</tr>
|
|
736
|
+
</table>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<div class="section">
|
|
740
|
+
<h3>Spectral Difference</h3>
|
|
741
|
+
<table>
|
|
742
|
+
<tr>
|
|
743
|
+
<td>Dominant Frequency 1</td>
|
|
744
|
+
<td>{results.get("spectral_difference", {}).get("dominant_freq1_hz", "N/A")} Hz</td>
|
|
745
|
+
</tr>
|
|
746
|
+
<tr>
|
|
747
|
+
<td>Dominant Frequency 2</td>
|
|
748
|
+
<td>{results.get("spectral_difference", {}).get("dominant_freq2_hz", "N/A")} Hz</td>
|
|
749
|
+
</tr>
|
|
750
|
+
<tr>
|
|
751
|
+
<td>Frequency Difference</td>
|
|
752
|
+
<td class="{"significant" if results.get("spectral_difference", {}).get("significant") else "ok"}">
|
|
753
|
+
{results.get("spectral_difference", {}).get("freq_diff_percent", "N/A")}
|
|
754
|
+
</td>
|
|
755
|
+
</tr>
|
|
756
|
+
<tr>
|
|
757
|
+
<td>Max Magnitude Difference</td>
|
|
758
|
+
<td>{results.get("spectral_difference", {}).get("max_magnitude_diff_db", "N/A")} dB</td>
|
|
759
|
+
</tr>
|
|
760
|
+
</table>
|
|
761
|
+
</div>
|
|
762
|
+
|
|
763
|
+
<div class="section">
|
|
764
|
+
<h3>Correlation</h3>
|
|
765
|
+
<p>Coefficient: {results.get("correlation", {}).get("coefficient", "N/A")}</p>
|
|
766
|
+
<p>Quality: {results.get("correlation", {}).get("quality", "N/A")}</p>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<footer style="margin-top: 30px; text-align: center; color: #6c757d;">
|
|
770
|
+
<p>Generated by Oscura - Signal Analysis Toolkit</p>
|
|
771
|
+
</footer>
|
|
772
|
+
</div>
|
|
773
|
+
</body>
|
|
774
|
+
</html>"""
|
|
775
|
+
return html
|