oscura 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
"""Result aggregation for batch analysis.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
This module provides statistical aggregation and reporting for batch
|
|
5
|
+
analysis results, including outlier detection and export capabilities.
|
|
6
|
+
|
|
7
|
+
**Requires pandas:**
|
|
8
|
+
This module requires pandas for DataFrame operations. Install with:
|
|
9
|
+
pip install oscura[dataframes] # Pandas + Excel export
|
|
10
|
+
pip install oscura[standard] # Recommended
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import pandas as pd
|
|
20
|
+
except ImportError as e:
|
|
21
|
+
raise ImportError(
|
|
22
|
+
"Batch aggregation requires pandas.\n\n"
|
|
23
|
+
"Install with:\n"
|
|
24
|
+
" pip install oscura[dataframes] # Pandas + Excel export\n"
|
|
25
|
+
" pip install oscura[standard] # Recommended\n"
|
|
26
|
+
" pip install oscura[all] # Everything\n"
|
|
27
|
+
) from e
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def aggregate_results(
|
|
31
|
+
results: pd.DataFrame,
|
|
32
|
+
*,
|
|
33
|
+
metrics: list[str] | None = None,
|
|
34
|
+
outlier_threshold: float = 3.0,
|
|
35
|
+
include_plots: bool = False,
|
|
36
|
+
output_format: str = "dict",
|
|
37
|
+
output_file: str | Path | None = None,
|
|
38
|
+
) -> dict[str, Any] | pd.DataFrame:
|
|
39
|
+
"""Aggregate results from batch analysis into summary statistics.
|
|
40
|
+
|
|
41
|
+
: Computes comprehensive statistics (mean, std, min, max,
|
|
42
|
+
outliers) for each metric in the batch results. Supports export to various
|
|
43
|
+
formats and optional visualization generation.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
results: DataFrame from batch_analyze() containing analysis results
|
|
47
|
+
metrics: List of column names to aggregate (default: all numeric columns)
|
|
48
|
+
outlier_threshold: Z-score threshold for outlier detection (default: 3.0)
|
|
49
|
+
include_plots: Generate comparison plots across files (default: False)
|
|
50
|
+
output_format: Output format - 'dict', 'dataframe', 'csv', 'excel', 'html'
|
|
51
|
+
output_file: Optional output file path for export formats
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Dictionary or DataFrame with summary statistics:
|
|
55
|
+
- count: Number of valid values
|
|
56
|
+
- mean: Mean value
|
|
57
|
+
- std: Standard deviation
|
|
58
|
+
- min: Minimum value
|
|
59
|
+
- max: Maximum value
|
|
60
|
+
- median: Median value
|
|
61
|
+
- q25: 25th percentile
|
|
62
|
+
- q75: 75th percentile
|
|
63
|
+
- outliers: List of outlier values
|
|
64
|
+
- outlier_files: List of files containing outliers
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If no numeric metrics are found in results.
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
>>> results = osc.batch_analyze(files, osc.characterize_buffer)
|
|
71
|
+
>>> summary = osc.aggregate_results(
|
|
72
|
+
... results,
|
|
73
|
+
... metrics=['rise_time', 'fall_time'],
|
|
74
|
+
... outlier_threshold=2.5
|
|
75
|
+
... )
|
|
76
|
+
>>> print(summary['rise_time']['mean'])
|
|
77
|
+
>>> print(summary['rise_time']['outlier_files'])
|
|
78
|
+
|
|
79
|
+
Notes:
|
|
80
|
+
- Outliers detected using IQR method: values outside [Q1 - k*IQR, Q3 + k*IQR]
|
|
81
|
+
where k = (threshold / 3.0) * 1.5 (more robust than z-score for heavy-tailed data)
|
|
82
|
+
- Non-numeric columns are automatically skipped
|
|
83
|
+
- Missing values (NaN) are excluded from statistics
|
|
84
|
+
- CSV/Excel/HTML export requires output_file parameter
|
|
85
|
+
|
|
86
|
+
References:
|
|
87
|
+
BATCH-002: Result Aggregation
|
|
88
|
+
"""
|
|
89
|
+
if results.empty:
|
|
90
|
+
return {} if output_format == "dict" else pd.DataFrame()
|
|
91
|
+
|
|
92
|
+
# Select and validate metrics
|
|
93
|
+
metrics_to_use = _select_metrics(results, metrics)
|
|
94
|
+
|
|
95
|
+
# Compute statistics for each metric
|
|
96
|
+
aggregated = _compute_metric_statistics(results, metrics_to_use, outlier_threshold)
|
|
97
|
+
|
|
98
|
+
# Generate plots if requested
|
|
99
|
+
if include_plots:
|
|
100
|
+
_generate_metric_plots(results, aggregated, metrics_to_use, output_file)
|
|
101
|
+
|
|
102
|
+
# Format and return results
|
|
103
|
+
return _format_output(aggregated, output_format, output_file, results, metrics_to_use)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _select_metrics(results: pd.DataFrame, metrics: list[str] | None) -> list[str]:
|
|
107
|
+
"""Select and validate metrics to aggregate.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
results: DataFrame containing results.
|
|
111
|
+
metrics: User-specified metrics or None for auto-selection.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of metric column names.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If no numeric metrics found.
|
|
118
|
+
"""
|
|
119
|
+
if metrics is None:
|
|
120
|
+
metrics = results.select_dtypes(include=[np.number]).columns.tolist()
|
|
121
|
+
metrics = [m for m in metrics if m not in ["file", "error"]]
|
|
122
|
+
|
|
123
|
+
if not metrics:
|
|
124
|
+
raise ValueError("No numeric metrics found in results")
|
|
125
|
+
|
|
126
|
+
return metrics
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _compute_metric_statistics(
|
|
130
|
+
results: pd.DataFrame,
|
|
131
|
+
metrics: list[str],
|
|
132
|
+
outlier_threshold: float,
|
|
133
|
+
) -> dict[str, dict[str, Any]]:
|
|
134
|
+
"""Compute statistics for all metrics.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
results: DataFrame containing results.
|
|
138
|
+
metrics: List of metrics to compute statistics for.
|
|
139
|
+
outlier_threshold: Threshold for outlier detection.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dictionary mapping metrics to statistics.
|
|
143
|
+
"""
|
|
144
|
+
aggregated: dict[str, dict[str, Any]] = {}
|
|
145
|
+
|
|
146
|
+
for metric in metrics:
|
|
147
|
+
if metric not in results.columns:
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
values = results[metric].dropna()
|
|
151
|
+
|
|
152
|
+
if values.empty:
|
|
153
|
+
aggregated[metric] = _create_empty_stats()
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
aggregated[metric] = _compute_single_metric_stats(values, results, outlier_threshold)
|
|
157
|
+
|
|
158
|
+
return aggregated
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _create_empty_stats() -> dict[str, Any]:
|
|
162
|
+
"""Create empty statistics dictionary.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Dictionary with NaN values and empty lists.
|
|
166
|
+
"""
|
|
167
|
+
return {
|
|
168
|
+
"count": 0,
|
|
169
|
+
"mean": np.nan,
|
|
170
|
+
"std": np.nan,
|
|
171
|
+
"min": np.nan,
|
|
172
|
+
"max": np.nan,
|
|
173
|
+
"median": np.nan,
|
|
174
|
+
"q25": np.nan,
|
|
175
|
+
"q75": np.nan,
|
|
176
|
+
"outliers": [],
|
|
177
|
+
"outlier_files": [],
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _compute_single_metric_stats(
|
|
182
|
+
values: pd.Series,
|
|
183
|
+
results: pd.DataFrame,
|
|
184
|
+
outlier_threshold: float,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
"""Compute statistics for a single metric.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
values: Series of non-null metric values.
|
|
190
|
+
results: Full results DataFrame (for file lookup).
|
|
191
|
+
outlier_threshold: Threshold for outlier detection.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Dictionary with computed statistics.
|
|
195
|
+
"""
|
|
196
|
+
stats = _compute_basic_statistics(values)
|
|
197
|
+
outliers_info = _detect_outliers(values, stats, outlier_threshold)
|
|
198
|
+
stats.update(outliers_info)
|
|
199
|
+
|
|
200
|
+
# Add outlier files if available
|
|
201
|
+
if outliers_info["outliers"]:
|
|
202
|
+
outlier_files: Any = _get_outlier_files(results, outliers_info["outlier_indices"])
|
|
203
|
+
stats["outlier_files"] = outlier_files
|
|
204
|
+
else:
|
|
205
|
+
stats["outlier_files"] = []
|
|
206
|
+
|
|
207
|
+
return stats
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _compute_basic_statistics(values: pd.Series) -> dict[str, Any]:
|
|
211
|
+
"""Compute basic statistics for metric values.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
values: Series of metric values.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Dictionary with basic statistics.
|
|
218
|
+
"""
|
|
219
|
+
return {
|
|
220
|
+
"count": len(values),
|
|
221
|
+
"mean": float(values.mean()),
|
|
222
|
+
"std": float(values.std()),
|
|
223
|
+
"min": float(values.min()),
|
|
224
|
+
"max": float(values.max()),
|
|
225
|
+
"median": float(values.median()),
|
|
226
|
+
"q25": float(values.quantile(0.25)),
|
|
227
|
+
"q75": float(values.quantile(0.75)),
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _detect_outliers(
|
|
232
|
+
values: pd.Series,
|
|
233
|
+
stats: dict[str, Any],
|
|
234
|
+
threshold: float,
|
|
235
|
+
) -> dict[str, Any]:
|
|
236
|
+
"""Detect outliers using IQR method.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
values: Series of metric values.
|
|
240
|
+
stats: Basic statistics containing q25 and q75.
|
|
241
|
+
threshold: IQR threshold multiplier.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
Dictionary with outlier information.
|
|
245
|
+
"""
|
|
246
|
+
if len(values) <= 3:
|
|
247
|
+
return {"outliers": [], "outlier_indices": []}
|
|
248
|
+
|
|
249
|
+
q1, q3 = stats["q25"], stats["q75"]
|
|
250
|
+
iqr = q3 - q1
|
|
251
|
+
k = (threshold / 3.0) * 1.5
|
|
252
|
+
|
|
253
|
+
lower_bound = q1 - k * iqr
|
|
254
|
+
upper_bound = q3 + k * iqr
|
|
255
|
+
|
|
256
|
+
outlier_mask = (values < lower_bound) | (values > upper_bound)
|
|
257
|
+
outlier_indices = values[outlier_mask].index.tolist()
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
"outliers": values[outlier_mask].tolist(),
|
|
261
|
+
"outlier_indices": outlier_indices,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _get_outlier_files(
|
|
266
|
+
results: pd.DataFrame,
|
|
267
|
+
outlier_indices: list[Any],
|
|
268
|
+
) -> Any:
|
|
269
|
+
"""Get filenames for outlier indices.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
results: Full results DataFrame.
|
|
273
|
+
outlier_indices: Indices of outlier values.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of filenames or indices.
|
|
277
|
+
"""
|
|
278
|
+
if "file" in results.columns:
|
|
279
|
+
return results.loc[outlier_indices, "file"].tolist()
|
|
280
|
+
return outlier_indices
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _generate_metric_plots(
|
|
284
|
+
results: pd.DataFrame,
|
|
285
|
+
aggregated: dict[str, dict[str, Any]],
|
|
286
|
+
metrics: list[str],
|
|
287
|
+
output_file: str | Path | None,
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Generate comparison plots for metrics.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
results: DataFrame with results.
|
|
293
|
+
aggregated: Aggregated statistics.
|
|
294
|
+
metrics: List of metrics to plot.
|
|
295
|
+
output_file: Output file path for saving plots.
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
import matplotlib.pyplot as plt
|
|
299
|
+
|
|
300
|
+
for metric in metrics:
|
|
301
|
+
if metric not in aggregated:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
_create_metric_plot(results, aggregated, metric, output_file)
|
|
305
|
+
plt.close()
|
|
306
|
+
|
|
307
|
+
except ImportError:
|
|
308
|
+
pass # Skip if matplotlib unavailable
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _create_metric_plot(
|
|
312
|
+
results: pd.DataFrame,
|
|
313
|
+
aggregated: dict[str, dict[str, Any]],
|
|
314
|
+
metric: str,
|
|
315
|
+
output_file: str | Path | None,
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Create histogram and box plot for a metric.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
results: DataFrame with results.
|
|
321
|
+
aggregated: Aggregated statistics.
|
|
322
|
+
metric: Metric name.
|
|
323
|
+
output_file: Output file path.
|
|
324
|
+
"""
|
|
325
|
+
import matplotlib.pyplot as plt
|
|
326
|
+
|
|
327
|
+
_fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
|
328
|
+
|
|
329
|
+
# Histogram
|
|
330
|
+
_plot_histogram(results, aggregated, metric, ax1)
|
|
331
|
+
|
|
332
|
+
# Box plot
|
|
333
|
+
_plot_boxplot(results, metric, ax2)
|
|
334
|
+
|
|
335
|
+
plt.tight_layout()
|
|
336
|
+
|
|
337
|
+
# Save or show
|
|
338
|
+
if output_file:
|
|
339
|
+
plot_file = Path(output_file).with_suffix("") / f"{metric}_plot.png"
|
|
340
|
+
plot_file.parent.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
plt.savefig(plot_file)
|
|
342
|
+
else:
|
|
343
|
+
plt.show()
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _plot_histogram(
|
|
347
|
+
results: pd.DataFrame,
|
|
348
|
+
aggregated: dict[str, dict[str, Any]],
|
|
349
|
+
metric: str,
|
|
350
|
+
ax: Any,
|
|
351
|
+
) -> None:
|
|
352
|
+
"""Plot histogram with mean and median lines.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
results: DataFrame with results.
|
|
356
|
+
aggregated: Aggregated statistics.
|
|
357
|
+
metric: Metric name.
|
|
358
|
+
ax: Matplotlib axes.
|
|
359
|
+
"""
|
|
360
|
+
results[metric].dropna().hist(ax=ax, bins=30)
|
|
361
|
+
ax.axvline(aggregated[metric]["mean"], color="r", linestyle="--", label="Mean")
|
|
362
|
+
ax.axvline(aggregated[metric]["median"], color="g", linestyle="--", label="Median")
|
|
363
|
+
ax.set_xlabel(metric)
|
|
364
|
+
ax.set_ylabel("Count")
|
|
365
|
+
ax.legend()
|
|
366
|
+
ax.set_title(f"{metric} Distribution")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _plot_boxplot(results: pd.DataFrame, metric: str, ax: Any) -> None:
|
|
370
|
+
"""Plot box plot for metric.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
results: DataFrame with results.
|
|
374
|
+
metric: Metric name.
|
|
375
|
+
ax: Matplotlib axes.
|
|
376
|
+
"""
|
|
377
|
+
ax.boxplot(results[metric].dropna())
|
|
378
|
+
ax.set_ylabel(metric)
|
|
379
|
+
ax.set_title(f"{metric} Box Plot")
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _format_output(
|
|
383
|
+
aggregated: dict[str, dict[str, Any]],
|
|
384
|
+
output_format: str,
|
|
385
|
+
output_file: str | Path | None,
|
|
386
|
+
results: pd.DataFrame,
|
|
387
|
+
metrics: list[str],
|
|
388
|
+
) -> dict[str, Any] | pd.DataFrame:
|
|
389
|
+
"""Format aggregated results based on output format.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
aggregated: Aggregated statistics.
|
|
393
|
+
output_format: Desired output format.
|
|
394
|
+
output_file: Output file path.
|
|
395
|
+
results: Original results DataFrame.
|
|
396
|
+
metrics: List of metrics.
|
|
397
|
+
|
|
398
|
+
Returns:
|
|
399
|
+
Formatted results.
|
|
400
|
+
|
|
401
|
+
Raises:
|
|
402
|
+
ValueError: If unknown format or missing output_file.
|
|
403
|
+
"""
|
|
404
|
+
if output_format == "dict":
|
|
405
|
+
return aggregated
|
|
406
|
+
|
|
407
|
+
if output_format == "dataframe":
|
|
408
|
+
return _convert_to_dataframe(aggregated)
|
|
409
|
+
|
|
410
|
+
if output_format in ["csv", "excel", "html"]:
|
|
411
|
+
return _export_to_file(aggregated, output_format, output_file, results, metrics)
|
|
412
|
+
|
|
413
|
+
raise ValueError(f"Unknown output_format: {output_format}")
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _convert_to_dataframe(aggregated: dict[str, dict[str, Any]]) -> pd.DataFrame:
|
|
417
|
+
"""Convert aggregated dict to DataFrame.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
aggregated: Aggregated statistics dictionary.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
DataFrame with metrics as rows.
|
|
424
|
+
"""
|
|
425
|
+
df = pd.DataFrame(aggregated).T
|
|
426
|
+
return df.drop(columns=["outliers", "outlier_files"], errors="ignore")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _export_to_file(
|
|
430
|
+
aggregated: dict[str, dict[str, Any]],
|
|
431
|
+
output_format: str,
|
|
432
|
+
output_file: str | Path | None,
|
|
433
|
+
results: pd.DataFrame,
|
|
434
|
+
metrics: list[str],
|
|
435
|
+
) -> pd.DataFrame:
|
|
436
|
+
"""Export aggregated results to file.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
aggregated: Aggregated statistics.
|
|
440
|
+
output_format: Format (csv, excel, html).
|
|
441
|
+
output_file: Output file path.
|
|
442
|
+
results: Original results DataFrame.
|
|
443
|
+
metrics: List of metrics.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
DataFrame with aggregated results.
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
ValueError: If output_file not provided.
|
|
450
|
+
"""
|
|
451
|
+
if not output_file:
|
|
452
|
+
raise ValueError(f"{output_format} format requires output_file parameter")
|
|
453
|
+
|
|
454
|
+
df = _convert_to_dataframe(aggregated)
|
|
455
|
+
|
|
456
|
+
if output_format == "csv":
|
|
457
|
+
df.to_csv(output_file)
|
|
458
|
+
elif output_format == "excel":
|
|
459
|
+
df.to_excel(output_file)
|
|
460
|
+
elif output_format == "html":
|
|
461
|
+
html = _generate_html_report(results, aggregated, metrics)
|
|
462
|
+
Path(output_file).write_text(html)
|
|
463
|
+
|
|
464
|
+
return df
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _generate_html_report(
|
|
468
|
+
results: pd.DataFrame,
|
|
469
|
+
aggregated: dict[str, dict[str, Any]],
|
|
470
|
+
metrics: list[str],
|
|
471
|
+
) -> str:
|
|
472
|
+
"""Generate HTML report for batch analysis results."""
|
|
473
|
+
html = """
|
|
474
|
+
<!DOCTYPE html>
|
|
475
|
+
<html>
|
|
476
|
+
<head>
|
|
477
|
+
<title>Batch Analysis Report</title>
|
|
478
|
+
<style>
|
|
479
|
+
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
480
|
+
h1 { color: #333; }
|
|
481
|
+
h2 { color: #666; margin-top: 30px; }
|
|
482
|
+
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
|
|
483
|
+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
|
484
|
+
th { background-color: #4CAF50; color: white; }
|
|
485
|
+
tr:nth-child(even) { background-color: #f2f2f2; }
|
|
486
|
+
.outlier { background-color: #ffcccc; }
|
|
487
|
+
</style>
|
|
488
|
+
</head>
|
|
489
|
+
<body>
|
|
490
|
+
<h1>Batch Analysis Report</h1>
|
|
491
|
+
"""
|
|
492
|
+
# Summary statistics table
|
|
493
|
+
html += "<h2>Summary Statistics</h2>\n<table>\n"
|
|
494
|
+
html += "<tr><th>Metric</th><th>Count</th><th>Mean</th><th>Std</th>"
|
|
495
|
+
html += "<th>Min</th><th>Median</th><th>Max</th><th>Outliers</th></tr>\n"
|
|
496
|
+
|
|
497
|
+
for metric in metrics:
|
|
498
|
+
if metric not in aggregated:
|
|
499
|
+
continue
|
|
500
|
+
stats = aggregated[metric]
|
|
501
|
+
html += "<tr>"
|
|
502
|
+
html += f"<td>{metric}</td>"
|
|
503
|
+
html += f"<td>{stats['count']}</td>"
|
|
504
|
+
html += f"<td>{stats['mean']:.4g}</td>"
|
|
505
|
+
html += f"<td>{stats['std']:.4g}</td>"
|
|
506
|
+
html += f"<td>{stats['min']:.4g}</td>"
|
|
507
|
+
html += f"<td>{stats['median']:.4g}</td>"
|
|
508
|
+
html += f"<td>{stats['max']:.4g}</td>"
|
|
509
|
+
html += f"<td>{len(stats['outliers'])}</td>"
|
|
510
|
+
html += "</tr>\n"
|
|
511
|
+
|
|
512
|
+
html += "</table>\n"
|
|
513
|
+
|
|
514
|
+
# Outlier details
|
|
515
|
+
has_outliers = any(len(aggregated[m]["outliers"]) > 0 for m in metrics if m in aggregated)
|
|
516
|
+
|
|
517
|
+
if has_outliers:
|
|
518
|
+
html += "<h2>Outliers Detected</h2>\n"
|
|
519
|
+
for metric in metrics:
|
|
520
|
+
if metric not in aggregated:
|
|
521
|
+
continue
|
|
522
|
+
stats = aggregated[metric]
|
|
523
|
+
if stats["outliers"]:
|
|
524
|
+
html += f"<h3>{metric}</h3>\n<table>\n"
|
|
525
|
+
html += "<tr><th>File</th><th>Value</th></tr>\n"
|
|
526
|
+
for file, value in zip(stats["outlier_files"], stats["outliers"], strict=False):
|
|
527
|
+
html += f"<tr class='outlier'><td>{file}</td><td>{value:.4g}</td></tr>\n"
|
|
528
|
+
html += "</table>\n"
|
|
529
|
+
|
|
530
|
+
html += "</body>\n</html>"
|
|
531
|
+
return html
|