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.
Files changed (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,93 +1,199 @@
1
- """State machine inference using RPNI algorithm.
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 typing import Any
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 automaton.
27
+ """A state in the inferred state machine.
25
28
 
26
- : State representation.
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 automaton.
74
+ """A transition in the state machine.
44
75
 
45
- : Transition representation.
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/symbol
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
- : Complete automaton representation with export capabilities.
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
- : DOT format export for visualization.
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 finite_automaton {", " rankdir=LR;", " node [shape=circle];"]
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(' __start__ [shape=none, label=""];')
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
- label = trans.symbol
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
- label = f"{trans.symbol} ({trans.count})"
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
- : NetworkX export for programmatic analysis.
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 # type: ignore[import-untyped]
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(trans.source, trans.target, symbol=trans.symbol, count=trans.count)
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
- : Sequence acceptance checking.
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
- : State successor lookup.
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
- : RPNI algorithm for DFA inference.
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
- def __init__(self) -> None:
207
- """Initialize inferrer."""
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 (alias for infer_rpni).
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.infer_rpni(pos, neg)
446
+ return self.extract(pos, neg)
239
447
 
240
448
  def infer_rpni(
241
- self, positive_traces: list[list[str]], negative_traces: list[list[str]] | None = None
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 (Regular Positive and Negative Inference).
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
- if not positive_traces:
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
- neg_traces = negative_traces if negative_traces is not None else []
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._build_pta(positive_traces)
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._is_compatible(automaton, states[j], states[i], neg_traces):
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 _build_pta(self, traces: list[list[str]]) -> FiniteAutomaton:
294
- """Build Prefix Tree Acceptor from traces.
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
- : PTA construction.
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
- traces: List of sequences
626
+ sequences: List of symbol sequences
300
627
 
301
628
  Returns:
302
- Prefix Tree Acceptor as FiniteAutomaton
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="q0", is_initial=True, is_accepting=False
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 traces
317
- for trace in traces:
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 trace
321
- for symbol in trace:
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"q{new_state_id}",
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 FiniteAutomaton(
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
- self, automaton: FiniteAutomaton, state_a: int, state_b: int
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
- : State merging operation.
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 automaton
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 automaton with merged states
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 _is_compatible(
745
+ def _is_merge_compatible(
422
746
  self,
423
- automaton: FiniteAutomaton,
747
+ automaton: StateMachine,
424
748
  state_a: int,
425
749
  state_b: int,
426
- negative_traces: list[list[str]],
750
+ negative: list[list[str | int]],
427
751
  ) -> bool:
428
- """Check if two states can be merged without accepting negatives.
752
+ """Check if merging would accept negative sequences.
429
753
 
430
- : Compatibility checking for state merging.
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 automaton
758
+ automaton: Current state machine
434
759
  state_a: First state ID
435
760
  state_b: Second state ID
436
- negative_traces: Negative example traces
761
+ negative: Negative example sequences
437
762
 
438
763
  Returns:
439
- True if states are compatible
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 negative_traces:
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
- def minimize_dfa(automaton: FiniteAutomaton) -> FiniteAutomaton:
474
- """Minimize DFA using partition refinement.
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
- : DFA minimization using Hopcroft's algorithm.
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 minimize
993
+ automaton: DFA to partition
480
994
 
481
995
  Returns:
482
- Minimized FiniteAutomaton
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
- # Refine partitions
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
- # Try to split this partition
503
- if len(partition) <= 1:
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
- # Build minimized automaton
541
- # Map old state IDs to partition IDs
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
- # Create new states
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(id=i, name=f"q{i}", is_initial=is_initial, is_accepting=is_accepting)
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
- # Create new transitions
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
- # Find new initial state
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
- : Convenience function for DOT export.
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
- : Convenience function for NetworkX export.
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
- : Top-level API for state machine inference.
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)