oscura 0.0.1__py3-none-any.whl → 0.1.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 +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.0.dist-info/METADATA +300 -0
- oscura-0.1.0.dist-info/RECORD +463 -0
- oscura-0.1.0.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
"""State machine inference using RPNI algorithm.
|
|
2
|
+
|
|
3
|
+
Requirements addressed: PSI-002
|
|
4
|
+
|
|
5
|
+
This module infers protocol state machines from observed message sequences using
|
|
6
|
+
passive learning algorithms (no system interaction required).
|
|
7
|
+
|
|
8
|
+
Key capabilities:
|
|
9
|
+
- RPNI algorithm for passive DFA learning
|
|
10
|
+
- State merging to minimize automaton
|
|
11
|
+
- Export to DOT format for visualization
|
|
12
|
+
- Export to NetworkX graph for analysis
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from copy import deepcopy
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class State:
|
|
22
|
+
"""A state in the inferred automaton.
|
|
23
|
+
|
|
24
|
+
: State representation.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
id: Unique state identifier
|
|
28
|
+
name: Human-readable state name
|
|
29
|
+
is_initial: Whether this is the initial state
|
|
30
|
+
is_accepting: Whether this is an accepting state
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
id: int
|
|
34
|
+
name: str
|
|
35
|
+
is_initial: bool = False
|
|
36
|
+
is_accepting: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Transition:
|
|
41
|
+
"""A transition in the automaton.
|
|
42
|
+
|
|
43
|
+
: Transition representation.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
source: Source state ID
|
|
47
|
+
target: Target state ID
|
|
48
|
+
symbol: Transition label/symbol
|
|
49
|
+
count: Number of times observed
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
source: int # State ID
|
|
53
|
+
target: int # State ID
|
|
54
|
+
symbol: str # Transition label
|
|
55
|
+
count: int = 1 # Number of observations
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class FiniteAutomaton:
|
|
60
|
+
"""An inferred finite automaton.
|
|
61
|
+
|
|
62
|
+
: Complete automaton representation with export capabilities.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
states: List of all states
|
|
66
|
+
transitions: List of all transitions
|
|
67
|
+
alphabet: Set of all symbols
|
|
68
|
+
initial_state: Initial state ID
|
|
69
|
+
accepting_states: Set of accepting state IDs
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
states: list[State]
|
|
73
|
+
transitions: list[Transition]
|
|
74
|
+
alphabet: set[str]
|
|
75
|
+
initial_state: int
|
|
76
|
+
accepting_states: set[int]
|
|
77
|
+
|
|
78
|
+
def to_dot(self) -> str:
|
|
79
|
+
"""Export to DOT format for Graphviz.
|
|
80
|
+
|
|
81
|
+
: DOT format export for visualization.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
DOT format string
|
|
85
|
+
"""
|
|
86
|
+
lines = ["digraph finite_automaton {", " rankdir=LR;", " node [shape=circle];"]
|
|
87
|
+
|
|
88
|
+
# Mark accepting states
|
|
89
|
+
if self.accepting_states:
|
|
90
|
+
accepting_names = [s.name for s in self.states if s.id in self.accepting_states]
|
|
91
|
+
lines.append(f" node [shape=doublecircle]; {' '.join(accepting_names)};")
|
|
92
|
+
lines.append(" node [shape=circle];")
|
|
93
|
+
|
|
94
|
+
# Add invisible start node for initial state
|
|
95
|
+
initial_state = next(s for s in self.states if s.id == self.initial_state)
|
|
96
|
+
lines.append(' __start__ [shape=none, label=""];')
|
|
97
|
+
lines.append(f" __start__ -> {initial_state.name};")
|
|
98
|
+
|
|
99
|
+
# Add transitions
|
|
100
|
+
for trans in self.transitions:
|
|
101
|
+
src_state = next(s for s in self.states if s.id == trans.source)
|
|
102
|
+
tgt_state = next(s for s in self.states if s.id == trans.target)
|
|
103
|
+
label = trans.symbol
|
|
104
|
+
if trans.count > 1:
|
|
105
|
+
label = f"{trans.symbol} ({trans.count})"
|
|
106
|
+
lines.append(f' {src_state.name} -> {tgt_state.name} [label="{label}"];')
|
|
107
|
+
|
|
108
|
+
lines.append("}")
|
|
109
|
+
return "\n".join(lines)
|
|
110
|
+
|
|
111
|
+
def to_networkx(self) -> Any:
|
|
112
|
+
"""Export to NetworkX graph.
|
|
113
|
+
|
|
114
|
+
: NetworkX export for programmatic analysis.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
NetworkX MultiDiGraph (supports multiple edges between same nodes)
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
ImportError: If NetworkX is not installed.
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
import networkx as nx # type: ignore[import-untyped]
|
|
124
|
+
except ImportError as err:
|
|
125
|
+
raise ImportError("NetworkX is required for graph export") from err
|
|
126
|
+
|
|
127
|
+
# Use MultiDiGraph to support multiple transitions between same states
|
|
128
|
+
G = nx.MultiDiGraph()
|
|
129
|
+
|
|
130
|
+
# Add nodes
|
|
131
|
+
for state in self.states:
|
|
132
|
+
G.add_node(
|
|
133
|
+
state.id,
|
|
134
|
+
name=state.name,
|
|
135
|
+
is_initial=state.is_initial,
|
|
136
|
+
is_accepting=state.is_accepting,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Add edges
|
|
140
|
+
for trans in self.transitions:
|
|
141
|
+
G.add_edge(trans.source, trans.target, symbol=trans.symbol, count=trans.count)
|
|
142
|
+
|
|
143
|
+
return G
|
|
144
|
+
|
|
145
|
+
def accepts(self, sequence: list[str]) -> bool:
|
|
146
|
+
"""Check if automaton accepts sequence.
|
|
147
|
+
|
|
148
|
+
: Sequence acceptance checking.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
sequence: List of symbols
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
True if sequence is accepted
|
|
155
|
+
"""
|
|
156
|
+
current_state = self.initial_state
|
|
157
|
+
|
|
158
|
+
for symbol in sequence:
|
|
159
|
+
# Find transition with this symbol
|
|
160
|
+
trans = None
|
|
161
|
+
for t in self.transitions:
|
|
162
|
+
if t.source == current_state and t.symbol == symbol:
|
|
163
|
+
trans = t
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
if trans is None:
|
|
167
|
+
return False # No valid transition
|
|
168
|
+
|
|
169
|
+
current_state = trans.target
|
|
170
|
+
|
|
171
|
+
# Check if we ended in accepting state
|
|
172
|
+
return current_state in self.accepting_states
|
|
173
|
+
|
|
174
|
+
def get_successors(self, state_id: int) -> dict[str, int]:
|
|
175
|
+
"""Get successor states from given state.
|
|
176
|
+
|
|
177
|
+
: State successor lookup.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
state_id: State ID to query
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Dictionary mapping symbols to target state IDs
|
|
184
|
+
"""
|
|
185
|
+
successors = {}
|
|
186
|
+
for trans in self.transitions:
|
|
187
|
+
if trans.source == state_id:
|
|
188
|
+
successors[trans.symbol] = trans.target
|
|
189
|
+
return successors
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class StateMachineInferrer:
|
|
193
|
+
"""Infer state machines using passive learning.
|
|
194
|
+
|
|
195
|
+
: RPNI algorithm for DFA inference.
|
|
196
|
+
|
|
197
|
+
The RPNI (Regular Positive and Negative Inference) algorithm:
|
|
198
|
+
1. Build Prefix Tree Acceptor from positive samples
|
|
199
|
+
2. Iteratively merge compatible state pairs
|
|
200
|
+
3. Validate against negative samples
|
|
201
|
+
4. Converge to minimal consistent DFA
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
"""Initialize inferrer."""
|
|
206
|
+
self._next_state_id = 0
|
|
207
|
+
|
|
208
|
+
def infer(
|
|
209
|
+
self,
|
|
210
|
+
positive_traces: list[list[str]] | None = None,
|
|
211
|
+
negative_traces: list[list[str]] | None = None,
|
|
212
|
+
positive_samples: list[list[str]] | None = None,
|
|
213
|
+
negative_samples: list[list[str]] | None = None,
|
|
214
|
+
) -> FiniteAutomaton:
|
|
215
|
+
"""Infer DFA from traces (alias for infer_rpni).
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
positive_traces: List of accepted sequences.
|
|
219
|
+
negative_traces: List of rejected sequences (optional).
|
|
220
|
+
positive_samples: Alias for positive_traces (deprecated).
|
|
221
|
+
negative_samples: Alias for negative_traces (deprecated).
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
Inferred FiniteAutomaton.
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
ValueError: If no positive traces provided.
|
|
228
|
+
"""
|
|
229
|
+
# Handle parameter aliases
|
|
230
|
+
pos = positive_traces if positive_traces is not None else positive_samples
|
|
231
|
+
neg = negative_traces if negative_traces is not None else negative_samples
|
|
232
|
+
|
|
233
|
+
if pos is None:
|
|
234
|
+
raise ValueError("Must provide either positive_traces or positive_samples")
|
|
235
|
+
|
|
236
|
+
return self.infer_rpni(pos, neg)
|
|
237
|
+
|
|
238
|
+
def infer_rpni(
|
|
239
|
+
self, positive_traces: list[list[str]], negative_traces: list[list[str]] | None = None
|
|
240
|
+
) -> FiniteAutomaton:
|
|
241
|
+
"""Infer DFA using RPNI (Regular Positive and Negative Inference).
|
|
242
|
+
|
|
243
|
+
: Complete RPNI algorithm.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
positive_traces: List of accepted sequences (list of symbols)
|
|
247
|
+
negative_traces: List of rejected sequences (optional)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Inferred FiniteAutomaton
|
|
251
|
+
|
|
252
|
+
Raises:
|
|
253
|
+
ValueError: If no positive traces provided.
|
|
254
|
+
"""
|
|
255
|
+
if not positive_traces:
|
|
256
|
+
raise ValueError("Need at least one positive trace")
|
|
257
|
+
|
|
258
|
+
# Build alphabet from all traces
|
|
259
|
+
alphabet: set[str] = set()
|
|
260
|
+
neg_traces = negative_traces if negative_traces is not None else []
|
|
261
|
+
for trace in positive_traces + neg_traces:
|
|
262
|
+
alphabet.update(trace)
|
|
263
|
+
|
|
264
|
+
# Build Prefix Tree Acceptor from positive traces
|
|
265
|
+
pta = self._build_pta(positive_traces)
|
|
266
|
+
|
|
267
|
+
# RPNI merging process
|
|
268
|
+
automaton = pta
|
|
269
|
+
states = sorted([s.id for s in automaton.states])
|
|
270
|
+
|
|
271
|
+
# Try to merge states in order
|
|
272
|
+
i = 1 # Start from second state (never merge initial state)
|
|
273
|
+
while i < len(states):
|
|
274
|
+
merged = False
|
|
275
|
+
|
|
276
|
+
# Try to merge states[i] with any earlier state
|
|
277
|
+
for j in range(i):
|
|
278
|
+
if self._is_compatible(automaton, states[j], states[i], neg_traces):
|
|
279
|
+
# Merge states[i] into states[j]
|
|
280
|
+
automaton = self._merge_states(automaton, states[j], states[i])
|
|
281
|
+
# Update state list
|
|
282
|
+
states = sorted([s.id for s in automaton.states])
|
|
283
|
+
merged = True
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
if not merged:
|
|
287
|
+
i += 1
|
|
288
|
+
|
|
289
|
+
return automaton
|
|
290
|
+
|
|
291
|
+
def _build_pta(self, traces: list[list[str]]) -> FiniteAutomaton:
|
|
292
|
+
"""Build Prefix Tree Acceptor from traces.
|
|
293
|
+
|
|
294
|
+
: PTA construction.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
traces: List of sequences
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Prefix Tree Acceptor as FiniteAutomaton
|
|
301
|
+
"""
|
|
302
|
+
# Reset state counter
|
|
303
|
+
self._next_state_id = 0
|
|
304
|
+
|
|
305
|
+
# Create initial state
|
|
306
|
+
initial_state = State(
|
|
307
|
+
id=self._get_next_state_id(), name="q0", is_initial=True, is_accepting=False
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
states: list[State] = [initial_state]
|
|
311
|
+
transitions: list[Transition] = []
|
|
312
|
+
alphabet: set[str] = set()
|
|
313
|
+
|
|
314
|
+
# Build tree from traces
|
|
315
|
+
for trace in traces:
|
|
316
|
+
current_state_id = initial_state.id
|
|
317
|
+
|
|
318
|
+
# Walk/build tree for this trace
|
|
319
|
+
for symbol in trace:
|
|
320
|
+
alphabet.add(symbol)
|
|
321
|
+
|
|
322
|
+
# Check if transition exists
|
|
323
|
+
next_state_id = None
|
|
324
|
+
for trans in transitions:
|
|
325
|
+
if trans.source == current_state_id and trans.symbol == symbol:
|
|
326
|
+
next_state_id = trans.target
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
if next_state_id is None:
|
|
330
|
+
# Create new state and transition
|
|
331
|
+
new_state_id = self._get_next_state_id()
|
|
332
|
+
new_state = State(
|
|
333
|
+
id=new_state_id,
|
|
334
|
+
name=f"q{new_state_id}",
|
|
335
|
+
is_initial=False,
|
|
336
|
+
is_accepting=False,
|
|
337
|
+
)
|
|
338
|
+
states.append(new_state)
|
|
339
|
+
|
|
340
|
+
new_trans = Transition(
|
|
341
|
+
source=current_state_id, target=new_state_id, symbol=symbol
|
|
342
|
+
)
|
|
343
|
+
transitions.append(new_trans)
|
|
344
|
+
|
|
345
|
+
next_state_id = new_state_id
|
|
346
|
+
|
|
347
|
+
current_state_id = next_state_id
|
|
348
|
+
|
|
349
|
+
# Mark final state as accepting
|
|
350
|
+
for state in states:
|
|
351
|
+
if state.id == current_state_id:
|
|
352
|
+
state.is_accepting = True
|
|
353
|
+
|
|
354
|
+
accepting_states = {s.id for s in states if s.is_accepting}
|
|
355
|
+
|
|
356
|
+
return FiniteAutomaton(
|
|
357
|
+
states=states,
|
|
358
|
+
transitions=transitions,
|
|
359
|
+
alphabet=alphabet,
|
|
360
|
+
initial_state=initial_state.id,
|
|
361
|
+
accepting_states=accepting_states,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _merge_states(
|
|
365
|
+
self, automaton: FiniteAutomaton, state_a: int, state_b: int
|
|
366
|
+
) -> FiniteAutomaton:
|
|
367
|
+
"""Merge two states in automaton.
|
|
368
|
+
|
|
369
|
+
: State merging operation.
|
|
370
|
+
|
|
371
|
+
Merges state_b into state_a.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
automaton: Current automaton
|
|
375
|
+
state_a: Target state ID (survives)
|
|
376
|
+
state_b: Source state ID (removed)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
New automaton with merged states
|
|
380
|
+
"""
|
|
381
|
+
# Deep copy to avoid modifying original
|
|
382
|
+
new_automaton = deepcopy(automaton)
|
|
383
|
+
|
|
384
|
+
# Remove state_b
|
|
385
|
+
new_automaton.states = [s for s in new_automaton.states if s.id != state_b]
|
|
386
|
+
|
|
387
|
+
# Update transitions: redirect all transitions to/from state_b to state_a
|
|
388
|
+
for trans in new_automaton.transitions:
|
|
389
|
+
if trans.source == state_b:
|
|
390
|
+
trans.source = state_a
|
|
391
|
+
if trans.target == state_b:
|
|
392
|
+
trans.target = state_a
|
|
393
|
+
|
|
394
|
+
# Merge accepting status
|
|
395
|
+
if state_b in new_automaton.accepting_states:
|
|
396
|
+
new_automaton.accepting_states.add(state_a)
|
|
397
|
+
new_automaton.accepting_states.discard(state_b)
|
|
398
|
+
|
|
399
|
+
# Merge duplicate transitions (same source, target, symbol)
|
|
400
|
+
unique_transitions = []
|
|
401
|
+
seen = set()
|
|
402
|
+
|
|
403
|
+
for trans in new_automaton.transitions:
|
|
404
|
+
key = (trans.source, trans.target, trans.symbol)
|
|
405
|
+
if key not in seen:
|
|
406
|
+
seen.add(key)
|
|
407
|
+
unique_transitions.append(trans)
|
|
408
|
+
else:
|
|
409
|
+
# Increment count on existing transition
|
|
410
|
+
for ut in unique_transitions:
|
|
411
|
+
if (ut.source, ut.target, ut.symbol) == key:
|
|
412
|
+
ut.count += trans.count
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
new_automaton.transitions = unique_transitions
|
|
416
|
+
|
|
417
|
+
return new_automaton
|
|
418
|
+
|
|
419
|
+
def _is_compatible(
|
|
420
|
+
self,
|
|
421
|
+
automaton: FiniteAutomaton,
|
|
422
|
+
state_a: int,
|
|
423
|
+
state_b: int,
|
|
424
|
+
negative_traces: list[list[str]],
|
|
425
|
+
) -> bool:
|
|
426
|
+
"""Check if two states can be merged without accepting negatives.
|
|
427
|
+
|
|
428
|
+
: Compatibility checking for state merging.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
automaton: Current automaton
|
|
432
|
+
state_a: First state ID
|
|
433
|
+
state_b: Second state ID
|
|
434
|
+
negative_traces: Negative example traces
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
True if states are compatible
|
|
438
|
+
"""
|
|
439
|
+
# Get accepting status
|
|
440
|
+
_a_accepting = state_a in automaton.accepting_states
|
|
441
|
+
_b_accepting = state_b in automaton.accepting_states
|
|
442
|
+
|
|
443
|
+
# If one is accepting and other is not, they might still be compatible
|
|
444
|
+
# (we'll merge accepting status), but check negative traces
|
|
445
|
+
|
|
446
|
+
# Try merging and test
|
|
447
|
+
test_automaton = self._merge_states(automaton, state_a, state_b)
|
|
448
|
+
|
|
449
|
+
# Check that no negative traces are accepted
|
|
450
|
+
for neg_trace in negative_traces:
|
|
451
|
+
if test_automaton.accepts(neg_trace):
|
|
452
|
+
return False
|
|
453
|
+
|
|
454
|
+
# Recursively check successor compatibility
|
|
455
|
+
_succ_a = test_automaton.get_successors(state_a)
|
|
456
|
+
# state_b has been merged, so its successors are now in state_a
|
|
457
|
+
|
|
458
|
+
return True
|
|
459
|
+
|
|
460
|
+
def _get_next_state_id(self) -> int:
|
|
461
|
+
"""Get next available state ID.
|
|
462
|
+
|
|
463
|
+
Returns:
|
|
464
|
+
Next state ID
|
|
465
|
+
"""
|
|
466
|
+
state_id = self._next_state_id
|
|
467
|
+
self._next_state_id += 1
|
|
468
|
+
return state_id
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def minimize_dfa(automaton: FiniteAutomaton) -> FiniteAutomaton:
|
|
472
|
+
"""Minimize DFA using partition refinement.
|
|
473
|
+
|
|
474
|
+
: DFA minimization using Hopcroft's algorithm.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
automaton: DFA to minimize
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
Minimized FiniteAutomaton
|
|
481
|
+
"""
|
|
482
|
+
# Use partition refinement (simplified version)
|
|
483
|
+
# Start with two partitions: accepting and non-accepting
|
|
484
|
+
accepting = automaton.accepting_states
|
|
485
|
+
non_accepting = {s.id for s in automaton.states if s.id not in accepting}
|
|
486
|
+
|
|
487
|
+
partitions = []
|
|
488
|
+
if accepting:
|
|
489
|
+
partitions.append(accepting)
|
|
490
|
+
if non_accepting:
|
|
491
|
+
partitions.append(non_accepting)
|
|
492
|
+
|
|
493
|
+
# Refine partitions
|
|
494
|
+
changed = True
|
|
495
|
+
while changed:
|
|
496
|
+
changed = False
|
|
497
|
+
new_partitions = []
|
|
498
|
+
|
|
499
|
+
for partition in partitions:
|
|
500
|
+
# Try to split this partition
|
|
501
|
+
if len(partition) <= 1:
|
|
502
|
+
new_partitions.append(partition)
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
# Group states by transition signatures
|
|
506
|
+
groups: dict[tuple[tuple[str, int | None], ...], set[int]] = {}
|
|
507
|
+
for state_id in partition:
|
|
508
|
+
successors = automaton.get_successors(state_id)
|
|
509
|
+
|
|
510
|
+
# Create signature based on which partition each successor is in
|
|
511
|
+
signature_list: list[tuple[str, int | None]] = []
|
|
512
|
+
for symbol in sorted(automaton.alphabet):
|
|
513
|
+
if symbol in successors:
|
|
514
|
+
target = successors[symbol]
|
|
515
|
+
# Find which partition target is in
|
|
516
|
+
target_partition: int | None = None
|
|
517
|
+
for i, p in enumerate(partitions):
|
|
518
|
+
if target in p:
|
|
519
|
+
target_partition = i
|
|
520
|
+
break
|
|
521
|
+
signature_list.append((symbol, target_partition))
|
|
522
|
+
else:
|
|
523
|
+
signature_list.append((symbol, None))
|
|
524
|
+
|
|
525
|
+
signature = tuple(signature_list)
|
|
526
|
+
if signature not in groups:
|
|
527
|
+
groups[signature] = set()
|
|
528
|
+
groups[signature].add(state_id)
|
|
529
|
+
|
|
530
|
+
# If we split, mark as changed
|
|
531
|
+
if len(groups) > 1:
|
|
532
|
+
changed = True
|
|
533
|
+
|
|
534
|
+
new_partitions.extend(groups.values())
|
|
535
|
+
|
|
536
|
+
partitions = new_partitions
|
|
537
|
+
|
|
538
|
+
# Build minimized automaton
|
|
539
|
+
# Map old state IDs to partition IDs
|
|
540
|
+
state_to_partition = {}
|
|
541
|
+
for i, partition in enumerate(partitions):
|
|
542
|
+
for state_id in partition:
|
|
543
|
+
state_to_partition[state_id] = i
|
|
544
|
+
|
|
545
|
+
# Create new states
|
|
546
|
+
new_states = []
|
|
547
|
+
for i, partition in enumerate(partitions):
|
|
548
|
+
# Pick representative state
|
|
549
|
+
rep_id = min(partition)
|
|
550
|
+
_rep_state = next(s for s in automaton.states if s.id == rep_id)
|
|
551
|
+
|
|
552
|
+
is_accepting = any(sid in automaton.accepting_states for sid in partition)
|
|
553
|
+
is_initial = automaton.initial_state in partition
|
|
554
|
+
|
|
555
|
+
new_state = State(id=i, name=f"q{i}", is_initial=is_initial, is_accepting=is_accepting)
|
|
556
|
+
new_states.append(new_state)
|
|
557
|
+
|
|
558
|
+
# Create new transitions
|
|
559
|
+
new_transitions = []
|
|
560
|
+
seen_transitions = set()
|
|
561
|
+
|
|
562
|
+
for trans in automaton.transitions:
|
|
563
|
+
src_partition = state_to_partition[trans.source]
|
|
564
|
+
tgt_partition = state_to_partition[trans.target]
|
|
565
|
+
|
|
566
|
+
key = (src_partition, tgt_partition, trans.symbol)
|
|
567
|
+
if key not in seen_transitions:
|
|
568
|
+
seen_transitions.add(key)
|
|
569
|
+
new_transitions.append(
|
|
570
|
+
Transition(
|
|
571
|
+
source=src_partition,
|
|
572
|
+
target=tgt_partition,
|
|
573
|
+
symbol=trans.symbol,
|
|
574
|
+
count=trans.count,
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Find new initial state
|
|
579
|
+
new_initial = state_to_partition[automaton.initial_state]
|
|
580
|
+
new_accepting = {s.id for s in new_states if s.is_accepting}
|
|
581
|
+
|
|
582
|
+
return FiniteAutomaton(
|
|
583
|
+
states=new_states,
|
|
584
|
+
transitions=new_transitions,
|
|
585
|
+
alphabet=automaton.alphabet,
|
|
586
|
+
initial_state=new_initial,
|
|
587
|
+
accepting_states=new_accepting,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def to_dot(automaton: FiniteAutomaton) -> str:
|
|
592
|
+
"""Export automaton to DOT format.
|
|
593
|
+
|
|
594
|
+
: Convenience function for DOT export.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
automaton: Automaton to export
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
DOT format string
|
|
601
|
+
"""
|
|
602
|
+
return automaton.to_dot()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def to_networkx(automaton: FiniteAutomaton) -> Any:
|
|
606
|
+
"""Export automaton to NetworkX graph.
|
|
607
|
+
|
|
608
|
+
: Convenience function for NetworkX export.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
automaton: Automaton to export
|
|
612
|
+
|
|
613
|
+
Returns:
|
|
614
|
+
NetworkX DiGraph
|
|
615
|
+
"""
|
|
616
|
+
return automaton.to_networkx()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def infer_rpni(
|
|
620
|
+
positive_traces: list[list[str]], negative_traces: list[list[str]] | None = None
|
|
621
|
+
) -> FiniteAutomaton:
|
|
622
|
+
"""Convenience function for RPNI inference.
|
|
623
|
+
|
|
624
|
+
: Top-level API for state machine inference.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
positive_traces: List of accepted sequences
|
|
628
|
+
negative_traces: List of rejected sequences (optional)
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
Inferred FiniteAutomaton
|
|
632
|
+
"""
|
|
633
|
+
inferrer = StateMachineInferrer()
|
|
634
|
+
return inferrer.infer_rpni(positive_traces, negative_traces)
|