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
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
"""Parallel processing for multi-core hardware analysis.
|
|
2
|
+
|
|
3
|
+
This module provides parallelization strategies for CPU-bound and I/O-bound tasks
|
|
4
|
+
in hardware reverse engineering workflows. It enables efficient processing of
|
|
5
|
+
multiple signals, protocols, files, and analysis operations across CPU cores.
|
|
6
|
+
|
|
7
|
+
Key capabilities:
|
|
8
|
+
- Process pool for CPU-intensive analysis (FFT, correlation, protocol decoding)
|
|
9
|
+
- Thread pool for I/O-bound operations (file loading, network requests)
|
|
10
|
+
- Batch processing (multiple files in parallel)
|
|
11
|
+
- Pipeline parallelism (different stages running concurrently)
|
|
12
|
+
- Data parallelism (split large datasets across workers)
|
|
13
|
+
- Automatic worker count based on CPU topology
|
|
14
|
+
- Progress tracking with tqdm integration
|
|
15
|
+
- Graceful error handling in worker processes
|
|
16
|
+
|
|
17
|
+
Typical use cases:
|
|
18
|
+
- Decode multiple protocol messages simultaneously
|
|
19
|
+
- Parallel FFT/spectral analysis on signal chunks
|
|
20
|
+
- Load multiple capture files at once
|
|
21
|
+
- Generate multiple export formats (Wireshark, Scapy, Kaitai) concurrently
|
|
22
|
+
- Batch CRC recovery across message sets
|
|
23
|
+
- Parallel side-channel analysis on traces
|
|
24
|
+
|
|
25
|
+
Performance expectations:
|
|
26
|
+
- CPU-bound tasks: ~(N-1)x speedup on N cores
|
|
27
|
+
- I/O-bound tasks: ~2-4x speedup with threading
|
|
28
|
+
- Mixed workloads: Automatic strategy selection
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> from oscura.utils.performance.parallel import ParallelProcessor, ParallelConfig
|
|
32
|
+
>>> # Configure parallel processor
|
|
33
|
+
>>> config = ParallelConfig(num_workers=4, strategy="process")
|
|
34
|
+
>>> processor = ParallelProcessor(config)
|
|
35
|
+
>>>
|
|
36
|
+
>>> # Parallel protocol decoding
|
|
37
|
+
>>> messages = [...] # List of message bytes
|
|
38
|
+
>>> def decode_message(msg):
|
|
39
|
+
... return protocol_decoder.decode(msg)
|
|
40
|
+
>>> result = processor.map(decode_message, messages)
|
|
41
|
+
>>> print(f"Decoded {len(result.results)} messages in {result.execution_time:.2f}s")
|
|
42
|
+
>>> print(f"Speedup: {result.speedup:.2f}x")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import logging
|
|
48
|
+
import multiprocessing as mp
|
|
49
|
+
import time
|
|
50
|
+
from collections.abc import Callable
|
|
51
|
+
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
|
|
52
|
+
from dataclasses import dataclass, field
|
|
53
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from collections.abc import Iterable, Sequence
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
# Strategy types for parallelization
|
|
61
|
+
StrategyType = Literal["process", "thread", "auto"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ParallelConfig:
|
|
66
|
+
"""Configuration for parallel processing.
|
|
67
|
+
|
|
68
|
+
Attributes:
|
|
69
|
+
num_workers: Number of worker processes/threads. If None, uses CPU count.
|
|
70
|
+
For process strategy: Defaults to cpu_count() - 1 (leave one core free)
|
|
71
|
+
For thread strategy: Defaults to cpu_count() * 2 (I/O-bound tasks)
|
|
72
|
+
strategy: Parallelization strategy.
|
|
73
|
+
"process": Use multiprocessing (CPU-bound tasks like FFT, correlation)
|
|
74
|
+
"thread": Use threading (I/O-bound tasks like file loading)
|
|
75
|
+
"auto": Automatically select based on task characteristics
|
|
76
|
+
batch_size: Number of items per worker batch. If None, auto-calculated.
|
|
77
|
+
Larger batches reduce overhead but may cause load imbalance.
|
|
78
|
+
Smaller batches improve load balancing but increase overhead.
|
|
79
|
+
show_progress: Enable tqdm progress bar for long-running operations.
|
|
80
|
+
timeout: Maximum execution time per task in seconds. None for no timeout.
|
|
81
|
+
|
|
82
|
+
Example:
|
|
83
|
+
>>> # CPU-bound tasks (protocol decoding, FFT)
|
|
84
|
+
>>> config = ParallelConfig(num_workers=4, strategy="process")
|
|
85
|
+
>>>
|
|
86
|
+
>>> # I/O-bound tasks (file loading)
|
|
87
|
+
>>> config = ParallelConfig(num_workers=8, strategy="thread")
|
|
88
|
+
>>>
|
|
89
|
+
>>> # Auto-detect strategy
|
|
90
|
+
>>> config = ParallelConfig(strategy="auto")
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
num_workers: int | None = None
|
|
94
|
+
strategy: StrategyType = "auto"
|
|
95
|
+
batch_size: int | None = None
|
|
96
|
+
show_progress: bool = False
|
|
97
|
+
timeout: float | None = None
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None:
|
|
100
|
+
"""Validate configuration and set defaults."""
|
|
101
|
+
if self.num_workers is not None and self.num_workers < 1:
|
|
102
|
+
raise ValueError(f"num_workers must be positive, got {self.num_workers}")
|
|
103
|
+
|
|
104
|
+
if self.batch_size is not None and self.batch_size < 1:
|
|
105
|
+
raise ValueError(f"batch_size must be positive, got {self.batch_size}")
|
|
106
|
+
|
|
107
|
+
if self.timeout is not None and self.timeout <= 0:
|
|
108
|
+
raise ValueError(f"timeout must be positive, got {self.timeout}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class WorkerStats:
|
|
113
|
+
"""Statistics for individual worker performance.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
worker_id: Worker identifier (0-indexed).
|
|
117
|
+
tasks_completed: Number of tasks processed by this worker.
|
|
118
|
+
execution_time: Total execution time for this worker in seconds.
|
|
119
|
+
errors: Number of errors encountered by this worker.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
worker_id: int
|
|
123
|
+
tasks_completed: int
|
|
124
|
+
execution_time: float
|
|
125
|
+
errors: int
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class ParallelResult:
|
|
130
|
+
"""Results from parallel processing operation.
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
results: List of results from each task (same order as input).
|
|
134
|
+
execution_time: Total wall-clock time in seconds.
|
|
135
|
+
speedup: Speedup factor vs sequential execution (estimated or measured).
|
|
136
|
+
worker_stats: Per-worker performance statistics.
|
|
137
|
+
num_workers: Number of workers used.
|
|
138
|
+
strategy: Parallelization strategy used ("process" or "thread").
|
|
139
|
+
errors: List of (index, exception) tuples for failed tasks.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> result = processor.map(decode_fn, messages)
|
|
143
|
+
>>> print(f"Processed {len(result.results)} items in {result.execution_time:.2f}s")
|
|
144
|
+
>>> print(f"Speedup: {result.speedup:.2f}x vs sequential")
|
|
145
|
+
>>> print(f"Workers: {result.num_workers}")
|
|
146
|
+
>>> if result.errors:
|
|
147
|
+
... print(f"Failed tasks: {len(result.errors)}")
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
results: list[Any]
|
|
151
|
+
execution_time: float
|
|
152
|
+
speedup: float
|
|
153
|
+
worker_stats: list[WorkerStats] = field(default_factory=list)
|
|
154
|
+
num_workers: int = 1
|
|
155
|
+
strategy: str = "sequential"
|
|
156
|
+
errors: list[tuple[int, Exception]] = field(default_factory=list)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class ParallelProcessor:
|
|
160
|
+
"""Parallel processing manager for hardware analysis tasks.
|
|
161
|
+
|
|
162
|
+
This class provides high-level parallelization APIs for common hardware
|
|
163
|
+
reverse engineering workflows. It automatically manages worker pools,
|
|
164
|
+
distributes work efficiently, aggregates results, and tracks progress.
|
|
165
|
+
|
|
166
|
+
Supported patterns:
|
|
167
|
+
- map: Apply function to each item independently (embarrassingly parallel)
|
|
168
|
+
- batch_process: Process batches of items with custom aggregation
|
|
169
|
+
- pipeline: Chain multiple processing stages with different strategies
|
|
170
|
+
|
|
171
|
+
Worker management:
|
|
172
|
+
- Automatic worker count based on CPU topology
|
|
173
|
+
- Process pools for CPU-bound tasks (bypass GIL)
|
|
174
|
+
- Thread pools for I/O-bound tasks
|
|
175
|
+
- Graceful error handling (tasks fail independently)
|
|
176
|
+
- Timeout support for long-running tasks
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
>>> from oscura.utils.performance.parallel import ParallelProcessor, ParallelConfig
|
|
180
|
+
>>> # CPU-bound: Parallel FFT analysis
|
|
181
|
+
>>> config = ParallelConfig(num_workers=4, strategy="process")
|
|
182
|
+
>>> processor = ParallelProcessor(config)
|
|
183
|
+
>>> signals = [...] # List of signal arrays
|
|
184
|
+
>>> result = processor.map(compute_fft, signals)
|
|
185
|
+
>>>
|
|
186
|
+
>>> # I/O-bound: Parallel file loading
|
|
187
|
+
>>> config = ParallelConfig(num_workers=8, strategy="thread")
|
|
188
|
+
>>> processor = ParallelProcessor(config)
|
|
189
|
+
>>> files = [Path("file1.wfm"), Path("file2.wfm")]
|
|
190
|
+
>>> result = processor.map(load_waveform, files)
|
|
191
|
+
>>>
|
|
192
|
+
>>> # Batch processing with custom aggregation
|
|
193
|
+
>>> def process_batch(messages):
|
|
194
|
+
... return analyze_protocol_batch(messages)
|
|
195
|
+
>>> result = processor.batch_process(all_messages, batch_fn=process_batch)
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, config: ParallelConfig | None = None) -> None:
|
|
199
|
+
"""Initialize parallel processor with configuration.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
config: Parallel processing configuration. If None, uses defaults.
|
|
203
|
+
|
|
204
|
+
Example:
|
|
205
|
+
>>> # Default configuration (auto-detect strategy)
|
|
206
|
+
>>> processor = ParallelProcessor()
|
|
207
|
+
>>>
|
|
208
|
+
>>> # Custom configuration
|
|
209
|
+
>>> config = ParallelConfig(num_workers=4, strategy="process")
|
|
210
|
+
>>> processor = ParallelProcessor(config)
|
|
211
|
+
"""
|
|
212
|
+
self.config = config or ParallelConfig()
|
|
213
|
+
self._cpu_count = mp.cpu_count()
|
|
214
|
+
|
|
215
|
+
logger.debug(
|
|
216
|
+
f"ParallelProcessor initialized: strategy={self.config.strategy}, "
|
|
217
|
+
f"workers={self._get_worker_count()}, cpus={self._cpu_count}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def map(
|
|
221
|
+
self,
|
|
222
|
+
func: Callable[[Any], Any],
|
|
223
|
+
items: Sequence[Any],
|
|
224
|
+
sequential_time: float | None = None,
|
|
225
|
+
) -> ParallelResult:
|
|
226
|
+
"""Apply function to each item in parallel.
|
|
227
|
+
|
|
228
|
+
This is the primary method for embarrassingly parallel tasks where
|
|
229
|
+
each item can be processed independently without shared state.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
func: Function to apply to each item. Must be picklable for process strategy.
|
|
233
|
+
items: Sequence of items to process.
|
|
234
|
+
sequential_time: Optional baseline sequential execution time for
|
|
235
|
+
accurate speedup calculation. If None, speedup is estimated.
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
ParallelResult with results, timing, and worker statistics.
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If items is empty.
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
>>> # Parallel protocol decoding
|
|
245
|
+
>>> def decode(msg):
|
|
246
|
+
... return protocol.decode(msg)
|
|
247
|
+
>>> messages = [b"\\x01\\x02", b"\\x03\\x04", b"\\x05\\x06"]
|
|
248
|
+
>>> result = processor.map(decode, messages)
|
|
249
|
+
>>> decoded = result.results
|
|
250
|
+
>>>
|
|
251
|
+
>>> # With progress tracking
|
|
252
|
+
>>> config = ParallelConfig(show_progress=True)
|
|
253
|
+
>>> processor = ParallelProcessor(config)
|
|
254
|
+
>>> result = processor.map(expensive_function, large_dataset)
|
|
255
|
+
"""
|
|
256
|
+
if not items:
|
|
257
|
+
raise ValueError("Cannot process empty item list")
|
|
258
|
+
|
|
259
|
+
start_time = time.time()
|
|
260
|
+
|
|
261
|
+
# Determine strategy
|
|
262
|
+
strategy = self._resolve_strategy(func, items)
|
|
263
|
+
num_workers = self._get_worker_count(strategy)
|
|
264
|
+
|
|
265
|
+
logger.info(
|
|
266
|
+
f"Starting parallel map: {len(items)} items, {num_workers} workers, strategy={strategy}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Execute based on strategy
|
|
270
|
+
if strategy == "process":
|
|
271
|
+
results, errors = self._map_process(func, items, num_workers)
|
|
272
|
+
elif strategy == "thread":
|
|
273
|
+
results, errors = self._map_thread(func, items, num_workers)
|
|
274
|
+
else:
|
|
275
|
+
# Sequential fallback
|
|
276
|
+
results, errors = self._map_sequential(func, items)
|
|
277
|
+
num_workers = 1
|
|
278
|
+
strategy = "sequential"
|
|
279
|
+
|
|
280
|
+
execution_time = time.time() - start_time
|
|
281
|
+
|
|
282
|
+
# Calculate speedup
|
|
283
|
+
if sequential_time is not None:
|
|
284
|
+
speedup = sequential_time / execution_time
|
|
285
|
+
else:
|
|
286
|
+
# Estimate speedup (assumes linear scaling with slight overhead)
|
|
287
|
+
if strategy == "process":
|
|
288
|
+
speedup = min(num_workers * 0.85, num_workers) # 15% overhead
|
|
289
|
+
elif strategy == "thread":
|
|
290
|
+
speedup = min(num_workers * 0.6, 4.0) # I/O-bound limited to ~4x
|
|
291
|
+
else:
|
|
292
|
+
speedup = 1.0
|
|
293
|
+
|
|
294
|
+
logger.info(
|
|
295
|
+
f"Parallel map completed: {len(results)} results, "
|
|
296
|
+
f"time={execution_time:.2f}s, speedup={speedup:.2f}x, "
|
|
297
|
+
f"errors={len(errors)}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return ParallelResult(
|
|
301
|
+
results=results,
|
|
302
|
+
execution_time=execution_time,
|
|
303
|
+
speedup=speedup,
|
|
304
|
+
num_workers=num_workers,
|
|
305
|
+
strategy=strategy,
|
|
306
|
+
errors=errors,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
def batch_process(
|
|
310
|
+
self,
|
|
311
|
+
items: Sequence[Any],
|
|
312
|
+
batch_fn: Callable[[Sequence[Any]], Any],
|
|
313
|
+
batch_size: int | None = None,
|
|
314
|
+
) -> ParallelResult:
|
|
315
|
+
"""Process items in batches across workers.
|
|
316
|
+
|
|
317
|
+
Useful when:
|
|
318
|
+
- Items should be grouped for efficiency (e.g., database bulk inserts)
|
|
319
|
+
- Batch-level aggregation is needed (e.g., statistical analysis)
|
|
320
|
+
- Setup/teardown cost is high (e.g., model loading)
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
items: Sequence of items to process.
|
|
324
|
+
batch_fn: Function that processes a batch of items and returns result.
|
|
325
|
+
batch_size: Items per batch. If None, auto-calculated based on config.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
ParallelResult with batch results (one per batch).
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If items is empty.
|
|
332
|
+
|
|
333
|
+
Example:
|
|
334
|
+
>>> # Batch CRC recovery
|
|
335
|
+
>>> def recover_batch_crc(messages):
|
|
336
|
+
... return crc_reverser.analyze_batch(messages)
|
|
337
|
+
>>> all_messages = [...] # 10000 messages
|
|
338
|
+
>>> result = processor.batch_process(
|
|
339
|
+
... all_messages,
|
|
340
|
+
... batch_fn=recover_batch_crc,
|
|
341
|
+
... batch_size=100
|
|
342
|
+
... )
|
|
343
|
+
>>> # Result contains 100 batch results (100 messages each)
|
|
344
|
+
"""
|
|
345
|
+
if not items:
|
|
346
|
+
raise ValueError("Cannot process empty item list")
|
|
347
|
+
|
|
348
|
+
# Determine batch size
|
|
349
|
+
if batch_size is None:
|
|
350
|
+
batch_size = self._calculate_batch_size(len(items))
|
|
351
|
+
|
|
352
|
+
# Create batches
|
|
353
|
+
batches = [items[i : i + batch_size] for i in range(0, len(items), batch_size)]
|
|
354
|
+
|
|
355
|
+
logger.info(
|
|
356
|
+
f"Starting batch processing: {len(items)} items, "
|
|
357
|
+
f"{len(batches)} batches of size ~{batch_size}"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
# Process batches in parallel
|
|
361
|
+
return self.map(batch_fn, batches)
|
|
362
|
+
|
|
363
|
+
def pipeline(
|
|
364
|
+
self,
|
|
365
|
+
stages: Sequence[tuple[Callable[[Any], Any], StrategyType]],
|
|
366
|
+
items: Sequence[Any],
|
|
367
|
+
) -> ParallelResult:
|
|
368
|
+
"""Execute multi-stage pipeline with per-stage parallelization.
|
|
369
|
+
|
|
370
|
+
Each stage can use a different parallelization strategy based on
|
|
371
|
+
whether it's CPU-bound or I/O-bound. Results flow from one stage
|
|
372
|
+
to the next.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
stages: Sequence of (function, strategy) tuples defining pipeline.
|
|
376
|
+
Each function receives output from previous stage.
|
|
377
|
+
items: Initial items to process.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
ParallelResult from final stage.
|
|
381
|
+
|
|
382
|
+
Raises:
|
|
383
|
+
ValueError: If stages or items are empty.
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
>>> # Multi-stage analysis pipeline
|
|
387
|
+
>>> stages = [
|
|
388
|
+
... (load_signal, "thread"), # I/O-bound
|
|
389
|
+
... (compute_fft, "process"), # CPU-bound
|
|
390
|
+
... (detect_peaks, "process"), # CPU-bound
|
|
391
|
+
... (export_results, "thread"), # I/O-bound
|
|
392
|
+
... ]
|
|
393
|
+
>>> files = [Path("sig1.bin"), Path("sig2.bin")]
|
|
394
|
+
>>> result = processor.pipeline(stages, files)
|
|
395
|
+
"""
|
|
396
|
+
if not stages:
|
|
397
|
+
raise ValueError("Pipeline must have at least one stage")
|
|
398
|
+
if not items:
|
|
399
|
+
raise ValueError("Cannot process empty item list")
|
|
400
|
+
|
|
401
|
+
current_items = items
|
|
402
|
+
total_time = 0.0
|
|
403
|
+
|
|
404
|
+
for i, (func, strategy) in enumerate(stages):
|
|
405
|
+
logger.info(f"Executing pipeline stage {i + 1}/{len(stages)}: strategy={strategy}")
|
|
406
|
+
|
|
407
|
+
# Save original strategy and override for this stage
|
|
408
|
+
original_strategy = self.config.strategy
|
|
409
|
+
self.config.strategy = strategy
|
|
410
|
+
|
|
411
|
+
result = self.map(func, current_items)
|
|
412
|
+
|
|
413
|
+
# Restore original strategy
|
|
414
|
+
self.config.strategy = original_strategy
|
|
415
|
+
|
|
416
|
+
current_items = result.results
|
|
417
|
+
total_time += result.execution_time
|
|
418
|
+
|
|
419
|
+
if result.errors:
|
|
420
|
+
logger.warning(f"Stage {i + 1} completed with {len(result.errors)} errors")
|
|
421
|
+
|
|
422
|
+
logger.info(f"Pipeline completed: {len(stages)} stages, total_time={total_time:.2f}s")
|
|
423
|
+
|
|
424
|
+
# Return result from final stage with cumulative timing
|
|
425
|
+
result.execution_time = total_time
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
# =========================================================================
|
|
429
|
+
# Internal Helper Methods
|
|
430
|
+
# =========================================================================
|
|
431
|
+
|
|
432
|
+
def _resolve_strategy(self, func: Callable[[Any], Any], items: Sequence[Any]) -> str:
|
|
433
|
+
"""Determine appropriate parallelization strategy.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
func: Function to be parallelized.
|
|
437
|
+
items: Items to process.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Strategy name: "process", "thread", or "sequential".
|
|
441
|
+
"""
|
|
442
|
+
if self.config.strategy != "auto":
|
|
443
|
+
return self.config.strategy
|
|
444
|
+
|
|
445
|
+
# Auto-detection heuristics
|
|
446
|
+
# 1. Small datasets (<10 items) - sequential is faster
|
|
447
|
+
if len(items) < 10:
|
|
448
|
+
logger.debug("Auto-selecting sequential (small dataset)")
|
|
449
|
+
return "sequential"
|
|
450
|
+
|
|
451
|
+
# 2. Function name heuristics
|
|
452
|
+
func_name = func.__name__.lower()
|
|
453
|
+
if any(
|
|
454
|
+
keyword in func_name for keyword in ["fft", "correlate", "decode", "analyze", "compute"]
|
|
455
|
+
):
|
|
456
|
+
logger.debug(f"Auto-selecting process (CPU-bound function: {func_name})")
|
|
457
|
+
return "process"
|
|
458
|
+
|
|
459
|
+
if any(keyword in func_name for keyword in ["load", "read", "fetch", "download"]):
|
|
460
|
+
logger.debug(f"Auto-selecting thread (I/O-bound function: {func_name})")
|
|
461
|
+
return "thread"
|
|
462
|
+
|
|
463
|
+
# 3. Default to process for medium+ datasets
|
|
464
|
+
logger.debug("Auto-selecting process (default for parallel workload)")
|
|
465
|
+
return "process"
|
|
466
|
+
|
|
467
|
+
def _get_worker_count(self, strategy: str = "process") -> int:
|
|
468
|
+
"""Get appropriate worker count for strategy.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
strategy: Parallelization strategy.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
Number of workers to use.
|
|
475
|
+
"""
|
|
476
|
+
if self.config.num_workers is not None:
|
|
477
|
+
return self.config.num_workers
|
|
478
|
+
|
|
479
|
+
# Auto-calculate based on strategy
|
|
480
|
+
if strategy == "process":
|
|
481
|
+
# Leave one core free for OS/orchestration
|
|
482
|
+
return max(1, self._cpu_count - 1)
|
|
483
|
+
elif strategy == "thread":
|
|
484
|
+
# I/O-bound tasks can use more threads than cores
|
|
485
|
+
return self._cpu_count * 2
|
|
486
|
+
else:
|
|
487
|
+
return 1
|
|
488
|
+
|
|
489
|
+
def _calculate_batch_size(self, total_items: int) -> int:
|
|
490
|
+
"""Calculate optimal batch size for dataset.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
total_items: Total number of items to process.
|
|
494
|
+
|
|
495
|
+
Returns:
|
|
496
|
+
Optimal batch size.
|
|
497
|
+
"""
|
|
498
|
+
if self.config.batch_size is not None:
|
|
499
|
+
return self.config.batch_size
|
|
500
|
+
|
|
501
|
+
num_workers = self._get_worker_count()
|
|
502
|
+
|
|
503
|
+
# Target: 4 batches per worker (allows good load balancing)
|
|
504
|
+
optimal_batches = num_workers * 4
|
|
505
|
+
batch_size = max(1, total_items // optimal_batches)
|
|
506
|
+
|
|
507
|
+
logger.debug(
|
|
508
|
+
f"Auto-calculated batch_size={batch_size} "
|
|
509
|
+
f"({total_items} items / {optimal_batches} batches)"
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return batch_size
|
|
513
|
+
|
|
514
|
+
def _map_process(
|
|
515
|
+
self,
|
|
516
|
+
func: Callable[[Any], Any],
|
|
517
|
+
items: Sequence[Any],
|
|
518
|
+
num_workers: int,
|
|
519
|
+
) -> tuple[list[Any], list[tuple[int, Exception]]]:
|
|
520
|
+
"""Execute map using process pool.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
func: Function to apply.
|
|
524
|
+
items: Items to process.
|
|
525
|
+
num_workers: Number of worker processes.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Tuple of (results list, errors list).
|
|
529
|
+
"""
|
|
530
|
+
results: list[Any] = [None] * len(items)
|
|
531
|
+
errors: list[tuple[int, Exception]] = []
|
|
532
|
+
|
|
533
|
+
try:
|
|
534
|
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
|
535
|
+
# Submit all tasks
|
|
536
|
+
futures = {executor.submit(func, item): idx for idx, item in enumerate(items)}
|
|
537
|
+
|
|
538
|
+
# Collect results with optional progress bar
|
|
539
|
+
iterator: Iterable[Any]
|
|
540
|
+
if self.config.show_progress:
|
|
541
|
+
try:
|
|
542
|
+
from tqdm import tqdm
|
|
543
|
+
|
|
544
|
+
iterator = tqdm(futures, total=len(items), desc="Processing")
|
|
545
|
+
except ImportError:
|
|
546
|
+
logger.warning("tqdm not available, progress bar disabled")
|
|
547
|
+
iterator = futures
|
|
548
|
+
else:
|
|
549
|
+
iterator = futures
|
|
550
|
+
|
|
551
|
+
for future in iterator:
|
|
552
|
+
idx = futures[future]
|
|
553
|
+
try:
|
|
554
|
+
result = future.result(timeout=self.config.timeout)
|
|
555
|
+
results[idx] = result
|
|
556
|
+
except Exception as e:
|
|
557
|
+
logger.warning(f"Task {idx} failed: {e}")
|
|
558
|
+
errors.append((idx, e))
|
|
559
|
+
results[idx] = None
|
|
560
|
+
|
|
561
|
+
except Exception as e:
|
|
562
|
+
logger.error(f"Process pool execution failed: {e}")
|
|
563
|
+
raise
|
|
564
|
+
|
|
565
|
+
return results, errors
|
|
566
|
+
|
|
567
|
+
def _map_thread(
|
|
568
|
+
self,
|
|
569
|
+
func: Callable[[Any], Any],
|
|
570
|
+
items: Sequence[Any],
|
|
571
|
+
num_workers: int,
|
|
572
|
+
) -> tuple[list[Any], list[tuple[int, Exception]]]:
|
|
573
|
+
"""Execute map using thread pool.
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
func: Function to apply.
|
|
577
|
+
items: Items to process.
|
|
578
|
+
num_workers: Number of worker threads.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Tuple of (results list, errors list).
|
|
582
|
+
"""
|
|
583
|
+
results: list[Any] = [None] * len(items)
|
|
584
|
+
errors: list[tuple[int, Exception]] = []
|
|
585
|
+
|
|
586
|
+
try:
|
|
587
|
+
with ThreadPoolExecutor(max_workers=num_workers) as executor:
|
|
588
|
+
# Submit all tasks
|
|
589
|
+
futures = {executor.submit(func, item): idx for idx, item in enumerate(items)}
|
|
590
|
+
|
|
591
|
+
# Collect results with optional progress bar
|
|
592
|
+
iterator: Iterable[Any]
|
|
593
|
+
if self.config.show_progress:
|
|
594
|
+
try:
|
|
595
|
+
from tqdm import tqdm
|
|
596
|
+
|
|
597
|
+
iterator = tqdm(futures, total=len(items), desc="Processing")
|
|
598
|
+
except ImportError:
|
|
599
|
+
logger.warning("tqdm not available, progress bar disabled")
|
|
600
|
+
iterator = futures
|
|
601
|
+
else:
|
|
602
|
+
iterator = futures
|
|
603
|
+
|
|
604
|
+
for future in iterator:
|
|
605
|
+
idx = futures[future]
|
|
606
|
+
try:
|
|
607
|
+
result = future.result(timeout=self.config.timeout)
|
|
608
|
+
results[idx] = result
|
|
609
|
+
except Exception as e:
|
|
610
|
+
logger.warning(f"Task {idx} failed: {e}")
|
|
611
|
+
errors.append((idx, e))
|
|
612
|
+
results[idx] = None
|
|
613
|
+
|
|
614
|
+
except Exception as e:
|
|
615
|
+
logger.error(f"Thread pool execution failed: {e}")
|
|
616
|
+
raise
|
|
617
|
+
|
|
618
|
+
return results, errors
|
|
619
|
+
|
|
620
|
+
def _map_sequential(
|
|
621
|
+
self,
|
|
622
|
+
func: Callable[[Any], Any],
|
|
623
|
+
items: Sequence[Any],
|
|
624
|
+
) -> tuple[list[Any], list[tuple[int, Exception]]]:
|
|
625
|
+
"""Execute map sequentially (fallback/baseline).
|
|
626
|
+
|
|
627
|
+
Args:
|
|
628
|
+
func: Function to apply.
|
|
629
|
+
items: Items to process.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
Tuple of (results list, errors list).
|
|
633
|
+
"""
|
|
634
|
+
results: list[Any] = []
|
|
635
|
+
errors: list[tuple[int, Exception]] = []
|
|
636
|
+
|
|
637
|
+
for idx, item in enumerate(items):
|
|
638
|
+
try:
|
|
639
|
+
result = func(item)
|
|
640
|
+
results.append(result)
|
|
641
|
+
except Exception as e:
|
|
642
|
+
logger.warning(f"Task {idx} failed: {e}")
|
|
643
|
+
errors.append((idx, e))
|
|
644
|
+
results.append(None)
|
|
645
|
+
|
|
646
|
+
return results, errors
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
__all__ = [
|
|
650
|
+
"ParallelConfig",
|
|
651
|
+
"ParallelProcessor",
|
|
652
|
+
"ParallelResult",
|
|
653
|
+
"WorkerStats",
|
|
654
|
+
]
|