oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/__init__.py +0 -48
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/extraction.py +0 -195
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/__init__.py +1 -22
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +2763 -0
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/core/schemas/bus_configuration.json +322 -0
- oscura/core/schemas/device_mapping.json +182 -0
- oscura/core/schemas/packet_format.json +418 -0
- oscura/core/schemas/protocol_definition.json +363 -0
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -20
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/README.md +15 -15
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/README.md +7 -7
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +171 -63
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -7
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/reporting/templates/index.md +13 -13
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/autodetect.py +1 -5
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +11 -3
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.6.0.dist-info/METADATA +643 -0
- oscura-0.6.0.dist-info/RECORD +590 -0
- oscura/analyzers/digital/ic_database.py +0 -498
- oscura/analyzers/digital/timing_paths.py +0 -339
- oscura/analyzers/digital/vintage.py +0 -377
- oscura/analyzers/digital/vintage_result.py +0 -148
- oscura/analyzers/protocols/parallel_bus.py +0 -449
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/export/wavedrom.py +0 -430
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -338
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/exporters/vintage_logic_csv.py +0 -247
- oscura/reporting/vintage_logic_report.py +0 -523
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/visualization/digital_advanced.py +0 -718
- oscura/visualization/figure_manager.py +0 -156
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.0.dist-info/METADATA +0 -407
- oscura-0.5.0.dist-info/RECORD +0 -486
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
oscura/exporters/json_export.py
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
"""JSON export functionality.
|
|
2
|
-
|
|
3
|
-
This module provides measurement results export to JSON format.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
Example:
|
|
7
|
-
>>> from oscura.exporters.json_export import export_json
|
|
8
|
-
>>> export_json(measurements, "results.json")
|
|
9
|
-
|
|
10
|
-
References:
|
|
11
|
-
RFC 8259 (JSON format)
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from __future__ import annotations
|
|
15
|
-
|
|
16
|
-
import json
|
|
17
|
-
import math
|
|
18
|
-
from dataclasses import asdict, is_dataclass
|
|
19
|
-
from datetime import datetime
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
from typing import Any
|
|
22
|
-
|
|
23
|
-
import numpy as np
|
|
24
|
-
|
|
25
|
-
from oscura.core.types import DigitalTrace, TraceMetadata, WaveformTrace
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class OscuraJSONEncoder(json.JSONEncoder):
|
|
29
|
-
"""JSON encoder with numpy, datetime, and Oscura object support."""
|
|
30
|
-
|
|
31
|
-
def default(self, obj: Any) -> Any:
|
|
32
|
-
if isinstance(obj, WaveformTrace):
|
|
33
|
-
return {
|
|
34
|
-
"_type": "WaveformTrace",
|
|
35
|
-
"data": obj.data.tolist(),
|
|
36
|
-
"metadata": self.default(obj.metadata),
|
|
37
|
-
}
|
|
38
|
-
if isinstance(obj, DigitalTrace):
|
|
39
|
-
return {
|
|
40
|
-
"_type": "DigitalTrace",
|
|
41
|
-
"data": obj.data.tolist(),
|
|
42
|
-
"metadata": self.default(obj.metadata),
|
|
43
|
-
"edges": obj.edges,
|
|
44
|
-
}
|
|
45
|
-
if isinstance(obj, TraceMetadata):
|
|
46
|
-
return {
|
|
47
|
-
"_type": "TraceMetadata",
|
|
48
|
-
"sample_rate": obj.sample_rate,
|
|
49
|
-
"time_base": obj.time_base,
|
|
50
|
-
"vertical_scale": obj.vertical_scale,
|
|
51
|
-
"vertical_offset": obj.vertical_offset,
|
|
52
|
-
"acquisition_time": obj.acquisition_time.isoformat()
|
|
53
|
-
if obj.acquisition_time
|
|
54
|
-
else None,
|
|
55
|
-
"trigger_info": obj.trigger_info,
|
|
56
|
-
"source_file": obj.source_file,
|
|
57
|
-
"channel_name": obj.channel_name,
|
|
58
|
-
}
|
|
59
|
-
if isinstance(obj, np.ndarray):
|
|
60
|
-
return obj.tolist()
|
|
61
|
-
if isinstance(obj, np.integer | np.floating):
|
|
62
|
-
val = float(obj)
|
|
63
|
-
# Handle Infinity and NaN - convert to null for JSON compliance (RFC 8259)
|
|
64
|
-
if math.isinf(val) or math.isnan(val):
|
|
65
|
-
return None
|
|
66
|
-
return val
|
|
67
|
-
if isinstance(obj, np.bool_):
|
|
68
|
-
return bool(obj)
|
|
69
|
-
if isinstance(obj, float):
|
|
70
|
-
# Also handle Python float inf/nan
|
|
71
|
-
if math.isinf(obj) or math.isnan(obj):
|
|
72
|
-
return None
|
|
73
|
-
return obj
|
|
74
|
-
if isinstance(obj, datetime):
|
|
75
|
-
return obj.isoformat()
|
|
76
|
-
if isinstance(obj, complex):
|
|
77
|
-
# Handle complex with inf/nan components
|
|
78
|
-
if (
|
|
79
|
-
math.isinf(obj.real)
|
|
80
|
-
or math.isnan(obj.real)
|
|
81
|
-
or math.isinf(obj.imag)
|
|
82
|
-
or math.isnan(obj.imag)
|
|
83
|
-
):
|
|
84
|
-
return None
|
|
85
|
-
return {"real": obj.real, "imag": obj.imag}
|
|
86
|
-
if isinstance(obj, bytes):
|
|
87
|
-
return obj.hex()
|
|
88
|
-
if is_dataclass(obj):
|
|
89
|
-
# Convert dataclasses to dict, then recursively encode
|
|
90
|
-
return asdict(obj) # type: ignore[arg-type]
|
|
91
|
-
return super().default(obj)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def export_json(
|
|
95
|
-
data: WaveformTrace | DigitalTrace | dict[str, Any] | list[Any],
|
|
96
|
-
path: str | Path,
|
|
97
|
-
*,
|
|
98
|
-
pretty: bool = True,
|
|
99
|
-
include_metadata: bool = True,
|
|
100
|
-
compress: bool = False,
|
|
101
|
-
) -> None:
|
|
102
|
-
"""Export data to JSON format.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
data: Data to export. Can be:
|
|
106
|
-
- WaveformTrace or DigitalTrace (full trace with metadata)
|
|
107
|
-
- Dictionary of measurements or data
|
|
108
|
-
- List of data
|
|
109
|
-
path: Output file path.
|
|
110
|
-
pretty: Use pretty printing with indentation.
|
|
111
|
-
include_metadata: Include export metadata.
|
|
112
|
-
compress: Compress output (save as .json.gz).
|
|
113
|
-
|
|
114
|
-
Example:
|
|
115
|
-
>>> results = measure(trace)
|
|
116
|
-
>>> export_json(results, "measurements.json")
|
|
117
|
-
>>> export_json(trace, "waveform.json", pretty=True)
|
|
118
|
-
>>> export_json(trace, "waveform.json.gz", compress=True)
|
|
119
|
-
|
|
120
|
-
References:
|
|
121
|
-
EXP-003
|
|
122
|
-
"""
|
|
123
|
-
path = Path(path)
|
|
124
|
-
|
|
125
|
-
output: dict[str, Any] = {}
|
|
126
|
-
|
|
127
|
-
if include_metadata:
|
|
128
|
-
output["_metadata"] = {
|
|
129
|
-
"format": "oscura_json",
|
|
130
|
-
"version": "1.0",
|
|
131
|
-
"exported_at": datetime.now().isoformat(),
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
output["data"] = data
|
|
135
|
-
|
|
136
|
-
# Sanitize to handle inf/nan in nested dictionaries (Python float inf/nan)
|
|
137
|
-
# are handled directly by json encoder before calling default()
|
|
138
|
-
from oscura.reporting.output import _sanitize_for_serialization
|
|
139
|
-
|
|
140
|
-
output = _sanitize_for_serialization(output)
|
|
141
|
-
|
|
142
|
-
# Serialize to JSON string
|
|
143
|
-
if pretty:
|
|
144
|
-
json_str = json.dumps(output, cls=OscuraJSONEncoder, indent=2)
|
|
145
|
-
else:
|
|
146
|
-
json_str = json.dumps(output, cls=OscuraJSONEncoder)
|
|
147
|
-
|
|
148
|
-
# Write to file (with optional compression)
|
|
149
|
-
if compress:
|
|
150
|
-
import gzip
|
|
151
|
-
|
|
152
|
-
# Ensure .gz extension
|
|
153
|
-
if not str(path).endswith(".gz"):
|
|
154
|
-
path = path.with_suffix(path.suffix + ".gz")
|
|
155
|
-
|
|
156
|
-
with gzip.open(path, "wt", encoding="utf-8") as f:
|
|
157
|
-
f.write(json_str)
|
|
158
|
-
else:
|
|
159
|
-
with open(path, "w", encoding="utf-8") as f:
|
|
160
|
-
f.write(json_str)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def export_measurements(
|
|
164
|
-
measurements: dict[str, Any],
|
|
165
|
-
path: str | Path,
|
|
166
|
-
*,
|
|
167
|
-
trace_info: dict[str, Any] | None = None,
|
|
168
|
-
pretty: bool = True,
|
|
169
|
-
) -> None:
|
|
170
|
-
"""Export measurement results to JSON.
|
|
171
|
-
|
|
172
|
-
Specialized function for measurement export with trace info.
|
|
173
|
-
|
|
174
|
-
Args:
|
|
175
|
-
measurements: Dictionary of measurements.
|
|
176
|
-
path: Output file path.
|
|
177
|
-
trace_info: Optional trace metadata.
|
|
178
|
-
pretty: Use pretty printing.
|
|
179
|
-
|
|
180
|
-
Example:
|
|
181
|
-
>>> measurements = measure(trace)
|
|
182
|
-
>>> trace_info = {
|
|
183
|
-
... "source_file": "scope_capture.wfm",
|
|
184
|
-
... "sample_rate": 1e9,
|
|
185
|
-
... "duration": 0.001
|
|
186
|
-
... }
|
|
187
|
-
>>> export_measurements(measurements, "results.json", trace_info=trace_info)
|
|
188
|
-
"""
|
|
189
|
-
path = Path(path)
|
|
190
|
-
|
|
191
|
-
output = {
|
|
192
|
-
"_metadata": {
|
|
193
|
-
"format": "oscura_measurements",
|
|
194
|
-
"version": "1.0",
|
|
195
|
-
"exported_at": datetime.now().isoformat(),
|
|
196
|
-
},
|
|
197
|
-
"measurements": measurements,
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if trace_info:
|
|
201
|
-
output["trace_info"] = trace_info
|
|
202
|
-
|
|
203
|
-
# Sanitize to ensure inf/nan handling
|
|
204
|
-
from oscura.reporting.output import _sanitize_for_serialization
|
|
205
|
-
|
|
206
|
-
output = _sanitize_for_serialization(output)
|
|
207
|
-
|
|
208
|
-
with open(path, "w") as f:
|
|
209
|
-
if pretty:
|
|
210
|
-
json.dump(output, f, cls=OscuraJSONEncoder, indent=2)
|
|
211
|
-
else:
|
|
212
|
-
json.dump(output, f, cls=OscuraJSONEncoder)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
def export_protocol_decode(
|
|
216
|
-
packets: list[dict[str, Any]],
|
|
217
|
-
path: str | Path,
|
|
218
|
-
*,
|
|
219
|
-
protocol: str = "unknown",
|
|
220
|
-
trace_info: dict[str, Any] | None = None,
|
|
221
|
-
pretty: bool = True,
|
|
222
|
-
) -> None:
|
|
223
|
-
"""Export protocol decode results to JSON.
|
|
224
|
-
|
|
225
|
-
Args:
|
|
226
|
-
packets: List of decoded packets.
|
|
227
|
-
path: Output file path.
|
|
228
|
-
protocol: Protocol name.
|
|
229
|
-
trace_info: Optional trace metadata.
|
|
230
|
-
pretty: Use pretty printing.
|
|
231
|
-
|
|
232
|
-
Example:
|
|
233
|
-
>>> packets = [{"timestamp": 0.001, "data": "0x48"}]
|
|
234
|
-
>>> export_protocol_decode(packets, "uart_decode.json", protocol="uart")
|
|
235
|
-
"""
|
|
236
|
-
path = Path(path)
|
|
237
|
-
|
|
238
|
-
output = {
|
|
239
|
-
"_metadata": {
|
|
240
|
-
"format": "oscura_protocol",
|
|
241
|
-
"version": "1.0",
|
|
242
|
-
"exported_at": datetime.now().isoformat(),
|
|
243
|
-
"protocol": protocol,
|
|
244
|
-
},
|
|
245
|
-
"packets": packets,
|
|
246
|
-
"summary": {
|
|
247
|
-
"total_packets": len(packets),
|
|
248
|
-
},
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if trace_info:
|
|
252
|
-
output["trace_info"] = trace_info
|
|
253
|
-
|
|
254
|
-
# Sanitize to ensure inf/nan handling
|
|
255
|
-
from oscura.reporting.output import _sanitize_for_serialization
|
|
256
|
-
|
|
257
|
-
output = _sanitize_for_serialization(output)
|
|
258
|
-
|
|
259
|
-
with open(path, "w") as f:
|
|
260
|
-
if pretty:
|
|
261
|
-
json.dump(output, f, cls=OscuraJSONEncoder, indent=2)
|
|
262
|
-
else:
|
|
263
|
-
json.dump(output, f, cls=OscuraJSONEncoder)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
def export_vintage_logic_json(
|
|
267
|
-
result: Any,
|
|
268
|
-
path: str | Path,
|
|
269
|
-
*,
|
|
270
|
-
indent: int = 2,
|
|
271
|
-
include_metadata: bool = True,
|
|
272
|
-
) -> None:
|
|
273
|
-
"""Export vintage logic analysis results to JSON.
|
|
274
|
-
|
|
275
|
-
Convenience function for exporting VintageLogicAnalysisResult objects.
|
|
276
|
-
The dataclass is automatically serialized to JSON with all nested objects.
|
|
277
|
-
|
|
278
|
-
Args:
|
|
279
|
-
result: VintageLogicAnalysisResult object.
|
|
280
|
-
path: Output JSON file path.
|
|
281
|
-
indent: Indentation level for pretty printing (default: 2).
|
|
282
|
-
include_metadata: Include export metadata (default: True).
|
|
283
|
-
|
|
284
|
-
Example:
|
|
285
|
-
>>> from oscura.analyzers.digital.vintage import analyze_vintage_logic
|
|
286
|
-
>>> result = analyze_vintage_logic(traces)
|
|
287
|
-
>>> export_vintage_logic_json(result, "analysis_results.json")
|
|
288
|
-
"""
|
|
289
|
-
path = Path(path)
|
|
290
|
-
|
|
291
|
-
output: dict[str, Any] = {}
|
|
292
|
-
|
|
293
|
-
if include_metadata:
|
|
294
|
-
output["_metadata"] = {
|
|
295
|
-
"format": "oscura_vintage_logic",
|
|
296
|
-
"version": "1.0",
|
|
297
|
-
"exported_at": datetime.now().isoformat(),
|
|
298
|
-
"analysis_timestamp": result.timestamp.isoformat(),
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
output["analysis_result"] = result
|
|
302
|
-
|
|
303
|
-
# Sanitize to handle inf/nan
|
|
304
|
-
from oscura.reporting.output import _sanitize_for_serialization
|
|
305
|
-
|
|
306
|
-
output = _sanitize_for_serialization(output)
|
|
307
|
-
|
|
308
|
-
with open(path, "w") as f:
|
|
309
|
-
json.dump(output, f, cls=OscuraJSONEncoder, indent=indent)
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
def load_json(path: str | Path) -> dict[str, Any]:
|
|
313
|
-
"""Load JSON data file.
|
|
314
|
-
|
|
315
|
-
Args:
|
|
316
|
-
path: Input file path.
|
|
317
|
-
|
|
318
|
-
Returns:
|
|
319
|
-
Loaded data dictionary.
|
|
320
|
-
|
|
321
|
-
Example:
|
|
322
|
-
>>> data = load_json("results.json")
|
|
323
|
-
>>> measurements = data.get("measurements", data.get("data", {}))
|
|
324
|
-
"""
|
|
325
|
-
path = Path(path)
|
|
326
|
-
|
|
327
|
-
with open(path) as f:
|
|
328
|
-
return json.load(f) # type: ignore[no-any-return]
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
__all__ = [
|
|
332
|
-
"OscuraJSONEncoder",
|
|
333
|
-
"export_json",
|
|
334
|
-
"export_measurements",
|
|
335
|
-
"export_protocol_decode",
|
|
336
|
-
"export_vintage_logic_json",
|
|
337
|
-
"load_json",
|
|
338
|
-
]
|
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
"""Markdown report export for Oscura.
|
|
2
|
-
|
|
3
|
-
This module provides Markdown report generation with measurement tables,
|
|
4
|
-
plot references, and configurable sections.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Example:
|
|
8
|
-
>>> from oscura.exporters.markdown_export import export_markdown
|
|
9
|
-
>>> export_markdown(measurements, "report.md", title="Analysis Report")
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import base64
|
|
15
|
-
from datetime import datetime
|
|
16
|
-
from io import BytesIO
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
from typing import Any
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def export_markdown(
|
|
22
|
-
data: dict[str, Any],
|
|
23
|
-
path: str | Path,
|
|
24
|
-
*,
|
|
25
|
-
title: str = "Oscura Analysis Report",
|
|
26
|
-
author: str | None = None,
|
|
27
|
-
include_plots: bool = True,
|
|
28
|
-
embed_images: bool = True,
|
|
29
|
-
sections: list[str] | None = None,
|
|
30
|
-
) -> None:
|
|
31
|
-
"""Export measurement results to Markdown format.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
data: Dictionary containing measurement results, plots, and metadata.
|
|
35
|
-
Expected keys:
|
|
36
|
-
- "measurements": dict of name -> value pairs
|
|
37
|
-
- "plots": list of matplotlib figures or paths
|
|
38
|
-
- "metadata": optional dict of metadata
|
|
39
|
-
- "summary": optional executive summary text
|
|
40
|
-
path: Output file path.
|
|
41
|
-
title: Report title.
|
|
42
|
-
author: Author name (optional).
|
|
43
|
-
include_plots: Include plots in report.
|
|
44
|
-
embed_images: Embed images as base64 (True) or save separately (False).
|
|
45
|
-
sections: List of sections to include. If None, includes all available.
|
|
46
|
-
Options: "metadata", "summary", "measurements", "plots", "conclusions"
|
|
47
|
-
|
|
48
|
-
References:
|
|
49
|
-
EXP-006
|
|
50
|
-
"""
|
|
51
|
-
lines: list[str] = []
|
|
52
|
-
|
|
53
|
-
# Header
|
|
54
|
-
lines.append(f"# {title}\n")
|
|
55
|
-
lines.append("")
|
|
56
|
-
|
|
57
|
-
# Metadata section
|
|
58
|
-
if sections is None or "metadata" in sections:
|
|
59
|
-
lines.extend(_generate_metadata_section(data, author))
|
|
60
|
-
|
|
61
|
-
# Executive summary
|
|
62
|
-
if (sections is None or "summary" in sections) and "summary" in data:
|
|
63
|
-
lines.append("## Executive Summary\n")
|
|
64
|
-
lines.append(data["summary"])
|
|
65
|
-
lines.append("")
|
|
66
|
-
|
|
67
|
-
# Measurements table
|
|
68
|
-
if (sections is None or "measurements" in sections) and "measurements" in data:
|
|
69
|
-
lines.extend(_generate_measurements_section(data["measurements"]))
|
|
70
|
-
|
|
71
|
-
# Plots
|
|
72
|
-
if include_plots and (sections is None or "plots" in sections) and "plots" in data:
|
|
73
|
-
lines.extend(_generate_plots_section(data["plots"], path, embed_images))
|
|
74
|
-
|
|
75
|
-
# Conclusions
|
|
76
|
-
if (sections is None or "conclusions" in sections) and "conclusions" in data:
|
|
77
|
-
lines.append("## Conclusions\n")
|
|
78
|
-
lines.append(data["conclusions"])
|
|
79
|
-
lines.append("")
|
|
80
|
-
|
|
81
|
-
# Write to file
|
|
82
|
-
content = "\n".join(lines)
|
|
83
|
-
Path(path).write_text(content, encoding="utf-8")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _generate_metadata_section(data: dict[str, Any], author: str | None) -> list[str]:
|
|
87
|
-
"""Generate metadata section."""
|
|
88
|
-
lines = ["## Report Information\n", ""]
|
|
89
|
-
|
|
90
|
-
metadata = data.get("metadata", {})
|
|
91
|
-
|
|
92
|
-
lines.append(f"- **Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
93
|
-
|
|
94
|
-
if author:
|
|
95
|
-
lines.append(f"- **Author**: {author}")
|
|
96
|
-
|
|
97
|
-
if "filename" in metadata:
|
|
98
|
-
lines.append(f"- **Source File**: `{metadata['filename']}`")
|
|
99
|
-
|
|
100
|
-
if "sample_rate" in metadata:
|
|
101
|
-
sr = metadata["sample_rate"]
|
|
102
|
-
if sr >= 1e9:
|
|
103
|
-
sr_str = f"{sr / 1e9:.3f} GS/s"
|
|
104
|
-
elif sr >= 1e6:
|
|
105
|
-
sr_str = f"{sr / 1e6:.3f} MS/s"
|
|
106
|
-
elif sr >= 1e3:
|
|
107
|
-
sr_str = f"{sr / 1e3:.3f} kS/s"
|
|
108
|
-
else:
|
|
109
|
-
sr_str = f"{sr:.3f} S/s"
|
|
110
|
-
lines.append(f"- **Sample Rate**: {sr_str}")
|
|
111
|
-
|
|
112
|
-
if "samples" in metadata:
|
|
113
|
-
lines.append(f"- **Samples**: {metadata['samples']:,}")
|
|
114
|
-
|
|
115
|
-
if "duration" in metadata:
|
|
116
|
-
dur = metadata["duration"]
|
|
117
|
-
if dur >= 1.0:
|
|
118
|
-
dur_str = f"{dur:.3f} s"
|
|
119
|
-
elif dur >= 1e-3:
|
|
120
|
-
dur_str = f"{dur * 1e3:.3f} ms"
|
|
121
|
-
elif dur >= 1e-6:
|
|
122
|
-
dur_str = f"{dur * 1e6:.3f} us"
|
|
123
|
-
else:
|
|
124
|
-
dur_str = f"{dur * 1e9:.3f} ns"
|
|
125
|
-
lines.append(f"- **Duration**: {dur_str}")
|
|
126
|
-
|
|
127
|
-
lines.append("")
|
|
128
|
-
return lines
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def _generate_measurements_section(measurements: dict[str, Any]) -> list[str]:
|
|
132
|
-
"""Generate measurements table section."""
|
|
133
|
-
lines = ["## Measurement Results\n", ""]
|
|
134
|
-
|
|
135
|
-
if not measurements:
|
|
136
|
-
lines.append("*No measurements available.*\n")
|
|
137
|
-
return lines
|
|
138
|
-
|
|
139
|
-
# Create table header
|
|
140
|
-
lines.append("| Parameter | Value | Unit | Status |")
|
|
141
|
-
lines.append("|-----------|-------|------|--------|")
|
|
142
|
-
|
|
143
|
-
for name, value in measurements.items():
|
|
144
|
-
if isinstance(value, dict):
|
|
145
|
-
# Structured measurement with value, unit, status
|
|
146
|
-
val = value.get("value", "N/A")
|
|
147
|
-
unit = value.get("unit", "")
|
|
148
|
-
status = value.get("status", "")
|
|
149
|
-
|
|
150
|
-
# Format value
|
|
151
|
-
val_str = _format_value(val, unit) if isinstance(val, float) else str(val)
|
|
152
|
-
|
|
153
|
-
# Format status with emoji
|
|
154
|
-
if status.upper() == "PASS":
|
|
155
|
-
status_str = "PASS"
|
|
156
|
-
elif status.upper() == "FAIL":
|
|
157
|
-
status_str = "FAIL"
|
|
158
|
-
elif status.upper() == "WARNING":
|
|
159
|
-
status_str = "WARNING"
|
|
160
|
-
else:
|
|
161
|
-
status_str = status
|
|
162
|
-
|
|
163
|
-
lines.append(f"| {name} | {val_str} | {unit} | {status_str} |")
|
|
164
|
-
else:
|
|
165
|
-
# Simple value
|
|
166
|
-
val_str = f"{value:.6g}" if isinstance(value, float) else str(value)
|
|
167
|
-
lines.append(f"| {name} | {val_str} | - | - |")
|
|
168
|
-
|
|
169
|
-
lines.append("")
|
|
170
|
-
return lines
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
def _format_value(value: float, unit: str) -> str:
|
|
174
|
-
"""Format value with appropriate SI prefix."""
|
|
175
|
-
if value == 0:
|
|
176
|
-
return "0"
|
|
177
|
-
|
|
178
|
-
abs_val = abs(value)
|
|
179
|
-
|
|
180
|
-
# Time units
|
|
181
|
-
if unit in ("s", "sec", "seconds"):
|
|
182
|
-
if abs_val >= 1.0:
|
|
183
|
-
return f"{value:.6g}"
|
|
184
|
-
elif abs_val >= 1e-3:
|
|
185
|
-
return f"{value * 1e3:.6g} m"
|
|
186
|
-
elif abs_val >= 1e-6:
|
|
187
|
-
return f"{value * 1e6:.6g} u"
|
|
188
|
-
elif abs_val >= 1e-9:
|
|
189
|
-
return f"{value * 1e9:.6g} n"
|
|
190
|
-
else:
|
|
191
|
-
return f"{value * 1e12:.6g} p"
|
|
192
|
-
|
|
193
|
-
# Frequency units
|
|
194
|
-
if unit in ("Hz", "hz"):
|
|
195
|
-
if abs_val >= 1e9:
|
|
196
|
-
return f"{value / 1e9:.6g} G"
|
|
197
|
-
elif abs_val >= 1e6:
|
|
198
|
-
return f"{value / 1e6:.6g} M"
|
|
199
|
-
elif abs_val >= 1e3:
|
|
200
|
-
return f"{value / 1e3:.6g} k"
|
|
201
|
-
else:
|
|
202
|
-
return f"{value:.6g}"
|
|
203
|
-
|
|
204
|
-
# Voltage units
|
|
205
|
-
if unit in ("V", "v", "volts"):
|
|
206
|
-
if abs_val >= 1.0:
|
|
207
|
-
return f"{value:.6g}"
|
|
208
|
-
elif abs_val >= 1e-3:
|
|
209
|
-
return f"{value * 1e3:.6g} m"
|
|
210
|
-
elif abs_val >= 1e-6:
|
|
211
|
-
return f"{value * 1e6:.6g} u"
|
|
212
|
-
else:
|
|
213
|
-
return f"{value * 1e9:.6g} n"
|
|
214
|
-
|
|
215
|
-
# Default formatting
|
|
216
|
-
return f"{value:.6g}"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _generate_plots_section(
|
|
220
|
-
plots: list[Any],
|
|
221
|
-
report_path: str | Path,
|
|
222
|
-
embed_images: bool,
|
|
223
|
-
) -> list[str]:
|
|
224
|
-
"""Generate plots section."""
|
|
225
|
-
lines = ["## Plots and Visualizations\n", ""]
|
|
226
|
-
|
|
227
|
-
report_path = Path(report_path)
|
|
228
|
-
plots_dir = report_path.parent / f"{report_path.stem}_plots"
|
|
229
|
-
|
|
230
|
-
for i, plot in enumerate(plots, start=1):
|
|
231
|
-
if isinstance(plot, dict):
|
|
232
|
-
# Plot with metadata
|
|
233
|
-
fig = plot.get("figure")
|
|
234
|
-
caption = plot.get("caption", f"Figure {i}")
|
|
235
|
-
alt_text = plot.get("alt_text", caption)
|
|
236
|
-
else:
|
|
237
|
-
fig = plot
|
|
238
|
-
caption = f"Figure {i}"
|
|
239
|
-
alt_text = caption
|
|
240
|
-
|
|
241
|
-
if fig is None:
|
|
242
|
-
continue
|
|
243
|
-
|
|
244
|
-
if isinstance(fig, str | Path):
|
|
245
|
-
# Path to existing image
|
|
246
|
-
if embed_images:
|
|
247
|
-
# Read and embed as base64
|
|
248
|
-
try:
|
|
249
|
-
img_data = Path(fig).read_bytes()
|
|
250
|
-
img_ext = Path(fig).suffix.lower()
|
|
251
|
-
mime_type = {
|
|
252
|
-
".png": "image/png",
|
|
253
|
-
".jpg": "image/jpeg",
|
|
254
|
-
".jpeg": "image/jpeg",
|
|
255
|
-
".svg": "image/svg+xml",
|
|
256
|
-
}.get(img_ext, "image/png")
|
|
257
|
-
|
|
258
|
-
b64 = base64.b64encode(img_data).decode("utf-8")
|
|
259
|
-
lines.append(f"### {caption}\n")
|
|
260
|
-
lines.append(f"\n")
|
|
261
|
-
except Exception:
|
|
262
|
-
lines.append(f"### {caption}\n")
|
|
263
|
-
lines.append(f"*Unable to embed image: {fig}*\n")
|
|
264
|
-
else:
|
|
265
|
-
lines.append(f"### {caption}\n")
|
|
266
|
-
lines.append(f"\n")
|
|
267
|
-
else:
|
|
268
|
-
# Matplotlib figure
|
|
269
|
-
try:
|
|
270
|
-
if embed_images:
|
|
271
|
-
# Embed as base64 PNG
|
|
272
|
-
buf = BytesIO()
|
|
273
|
-
fig.savefig(buf, format="png", dpi=150, bbox_inches="tight")
|
|
274
|
-
buf.seek(0)
|
|
275
|
-
b64 = base64.b64encode(buf.read()).decode("utf-8")
|
|
276
|
-
lines.append(f"### {caption}\n")
|
|
277
|
-
lines.append(f"\n")
|
|
278
|
-
else:
|
|
279
|
-
# Save to separate file
|
|
280
|
-
plots_dir.mkdir(exist_ok=True)
|
|
281
|
-
plot_path = plots_dir / f"figure_{i}.png"
|
|
282
|
-
fig.savefig(plot_path, format="png", dpi=150, bbox_inches="tight")
|
|
283
|
-
rel_path = plot_path.relative_to(report_path.parent)
|
|
284
|
-
lines.append(f"### {caption}\n")
|
|
285
|
-
lines.append(f"\n")
|
|
286
|
-
except Exception as e:
|
|
287
|
-
lines.append(f"### {caption}\n")
|
|
288
|
-
lines.append(f"*Unable to render figure: {e}*\n")
|
|
289
|
-
|
|
290
|
-
lines.append("")
|
|
291
|
-
|
|
292
|
-
return lines
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
def generate_markdown_report(
|
|
296
|
-
data: dict[str, Any],
|
|
297
|
-
*,
|
|
298
|
-
title: str = "Oscura Analysis Report",
|
|
299
|
-
author: str | None = None,
|
|
300
|
-
include_plots: bool = True,
|
|
301
|
-
embed_images: bool = True,
|
|
302
|
-
sections: list[str] | None = None,
|
|
303
|
-
) -> str:
|
|
304
|
-
"""Generate Markdown report as string.
|
|
305
|
-
|
|
306
|
-
Args:
|
|
307
|
-
data: Dictionary containing measurement results, plots, and metadata.
|
|
308
|
-
title: Report title.
|
|
309
|
-
author: Author name (optional).
|
|
310
|
-
include_plots: Include plots in report.
|
|
311
|
-
embed_images: Embed images as base64.
|
|
312
|
-
sections: List of sections to include.
|
|
313
|
-
|
|
314
|
-
Returns:
|
|
315
|
-
Markdown content as string.
|
|
316
|
-
|
|
317
|
-
References:
|
|
318
|
-
EXP-006
|
|
319
|
-
"""
|
|
320
|
-
lines: list[str] = []
|
|
321
|
-
|
|
322
|
-
# Header
|
|
323
|
-
lines.append(f"# {title}\n")
|
|
324
|
-
lines.append("")
|
|
325
|
-
|
|
326
|
-
# Metadata section
|
|
327
|
-
if sections is None or "metadata" in sections:
|
|
328
|
-
lines.extend(_generate_metadata_section(data, author))
|
|
329
|
-
|
|
330
|
-
# Executive summary
|
|
331
|
-
if (sections is None or "summary" in sections) and "summary" in data:
|
|
332
|
-
lines.append("## Executive Summary\n")
|
|
333
|
-
lines.append(data["summary"])
|
|
334
|
-
lines.append("")
|
|
335
|
-
|
|
336
|
-
# Measurements table
|
|
337
|
-
if (sections is None or "measurements" in sections) and "measurements" in data:
|
|
338
|
-
lines.extend(_generate_measurements_section(data["measurements"]))
|
|
339
|
-
|
|
340
|
-
# For string generation, only include plots if embed_images is True
|
|
341
|
-
if include_plots and embed_images and (sections is None or "plots" in sections):
|
|
342
|
-
if "plots" in data:
|
|
343
|
-
# Simplified plot handling for string output
|
|
344
|
-
lines.append("## Plots and Visualizations\n")
|
|
345
|
-
lines.append("")
|
|
346
|
-
for i, plot in enumerate(data["plots"], start=1):
|
|
347
|
-
if isinstance(plot, dict):
|
|
348
|
-
caption = plot.get("caption", f"Figure {i}")
|
|
349
|
-
else:
|
|
350
|
-
caption = f"Figure {i}"
|
|
351
|
-
lines.append(f"### {caption}\n")
|
|
352
|
-
lines.append("*[Embedded plot - save to file to view]*\n")
|
|
353
|
-
lines.append("")
|
|
354
|
-
|
|
355
|
-
# Conclusions
|
|
356
|
-
if (sections is None or "conclusions" in sections) and "conclusions" in data:
|
|
357
|
-
lines.append("## Conclusions\n")
|
|
358
|
-
lines.append(data["conclusions"])
|
|
359
|
-
lines.append("")
|
|
360
|
-
|
|
361
|
-
return "\n".join(lines)
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
__all__ = [
|
|
365
|
-
"export_markdown",
|
|
366
|
-
"generate_markdown_report",
|
|
367
|
-
]
|