Skip to content

Commit 02f5f54

Browse files
authored
yices2_parallel.py (#564)
* Create yices2_mcsat_parallel.py * Update yices2_mcsat_parallel.py * Update yices2_mcsat_parallel.py * update portfolio * update portfolio * Update yices2_parallel.py * Update yices2_parallel.py
1 parent ed9086a commit 02f5f54

File tree

1 file changed

+236
-0
lines changed

1 file changed

+236
-0
lines changed

utils/yices2_parallel.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
#!/usr/bin/env python3
2+
3+
#
4+
# This file is part of the Yices SMT Solver.
5+
# Copyright (C) 2025 SRI International.
6+
#
7+
# Yices is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU General Public License as published by
9+
# the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# Yices is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with Yices. If not, see <http://www.gnu.org/licenses/>.
19+
#
20+
21+
import subprocess
22+
import threading
23+
import sys
24+
import os
25+
import signal
26+
import time
27+
import argparse
28+
import random
29+
from typing import List
30+
31+
def _global_signal_handler(signum, frame):
32+
"""Global signal handler for the main process"""
33+
if signum == signal.SIGINT:
34+
print("\nReceived Ctrl+C, shutting down gracefully...", file=sys.stderr)
35+
else:
36+
print(f"\nReceived signal {signum}, shutting down gracefully...", file=sys.stderr)
37+
38+
# Clean up any running threads
39+
if hasattr(_global_signal_handler, 'solver'):
40+
_global_signal_handler.solver.cleanup_threads()
41+
42+
sys.exit(0)
43+
44+
class ConfigGenerator:
45+
def __init__(self, num_configs: int, seed: int = None, use_l2o: bool = False):
46+
self.num_configs = num_configs
47+
self.use_l2o = use_l2o
48+
if seed is not None:
49+
random.seed(seed)
50+
51+
def generate_configs(self) -> List[List[str]]:
52+
configs = [[], ["--mcsat", "--mcsat-na-bound"]] # Always include empty config and na bound
53+
if self.use_l2o:
54+
configs.append(["--mcsat", "--mcsat-l2o"])
55+
56+
for _ in range(self.num_configs - 2):
57+
options = ["--mcsat"] # Start with base mcsat option
58+
if random.random() < 0.9:
59+
options.append(f"--mcsat-rand-dec-seed={random.randint(1, 1000000)}")
60+
if random.random() < 0.9:
61+
options.append(f"--mcsat-rand-dec-freq={random.uniform(0, 1):.2f}")
62+
if random.random() < 0.5:
63+
options.append("--mcsat-na-nlsat")
64+
if random.random() < 0.5:
65+
options.append("--mcsat-na-mgcd")
66+
if random.random() < 0.5:
67+
options.append("--mcsat-na-bound")
68+
if random.random() < 0.5:
69+
options.append("--mcsat-partial-restart")
70+
if self.use_l2o and random.random() < 0.5:
71+
options.append("--mcsat-l2o")
72+
73+
if len(options) == 1: # If only base option, add a random one
74+
options.append(random.choice([
75+
f"--mcsat-rand-dec-freq={random.uniform(0, 1):.2f}",
76+
f"--mcsat-rand-dec-seed={random.randint(1, 1000000)}"
77+
]))
78+
79+
configs.append(options)
80+
81+
return configs
82+
83+
class PortfolioSolver:
84+
def __init__(self, yices_path, smt2_file, num_threads, verbose=False, seed=None, use_l2o=False):
85+
self.yices_path = yices_path
86+
self.smt2_file = smt2_file
87+
self.num_threads = num_threads
88+
self.verbose = verbose
89+
self.seed = seed
90+
self.use_l2o = use_l2o
91+
self.threads = []
92+
self.stop_event = threading.Event()
93+
self.start_time = None
94+
self.processes = []
95+
self._solution = None
96+
_global_signal_handler.solver = self
97+
98+
@property
99+
def solution(self):
100+
return self._solution
101+
102+
@solution.setter
103+
def solution(self, value):
104+
if not self.stop_event.is_set():
105+
self._solution = value
106+
self.stop_event.set()
107+
self.cleanup_threads()
108+
109+
def log(self, message):
110+
if self.verbose:
111+
print(f"[{time.time() - self.start_time:.2f}s] {message}", file=sys.stderr)
112+
113+
def cleanup_threads(self):
114+
self.stop_event.set()
115+
for process in self.processes:
116+
try:
117+
if process and process.poll() is None:
118+
process.kill()
119+
except (ProcessLookupError, OSError):
120+
pass
121+
self.threads.clear()
122+
self.processes.clear()
123+
124+
def run_yices(self, thread_id, params):
125+
process = None
126+
try:
127+
cmd = [self.yices_path] + params + [self.smt2_file]
128+
self.log(f"Thread {thread_id} starting with params: {' '.join(params)}")
129+
130+
process = subprocess.Popen(
131+
cmd,
132+
stdout=subprocess.PIPE,
133+
stderr=subprocess.PIPE,
134+
text=True
135+
)
136+
self.processes.append(process)
137+
138+
stdout, stderr = process.communicate()
139+
140+
if not self.stop_event.is_set():
141+
if "unsat" in stdout.lower():
142+
self.log(f"Thread {thread_id} found UNSAT solution")
143+
self.solution = ("unsat", stdout, thread_id, params)
144+
elif "sat" in stdout.lower():
145+
self.log(f"Thread {thread_id} found SAT solution")
146+
self.solution = ("sat", stdout, thread_id, params)
147+
else:
148+
self.log(f"Thread {thread_id} finished without finding solution")
149+
150+
except Exception as e:
151+
if not self.stop_event.is_set():
152+
self.log(f"Error in thread {thread_id}: {e}")
153+
finally:
154+
if process and process.poll() is None:
155+
try:
156+
process.kill()
157+
except (ProcessLookupError, OSError):
158+
pass
159+
160+
def solve(self):
161+
try:
162+
config_generator = ConfigGenerator(self.num_threads, self.seed, self.use_l2o)
163+
param_sets = config_generator.generate_configs()
164+
165+
self.start_time = time.time()
166+
self.log(f"Starting portfolio solver with {self.num_threads} threads")
167+
self.log(f"Using yices_smt2 from: {self.yices_path}")
168+
self.log(f"Solving file: {self.smt2_file}")
169+
if self.seed is not None:
170+
self.log(f"Using random seed: {self.seed}")
171+
self.log("Generated configurations:")
172+
for i, params in enumerate(param_sets):
173+
self.log(f" Config {i}: {' '.join(params) if params else '(default)'}")
174+
175+
for i in range(min(self.num_threads, len(param_sets))):
176+
thread = threading.Thread(target=self.run_yices, args=(i, param_sets[i]))
177+
thread.daemon = True
178+
self.threads.append(thread)
179+
thread.start()
180+
self.log(f"Started thread {i}")
181+
182+
for thread in self.threads:
183+
thread.join()
184+
185+
if self.solution is not None:
186+
result, output, thread_id, params = self.solution
187+
self.log(f"Solution found by thread {thread_id} with params: {' '.join(params) if params else '(default)'}")
188+
return result, output
189+
return "unknown", "No solution found"
190+
191+
finally:
192+
self.cleanup_threads()
193+
194+
def main():
195+
signal.signal(signal.SIGINT, _global_signal_handler)
196+
signal.signal(signal.SIGTERM, _global_signal_handler)
197+
signal.signal(signal.SIGQUIT, _global_signal_handler)
198+
199+
parser = argparse.ArgumentParser(description='Portfolio solver using yices_smt2')
200+
parser.add_argument('--yices', default='yices_smt2', help='Path to yices_smt2 executable (default: yices_smt2)')
201+
parser.add_argument('-n', '--num-threads', type=int, default=4, help='Number of threads to use (default: 4)')
202+
parser.add_argument('-v', '--verbose', action='store_true', help='Enable verbose output')
203+
parser.add_argument('--seed', type=int, help='Random seed for configuration generation')
204+
parser.add_argument('--mcsat-l2o', action='store_true', help='Use MCSAT L2O option as one of the configuration')
205+
parser.add_argument('smt2_file', help='Path to SMT2 benchmark file')
206+
207+
args = parser.parse_args()
208+
209+
# Get the directory of this script
210+
script_dir = os.path.dirname(os.path.abspath(__file__))
211+
212+
# If yices path is not absolute, make it relative to script directory
213+
if not os.path.isabs(args.yices):
214+
args.yices = os.path.join(script_dir, args.yices)
215+
216+
if not os.path.exists(args.yices):
217+
print(f"Error: yices_smt2 executable not found at {args.yices}", file=sys.stderr)
218+
sys.exit(1)
219+
220+
if not os.path.exists(args.smt2_file):
221+
print(f"Error: SMT2 file not found at {args.smt2_file}", file=sys.stderr)
222+
sys.exit(1)
223+
224+
try:
225+
solver = PortfolioSolver(args.yices, args.smt2_file, args.num_threads, args.verbose, args.seed, args.mcsat_l2o)
226+
result, output = solver.solve()
227+
print(result)
228+
except KeyboardInterrupt:
229+
print("\nInterrupted by user", file=sys.stderr)
230+
sys.exit(1)
231+
except Exception as e:
232+
print(f"Error: {e}", file=sys.stderr)
233+
sys.exit(1)
234+
235+
if __name__ == "__main__":
236+
main()

0 commit comments

Comments
 (0)