Skip to content

Commit 098c9c1

Browse files
committed
example: iterative majority
1 parent 05329bf commit 098c9c1

File tree

4 files changed

+234
-0
lines changed

4 files changed

+234
-0
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
2+
# A family of uncontrollable NFAs
3+
4+
Each automaton in this family is a chain of diamonds where after a split, controller can move a majority of the population into the target and moves the rest to the next round. The last round is a bad sink.
5+
6+
![nfa for N=3](nfa-3.png)
7+
8+
All of these models are **not** congrollable for arbitrary number of processes, but in the size-$n$ chain one can control $\le 2^n$ tokens.
9+
10+
## Generate model files
11+
12+
Starting in shepherd's main directory, run the python script to generate 10 models.
13+
14+
```console
15+
./generate_models.py 10
16+
nfa-10.dot nfa-2.dot nfa-4.dot nfa-6.dot nfa-8.dot generate_models.py
17+
nfa-1.dot nfa-3.dot nfa-5.dot nfa-7.dot nfa-9.dot
18+
```
19+
20+
## Solve them using prism
21+
22+
This assumes the `schaeppert` binary is in your PATH, which can be done by running `cargo install --path .`
23+
24+
```console
25+
schaeppert -f dot nfa-3.dot iterate tmp/
26+
n=1 -> 1.000
27+
n=2 -> 1.000
28+
n=3 -> 1.000
29+
n=4 -> 1.000
30+
n=5 -> 1.000
31+
n=6 -> 1.000
32+
n=7 -> 1.000
33+
n=8 -> 0.949
34+
The value is less than 1.0, stopping the search.
35+
The 8-fold power of this NFA is not controllable.
36+
```
37+
38+
39+
## Solve them using shepherd
40+
41+
42+
```console
43+
shepherd -f dot nfa-2.dot
44+
45+
Maximal winning strategy;
46+
Answer:
47+
NO (uncontrollable)
48+
49+
States: ( 1a , 0a , T , 1 , 0b , 2 , 1b , 0 )
50+
Play action 'a' in the downward-closure of
51+
( ω , ω , ω , 1 , 1 , _ , _ , 1 )
52+
( ω , ω , ω , _ , 1 , _ , _ , 3 )
53+
54+
55+
Play action 'b' in the downward-closure of
56+
( _ , 1 , ω , 1 , ω , _ , ω , 1 )
57+
( _ , 1 , ω , _ , ω , _ , ω , 3 )
58+
```
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
"""
3+
generate_models.py
4+
5+
This script generates DOT files representing a family of NFA (nondeterministic finite automaton) models.
6+
For each integer i from 1 to n (inclusive), it creates a file named 'nfa-i.dot' describing an NFA with a specific structure:
7+
- States 0 through i, plus a target state T.
8+
- Initial transitions from state 0 to each state 1..i+1 labeled 'a'.
9+
- For each state j in 1..i+1, transitions from j to T labeled 'a{k}' for every k in 1..i+1 where k != j.
10+
11+
Usage:
12+
python generate_models.py <n>
13+
Where <n> is a positive integer.
14+
15+
16+
Note: All of these models are not congrollable for arbitrary number of processes, but the n-fold product of the size-m model is controllable by playing action "a", then "aj" if node "j" if none of the n processes reside in node j.
17+
"""
18+
import sys
19+
def generate_model_file(n):
20+
filename = f"nfa-{n}.dot"
21+
with open(filename, "w") as f:
22+
f.write('digraph NFA {\n')
23+
f.write(' init [label=" ",shape=none,height=0,width=0];\n')
24+
# States 0..N
25+
for i in range(n):
26+
f.write(
27+
f' s{i} [label="{i}", shape=circle];\n'
28+
f' s{i}a [label="{i}a", shape=circle];\n'
29+
f' s{i}b [label="{i}b", shape=circle];\n'
30+
)
31+
# Sink
32+
f.write(f' s{n} [label="{n}", shape=circle];\n')
33+
# Target state T
34+
f.write(' T [label="T", shape=doublecircle];\n\n')
35+
# Initial arrow
36+
f.write(' init -> s0;\n')
37+
# Transitions from 0 to 1..N
38+
for i in range(n):
39+
f.write(
40+
f' s{i} -> s{i}a [label="a"];\n'
41+
f' s{i} -> s{i}b [label="a"];\n'
42+
f' s{i} -> s{i}a [label="b"];\n'
43+
f' s{i} -> s{i}b [label="b"];\n'
44+
45+
f' s{i}a -> T [label="a"];\n'
46+
f' s{i}a -> s{i+1} [label="b"];\n'
47+
f' s{i}b -> T [label="b"];\n'
48+
f' s{i}b -> s{i+1} [label="a"];\n'
49+
)
50+
51+
# Good and bad sink
52+
f.write(
53+
' T -> T [label="a"];\n'
54+
' T -> T [label="b"];\n'
55+
f' s{n} -> s{n} [label="a"];\n'
56+
f' s{n} -> s{n} [label="b"];\n'
57+
)
58+
f.write('}\n')
59+
60+
def main():
61+
if len(sys.argv) != 2:
62+
print("Usage: python generate_models.py <n>")
63+
sys.exit(1)
64+
try:
65+
n = int(sys.argv[1])
66+
if n < 1:
67+
raise ValueError
68+
except ValueError:
69+
print("n must be a positive integer")
70+
sys.exit(1)
71+
for i in range(1, n + 1):
72+
generate_model_file(i)
73+
74+
if __name__ == "__main__":
75+
main()
70.5 KB
Loading
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import shutil
4+
import subprocess
5+
import sys
6+
import time
7+
import csv
8+
import matplotlib.pyplot as plt
9+
10+
# Constants
11+
COMMANDS = [
12+
"schaeppert -vv -f dot nfa-{n}.dot iterate tmp/",
13+
# "schaeppert -f dot nfa-{n}.dot iterate tmp/",
14+
#"shepherd -vv -f dot nfa-{n}.dot",
15+
"shepherd -f dot nfa-{n}.dot",
16+
]
17+
TIMEOUT = 60 # seconds
18+
TMP_DIR = "tmp"
19+
LOGFILE_TEMPLATE = "log_{cmd_idx}_n{n}.txt"
20+
21+
def ensure_empty_tmp():
22+
if os.path.exists(TMP_DIR):
23+
shutil.rmtree(TMP_DIR)
24+
os.makedirs(TMP_DIR)
25+
26+
def run_command(cmd, logfile, timeout):
27+
start = time.time()
28+
try:
29+
proc = subprocess.run(
30+
cmd,
31+
shell=True,
32+
stdout=subprocess.PIPE,
33+
stderr=subprocess.PIPE,
34+
timeout=timeout
35+
)
36+
elapsed = time.time() - start
37+
with open(logfile, 'wb') as f:
38+
f.write(b'=== STDOUT ===\n')
39+
f.write(proc.stdout)
40+
f.write(b'\n=== STDERR ===\n')
41+
f.write(proc.stderr)
42+
return elapsed
43+
except subprocess.TimeoutExpired as e:
44+
with open(logfile, 'wb') as f:
45+
f.write(b'=== STDOUT ===\n')
46+
f.write((e.stdout or b''))
47+
f.write(b'\n=== STDERR ===\n')
48+
f.write((e.stderr or b''))
49+
f.write(b'\n=== TIMEOUT ===\n')
50+
return None
51+
52+
def main():
53+
if len(sys.argv) != 2:
54+
print(f"Usage: {sys.argv[0]} N", file=sys.stderr)
55+
sys.exit(1)
56+
try:
57+
N = int(sys.argv[1])
58+
except ValueError:
59+
print("N must be an integer", file=sys.stderr)
60+
sys.exit(1)
61+
62+
ensure_empty_tmp()
63+
results = []
64+
for n in range(1, N + 1):
65+
row = {'n': n}
66+
for cmd_idx, cmd_template in enumerate(COMMANDS):
67+
cmd = cmd_template.format(n=n)
68+
logfile = LOGFILE_TEMPLATE.format(cmd_idx=cmd_idx, n=n)
69+
print(f"Running command {cmd_idx+1} for n={n}: {cmd_template}")
70+
t = run_command(cmd, logfile, TIMEOUT)
71+
if t is None:
72+
break
73+
row[f'cmd{cmd_idx+1}_time'] = t
74+
else:
75+
results.append(row)
76+
continue
77+
break
78+
79+
# Write CSV to stdout
80+
fieldnames = ['n'] + [f'cmd{i+1}_time' for i in range(len(COMMANDS))]
81+
writer = csv.DictWriter(sys.stdout, fieldnames=fieldnames)
82+
writer.writeheader()
83+
for row in results:
84+
writer.writerow(row)
85+
86+
# Plotting
87+
ns = [row['n'] for row in results]
88+
for cmd_idx, cmd_template in enumerate(COMMANDS):
89+
times = [row.get(f'cmd{cmd_idx+1}_time') for row in results]
90+
plt.plot(ns, times, label=cmd_template)
91+
plt.xlabel('n')
92+
plt.ylabel('Wallclock time (s)')
93+
plt.legend()
94+
plt.title('Command Running Times')
95+
plt.grid(True)
96+
plt.tight_layout()
97+
plt.savefig("benchmark_results.png")
98+
plt.show()
99+
100+
if __name__ == '__main__':
101+
main()

0 commit comments

Comments
 (0)