Skip to content

Commit 2e4eebb

Browse files
committed
parameterized example
Each automaton in this family is a bottleneck of capacity n, facilitated by $O(n)$ states and actions. The idea is to let all processes go to one of $n+1$ distinct nodes; afterwards, one has to play an action $aj$ identifying a node $j$ that is not occupied. This move wins, any other loses. All of these models are **not** congrollable for arbitrary number of processes, but the $n$-fold product of the size-$m$ model is controllable iff $n\le m$. The two python scripts generate (dot) NFAs and run commands on them, respectively. See `examples/guess-empty-n/README.md` for details.
1 parent a142b5d commit 2e4eebb

4 files changed

Lines changed: 170 additions & 9 deletions

File tree

examples/guess-empty-n/README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11

22
# A family of uncontrollable NFAs
33

4-
Each automaton in this family is a bottleneck of capacity n, facilitated by n states and actions.
5-
The idea is to let all processes go to one of $n$ distinct nodes; afterwards, one has to play an action $aj$
4+
Each automaton in this family is a bottleneck of capacity n, facilitated by $O(n)$ states and actions.
5+
The idea is to let all processes go to one of $n+1$ distinct nodes; afterwards, one has to play an action $aj$
66
identifying a node $j$ that is not occupied. This move wins, any other loses.
77

8-
From $n>1$ onwards, all of these models are **not** congrollable for arbitrary number of processes, but the $n$-fold product of the size-$m$ model is controllable.
8+
![nfa for N=2](nfa-2.png)
9+
10+
All of these models are **not** congrollable for arbitrary number of processes, but the $n$-fold product of the size-$m$ model is controllable iff $n\le m$.
911

1012
## Generate model files
1113

@@ -16,14 +18,14 @@ cd examples/guess-empty-n
1618
./generate_models.py 10
1719

1820
ls
19-
bottleneck-10.dot bottleneck-2.dot bottleneck-4.dot bottleneck-6.dot bottleneck-8.dot generate_models.py
20-
bottleneck-1.dot bottleneck-3.dot bottleneck-5.dot bottleneck-7.dot bottleneck-9.dot
21+
nfa-10.dot nfa-2.dot nfa-4.dot nfa-6.dot nfa-8.dot generate_models.py
22+
nfa-1.dot nfa-3.dot nfa-5.dot nfa-7.dot nfa-9.dot
2123
```
2224

2325
## Solve them using prism
2426

2527
```console
26-
time ./target/release/schaeppert -f dot examples/guess-empty-n/bottleneck-5.dot iterate tmp/
28+
time ./target/release/schaeppert -f dot examples/guess-empty-n/nfa-5.dot iterate tmp/
2729
n=1 -> 1.000
2830
n=2 -> 1.000
2931
n=3 -> 1.000
@@ -32,15 +34,15 @@ n=5 -> 0.962
3234
The value is less than 1.0, stopping the search.
3335
The 5-fold power of this NFA is not controllable.
3436

35-
./target/release/schaeppert -f dot examples/guess-empty-n/bottleneck-5.dot 5.82s user 0.33s system 216% cpu 2.847 total
37+
./target/release/schaeppert -f dot examples/guess-empty-n/nfa-5.dot 5.82s user 0.33s system 216% cpu 2.847 total
3638
```
3739

3840

3941
## Solve them using shepherd
4042

4143

4244
```console
43-
time ./target/release/shepherd -f dot examples/guess-empty-n/bottleneck-5.dot
45+
time ./target/release/shepherd -f dot examples/guess-empty-n/nfa-5.dot
4446

4547
Maximal winning strategy;
4648
Answer:
@@ -71,5 +73,5 @@ Play action 'a5' in the downward-closure of
7173
( ω , ω , _ , _ , ω , ω , _ )
7274

7375

74-
./target/release/shepherd -f dot examples/guess-empty-n/bottleneck-5.dot 0.37s user 0.06s system 175% cpu 0.248 total
76+
./target/release/shepherd -f dot examples/guess-empty-n/nfa-5.dot 0.37s user 0.06s system 175% cpu 0.248 total
7577
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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
7+
with a specific structure:
8+
- States 0 through i, plus a target state T.
9+
- Initial transitions from state 0 to each state 1..i+1 labeled 'a'.
10+
- 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.
11+
12+
Usage:
13+
python generate_models.py <n>
14+
Where <n> is a positive integer.
15+
16+
17+
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.
18+
"""
19+
import sys
20+
def generate_model_file(n):
21+
filename = f"nfa-{n}.dot"
22+
with open(filename, "w") as f:
23+
f.write('digraph NFA {\n')
24+
f.write(' rankdir=TB;\n')
25+
f.write(' init [label=" ",shape=none,height=0,width=0];\n')
26+
# States 0..N
27+
for i in range(n + 2):
28+
f.write(f' {i} [label="{i}", shape=circle];\n')
29+
# Target state T
30+
f.write(' T [label="T", shape=doublecircle];\n\n')
31+
# Initial arrow
32+
f.write(' init -> 0;\n')
33+
# Transitions from 0 to 1..N
34+
for i in range(1, n + 2):
35+
f.write(f' 0 -> {i} [label="a"];\n')
36+
# For every two nodes 1 <= i, j <= N with i != j, add j -> T [label="i"]
37+
for j in range(1, n + 2):
38+
for i in range(1, n + 2):
39+
if i != j:
40+
f.write(f' {j} -> T [label="a{i}"];\n')
41+
f.write('}\n')
42+
43+
def main():
44+
if len(sys.argv) != 2:
45+
print("Usage: python generate_models.py <n>")
46+
sys.exit(1)
47+
try:
48+
n = int(sys.argv[1])
49+
if n < 1:
50+
raise ValueError
51+
except ValueError:
52+
print("n must be a positive integer")
53+
sys.exit(1)
54+
for i in range(1, n + 1):
55+
generate_model_file(i)
56+
57+
if __name__ == "__main__":
58+
main()

examples/guess-empty-n/nfa-2.png

24 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)