ReForma 0.1.0__tar.gz

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.
@@ -0,0 +1 @@
1
+ include ReForma/bin/ReFormaTool.jar
reforma-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: ReForma
3
+ Version: 0.1.0
4
+ Summary: Python bindings for the RePA/ReForma probabilistic automaton tool
5
+ Author-email: Joshua Dourado <joshuadourado@ua.pt>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: networkx
14
+ Requires-Dist: matplotlib
15
+
16
+ # ReForma — Python bindings for the RePA/ReForma tool
17
+
18
+ A clean Python library that wraps the `ReFormaTool.jar` CLI via subprocess,
19
+ giving you a Pythonic interface for simulation, training, PDL/PCTL
20
+ verification, and export.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # From the project root (where pyproject.toml lives)
28
+ pip install -e .
29
+ ```
30
+
31
+ Requires **Python 3.10+** and a working `java` on your PATH.
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ from ReForma import ReForma
39
+
40
+ # Point to your compiled JAR
41
+ ReForma = ReForma("path/to/ReFormaTool.jar")
42
+
43
+ # Load a model
44
+ state = ReForma.load_file("examples/recommender.r")
45
+
46
+ print(state.current_states) # ['Home']
47
+ print(state.enabled) # [Transition('go_work': Home → Office, p=0.500), ...]
48
+
49
+ # Simulate
50
+ state = ReForma.step("go_work")
51
+ state = ReForma.step("easy_task")
52
+ state = ReForma.undo() # undo last step
53
+
54
+ # Reset to initial state
55
+ ReForma.reset()
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Simulation
61
+
62
+ ```python
63
+ state = ReForma.load_file("model.r")
64
+
65
+ # Check what's enabled
66
+ for t in state.enabled:
67
+ print(f"{t.label}: {t.from_state} → {t.to_state} (p={t.probability:.3f})")
68
+
69
+ # Take a step by label
70
+ state = ReForma.step("go_work")
71
+
72
+ # Undo / reset
73
+ state = ReForma.undo()
74
+ state = ReForma.reset()
75
+
76
+ # Inspect variables
77
+ print(state.variables) # {'counter': 0, 'flag': 1}
78
+
79
+ # History of labels taken
80
+ print(ReForma.history) # ['go_work']
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Training
86
+
87
+ Train the model on a batch of sessions (lists of event labels):
88
+
89
+ ```python
90
+ ReForma.train([
91
+ ["go_work", "easy_task", "easy_task", "go_home"],
92
+ ["battery_low", "go_charge", "finish_charge", "socialize"],
93
+ ["no_money", "go_work", "go_home"],
94
+ ])
95
+
96
+ # Or train directly from a log file (one session per line, comma-separated)
97
+ ReForma.train_from_file("logs/sessions.txt")
98
+
99
+ # sessions.txt format:
100
+ # go_work,easy_task,go_home
101
+ # battery_low,go_charge,finish_charge
102
+
103
+ # Save the updated model with new weights
104
+ ReForma.save_source("model_trained.r")
105
+ ```
106
+
107
+ ---
108
+
109
+ ## PDL / PCTL Verification
110
+
111
+ ```python
112
+ # Quantitative: probability of eventually reaching Office
113
+ prob = ReForma.check_pdl_value("Home", "{P=?[F Office]}")
114
+ print(f"P(reach Office from Home) = {prob:.4f}")
115
+
116
+ # Qualitative: is it probable?
117
+ holds = ReForma.check_pdl_value("Home", "{P>=0.4[F Office]}")
118
+ print(f"P>=0.4? {holds}") # True / False
119
+
120
+ # PDL: is there a path via go_work to Office?
121
+ holds = ReForma.check_pdl_value("Home", "<go_work>Office")
122
+ print(holds) # True
123
+
124
+ # Get the raw string result
125
+ raw = ReForma.check_pdl("Home", "{P=?[F Office]}")
126
+ print(raw) # "Result: 0.50000"
127
+ ```
128
+
129
+ ### Formula syntax reference
130
+
131
+ | Formula | Meaning |
132
+ |-----------------------------|------------------------------------------------|
133
+ | `{P=?[F target]}` | Probability of eventually reaching `target` |
134
+ | `{P=?[G safe]}` | Probability of staying in `safe` forever |
135
+ | `{P=?[X next]}` | Probability of reaching `next` in one step |
136
+ | `{P=?[a U b]}` | Probability of `a` until `b` |
137
+ | `{P>=0.5[F target]}` | Is probability of reaching target ≥ 0.5? |
138
+ | `<action>state` | There exists a path via `action` to `state` |
139
+ | `[action]state` | All paths via `action` lead to `state` |
140
+
141
+ ---
142
+
143
+ ## Export
144
+
145
+ ```python
146
+ # PRISM DTMC
147
+ prism_code = ReForma.export_prism()
148
+ ReForma.save_prism("output/model.pm")
149
+
150
+ # mCRL2
151
+ mcrl2_code = ReForma.export_mcrl2()
152
+
153
+ # GLTS (imperative translation)
154
+ glts_code = ReForma.export_glts()
155
+
156
+ # Mermaid diagram (initial state)
157
+ diagram = ReForma.export_mermaid()
158
+
159
+ # Mermaid diagram (full LTS — all reachable states)
160
+ full_diagram = ReForma.export_mermaid(full_lts=True)
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Loading from a string
166
+
167
+ ```python
168
+ source = """
169
+ name MyModel
170
+ init s0
171
+ s0 ---> s1: a (0.6)
172
+ s0 ---> s2: b (0.4)
173
+ s1 ---> s0: back (1.0)
174
+ """
175
+
176
+ state = ReForma.load(source, name="MyModel")
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Running the tests
182
+
183
+ ```bash
184
+ pip install pytest
185
+ pytest tests/ -v
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Project structure
191
+
192
+ ```
193
+ ReForma/
194
+ ├── __init__.py # Public API exports
195
+ ├── client.py # ReForma — high-level Python API
196
+ ├── jar_bridge.py # JarBridge — low-level subprocess wrapper
197
+ └── model.py # ReFormaModel, SimulationState, Transition data classes
198
+ tests/
199
+ └── test_ReForma.py # Full test suite (mocked, no JAR needed)
200
+ pyproject.toml
201
+ README.md
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Error handling
207
+
208
+ ```python
209
+ from ReForma.jar_bridge import JarError
210
+
211
+ try:
212
+ result = ReForma.check_pdl("Home", "{P=?[F Office]}")
213
+ except JarError as e:
214
+ print(f"JAR error: {e}")
215
+ except RuntimeError as e:
216
+ print(f"Usage error: {e}") # e.g. no model loaded, invalid transition
217
+ ```
@@ -0,0 +1,202 @@
1
+ # ReForma — Python bindings for the RePA/ReForma tool
2
+
3
+ A clean Python library that wraps the `ReFormaTool.jar` CLI via subprocess,
4
+ giving you a Pythonic interface for simulation, training, PDL/PCTL
5
+ verification, and export.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ # From the project root (where pyproject.toml lives)
13
+ pip install -e .
14
+ ```
15
+
16
+ Requires **Python 3.10+** and a working `java` on your PATH.
17
+
18
+ ---
19
+
20
+ ## Quick Start
21
+
22
+ ```python
23
+ from ReForma import ReForma
24
+
25
+ # Point to your compiled JAR
26
+ ReForma = ReForma("path/to/ReFormaTool.jar")
27
+
28
+ # Load a model
29
+ state = ReForma.load_file("examples/recommender.r")
30
+
31
+ print(state.current_states) # ['Home']
32
+ print(state.enabled) # [Transition('go_work': Home → Office, p=0.500), ...]
33
+
34
+ # Simulate
35
+ state = ReForma.step("go_work")
36
+ state = ReForma.step("easy_task")
37
+ state = ReForma.undo() # undo last step
38
+
39
+ # Reset to initial state
40
+ ReForma.reset()
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Simulation
46
+
47
+ ```python
48
+ state = ReForma.load_file("model.r")
49
+
50
+ # Check what's enabled
51
+ for t in state.enabled:
52
+ print(f"{t.label}: {t.from_state} → {t.to_state} (p={t.probability:.3f})")
53
+
54
+ # Take a step by label
55
+ state = ReForma.step("go_work")
56
+
57
+ # Undo / reset
58
+ state = ReForma.undo()
59
+ state = ReForma.reset()
60
+
61
+ # Inspect variables
62
+ print(state.variables) # {'counter': 0, 'flag': 1}
63
+
64
+ # History of labels taken
65
+ print(ReForma.history) # ['go_work']
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Training
71
+
72
+ Train the model on a batch of sessions (lists of event labels):
73
+
74
+ ```python
75
+ ReForma.train([
76
+ ["go_work", "easy_task", "easy_task", "go_home"],
77
+ ["battery_low", "go_charge", "finish_charge", "socialize"],
78
+ ["no_money", "go_work", "go_home"],
79
+ ])
80
+
81
+ # Or train directly from a log file (one session per line, comma-separated)
82
+ ReForma.train_from_file("logs/sessions.txt")
83
+
84
+ # sessions.txt format:
85
+ # go_work,easy_task,go_home
86
+ # battery_low,go_charge,finish_charge
87
+
88
+ # Save the updated model with new weights
89
+ ReForma.save_source("model_trained.r")
90
+ ```
91
+
92
+ ---
93
+
94
+ ## PDL / PCTL Verification
95
+
96
+ ```python
97
+ # Quantitative: probability of eventually reaching Office
98
+ prob = ReForma.check_pdl_value("Home", "{P=?[F Office]}")
99
+ print(f"P(reach Office from Home) = {prob:.4f}")
100
+
101
+ # Qualitative: is it probable?
102
+ holds = ReForma.check_pdl_value("Home", "{P>=0.4[F Office]}")
103
+ print(f"P>=0.4? {holds}") # True / False
104
+
105
+ # PDL: is there a path via go_work to Office?
106
+ holds = ReForma.check_pdl_value("Home", "<go_work>Office")
107
+ print(holds) # True
108
+
109
+ # Get the raw string result
110
+ raw = ReForma.check_pdl("Home", "{P=?[F Office]}")
111
+ print(raw) # "Result: 0.50000"
112
+ ```
113
+
114
+ ### Formula syntax reference
115
+
116
+ | Formula | Meaning |
117
+ |-----------------------------|------------------------------------------------|
118
+ | `{P=?[F target]}` | Probability of eventually reaching `target` |
119
+ | `{P=?[G safe]}` | Probability of staying in `safe` forever |
120
+ | `{P=?[X next]}` | Probability of reaching `next` in one step |
121
+ | `{P=?[a U b]}` | Probability of `a` until `b` |
122
+ | `{P>=0.5[F target]}` | Is probability of reaching target ≥ 0.5? |
123
+ | `<action>state` | There exists a path via `action` to `state` |
124
+ | `[action]state` | All paths via `action` lead to `state` |
125
+
126
+ ---
127
+
128
+ ## Export
129
+
130
+ ```python
131
+ # PRISM DTMC
132
+ prism_code = ReForma.export_prism()
133
+ ReForma.save_prism("output/model.pm")
134
+
135
+ # mCRL2
136
+ mcrl2_code = ReForma.export_mcrl2()
137
+
138
+ # GLTS (imperative translation)
139
+ glts_code = ReForma.export_glts()
140
+
141
+ # Mermaid diagram (initial state)
142
+ diagram = ReForma.export_mermaid()
143
+
144
+ # Mermaid diagram (full LTS — all reachable states)
145
+ full_diagram = ReForma.export_mermaid(full_lts=True)
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Loading from a string
151
+
152
+ ```python
153
+ source = """
154
+ name MyModel
155
+ init s0
156
+ s0 ---> s1: a (0.6)
157
+ s0 ---> s2: b (0.4)
158
+ s1 ---> s0: back (1.0)
159
+ """
160
+
161
+ state = ReForma.load(source, name="MyModel")
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Running the tests
167
+
168
+ ```bash
169
+ pip install pytest
170
+ pytest tests/ -v
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Project structure
176
+
177
+ ```
178
+ ReForma/
179
+ ├── __init__.py # Public API exports
180
+ ├── client.py # ReForma — high-level Python API
181
+ ├── jar_bridge.py # JarBridge — low-level subprocess wrapper
182
+ └── model.py # ReFormaModel, SimulationState, Transition data classes
183
+ tests/
184
+ └── test_ReForma.py # Full test suite (mocked, no JAR needed)
185
+ pyproject.toml
186
+ README.md
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Error handling
192
+
193
+ ```python
194
+ from ReForma.jar_bridge import JarError
195
+
196
+ try:
197
+ result = ReForma.check_pdl("Home", "{P=?[F Office]}")
198
+ except JarError as e:
199
+ print(f"JAR error: {e}")
200
+ except RuntimeError as e:
201
+ print(f"Usage error: {e}") # e.g. no model loaded, invalid transition
202
+ ```
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: ReForma
3
+ Version: 0.1.0
4
+ Summary: Python bindings for the RePA/ReForma probabilistic automaton tool
5
+ Author-email: Joshua Dourado <joshuadourado@ua.pt>
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: networkx
14
+ Requires-Dist: matplotlib
15
+
16
+ # ReForma — Python bindings for the RePA/ReForma tool
17
+
18
+ A clean Python library that wraps the `ReFormaTool.jar` CLI via subprocess,
19
+ giving you a Pythonic interface for simulation, training, PDL/PCTL
20
+ verification, and export.
21
+
22
+ ---
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ # From the project root (where pyproject.toml lives)
28
+ pip install -e .
29
+ ```
30
+
31
+ Requires **Python 3.10+** and a working `java` on your PATH.
32
+
33
+ ---
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ from ReForma import ReForma
39
+
40
+ # Point to your compiled JAR
41
+ ReForma = ReForma("path/to/ReFormaTool.jar")
42
+
43
+ # Load a model
44
+ state = ReForma.load_file("examples/recommender.r")
45
+
46
+ print(state.current_states) # ['Home']
47
+ print(state.enabled) # [Transition('go_work': Home → Office, p=0.500), ...]
48
+
49
+ # Simulate
50
+ state = ReForma.step("go_work")
51
+ state = ReForma.step("easy_task")
52
+ state = ReForma.undo() # undo last step
53
+
54
+ # Reset to initial state
55
+ ReForma.reset()
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Simulation
61
+
62
+ ```python
63
+ state = ReForma.load_file("model.r")
64
+
65
+ # Check what's enabled
66
+ for t in state.enabled:
67
+ print(f"{t.label}: {t.from_state} → {t.to_state} (p={t.probability:.3f})")
68
+
69
+ # Take a step by label
70
+ state = ReForma.step("go_work")
71
+
72
+ # Undo / reset
73
+ state = ReForma.undo()
74
+ state = ReForma.reset()
75
+
76
+ # Inspect variables
77
+ print(state.variables) # {'counter': 0, 'flag': 1}
78
+
79
+ # History of labels taken
80
+ print(ReForma.history) # ['go_work']
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Training
86
+
87
+ Train the model on a batch of sessions (lists of event labels):
88
+
89
+ ```python
90
+ ReForma.train([
91
+ ["go_work", "easy_task", "easy_task", "go_home"],
92
+ ["battery_low", "go_charge", "finish_charge", "socialize"],
93
+ ["no_money", "go_work", "go_home"],
94
+ ])
95
+
96
+ # Or train directly from a log file (one session per line, comma-separated)
97
+ ReForma.train_from_file("logs/sessions.txt")
98
+
99
+ # sessions.txt format:
100
+ # go_work,easy_task,go_home
101
+ # battery_low,go_charge,finish_charge
102
+
103
+ # Save the updated model with new weights
104
+ ReForma.save_source("model_trained.r")
105
+ ```
106
+
107
+ ---
108
+
109
+ ## PDL / PCTL Verification
110
+
111
+ ```python
112
+ # Quantitative: probability of eventually reaching Office
113
+ prob = ReForma.check_pdl_value("Home", "{P=?[F Office]}")
114
+ print(f"P(reach Office from Home) = {prob:.4f}")
115
+
116
+ # Qualitative: is it probable?
117
+ holds = ReForma.check_pdl_value("Home", "{P>=0.4[F Office]}")
118
+ print(f"P>=0.4? {holds}") # True / False
119
+
120
+ # PDL: is there a path via go_work to Office?
121
+ holds = ReForma.check_pdl_value("Home", "<go_work>Office")
122
+ print(holds) # True
123
+
124
+ # Get the raw string result
125
+ raw = ReForma.check_pdl("Home", "{P=?[F Office]}")
126
+ print(raw) # "Result: 0.50000"
127
+ ```
128
+
129
+ ### Formula syntax reference
130
+
131
+ | Formula | Meaning |
132
+ |-----------------------------|------------------------------------------------|
133
+ | `{P=?[F target]}` | Probability of eventually reaching `target` |
134
+ | `{P=?[G safe]}` | Probability of staying in `safe` forever |
135
+ | `{P=?[X next]}` | Probability of reaching `next` in one step |
136
+ | `{P=?[a U b]}` | Probability of `a` until `b` |
137
+ | `{P>=0.5[F target]}` | Is probability of reaching target ≥ 0.5? |
138
+ | `<action>state` | There exists a path via `action` to `state` |
139
+ | `[action]state` | All paths via `action` lead to `state` |
140
+
141
+ ---
142
+
143
+ ## Export
144
+
145
+ ```python
146
+ # PRISM DTMC
147
+ prism_code = ReForma.export_prism()
148
+ ReForma.save_prism("output/model.pm")
149
+
150
+ # mCRL2
151
+ mcrl2_code = ReForma.export_mcrl2()
152
+
153
+ # GLTS (imperative translation)
154
+ glts_code = ReForma.export_glts()
155
+
156
+ # Mermaid diagram (initial state)
157
+ diagram = ReForma.export_mermaid()
158
+
159
+ # Mermaid diagram (full LTS — all reachable states)
160
+ full_diagram = ReForma.export_mermaid(full_lts=True)
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Loading from a string
166
+
167
+ ```python
168
+ source = """
169
+ name MyModel
170
+ init s0
171
+ s0 ---> s1: a (0.6)
172
+ s0 ---> s2: b (0.4)
173
+ s1 ---> s0: back (1.0)
174
+ """
175
+
176
+ state = ReForma.load(source, name="MyModel")
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Running the tests
182
+
183
+ ```bash
184
+ pip install pytest
185
+ pytest tests/ -v
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Project structure
191
+
192
+ ```
193
+ ReForma/
194
+ ├── __init__.py # Public API exports
195
+ ├── client.py # ReForma — high-level Python API
196
+ ├── jar_bridge.py # JarBridge — low-level subprocess wrapper
197
+ └── model.py # ReFormaModel, SimulationState, Transition data classes
198
+ tests/
199
+ └── test_ReForma.py # Full test suite (mocked, no JAR needed)
200
+ pyproject.toml
201
+ README.md
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Error handling
207
+
208
+ ```python
209
+ from ReForma.jar_bridge import JarError
210
+
211
+ try:
212
+ result = ReForma.check_pdl("Home", "{P=?[F Office]}")
213
+ except JarError as e:
214
+ print(f"JAR error: {e}")
215
+ except RuntimeError as e:
216
+ print(f"Usage error: {e}") # e.g. no model loaded, invalid transition
217
+ ```
@@ -0,0 +1,9 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ ReForma.egg-info/PKG-INFO
5
+ ReForma.egg-info/SOURCES.txt
6
+ ReForma.egg-info/dependency_links.txt
7
+ ReForma.egg-info/requires.txt
8
+ ReForma.egg-info/top_level.txt
9
+ tests/test_pyRe.py
@@ -0,0 +1,2 @@
1
+ networkx
2
+ matplotlib
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ReForma"
7
+ version = "0.1.0"
8
+ description = "Python bindings for the RePA/ReForma probabilistic automaton tool"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Joshua Dourado", email = "joshuadourado@ua.pt" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Scientific/Engineering",
20
+ ]
21
+ dependencies = [
22
+ "networkx",
23
+ "matplotlib"
24
+ ]
25
+
26
+ [tool.setuptools.packages.find]
27
+ where = ["."]
28
+ include = ["ReForma*"]
29
+
30
+ [tool.setuptools.package-data]
31
+ ReForma = ["bin/RePATool.jar"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,285 @@
1
+ """
2
+ tests/test_reforma.py — full test suite (mocked, no real JAR needed).
3
+ """
4
+
5
+ import pytest
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ from reforma import reforma
9
+
10
+ from reforma.model import reformaModel, Transition, SimulationState
11
+ from reforma.jar_bridge import JarBridge, JarError
12
+ from reforma.client import _parse_pdl_result
13
+
14
+ # ---------------------------------------------------------------------------
15
+ SIMPLE_SOURCE = """\
16
+ name Simple
17
+ init Home
18
+ Home ---> Office: go_work (0.5)
19
+ Home ---> Station: go_charge (0.5)
20
+ Office ---> Home: go_home (1.0)
21
+ """
22
+ STEP_OUTPUT = """\
23
+ Estado Atual: Home
24
+ Transicoes Habilitadas:
25
+ - [go_work] de Home para Office (P=0.500)
26
+ - [go_charge] de Home para Station (P=0.500)
27
+ """
28
+ STEP_OUTPUT_OFFICE = """\
29
+ Estado Atual: Office
30
+ Transicoes Habilitadas:
31
+ - [go_home] de Office para Home (P=1.000)
32
+ """
33
+
34
+
35
+ def make_reforma(step_output: str = STEP_OUTPUT) -> reforma:
36
+ """reforma with a fully mocked JarBridge — no JAR or Java needed."""
37
+ with patch("reforma.client.JarBridge") as MockBridge:
38
+ instance = MockBridge.return_value
39
+ instance.list_transitions.return_value = step_output
40
+ reforma = reforma() # no jar_path — uses bundled (mocked away)
41
+ reforma._bridge = instance
42
+ reforma.load(SIMPLE_SOURCE)
43
+ return reforma
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # reformaModel
48
+ # ---------------------------------------------------------------------------
49
+ class TestreformaModel:
50
+ def test_from_string(self):
51
+ m = reformaModel.from_string("init s0\ns0 ---> s1: a", name="test")
52
+ assert m.name == "test"
53
+
54
+ def test_from_file(self, tmp_path):
55
+ p = tmp_path / "model.r"
56
+ p.write_text(SIMPLE_SOURCE)
57
+ m = reformaModel.from_file(str(p))
58
+ assert m.name == "model"
59
+ assert "Home" in m.source
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # SimulationState
64
+ # ---------------------------------------------------------------------------
65
+ class TestSimulationState:
66
+ def test_transition_named_found(self):
67
+ t = Transition("Home", "Office", "go_work", "go_work", 0.5)
68
+ s = SimulationState(["Home"], [t], {}, False, None)
69
+ assert s.transition_named("go_work") is t
70
+
71
+ def test_transition_named_not_found(self):
72
+ s = SimulationState(["Home"], [], {}, False, None)
73
+ assert s.transition_named("missing") is None
74
+
75
+
76
+ # ---------------------------------------------------------------------------
77
+ # JarBridge — bundled JAR discovery
78
+ # ---------------------------------------------------------------------------
79
+ class TestJarBridge:
80
+ def test_explicit_missing_raises(self):
81
+ with pytest.raises(FileNotFoundError):
82
+ JarBridge("/nonexistent/RePATool.jar")
83
+
84
+ def test_no_bundled_jar_raises(self):
85
+ with patch("reforma.jar_bridge._bundled_jar_path", return_value=None):
86
+ with pytest.raises(FileNotFoundError, match="bundled"):
87
+ JarBridge() # no explicit path, no bundled jar
88
+
89
+ def test_bundled_jar_used_when_present(self, tmp_path):
90
+ fake_jar = tmp_path / "RePATool.jar"
91
+ fake_jar.write_bytes(b"PK") # minimal fake
92
+ with patch("reforma.jar_bridge._bundled_jar_path", return_value=fake_jar):
93
+ bridge = JarBridge()
94
+ assert bridge.jar_path == str(fake_jar)
95
+
96
+ def test_explicit_jar_overrides_bundled(self, tmp_path):
97
+ custom = tmp_path / "custom.jar"
98
+ custom.write_bytes(b"PK")
99
+ bridge = JarBridge(str(custom))
100
+ assert "custom" in bridge.jar_path
101
+
102
+ def test_run_nonzero_raises(self, tmp_path):
103
+ fake = tmp_path / "f.jar"
104
+ fake.write_bytes(b"")
105
+ bridge = JarBridge.__new__(JarBridge)
106
+ bridge.jar_path = str(fake)
107
+ bridge.java_bin = "java"
108
+ import subprocess
109
+ with patch("reforma.jar_bridge.subprocess.run") as mock_run:
110
+ mock_run.return_value = MagicMock(returncode=1, stdout="", stderr="err")
111
+ with pytest.raises(JarError):
112
+ bridge._run("-step", "x.r")
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # reforma.bundled_jar()
117
+ # ---------------------------------------------------------------------------
118
+ class TestBundledJar:
119
+ def test_returns_none_when_absent(self):
120
+ with patch("reforma.jar_bridge._bundled_jar_path", return_value=None):
121
+ assert reforma.bundled_jar() is None
122
+
123
+ def test_returns_path_when_present(self, tmp_path):
124
+ fake = tmp_path / "RePATool.jar"
125
+ fake.write_bytes(b"PK")
126
+ with patch("reforma.jar_bridge._bundled_jar_path", return_value=fake):
127
+ assert reforma.bundled_jar() == str(fake)
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # reforma high-level
132
+ # ---------------------------------------------------------------------------
133
+ class TestreformaLoad:
134
+ def test_load_parses_state(self):
135
+ reforma = make_reforma()
136
+ assert reforma.state.current_states == ["Home"]
137
+
138
+ def test_enabled_transitions(self):
139
+ reforma = make_reforma()
140
+ labels = {t.label for t in reforma.state.enabled}
141
+ assert {"go_work", "go_charge"} == labels
142
+
143
+ def test_probabilities(self):
144
+ reforma = make_reforma()
145
+ t = reforma.state.transition_named("go_work")
146
+ assert abs(t.probability - 0.5) < 1e-6
147
+
148
+ def test_no_model_raises(self):
149
+ with patch("reforma.client.JarBridge"):
150
+ reforma = reforma()
151
+ with pytest.raises(RuntimeError, match="No model loaded"):
152
+ reforma.step("x")
153
+
154
+
155
+ class TestreformaSimulation:
156
+ def test_step_moves_state(self):
157
+ reforma = make_reforma()
158
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
159
+ state = reforma.step("go_work")
160
+ assert state.current_states == ["Office"]
161
+
162
+ def test_step_invalid_raises(self):
163
+ reforma = make_reforma()
164
+ with pytest.raises(RuntimeError, match="not enabled"):
165
+ reforma.step("fly_to_moon")
166
+
167
+ def test_history_tracks_steps(self):
168
+ reforma = make_reforma()
169
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
170
+ reforma.step("go_work")
171
+ assert reforma.history == ["go_work"]
172
+
173
+ def test_undo_pops_history(self):
174
+ reforma = make_reforma()
175
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
176
+ reforma.step("go_work")
177
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT
178
+ reforma.undo()
179
+ assert reforma.history == []
180
+
181
+ def test_undo_empty_raises(self):
182
+ reforma = make_reforma()
183
+ with pytest.raises(RuntimeError, match="Nothing to undo"):
184
+ reforma.undo()
185
+
186
+ def test_reset_clears_history(self):
187
+ reforma = make_reforma()
188
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
189
+ reforma.step("go_work")
190
+ reforma.reset()
191
+ assert reforma.history == []
192
+
193
+
194
+ class TestreformaTraining:
195
+ def test_train_updates_source(self):
196
+ reforma = make_reforma()
197
+ updated = SIMPLE_SOURCE.replace("(0.5)", "(0.7)")
198
+ reforma._bridge.train.return_value = updated
199
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT
200
+ reforma.train([["go_work", "go_home"]])
201
+ assert "(0.7)" in reforma.source
202
+
203
+ def test_train_resets_history(self):
204
+ reforma = make_reforma()
205
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT_OFFICE
206
+ reforma.step("go_work")
207
+ reforma._bridge.train.return_value = SIMPLE_SOURCE
208
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT
209
+ reforma.train([["go_work"]])
210
+ assert reforma.history == []
211
+
212
+ def test_train_from_file(self, tmp_path):
213
+ log = tmp_path / "log.txt"
214
+ log.write_text("go_work,go_home\ngo_charge\n")
215
+ reforma = make_reforma()
216
+ reforma._bridge.train.return_value = SIMPLE_SOURCE
217
+ reforma._bridge.list_transitions.return_value = STEP_OUTPUT
218
+ reforma.train_from_file(str(log))
219
+ assert reforma._bridge.train.call_args[0][1] == ["go_work,go_home", "go_charge"]
220
+
221
+
222
+ class TestreformaVerification:
223
+ def test_check_pdl_string(self):
224
+ reforma = make_reforma()
225
+ reforma._bridge.check_pdl.return_value = "Result: 0.50000"
226
+ assert "0.50000" in reforma.check_pdl("Home", "{P=?[F Office]}")
227
+
228
+ def test_check_pdl_value_float(self):
229
+ reforma = make_reforma()
230
+ reforma._bridge.check_pdl.return_value = "Result: 0.75000"
231
+ assert abs(reforma.check_pdl_value("Home", "{P=?[F Office]}") - 0.75) < 1e-6
232
+
233
+ def test_check_pdl_true(self):
234
+ reforma = make_reforma()
235
+ reforma._bridge.check_pdl.return_value = "Result: true"
236
+ assert reforma.check_pdl_value("Home", "<go_work>Office") is True
237
+
238
+ def test_check_pdl_false(self):
239
+ reforma = make_reforma()
240
+ reforma._bridge.check_pdl.return_value = "Result: false"
241
+ assert reforma.check_pdl_value("Home", "[]false") is False
242
+
243
+
244
+ class TestreformaExport:
245
+ def test_export_prism(self):
246
+ reforma = make_reforma()
247
+ reforma._bridge.get_prism.return_value = "dtmc\n..."
248
+ assert reforma.export_prism().startswith("dtmc")
249
+
250
+ def test_export_mcrl2(self):
251
+ reforma = make_reforma()
252
+ reforma._bridge.get_mcrl2.return_value = "act\n..."
253
+ assert reforma.export_mcrl2().startswith("act")
254
+
255
+ def test_export_glts(self):
256
+ reforma = make_reforma()
257
+ reforma._bridge.get_glts.return_value = "int go_work_active = 1"
258
+ assert "go_work_active" in reforma.export_glts()
259
+
260
+ def test_save_prism(self, tmp_path):
261
+ reforma = make_reforma()
262
+ reforma._bridge.get_prism.return_value = "dtmc\n..."
263
+ out = tmp_path / "model.pm"
264
+ reforma.save_prism(str(out))
265
+ assert out.read_text().startswith("dtmc")
266
+
267
+ def test_save_source(self, tmp_path):
268
+ reforma = make_reforma()
269
+ out = tmp_path / "out.r"
270
+ reforma.save_source(str(out))
271
+ assert "Home" in out.read_text()
272
+
273
+
274
+ class TestParsePdlResult:
275
+ def test_float(self):
276
+ assert abs(_parse_pdl_result("Result: 0.33333") - 0.33333) < 1e-4
277
+
278
+ def test_true(self):
279
+ assert _parse_pdl_result("Result: true") is True
280
+
281
+ def test_false(self):
282
+ assert _parse_pdl_result("Result: false") is False
283
+
284
+ def test_unparseable(self):
285
+ assert isinstance(_parse_pdl_result("Deadlock found"), str)