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
|
@@ -1,93 +1,199 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Enhanced state machine inference using RPNI and EDSM algorithms.
|
|
2
2
|
|
|
3
|
-
Requirements addressed: PSI-002
|
|
3
|
+
Requirements addressed: PSI-002, Feature 29
|
|
4
4
|
|
|
5
5
|
This module infers protocol state machines from observed message sequences using
|
|
6
6
|
passive learning algorithms (no system interaction required).
|
|
7
7
|
|
|
8
8
|
Key capabilities:
|
|
9
9
|
- RPNI algorithm for passive DFA learning
|
|
10
|
+
- EDSM (Evidence-Driven State Merging) algorithm for improved inference
|
|
11
|
+
- State machines with guards and probabilistic transitions
|
|
12
|
+
- Export to DOT, PlantUML, and SMV formats
|
|
13
|
+
- Validation against captured sequences
|
|
10
14
|
- State merging to minimize automaton
|
|
11
|
-
- Export to DOT format for visualization
|
|
12
|
-
- Export to NetworkX graph for analysis
|
|
13
15
|
"""
|
|
14
16
|
|
|
15
17
|
from __future__ import annotations
|
|
16
18
|
|
|
17
19
|
from copy import deepcopy
|
|
18
|
-
from dataclasses import dataclass
|
|
19
|
-
from
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, ClassVar
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
@dataclass
|
|
23
26
|
class State:
|
|
24
|
-
"""A state in the inferred
|
|
27
|
+
"""A state in the inferred state machine.
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
Represents a state with metadata for protocol state machines.
|
|
27
30
|
|
|
28
31
|
Attributes:
|
|
29
32
|
id: Unique state identifier
|
|
30
33
|
name: Human-readable state name
|
|
31
34
|
is_initial: Whether this is the initial state
|
|
32
|
-
is_accepting: Whether this is an accepting state
|
|
35
|
+
is_accepting: Whether this is an accepting/final state (alias: is_final)
|
|
36
|
+
is_error: Whether this is an error state
|
|
37
|
+
metadata: Additional state information (dict)
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> state = State(id=0, name="IDLE", is_initial=True)
|
|
41
|
+
>>> state.is_initial
|
|
42
|
+
True
|
|
43
|
+
>>> state.metadata["description"] = "Waiting for connection"
|
|
33
44
|
"""
|
|
34
45
|
|
|
35
46
|
id: int
|
|
36
47
|
name: str
|
|
37
48
|
is_initial: bool = False
|
|
38
49
|
is_accepting: bool = False
|
|
50
|
+
is_error: bool = False
|
|
51
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_final(self) -> bool:
|
|
55
|
+
"""Alias for is_accepting (more intuitive for state machines).
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
True if this is a final state
|
|
59
|
+
"""
|
|
60
|
+
return self.is_accepting
|
|
61
|
+
|
|
62
|
+
@is_final.setter
|
|
63
|
+
def is_final(self, value: bool) -> None:
|
|
64
|
+
"""Set final state status.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
value: Whether this is a final state
|
|
68
|
+
"""
|
|
69
|
+
self.is_accepting = value
|
|
39
70
|
|
|
40
71
|
|
|
41
72
|
@dataclass
|
|
42
73
|
class Transition:
|
|
43
|
-
"""A transition in the
|
|
74
|
+
"""A transition in the state machine.
|
|
44
75
|
|
|
45
|
-
|
|
76
|
+
Represents a state transition with optional guards and probabilities.
|
|
46
77
|
|
|
47
78
|
Attributes:
|
|
48
|
-
source: Source state ID
|
|
49
|
-
target: Target state ID
|
|
50
|
-
symbol: Transition label/
|
|
79
|
+
source: Source state ID (alias: from_state)
|
|
80
|
+
target: Target state ID (alias: to_state)
|
|
81
|
+
symbol: Transition label/event (alias: event)
|
|
82
|
+
guard: Optional condition for transition (e.g., "x > 10")
|
|
83
|
+
probability: Probability of transition (1.0 = deterministic)
|
|
51
84
|
count: Number of times observed
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> trans = Transition(source=0, target=1, symbol="CONNECT")
|
|
88
|
+
>>> trans.probability
|
|
89
|
+
1.0
|
|
90
|
+
>>> trans2 = Transition(source=0, target=2, symbol="TIMEOUT",
|
|
91
|
+
... guard="timer > 5", probability=0.1)
|
|
52
92
|
"""
|
|
53
93
|
|
|
54
94
|
source: int # State ID
|
|
55
95
|
target: int # State ID
|
|
56
|
-
symbol: str # Transition label
|
|
96
|
+
symbol: str | int # Transition label/event
|
|
97
|
+
guard: str | None = None # Condition for transition
|
|
98
|
+
probability: float = 1.0 # Probability (1.0 = deterministic)
|
|
57
99
|
count: int = 1 # Number of observations
|
|
58
100
|
|
|
101
|
+
@property
|
|
102
|
+
def from_state(self) -> int:
|
|
103
|
+
"""Alias for source state ID.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Source state ID
|
|
107
|
+
"""
|
|
108
|
+
return self.source
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def to_state(self) -> int:
|
|
112
|
+
"""Alias for target state ID.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Target state ID
|
|
116
|
+
"""
|
|
117
|
+
return self.target
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def event(self) -> str | int:
|
|
121
|
+
"""Alias for symbol/event.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Transition event/symbol
|
|
125
|
+
"""
|
|
126
|
+
return self.symbol
|
|
127
|
+
|
|
59
128
|
|
|
60
129
|
@dataclass
|
|
61
130
|
class FiniteAutomaton:
|
|
62
|
-
"""An inferred finite automaton.
|
|
131
|
+
"""An inferred finite automaton / state machine.
|
|
63
132
|
|
|
64
|
-
|
|
133
|
+
Complete automaton representation with export capabilities.
|
|
65
134
|
|
|
66
135
|
Attributes:
|
|
67
136
|
states: List of all states
|
|
68
137
|
transitions: List of all transitions
|
|
69
|
-
alphabet: Set of all symbols
|
|
138
|
+
alphabet: Set of all symbols/events
|
|
70
139
|
initial_state: Initial state ID
|
|
71
|
-
accepting_states: Set of accepting state IDs
|
|
140
|
+
accepting_states: Set of accepting/final state IDs (alias: final_states)
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
>>> states = [State(id=0, name="q0", is_initial=True),
|
|
144
|
+
... State(id=1, name="q1", is_accepting=True)]
|
|
145
|
+
>>> transitions = [Transition(source=0, target=1, symbol="A")]
|
|
146
|
+
>>> fa = FiniteAutomaton(states=states, transitions=transitions,
|
|
147
|
+
... alphabet={"A"}, initial_state=0,
|
|
148
|
+
... accepting_states={1})
|
|
149
|
+
>>> fa.accepts(["A"])
|
|
150
|
+
True
|
|
72
151
|
"""
|
|
73
152
|
|
|
74
153
|
states: list[State]
|
|
75
154
|
transitions: list[Transition]
|
|
76
|
-
alphabet: set[str]
|
|
155
|
+
alphabet: set[str | int]
|
|
77
156
|
initial_state: int
|
|
78
157
|
accepting_states: set[int]
|
|
79
158
|
|
|
159
|
+
@property
|
|
160
|
+
def final_states(self) -> set[int]:
|
|
161
|
+
"""Alias for accepting_states (more intuitive for state machines).
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Set of final state IDs
|
|
165
|
+
"""
|
|
166
|
+
return self.accepting_states
|
|
167
|
+
|
|
168
|
+
@final_states.setter
|
|
169
|
+
def final_states(self, value: set[int]) -> None:
|
|
170
|
+
"""Set final states.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
value: Set of final state IDs
|
|
174
|
+
"""
|
|
175
|
+
self.accepting_states = value
|
|
176
|
+
|
|
80
177
|
def to_dot(self) -> str:
|
|
81
178
|
"""Export to DOT format for Graphviz.
|
|
82
179
|
|
|
83
|
-
|
|
180
|
+
Generates GraphViz DOT format with:
|
|
181
|
+
- Double circles for final states
|
|
182
|
+
- Red circles for error states
|
|
183
|
+
- Transition labels with events and probabilities
|
|
184
|
+
- Guard conditions shown in labels
|
|
84
185
|
|
|
85
186
|
Returns:
|
|
86
187
|
DOT format string
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> fa = FiniteAutomaton(...)
|
|
191
|
+
>>> dot = fa.to_dot()
|
|
192
|
+
>>> Path("automaton.dot").write_text(dot)
|
|
87
193
|
"""
|
|
88
|
-
lines = ["digraph
|
|
194
|
+
lines = ["digraph StateMachine {", " rankdir=LR;", " node [shape=circle];"]
|
|
89
195
|
|
|
90
|
-
# Mark accepting states
|
|
196
|
+
# Mark accepting/final states
|
|
91
197
|
if self.accepting_states:
|
|
92
198
|
accepting_names = [s.name for s in self.states if s.id in self.accepting_states]
|
|
93
199
|
lines.append(f" node [shape=doublecircle]; {' '.join(accepting_names)};")
|
|
@@ -95,16 +201,32 @@ class FiniteAutomaton:
|
|
|
95
201
|
|
|
96
202
|
# Add invisible start node for initial state
|
|
97
203
|
initial_state = next(s for s in self.states if s.id == self.initial_state)
|
|
98
|
-
lines.append(
|
|
204
|
+
lines.append(" __start__ [shape=point];")
|
|
99
205
|
lines.append(f" __start__ -> {initial_state.name};")
|
|
100
206
|
|
|
207
|
+
# Add states with colors
|
|
208
|
+
for state in self.states:
|
|
209
|
+
if state.is_error:
|
|
210
|
+
lines.append(f" {state.name} [color=red];")
|
|
211
|
+
|
|
101
212
|
# Add transitions
|
|
102
213
|
for trans in self.transitions:
|
|
103
214
|
src_state = next(s for s in self.states if s.id == trans.source)
|
|
104
215
|
tgt_state = next(s for s in self.states if s.id == trans.target)
|
|
105
|
-
|
|
216
|
+
|
|
217
|
+
# Build label with event, guard, probability, count
|
|
218
|
+
label_parts = [str(trans.symbol)]
|
|
219
|
+
|
|
220
|
+
if trans.guard:
|
|
221
|
+
label_parts.append(f"[{trans.guard}]")
|
|
222
|
+
|
|
223
|
+
if trans.probability < 1.0:
|
|
224
|
+
label_parts.append(f"(p={trans.probability:.2f})")
|
|
225
|
+
|
|
106
226
|
if trans.count > 1:
|
|
107
|
-
|
|
227
|
+
label_parts.append(f"(cnt={trans.count})")
|
|
228
|
+
|
|
229
|
+
label = " ".join(label_parts)
|
|
108
230
|
lines.append(f' {src_state.name} -> {tgt_state.name} [label="{label}"];')
|
|
109
231
|
|
|
110
232
|
lines.append("}")
|
|
@@ -113,16 +235,22 @@ class FiniteAutomaton:
|
|
|
113
235
|
def to_networkx(self) -> Any:
|
|
114
236
|
"""Export to NetworkX graph.
|
|
115
237
|
|
|
116
|
-
|
|
238
|
+
Returns NetworkX MultiDiGraph for programmatic analysis.
|
|
117
239
|
|
|
118
240
|
Returns:
|
|
119
241
|
NetworkX MultiDiGraph (supports multiple edges between same nodes)
|
|
120
242
|
|
|
121
243
|
Raises:
|
|
122
244
|
ImportError: If NetworkX is not installed.
|
|
245
|
+
|
|
246
|
+
Example:
|
|
247
|
+
>>> fa = FiniteAutomaton(...)
|
|
248
|
+
>>> graph = fa.to_networkx()
|
|
249
|
+
>>> graph.number_of_nodes()
|
|
250
|
+
3
|
|
123
251
|
"""
|
|
124
252
|
try:
|
|
125
|
-
import networkx as nx
|
|
253
|
+
import networkx as nx
|
|
126
254
|
except ImportError as err:
|
|
127
255
|
raise ImportError("NetworkX is required for graph export") from err
|
|
128
256
|
|
|
@@ -136,24 +264,37 @@ class FiniteAutomaton:
|
|
|
136
264
|
name=state.name,
|
|
137
265
|
is_initial=state.is_initial,
|
|
138
266
|
is_accepting=state.is_accepting,
|
|
267
|
+
is_error=state.is_error,
|
|
268
|
+
metadata=state.metadata,
|
|
139
269
|
)
|
|
140
270
|
|
|
141
271
|
# Add edges
|
|
142
272
|
for trans in self.transitions:
|
|
143
|
-
G.add_edge(
|
|
273
|
+
G.add_edge(
|
|
274
|
+
trans.source,
|
|
275
|
+
trans.target,
|
|
276
|
+
symbol=trans.symbol,
|
|
277
|
+
guard=trans.guard,
|
|
278
|
+
probability=trans.probability,
|
|
279
|
+
count=trans.count,
|
|
280
|
+
)
|
|
144
281
|
|
|
145
282
|
return G
|
|
146
283
|
|
|
147
|
-
def accepts(self, sequence: list[str]) -> bool:
|
|
284
|
+
def accepts(self, sequence: list[str | int]) -> bool:
|
|
148
285
|
"""Check if automaton accepts sequence.
|
|
149
286
|
|
|
150
|
-
|
|
287
|
+
Simulates execution on the sequence, following deterministic transitions.
|
|
151
288
|
|
|
152
289
|
Args:
|
|
153
|
-
sequence: List of symbols
|
|
290
|
+
sequence: List of symbols/events
|
|
154
291
|
|
|
155
292
|
Returns:
|
|
156
|
-
True if sequence is accepted
|
|
293
|
+
True if sequence is accepted (ends in accepting state)
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
>>> fa.accepts(["CONNECT", "DATA", "DISCONNECT"])
|
|
297
|
+
True
|
|
157
298
|
"""
|
|
158
299
|
current_state = self.initial_state
|
|
159
300
|
|
|
@@ -173,48 +314,115 @@ class FiniteAutomaton:
|
|
|
173
314
|
# Check if we ended in accepting state
|
|
174
315
|
return current_state in self.accepting_states
|
|
175
316
|
|
|
176
|
-
def get_successors(self, state_id: int) -> dict[str, int]:
|
|
317
|
+
def get_successors(self, state_id: int) -> dict[str | int, int]:
|
|
177
318
|
"""Get successor states from given state.
|
|
178
319
|
|
|
179
|
-
|
|
320
|
+
Finds all outgoing transitions from a state.
|
|
180
321
|
|
|
181
322
|
Args:
|
|
182
323
|
state_id: State ID to query
|
|
183
324
|
|
|
184
325
|
Returns:
|
|
185
326
|
Dictionary mapping symbols to target state IDs
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
>>> fa.get_successors(0)
|
|
330
|
+
{'A': 1, 'B': 2}
|
|
186
331
|
"""
|
|
187
|
-
successors = {}
|
|
332
|
+
successors: dict[str | int, int] = {}
|
|
188
333
|
for trans in self.transitions:
|
|
189
334
|
if trans.source == state_id:
|
|
190
335
|
successors[trans.symbol] = trans.target
|
|
191
336
|
return successors
|
|
192
337
|
|
|
193
338
|
|
|
339
|
+
# Alias for backward compatibility
|
|
340
|
+
StateMachine = FiniteAutomaton
|
|
341
|
+
|
|
342
|
+
|
|
194
343
|
class StateMachineInferrer:
|
|
195
|
-
"""Infer state machines using passive learning.
|
|
344
|
+
"""Infer state machines using passive learning algorithms.
|
|
196
345
|
|
|
197
|
-
|
|
346
|
+
Implements RPNI and EDSM algorithms for DFA inference from traces.
|
|
198
347
|
|
|
199
348
|
The RPNI (Regular Positive and Negative Inference) algorithm:
|
|
200
349
|
1. Build Prefix Tree Acceptor from positive samples
|
|
201
350
|
2. Iteratively merge compatible state pairs
|
|
202
351
|
3. Validate against negative samples
|
|
203
352
|
4. Converge to minimal consistent DFA
|
|
353
|
+
|
|
354
|
+
The EDSM (Evidence-Driven State Merging) algorithm:
|
|
355
|
+
1. Build Prefix Tree Acceptor
|
|
356
|
+
2. Score state pairs by evidence (shared suffix behavior)
|
|
357
|
+
3. Merge highest-scoring compatible pairs first
|
|
358
|
+
4. More accurate than RPNI for noisy data
|
|
359
|
+
|
|
360
|
+
Example:
|
|
361
|
+
>>> inferrer = StateMachineInferrer(algorithm="edsm")
|
|
362
|
+
>>> positive = [["CONNECT", "DATA", "CLOSE"], ["CONNECT", "CLOSE"]]
|
|
363
|
+
>>> negative = [["DATA", "CONNECT"], ["CLOSE", "DATA"]]
|
|
364
|
+
>>> sm = inferrer.extract(positive, negative)
|
|
365
|
+
>>> sm.accepts(["CONNECT", "DATA", "CLOSE"])
|
|
366
|
+
True
|
|
204
367
|
"""
|
|
205
368
|
|
|
206
|
-
|
|
207
|
-
|
|
369
|
+
ALGORITHMS: ClassVar[list[str]] = ["rpni", "edsm"] # Supported algorithms
|
|
370
|
+
|
|
371
|
+
def __init__(self, algorithm: str = "rpni") -> None:
|
|
372
|
+
"""Initialize inferrer with algorithm choice.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
algorithm: Algorithm to use ("rpni" or "edsm")
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
ValueError: If algorithm is not supported
|
|
379
|
+
"""
|
|
380
|
+
if algorithm not in self.ALGORITHMS:
|
|
381
|
+
raise ValueError(
|
|
382
|
+
f"Algorithm '{algorithm}' not supported. Choose from: {self.ALGORITHMS}"
|
|
383
|
+
)
|
|
384
|
+
self.algorithm = algorithm
|
|
208
385
|
self._next_state_id = 0
|
|
209
386
|
|
|
387
|
+
def extract(
|
|
388
|
+
self,
|
|
389
|
+
positive_sequences: list[list[str | int]],
|
|
390
|
+
negative_sequences: list[list[str | int]] | None = None,
|
|
391
|
+
) -> StateMachine:
|
|
392
|
+
"""Extract state machine from sequences.
|
|
393
|
+
|
|
394
|
+
Main entry point for state machine extraction.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
positive_sequences: Sequences that should be accepted
|
|
398
|
+
negative_sequences: Sequences that should be rejected (optional)
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Inferred StateMachine
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
ValueError: If no positive sequences provided
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
>>> inferrer = StateMachineInferrer()
|
|
408
|
+
>>> positive = [["A", "B"], ["A", "C"]]
|
|
409
|
+
>>> sm = inferrer.extract(positive)
|
|
410
|
+
"""
|
|
411
|
+
if self.algorithm == "rpni":
|
|
412
|
+
return self._rpni(positive_sequences, negative_sequences or [])
|
|
413
|
+
elif self.algorithm == "edsm":
|
|
414
|
+
return self._edsm(positive_sequences, negative_sequences or [])
|
|
415
|
+
else:
|
|
416
|
+
raise ValueError(f"Unknown algorithm: {self.algorithm}")
|
|
417
|
+
|
|
210
418
|
def infer(
|
|
211
419
|
self,
|
|
212
|
-
positive_traces: list[list[str]] | None = None,
|
|
213
|
-
negative_traces: list[list[str]] | None = None,
|
|
214
|
-
positive_samples: list[list[str]] | None = None,
|
|
215
|
-
negative_samples: list[list[str]] | None = None,
|
|
420
|
+
positive_traces: list[list[str | int]] | None = None,
|
|
421
|
+
negative_traces: list[list[str | int]] | None = None,
|
|
422
|
+
positive_samples: list[list[str | int]] | None = None,
|
|
423
|
+
negative_samples: list[list[str | int]] | None = None,
|
|
216
424
|
) -> FiniteAutomaton:
|
|
217
|
-
"""Infer DFA from traces (
|
|
425
|
+
"""Infer DFA from traces (backward compatibility API).
|
|
218
426
|
|
|
219
427
|
Args:
|
|
220
428
|
positive_traces: List of accepted sequences.
|
|
@@ -235,14 +443,14 @@ class StateMachineInferrer:
|
|
|
235
443
|
if pos is None:
|
|
236
444
|
raise ValueError("Must provide either positive_traces or positive_samples")
|
|
237
445
|
|
|
238
|
-
return self.
|
|
446
|
+
return self.extract(pos, neg)
|
|
239
447
|
|
|
240
448
|
def infer_rpni(
|
|
241
|
-
self,
|
|
449
|
+
self,
|
|
450
|
+
positive_traces: list[list[str | int]],
|
|
451
|
+
negative_traces: list[list[str | int]] | None = None,
|
|
242
452
|
) -> FiniteAutomaton:
|
|
243
|
-
"""Infer DFA using RPNI (
|
|
244
|
-
|
|
245
|
-
: Complete RPNI algorithm.
|
|
453
|
+
"""Infer DFA using RPNI (backward compatibility API).
|
|
246
454
|
|
|
247
455
|
Args:
|
|
248
456
|
positive_traces: List of accepted sequences (list of symbols)
|
|
@@ -254,17 +462,46 @@ class StateMachineInferrer:
|
|
|
254
462
|
Raises:
|
|
255
463
|
ValueError: If no positive traces provided.
|
|
256
464
|
"""
|
|
257
|
-
|
|
465
|
+
return self._rpni(positive_traces, negative_traces or [])
|
|
466
|
+
|
|
467
|
+
def _rpni(
|
|
468
|
+
self,
|
|
469
|
+
positive: list[list[str | int]],
|
|
470
|
+
negative: list[list[str | int]],
|
|
471
|
+
) -> StateMachine:
|
|
472
|
+
"""RPNI algorithm: builds prefix tree, then merges states.
|
|
473
|
+
|
|
474
|
+
Regular Positive and Negative Inference algorithm.
|
|
475
|
+
|
|
476
|
+
Steps:
|
|
477
|
+
1. Build prefix tree acceptor (PTA) from positive examples
|
|
478
|
+
2. Order states (lexicographic by state ID)
|
|
479
|
+
3. For each pair of states (red, blue):
|
|
480
|
+
- Try merging blue into red
|
|
481
|
+
- Check if merge still rejects all negative examples
|
|
482
|
+
- If yes, keep merge; if no, color blue as red and continue
|
|
483
|
+
4. Return resulting automaton
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
positive: Positive example sequences
|
|
487
|
+
negative: Negative example sequences
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Inferred StateMachine
|
|
491
|
+
|
|
492
|
+
Raises:
|
|
493
|
+
ValueError: If no positive sequences provided
|
|
494
|
+
"""
|
|
495
|
+
if not positive:
|
|
258
496
|
raise ValueError("Need at least one positive trace")
|
|
259
497
|
|
|
260
498
|
# Build alphabet from all traces
|
|
261
|
-
alphabet: set[str] = set()
|
|
262
|
-
|
|
263
|
-
for trace in positive_traces + neg_traces:
|
|
499
|
+
alphabet: set[str | int] = set()
|
|
500
|
+
for trace in positive + negative:
|
|
264
501
|
alphabet.update(trace)
|
|
265
502
|
|
|
266
503
|
# Build Prefix Tree Acceptor from positive traces
|
|
267
|
-
pta = self.
|
|
504
|
+
pta = self._build_prefix_tree(positive)
|
|
268
505
|
|
|
269
506
|
# RPNI merging process
|
|
270
507
|
automaton = pta
|
|
@@ -277,7 +514,7 @@ class StateMachineInferrer:
|
|
|
277
514
|
|
|
278
515
|
# Try to merge states[i] with any earlier state
|
|
279
516
|
for j in range(i):
|
|
280
|
-
if self.
|
|
517
|
+
if self._is_merge_compatible(automaton, states[j], states[i], negative):
|
|
281
518
|
# Merge states[i] into states[j]
|
|
282
519
|
automaton = self._merge_states(automaton, states[j], states[i])
|
|
283
520
|
# Update state list
|
|
@@ -290,35 +527,125 @@ class StateMachineInferrer:
|
|
|
290
527
|
|
|
291
528
|
return automaton
|
|
292
529
|
|
|
293
|
-
def
|
|
294
|
-
|
|
530
|
+
def _edsm(
|
|
531
|
+
self,
|
|
532
|
+
positive: list[list[str | int]],
|
|
533
|
+
negative: list[list[str | int]],
|
|
534
|
+
) -> StateMachine:
|
|
535
|
+
"""EDSM algorithm: evidence-driven state merging.
|
|
536
|
+
|
|
537
|
+
Evidence-Driven State Merging algorithm (more accurate than RPNI).
|
|
538
|
+
|
|
539
|
+
Steps:
|
|
540
|
+
1. Build prefix tree acceptor (PTA)
|
|
541
|
+
2. Compute evidence scores for all state pairs
|
|
542
|
+
3. Sort pairs by score (higher = more evidence they should merge)
|
|
543
|
+
4. Try merging in score order, keeping compatible merges
|
|
544
|
+
5. Return resulting automaton
|
|
545
|
+
|
|
546
|
+
Evidence score = number of shared suffix behaviors (transitions)
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
positive: Positive example sequences
|
|
550
|
+
negative: Negative example sequences
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Inferred StateMachine
|
|
554
|
+
|
|
555
|
+
Raises:
|
|
556
|
+
ValueError: If no positive sequences provided
|
|
557
|
+
"""
|
|
558
|
+
if not positive:
|
|
559
|
+
raise ValueError("Need at least one positive trace")
|
|
560
|
+
|
|
561
|
+
# Build Prefix Tree Acceptor
|
|
562
|
+
pta = self._build_prefix_tree(positive)
|
|
563
|
+
automaton = pta
|
|
564
|
+
|
|
565
|
+
# Iteratively merge states based on evidence
|
|
566
|
+
while True:
|
|
567
|
+
# Compute evidence scores for all state pairs
|
|
568
|
+
state_ids = [s.id for s in automaton.states]
|
|
569
|
+
best_pair: tuple[int, int] | None = None
|
|
570
|
+
best_score = 0
|
|
571
|
+
|
|
572
|
+
for i, state_a in enumerate(state_ids):
|
|
573
|
+
for state_b in state_ids[i + 1 :]:
|
|
574
|
+
# Don't merge initial state
|
|
575
|
+
if state_a == automaton.initial_state or state_b == automaton.initial_state:
|
|
576
|
+
continue
|
|
577
|
+
|
|
578
|
+
# Compute evidence score
|
|
579
|
+
score = self._compute_evidence(automaton, state_a, state_b)
|
|
580
|
+
|
|
581
|
+
if score > best_score:
|
|
582
|
+
# Check compatibility before considering
|
|
583
|
+
if self._is_merge_compatible(automaton, state_a, state_b, negative):
|
|
584
|
+
best_score = score
|
|
585
|
+
best_pair = (state_a, state_b)
|
|
586
|
+
|
|
587
|
+
# If no merge found, done
|
|
588
|
+
if best_pair is None:
|
|
589
|
+
break
|
|
590
|
+
|
|
591
|
+
# Merge best pair
|
|
592
|
+
automaton = self._merge_states(automaton, best_pair[0], best_pair[1])
|
|
593
|
+
|
|
594
|
+
return automaton
|
|
595
|
+
|
|
596
|
+
def _compute_evidence(self, automaton: StateMachine, state_a: int, state_b: int) -> int:
|
|
597
|
+
"""Compute evidence score for merging two states.
|
|
598
|
+
|
|
599
|
+
Evidence = number of symbols for which both states have transitions
|
|
600
|
+
to states that could also be merged (shared suffix behavior).
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
automaton: Current automaton
|
|
604
|
+
state_a: First state ID
|
|
605
|
+
state_b: Second state ID
|
|
606
|
+
|
|
607
|
+
Returns:
|
|
608
|
+
Evidence score (higher = more evidence for merge)
|
|
609
|
+
"""
|
|
610
|
+
succ_a = automaton.get_successors(state_a)
|
|
611
|
+
succ_b = automaton.get_successors(state_b)
|
|
295
612
|
|
|
296
|
-
|
|
613
|
+
# Count shared symbols (evidence)
|
|
614
|
+
shared_symbols = set(succ_a.keys()) & set(succ_b.keys())
|
|
615
|
+
|
|
616
|
+
# Simple evidence: number of shared transition symbols
|
|
617
|
+
return len(shared_symbols)
|
|
618
|
+
|
|
619
|
+
def _build_prefix_tree(self, sequences: list[list[str | int]]) -> StateMachine:
|
|
620
|
+
"""Build Prefix Tree Automaton (PTA) from sequences.
|
|
621
|
+
|
|
622
|
+
Each unique prefix gets a state. Transitions labeled with symbols.
|
|
623
|
+
All sequences accepted (final states at end of each sequence).
|
|
297
624
|
|
|
298
625
|
Args:
|
|
299
|
-
|
|
626
|
+
sequences: List of symbol sequences
|
|
300
627
|
|
|
301
628
|
Returns:
|
|
302
|
-
Prefix Tree
|
|
629
|
+
Prefix Tree Automaton as StateMachine
|
|
303
630
|
"""
|
|
304
631
|
# Reset state counter
|
|
305
632
|
self._next_state_id = 0
|
|
306
633
|
|
|
307
634
|
# Create initial state
|
|
308
635
|
initial_state = State(
|
|
309
|
-
id=self._get_next_state_id(), name="
|
|
636
|
+
id=self._get_next_state_id(), name="s0", is_initial=True, is_accepting=False
|
|
310
637
|
)
|
|
311
638
|
|
|
312
639
|
states: list[State] = [initial_state]
|
|
313
640
|
transitions: list[Transition] = []
|
|
314
|
-
alphabet: set[str] = set()
|
|
641
|
+
alphabet: set[str | int] = set()
|
|
315
642
|
|
|
316
|
-
# Build tree from
|
|
317
|
-
for
|
|
643
|
+
# Build tree from sequences
|
|
644
|
+
for seq in sequences:
|
|
318
645
|
current_state_id = initial_state.id
|
|
319
646
|
|
|
320
|
-
# Walk/build tree for this
|
|
321
|
-
for symbol in
|
|
647
|
+
# Walk/build tree for this sequence
|
|
648
|
+
for symbol in seq:
|
|
322
649
|
alphabet.add(symbol)
|
|
323
650
|
|
|
324
651
|
# Check if transition exists
|
|
@@ -326,6 +653,7 @@ class StateMachineInferrer:
|
|
|
326
653
|
for trans in transitions:
|
|
327
654
|
if trans.source == current_state_id and trans.symbol == symbol:
|
|
328
655
|
next_state_id = trans.target
|
|
656
|
+
trans.count += 1 # Increment observation count
|
|
329
657
|
break
|
|
330
658
|
|
|
331
659
|
if next_state_id is None:
|
|
@@ -333,7 +661,7 @@ class StateMachineInferrer:
|
|
|
333
661
|
new_state_id = self._get_next_state_id()
|
|
334
662
|
new_state = State(
|
|
335
663
|
id=new_state_id,
|
|
336
|
-
name=f"
|
|
664
|
+
name=f"s{new_state_id}",
|
|
337
665
|
is_initial=False,
|
|
338
666
|
is_accepting=False,
|
|
339
667
|
)
|
|
@@ -355,7 +683,7 @@ class StateMachineInferrer:
|
|
|
355
683
|
|
|
356
684
|
accepting_states = {s.id for s in states if s.is_accepting}
|
|
357
685
|
|
|
358
|
-
return
|
|
686
|
+
return StateMachine(
|
|
359
687
|
states=states,
|
|
360
688
|
transitions=transitions,
|
|
361
689
|
alphabet=alphabet,
|
|
@@ -363,22 +691,18 @@ class StateMachineInferrer:
|
|
|
363
691
|
accepting_states=accepting_states,
|
|
364
692
|
)
|
|
365
693
|
|
|
366
|
-
def _merge_states(
|
|
367
|
-
|
|
368
|
-
) -> FiniteAutomaton:
|
|
369
|
-
"""Merge two states in automaton.
|
|
694
|
+
def _merge_states(self, automaton: StateMachine, state_a: int, state_b: int) -> StateMachine:
|
|
695
|
+
"""Merge two states, updating all transitions.
|
|
370
696
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
Merges state_b into state_a.
|
|
697
|
+
Merges state_b into state_a (state_b is removed).
|
|
374
698
|
|
|
375
699
|
Args:
|
|
376
|
-
automaton: Current
|
|
700
|
+
automaton: Current state machine
|
|
377
701
|
state_a: Target state ID (survives)
|
|
378
702
|
state_b: Source state ID (removed)
|
|
379
703
|
|
|
380
704
|
Returns:
|
|
381
|
-
New
|
|
705
|
+
New state machine with merged states
|
|
382
706
|
"""
|
|
383
707
|
# Deep copy to avoid modifying original
|
|
384
708
|
new_automaton = deepcopy(automaton)
|
|
@@ -418,46 +742,32 @@ class StateMachineInferrer:
|
|
|
418
742
|
|
|
419
743
|
return new_automaton
|
|
420
744
|
|
|
421
|
-
def
|
|
745
|
+
def _is_merge_compatible(
|
|
422
746
|
self,
|
|
423
|
-
automaton:
|
|
747
|
+
automaton: StateMachine,
|
|
424
748
|
state_a: int,
|
|
425
749
|
state_b: int,
|
|
426
|
-
|
|
750
|
+
negative: list[list[str | int]],
|
|
427
751
|
) -> bool:
|
|
428
|
-
"""Check if
|
|
752
|
+
"""Check if merging would accept negative sequences.
|
|
429
753
|
|
|
430
|
-
|
|
754
|
+
Tests whether merging two states would cause the automaton
|
|
755
|
+
to accept any negative example sequences.
|
|
431
756
|
|
|
432
757
|
Args:
|
|
433
|
-
automaton: Current
|
|
758
|
+
automaton: Current state machine
|
|
434
759
|
state_a: First state ID
|
|
435
760
|
state_b: Second state ID
|
|
436
|
-
|
|
761
|
+
negative: Negative example sequences
|
|
437
762
|
|
|
438
763
|
Returns:
|
|
439
|
-
True if states
|
|
764
|
+
True if states can be merged without accepting negatives
|
|
440
765
|
"""
|
|
441
|
-
# Get accepting status
|
|
442
|
-
_a_accepting = state_a in automaton.accepting_states
|
|
443
|
-
_b_accepting = state_b in automaton.accepting_states
|
|
444
|
-
|
|
445
|
-
# If one is accepting and other is not, they might still be compatible
|
|
446
|
-
# (we'll merge accepting status), but check negative traces
|
|
447
|
-
|
|
448
766
|
# Try merging and test
|
|
449
767
|
test_automaton = self._merge_states(automaton, state_a, state_b)
|
|
450
768
|
|
|
451
769
|
# Check that no negative traces are accepted
|
|
452
|
-
for neg_trace in
|
|
453
|
-
if test_automaton.accepts(neg_trace):
|
|
454
|
-
return False
|
|
455
|
-
|
|
456
|
-
# Recursively check successor compatibility
|
|
457
|
-
_succ_a = test_automaton.get_successors(state_a)
|
|
458
|
-
# state_b has been merged, so its successors are now in state_a
|
|
459
|
-
|
|
460
|
-
return True
|
|
770
|
+
return all(not test_automaton.accepts(neg_trace) for neg_trace in negative)
|
|
461
771
|
|
|
462
772
|
def _get_next_state_id(self) -> int:
|
|
463
773
|
"""Get next available state ID.
|
|
@@ -470,19 +780,221 @@ class StateMachineInferrer:
|
|
|
470
780
|
return state_id
|
|
471
781
|
|
|
472
782
|
|
|
473
|
-
|
|
474
|
-
"""
|
|
783
|
+
class StateMachineExtractor:
|
|
784
|
+
"""Enhanced state machine extraction with multiple algorithms.
|
|
785
|
+
|
|
786
|
+
Provides high-level API for state machine extraction with
|
|
787
|
+
export and validation capabilities.
|
|
788
|
+
|
|
789
|
+
Example:
|
|
790
|
+
>>> extractor = StateMachineExtractor(algorithm="edsm")
|
|
791
|
+
>>> positive = [["CONNECT", "DATA", "CLOSE"]]
|
|
792
|
+
>>> sm = extractor.extract(positive)
|
|
793
|
+
>>> extractor.export_graphviz(sm, Path("machine.dot"))
|
|
794
|
+
>>> extractor.export_plantuml(sm, Path("machine.puml"))
|
|
795
|
+
>>> accepted, rejected = extractor.validate_sequences(sm, positive)
|
|
796
|
+
>>> accepted == len(positive)
|
|
797
|
+
True
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
ALGORITHMS: ClassVar[list[str]] = StateMachineInferrer.ALGORITHMS
|
|
801
|
+
|
|
802
|
+
def __init__(self, algorithm: str = "rpni") -> None:
|
|
803
|
+
"""Initialize extractor with algorithm choice.
|
|
804
|
+
|
|
805
|
+
Args:
|
|
806
|
+
algorithm: Algorithm to use ("rpni" or "edsm")
|
|
807
|
+
"""
|
|
808
|
+
self.algorithm = algorithm
|
|
809
|
+
self._inferrer = StateMachineInferrer(algorithm=algorithm)
|
|
810
|
+
|
|
811
|
+
def extract(
|
|
812
|
+
self,
|
|
813
|
+
positive_sequences: list[list[str | int]],
|
|
814
|
+
negative_sequences: list[list[str | int]] | None = None,
|
|
815
|
+
) -> StateMachine:
|
|
816
|
+
"""Extract state machine from sequences.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
positive_sequences: Sequences that should be accepted
|
|
820
|
+
negative_sequences: Sequences that should be rejected (optional)
|
|
821
|
+
|
|
822
|
+
Returns:
|
|
823
|
+
Inferred StateMachine
|
|
824
|
+
"""
|
|
825
|
+
return self._inferrer.extract(positive_sequences, negative_sequences)
|
|
475
826
|
|
|
476
|
-
:
|
|
827
|
+
def export_graphviz(self, sm: StateMachine, output_path: Path) -> None:
|
|
828
|
+
"""Export as GraphViz DOT format.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
sm: State machine to export
|
|
832
|
+
output_path: Path to write DOT file
|
|
833
|
+
|
|
834
|
+
Example:
|
|
835
|
+
>>> extractor.export_graphviz(sm, Path("machine.dot"))
|
|
836
|
+
"""
|
|
837
|
+
dot_content = sm.to_dot()
|
|
838
|
+
output_path.write_text(dot_content)
|
|
839
|
+
|
|
840
|
+
def export_plantuml(self, sm: StateMachine, output_path: Path) -> None:
|
|
841
|
+
"""Export as PlantUML state diagram.
|
|
842
|
+
|
|
843
|
+
Generates PlantUML format for state diagrams.
|
|
844
|
+
|
|
845
|
+
Args:
|
|
846
|
+
sm: State machine to export
|
|
847
|
+
output_path: Path to write PlantUML file
|
|
848
|
+
|
|
849
|
+
Example:
|
|
850
|
+
>>> extractor.export_plantuml(sm, Path("machine.puml"))
|
|
851
|
+
"""
|
|
852
|
+
lines = ["@startuml"]
|
|
853
|
+
|
|
854
|
+
# Find initial state
|
|
855
|
+
initial_state = next(s for s in sm.states if s.id == sm.initial_state)
|
|
856
|
+
lines.append(f"[*] --> {initial_state.name}")
|
|
857
|
+
|
|
858
|
+
# Add states
|
|
859
|
+
for state in sm.states:
|
|
860
|
+
if state.is_error:
|
|
861
|
+
lines.append(f"{state.name} : <<error>>")
|
|
862
|
+
if state.metadata:
|
|
863
|
+
for key, value in state.metadata.items():
|
|
864
|
+
lines.append(f"{state.name} : {key}={value}")
|
|
865
|
+
|
|
866
|
+
# Add transitions
|
|
867
|
+
for trans in sm.transitions:
|
|
868
|
+
src_state = next(s for s in sm.states if s.id == trans.source)
|
|
869
|
+
tgt_state = next(s for s in sm.states if s.id == trans.target)
|
|
870
|
+
|
|
871
|
+
# Build label
|
|
872
|
+
label = str(trans.symbol)
|
|
873
|
+
if trans.guard:
|
|
874
|
+
label += f" [{trans.guard}]"
|
|
875
|
+
if trans.probability < 1.0:
|
|
876
|
+
label += f" (p={trans.probability:.2f})"
|
|
877
|
+
|
|
878
|
+
lines.append(f"{src_state.name} --> {tgt_state.name} : {label}")
|
|
879
|
+
|
|
880
|
+
# Add final states
|
|
881
|
+
for state_id in sm.accepting_states:
|
|
882
|
+
state = next(s for s in sm.states if s.id == state_id)
|
|
883
|
+
lines.append(f"{state.name} --> [*]")
|
|
884
|
+
|
|
885
|
+
lines.append("@enduml")
|
|
886
|
+
output_path.write_text("\n".join(lines))
|
|
887
|
+
|
|
888
|
+
def export_smv(self, sm: StateMachine, output_path: Path) -> None:
|
|
889
|
+
"""Export as SMV (Symbolic Model Verifier) format.
|
|
890
|
+
|
|
891
|
+
Generates NuSMV/SMV format for formal verification.
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
sm: State machine to export
|
|
895
|
+
output_path: Path to write SMV file
|
|
896
|
+
|
|
897
|
+
Example:
|
|
898
|
+
>>> extractor.export_smv(sm, Path("machine.smv"))
|
|
899
|
+
"""
|
|
900
|
+
lines = ["MODULE main", "VAR"]
|
|
901
|
+
|
|
902
|
+
# State variable
|
|
903
|
+
state_names = [s.name for s in sm.states]
|
|
904
|
+
lines.append(f" state : {{{', '.join(state_names)}}};")
|
|
905
|
+
|
|
906
|
+
# Event/input variable
|
|
907
|
+
alphabet_str = ", ".join(str(s) for s in sorted(sm.alphabet, key=str))
|
|
908
|
+
lines.append(f" event : {{{alphabet_str}}};")
|
|
909
|
+
|
|
910
|
+
# Initial state
|
|
911
|
+
lines.append("")
|
|
912
|
+
lines.append("ASSIGN")
|
|
913
|
+
initial_state_obj = next(s for s in sm.states if s.id == sm.initial_state)
|
|
914
|
+
lines.append(f" init(state) := {initial_state_obj.name};")
|
|
915
|
+
|
|
916
|
+
# Transition relation
|
|
917
|
+
lines.append(" next(state) := case")
|
|
918
|
+
|
|
919
|
+
for trans in sm.transitions:
|
|
920
|
+
src = next(s for s in sm.states if s.id == trans.source)
|
|
921
|
+
tgt = next(s for s in sm.states if s.id == trans.target)
|
|
922
|
+
|
|
923
|
+
# Build condition
|
|
924
|
+
condition = f"state = {src.name} & event = {trans.symbol}"
|
|
925
|
+
if trans.guard:
|
|
926
|
+
condition += f" & ({trans.guard})"
|
|
927
|
+
|
|
928
|
+
lines.append(f" {condition} : {tgt.name};")
|
|
929
|
+
|
|
930
|
+
lines.append(" TRUE : state;")
|
|
931
|
+
lines.append(" esac;")
|
|
932
|
+
|
|
933
|
+
# Specifications (accepting states)
|
|
934
|
+
if sm.accepting_states:
|
|
935
|
+
lines.append("")
|
|
936
|
+
lines.append("-- Final/Accepting states")
|
|
937
|
+
for state_id in sm.accepting_states:
|
|
938
|
+
state = next(s for s in sm.states if s.id == state_id)
|
|
939
|
+
lines.append(f"DEFINE final_{state.name} := state = {state.name};")
|
|
940
|
+
|
|
941
|
+
output_path.write_text("\n".join(lines))
|
|
942
|
+
|
|
943
|
+
def validate_sequences(
|
|
944
|
+
self, sm: StateMachine, sequences: list[list[str | int]]
|
|
945
|
+
) -> tuple[int, int]:
|
|
946
|
+
"""Validate sequences against state machine.
|
|
947
|
+
|
|
948
|
+
Tests sequences to count how many are accepted vs rejected.
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
sm: State machine
|
|
952
|
+
sequences: List of sequences to validate
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
Tuple of (accepted_count, rejected_count)
|
|
956
|
+
|
|
957
|
+
Example:
|
|
958
|
+
>>> accepted, rejected = extractor.validate_sequences(sm, test_seqs)
|
|
959
|
+
>>> print(f"Accepted: {accepted}/{len(test_seqs)}")
|
|
960
|
+
"""
|
|
961
|
+
accepted = 0
|
|
962
|
+
rejected = 0
|
|
963
|
+
|
|
964
|
+
for seq in sequences:
|
|
965
|
+
if sm.accepts(seq):
|
|
966
|
+
accepted += 1
|
|
967
|
+
else:
|
|
968
|
+
rejected += 1
|
|
969
|
+
|
|
970
|
+
return (accepted, rejected)
|
|
971
|
+
|
|
972
|
+
def minimize_automaton(self, sm: StateMachine) -> StateMachine:
|
|
973
|
+
"""Minimize DFA using Hopcroft's algorithm.
|
|
974
|
+
|
|
975
|
+
Args:
|
|
976
|
+
sm: State machine to minimize
|
|
977
|
+
|
|
978
|
+
Returns:
|
|
979
|
+
Minimized state machine
|
|
980
|
+
|
|
981
|
+
Example:
|
|
982
|
+
>>> minimized = extractor.minimize_automaton(sm)
|
|
983
|
+
>>> len(minimized.states) <= len(sm.states)
|
|
984
|
+
True
|
|
985
|
+
"""
|
|
986
|
+
return minimize_dfa(sm)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _initialize_partitions(automaton: FiniteAutomaton) -> list[set[int]]:
|
|
990
|
+
"""Initialize partitions with accepting and non-accepting states.
|
|
477
991
|
|
|
478
992
|
Args:
|
|
479
|
-
automaton: DFA to
|
|
993
|
+
automaton: DFA to partition
|
|
480
994
|
|
|
481
995
|
Returns:
|
|
482
|
-
|
|
996
|
+
Initial partition list
|
|
483
997
|
"""
|
|
484
|
-
# Use partition refinement (simplified version)
|
|
485
|
-
# Start with two partitions: accepting and non-accepting
|
|
486
998
|
accepting = automaton.accepting_states
|
|
487
999
|
non_accepting = {s.id for s in automaton.states if s.id not in accepting}
|
|
488
1000
|
|
|
@@ -491,73 +1003,173 @@ def minimize_dfa(automaton: FiniteAutomaton) -> FiniteAutomaton:
|
|
|
491
1003
|
partitions.append(accepting)
|
|
492
1004
|
if non_accepting:
|
|
493
1005
|
partitions.append(non_accepting)
|
|
1006
|
+
return partitions
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _find_target_partition(target: int, partitions: list[set[int]]) -> int | None:
|
|
1010
|
+
"""Find which partition a state belongs to.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
target: State ID to find
|
|
1014
|
+
partitions: Current partitions
|
|
1015
|
+
|
|
1016
|
+
Returns:
|
|
1017
|
+
Partition index or None if not found
|
|
1018
|
+
"""
|
|
1019
|
+
for i, p in enumerate(partitions):
|
|
1020
|
+
if target in p:
|
|
1021
|
+
return i
|
|
1022
|
+
return None
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _create_transition_signature(
|
|
1026
|
+
state_id: int,
|
|
1027
|
+
automaton: FiniteAutomaton,
|
|
1028
|
+
partitions: list[set[int]],
|
|
1029
|
+
) -> tuple[tuple[str | int, int | None], ...]:
|
|
1030
|
+
"""Create transition signature for partition refinement.
|
|
1031
|
+
|
|
1032
|
+
Args:
|
|
1033
|
+
state_id: State to create signature for
|
|
1034
|
+
automaton: Automaton containing the state
|
|
1035
|
+
partitions: Current partitions
|
|
494
1036
|
|
|
495
|
-
|
|
1037
|
+
Returns:
|
|
1038
|
+
Transition signature tuple
|
|
1039
|
+
"""
|
|
1040
|
+
successors = automaton.get_successors(state_id)
|
|
1041
|
+
signature_list: list[tuple[str | int, int | None]] = []
|
|
1042
|
+
|
|
1043
|
+
for symbol in sorted(automaton.alphabet, key=str):
|
|
1044
|
+
if symbol in successors:
|
|
1045
|
+
target = successors[symbol]
|
|
1046
|
+
target_partition = _find_target_partition(target, partitions)
|
|
1047
|
+
signature_list.append((symbol, target_partition))
|
|
1048
|
+
else:
|
|
1049
|
+
signature_list.append((symbol, None))
|
|
1050
|
+
|
|
1051
|
+
return tuple(signature_list)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _split_partition(
|
|
1055
|
+
partition: set[int],
|
|
1056
|
+
automaton: FiniteAutomaton,
|
|
1057
|
+
partitions: list[set[int]],
|
|
1058
|
+
) -> list[set[int]]:
|
|
1059
|
+
"""Split partition by grouping states with identical signatures.
|
|
1060
|
+
|
|
1061
|
+
Args:
|
|
1062
|
+
partition: Partition to split
|
|
1063
|
+
automaton: Automaton containing states
|
|
1064
|
+
partitions: Current partitions for signature creation
|
|
1065
|
+
|
|
1066
|
+
Returns:
|
|
1067
|
+
List of sub-partitions
|
|
1068
|
+
"""
|
|
1069
|
+
if len(partition) <= 1:
|
|
1070
|
+
return [partition]
|
|
1071
|
+
|
|
1072
|
+
groups: dict[tuple[tuple[str | int, int | None], ...], set[int]] = {}
|
|
1073
|
+
for state_id in partition:
|
|
1074
|
+
signature = _create_transition_signature(state_id, automaton, partitions)
|
|
1075
|
+
if signature not in groups:
|
|
1076
|
+
groups[signature] = set()
|
|
1077
|
+
groups[signature].add(state_id)
|
|
1078
|
+
|
|
1079
|
+
return list(groups.values())
|
|
1080
|
+
|
|
1081
|
+
|
|
1082
|
+
def _refine_partitions(
|
|
1083
|
+
automaton: FiniteAutomaton,
|
|
1084
|
+
partitions: list[set[int]],
|
|
1085
|
+
) -> list[set[int]]:
|
|
1086
|
+
"""Refine partitions until no more splits occur.
|
|
1087
|
+
|
|
1088
|
+
Args:
|
|
1089
|
+
automaton: DFA to minimize
|
|
1090
|
+
partitions: Initial partitions
|
|
1091
|
+
|
|
1092
|
+
Returns:
|
|
1093
|
+
Refined partitions
|
|
1094
|
+
"""
|
|
496
1095
|
changed = True
|
|
497
1096
|
while changed:
|
|
498
1097
|
changed = False
|
|
499
1098
|
new_partitions = []
|
|
500
1099
|
|
|
501
1100
|
for partition in partitions:
|
|
502
|
-
|
|
503
|
-
if len(
|
|
504
|
-
new_partitions.append(partition)
|
|
505
|
-
continue
|
|
506
|
-
|
|
507
|
-
# Group states by transition signatures
|
|
508
|
-
groups: dict[tuple[tuple[str, int | None], ...], set[int]] = {}
|
|
509
|
-
for state_id in partition:
|
|
510
|
-
successors = automaton.get_successors(state_id)
|
|
511
|
-
|
|
512
|
-
# Create signature based on which partition each successor is in
|
|
513
|
-
signature_list: list[tuple[str, int | None]] = []
|
|
514
|
-
for symbol in sorted(automaton.alphabet):
|
|
515
|
-
if symbol in successors:
|
|
516
|
-
target = successors[symbol]
|
|
517
|
-
# Find which partition target is in
|
|
518
|
-
target_partition: int | None = None
|
|
519
|
-
for i, p in enumerate(partitions):
|
|
520
|
-
if target in p:
|
|
521
|
-
target_partition = i
|
|
522
|
-
break
|
|
523
|
-
signature_list.append((symbol, target_partition))
|
|
524
|
-
else:
|
|
525
|
-
signature_list.append((symbol, None))
|
|
526
|
-
|
|
527
|
-
signature = tuple(signature_list)
|
|
528
|
-
if signature not in groups:
|
|
529
|
-
groups[signature] = set()
|
|
530
|
-
groups[signature].add(state_id)
|
|
531
|
-
|
|
532
|
-
# If we split, mark as changed
|
|
533
|
-
if len(groups) > 1:
|
|
1101
|
+
splits = _split_partition(partition, automaton, partitions)
|
|
1102
|
+
if len(splits) > 1:
|
|
534
1103
|
changed = True
|
|
535
|
-
|
|
536
|
-
new_partitions.extend(groups.values())
|
|
1104
|
+
new_partitions.extend(splits)
|
|
537
1105
|
|
|
538
1106
|
partitions = new_partitions
|
|
539
1107
|
|
|
540
|
-
|
|
541
|
-
|
|
1108
|
+
return partitions
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _build_state_mapping(partitions: list[set[int]]) -> dict[int, int]:
|
|
1112
|
+
"""Map original state IDs to partition IDs.
|
|
1113
|
+
|
|
1114
|
+
Args:
|
|
1115
|
+
partitions: Final partitions
|
|
1116
|
+
|
|
1117
|
+
Returns:
|
|
1118
|
+
Mapping from old state ID to new state ID
|
|
1119
|
+
"""
|
|
542
1120
|
state_to_partition = {}
|
|
543
1121
|
for i, partition in enumerate(partitions):
|
|
544
1122
|
for state_id in partition:
|
|
545
1123
|
state_to_partition[state_id] = i
|
|
1124
|
+
return state_to_partition
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def _create_minimized_states(
|
|
1128
|
+
partitions: list[set[int]],
|
|
1129
|
+
automaton: FiniteAutomaton,
|
|
1130
|
+
) -> list[State]:
|
|
1131
|
+
"""Create new states for minimized automaton.
|
|
1132
|
+
|
|
1133
|
+
Args:
|
|
1134
|
+
partitions: Final partitions
|
|
1135
|
+
automaton: Original automaton
|
|
546
1136
|
|
|
547
|
-
|
|
1137
|
+
Returns:
|
|
1138
|
+
List of new states
|
|
1139
|
+
"""
|
|
548
1140
|
new_states = []
|
|
549
1141
|
for i, partition in enumerate(partitions):
|
|
550
|
-
# Pick representative state
|
|
551
|
-
rep_id = min(partition)
|
|
552
|
-
_rep_state = next(s for s in automaton.states if s.id == rep_id)
|
|
553
|
-
|
|
554
1142
|
is_accepting = any(sid in automaton.accepting_states for sid in partition)
|
|
555
1143
|
is_initial = automaton.initial_state in partition
|
|
1144
|
+
is_error = any(
|
|
1145
|
+
next(s for s in automaton.states if s.id == sid).is_error for sid in partition
|
|
1146
|
+
)
|
|
556
1147
|
|
|
557
|
-
new_state = State(
|
|
1148
|
+
new_state = State(
|
|
1149
|
+
id=i,
|
|
1150
|
+
name=f"q{i}",
|
|
1151
|
+
is_initial=is_initial,
|
|
1152
|
+
is_accepting=is_accepting,
|
|
1153
|
+
is_error=is_error,
|
|
1154
|
+
)
|
|
558
1155
|
new_states.append(new_state)
|
|
559
1156
|
|
|
560
|
-
|
|
1157
|
+
return new_states
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _create_minimized_transitions(
|
|
1161
|
+
automaton: FiniteAutomaton,
|
|
1162
|
+
state_to_partition: dict[int, int],
|
|
1163
|
+
) -> list[Transition]:
|
|
1164
|
+
"""Create transitions for minimized automaton.
|
|
1165
|
+
|
|
1166
|
+
Args:
|
|
1167
|
+
automaton: Original automaton
|
|
1168
|
+
state_to_partition: Mapping from old to new state IDs
|
|
1169
|
+
|
|
1170
|
+
Returns:
|
|
1171
|
+
List of new transitions
|
|
1172
|
+
"""
|
|
561
1173
|
new_transitions = []
|
|
562
1174
|
seen_transitions = set()
|
|
563
1175
|
|
|
@@ -573,11 +1185,41 @@ def minimize_dfa(automaton: FiniteAutomaton) -> FiniteAutomaton:
|
|
|
573
1185
|
source=src_partition,
|
|
574
1186
|
target=tgt_partition,
|
|
575
1187
|
symbol=trans.symbol,
|
|
1188
|
+
guard=trans.guard,
|
|
1189
|
+
probability=trans.probability,
|
|
576
1190
|
count=trans.count,
|
|
577
1191
|
)
|
|
578
1192
|
)
|
|
579
1193
|
|
|
580
|
-
|
|
1194
|
+
return new_transitions
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def minimize_dfa(automaton: FiniteAutomaton) -> FiniteAutomaton:
|
|
1198
|
+
"""Minimize DFA using partition refinement.
|
|
1199
|
+
|
|
1200
|
+
Uses Hopcroft's algorithm for DFA minimization.
|
|
1201
|
+
|
|
1202
|
+
Args:
|
|
1203
|
+
automaton: DFA to minimize
|
|
1204
|
+
|
|
1205
|
+
Returns:
|
|
1206
|
+
Minimized FiniteAutomaton
|
|
1207
|
+
|
|
1208
|
+
Example:
|
|
1209
|
+
>>> minimized = minimize_dfa(original_dfa)
|
|
1210
|
+
"""
|
|
1211
|
+
# Initialize partitions
|
|
1212
|
+
partitions = _initialize_partitions(automaton)
|
|
1213
|
+
|
|
1214
|
+
# Refine partitions until stable
|
|
1215
|
+
partitions = _refine_partitions(automaton, partitions)
|
|
1216
|
+
|
|
1217
|
+
# Build minimized automaton
|
|
1218
|
+
state_to_partition = _build_state_mapping(partitions)
|
|
1219
|
+
new_states = _create_minimized_states(partitions, automaton)
|
|
1220
|
+
new_transitions = _create_minimized_transitions(automaton, state_to_partition)
|
|
1221
|
+
|
|
1222
|
+
# Find new initial state and accepting states
|
|
581
1223
|
new_initial = state_to_partition[automaton.initial_state]
|
|
582
1224
|
new_accepting = {s.id for s in new_states if s.is_accepting}
|
|
583
1225
|
|
|
@@ -593,13 +1235,17 @@ def minimize_dfa(automaton: FiniteAutomaton) -> FiniteAutomaton:
|
|
|
593
1235
|
def to_dot(automaton: FiniteAutomaton) -> str:
|
|
594
1236
|
"""Export automaton to DOT format.
|
|
595
1237
|
|
|
596
|
-
|
|
1238
|
+
Convenience function for DOT export.
|
|
597
1239
|
|
|
598
1240
|
Args:
|
|
599
1241
|
automaton: Automaton to export
|
|
600
1242
|
|
|
601
1243
|
Returns:
|
|
602
1244
|
DOT format string
|
|
1245
|
+
|
|
1246
|
+
Example:
|
|
1247
|
+
>>> dot = to_dot(automaton)
|
|
1248
|
+
>>> Path("machine.dot").write_text(dot)
|
|
603
1249
|
"""
|
|
604
1250
|
return automaton.to_dot()
|
|
605
1251
|
|
|
@@ -607,23 +1253,26 @@ def to_dot(automaton: FiniteAutomaton) -> str:
|
|
|
607
1253
|
def to_networkx(automaton: FiniteAutomaton) -> Any:
|
|
608
1254
|
"""Export automaton to NetworkX graph.
|
|
609
1255
|
|
|
610
|
-
|
|
1256
|
+
Convenience function for NetworkX export.
|
|
611
1257
|
|
|
612
1258
|
Args:
|
|
613
1259
|
automaton: Automaton to export
|
|
614
1260
|
|
|
615
1261
|
Returns:
|
|
616
1262
|
NetworkX DiGraph
|
|
1263
|
+
|
|
1264
|
+
Example:
|
|
1265
|
+
>>> graph = to_networkx(automaton)
|
|
617
1266
|
"""
|
|
618
1267
|
return automaton.to_networkx()
|
|
619
1268
|
|
|
620
1269
|
|
|
621
1270
|
def infer_rpni(
|
|
622
|
-
positive_traces: list[list[str]], negative_traces: list[list[str]] | None = None
|
|
1271
|
+
positive_traces: list[list[str | int]], negative_traces: list[list[str | int]] | None = None
|
|
623
1272
|
) -> FiniteAutomaton:
|
|
624
1273
|
"""Convenience function for RPNI inference.
|
|
625
1274
|
|
|
626
|
-
|
|
1275
|
+
Top-level API for state machine inference.
|
|
627
1276
|
|
|
628
1277
|
Args:
|
|
629
1278
|
positive_traces: List of accepted sequences
|
|
@@ -631,6 +1280,9 @@ def infer_rpni(
|
|
|
631
1280
|
|
|
632
1281
|
Returns:
|
|
633
1282
|
Inferred FiniteAutomaton
|
|
1283
|
+
|
|
1284
|
+
Example:
|
|
1285
|
+
>>> dfa = infer_rpni([["A", "B"], ["A", "C"]])
|
|
634
1286
|
"""
|
|
635
1287
|
inferrer = StateMachineInferrer()
|
|
636
1288
|
return inferrer.infer_rpni(positive_traces, negative_traces)
|