Skip to content

Commit 2ca010c

Browse files
committed
Create enhance.py
1 parent f8316a4 commit 2ca010c

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed

interface/enhance.py

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
#!/usr/bin/env python
2+
3+
4+
""""
5+
Enhance.py
6+
7+
Run engine bench command and return nodes.
8+
9+
format:
10+
bench <hash> <threads> <limitvalue> <fenfile | default>
11+
<limittype [depth(default), perft, nodes, movetime]> <evaltype [mixed(default), classical, NNUE]>
12+
13+
bench 128 1 4 file.epd depth mixed
14+
"""
15+
16+
17+
__author__ = 'fsmosca'
18+
__script_name__ = 'enhance'
19+
__version__ = 'v0.3.1'
20+
__credits__ = ['joergoster', 'musketeerchess']
21+
22+
23+
from pathlib import Path
24+
import subprocess
25+
import argparse
26+
import time
27+
import random
28+
import concurrent.futures
29+
from concurrent.futures import ProcessPoolExecutor
30+
import logging
31+
from statistics import mean
32+
33+
34+
logging.basicConfig(
35+
filename='enhance_log.txt', filemode='a',
36+
level=logging.DEBUG,
37+
format='%(asctime)s - pid%(process)5d - %(levelname)5s - %(message)s')
38+
39+
40+
class Enhance:
41+
def __init__(self, engineinfo, hashmb=64, threads=1, limitvalue=15,
42+
fenfile='default', limittype='depth', evaltype='mixed',
43+
concurrency=1, randomizefen=False, posperfile=50):
44+
self.engineinfo = engineinfo
45+
self.hashmb = hashmb
46+
self.threads = threads
47+
self.limitvalue = limitvalue
48+
self.fenfile=fenfile
49+
self.limittype=limittype
50+
self.evaltype=evaltype
51+
self.concurrency = concurrency
52+
self.randomizefen = randomizefen
53+
self.posperfile = posperfile
54+
55+
self.nodes = None # objective value
56+
57+
def send(self, p, command):
58+
""" Send msg to engine """
59+
p.stdin.write('%s\n' % command)
60+
logging.debug('>> %s' % command)
61+
62+
def read_engine_reply(self, p, command):
63+
""" Read reply from engine """
64+
self.nodes = None
65+
for eline in iter(p.stdout.readline, ''):
66+
line = eline.strip()
67+
logging.debug('<< %s' % line)
68+
if command == 'uci' and 'uciok' in line:
69+
break
70+
if command == 'isready' and 'readyok' in line:
71+
break
72+
# Nodes searched : 3766422
73+
if command == 'bench' and 'nodes searched' in line.lower():
74+
self.nodes = int(line.split(': ')[1])
75+
elif command == 'bench' and 'Nodes/second' in line:
76+
break
77+
78+
def bench(self, fenfn) -> int:
79+
"""
80+
Run the engine, send a bench command and return the nodes searched.
81+
"""
82+
folder = Path(self.engineinfo['cmd']).parent
83+
84+
# Start the engine.
85+
proc = subprocess.Popen(self.engineinfo['cmd'], stdin=subprocess.PIPE,
86+
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
87+
universal_newlines=True, bufsize=1, cwd=folder)
88+
89+
self.send(proc, 'uci')
90+
self.read_engine_reply(proc, 'uci')
91+
92+
# Set param values to be optimized.
93+
for k, v in self.engineinfo['opt'].items():
94+
self.send(proc, f'setoption name {k} value {v}')
95+
96+
self.send(proc, 'isready')
97+
self.read_engine_reply(proc, 'isready')
98+
99+
self.send(proc, f'bench {self.hashmb} {self.threads} {self.limitvalue} {fenfn} {self.limittype} {self.evaltype}')
100+
# self.send(proc, f'bench')
101+
self.read_engine_reply(proc, 'bench')
102+
103+
# Quit the engine.
104+
self.send(proc, 'quit')
105+
106+
return self.nodes
107+
108+
def delete_file(self, fn):
109+
filepath = Path(fn)
110+
filepath.unlink(missing_ok=True) # python 3.8 missing_ok is added
111+
logging.info(f'The file {fn} was deleted.')
112+
113+
def generate_files(self):
114+
"""
115+
Read the input fen file, convert it to a list, sort it randomly if required.
116+
If there are 2 concurrencies, 2 files will be created (file0.fen, file1.fen).
117+
The number of positions in each file is determined by the posperfile option value.
118+
These files will be used in the bench command as in:
119+
bench 64 1 12 file0.fen depth mixed
120+
"""
121+
t0 = time.perf_counter()
122+
123+
fenlist, filelist = [], []
124+
numpos = self.posperfile
125+
126+
fen_file = Path(self.fenfile)
127+
if not fen_file.is_file():
128+
if self.fenfile != 'default':
129+
print(f'Warning, {self.fenfile} is missing, default will be used!')
130+
logging.warning(f'{self.fenfile} is missing, default will be used!')
131+
return filelist
132+
133+
ext = fen_file.suffix
134+
135+
with open(self.fenfile) as f:
136+
for lines in f:
137+
fen = lines.rstrip()
138+
fenlist.append(fen)
139+
140+
# sort randomly this big fen list
141+
if self.randomizefen:
142+
random.shuffle(fenlist)
143+
144+
# Extract the specified number of fens per bench run.
145+
# If concurrency is 2 we will create 2 files namely file0.fen and file1.fen
146+
for i in range(self.concurrency):
147+
# i starts at 0 and if numpos is 50:
148+
# start=0, end=50
149+
# fenlist[0:50] will extract the first 50 fens as a list.
150+
start = i * numpos
151+
end = (i+1) * numpos
152+
153+
fens = fenlist[start:end]
154+
fn = f'file{i}{ext}'
155+
156+
# Save the list of fens in the file.
157+
with open(fn, 'w') as f:
158+
for fen in fens:
159+
f.write(f'{fen}\n')
160+
161+
pathandfile = Path(Path().absolute(), fn).as_posix()
162+
filelist.append(pathandfile)
163+
164+
logging.info(f'numfiles: {len(filelist)}')
165+
logging.info(f'file generation elapse {time.perf_counter() - t0:0.2f}s')
166+
167+
return filelist
168+
169+
def run(self):
170+
"""
171+
Run the engine with bench command to get the nodes searched and
172+
return it as the objective value.
173+
"""
174+
objectivelist, joblist = [], []
175+
176+
fenfiles = self.generate_files()
177+
178+
# Use Python 3.8 or higher.
179+
with ProcessPoolExecutor(max_workers=self.concurrency) as executor:
180+
if len(fenfiles) == 0:
181+
job = executor.submit(self.bench, 'default')
182+
joblist.append(job)
183+
else:
184+
for fn in fenfiles:
185+
job = executor.submit(self.bench, fn)
186+
joblist.append(job)
187+
188+
for future in concurrent.futures.as_completed(joblist):
189+
try:
190+
nodes_searched = future.result()
191+
objectivelist.append(nodes_searched)
192+
except concurrent.futures.process.BrokenProcessPool as ex:
193+
logging.exception(f'exception: {ex}')
194+
raise
195+
196+
# Delete fen files.
197+
for fn in fenfiles:
198+
pass # self.delete_file(fn)
199+
200+
logging.debug(f'bench nodes: {objectivelist}')
201+
logging.debug(f'mean nodes: {int(mean(objectivelist))}')
202+
logging.debug(f'sum nodes: {sum(objectivelist)}')
203+
204+
# This is used by optimizer to signal that the job is done.
205+
print(f'total nodes searched from {self.concurrency} workers: {sum(objectivelist)}')
206+
print('bench done')
207+
208+
209+
def define_engine(engine_option_value):
210+
"""
211+
Define engine files, and options.
212+
"""
213+
optdict = {}
214+
engineinfo = {'cmd': None, 'opt': optdict}
215+
216+
for eng_opt_val in engine_option_value:
217+
for value in eng_opt_val:
218+
if 'cmd=' in value:
219+
engineinfo.update({'cmd': value.split('=')[1]})
220+
elif 'option.' in value:
221+
# option.QueenValueOpening=1000
222+
optn = value.split('option.')[1].split('=')[0]
223+
optv = value.split('option.')[1].split('=')[1]
224+
optdict.update({optn: optv})
225+
engineinfo.update({'opt': optdict})
226+
227+
return engineinfo
228+
229+
230+
def main():
231+
parser = argparse.ArgumentParser(
232+
formatter_class=argparse.RawTextHelpFormatter,
233+
prog='%s %s' % (__script_name__, __version__),
234+
description='Run bench command.',
235+
epilog='%(prog)s')
236+
parser.add_argument('-engine', nargs='*', action='append', required=True,
237+
metavar=('cmd=', 'option.<optionname>=value'),
238+
help='Define engine filename and option, required=True. Example:\n'
239+
'-engine cmd=eng.exe option.FutilityMargin=120 option.MoveCount=1000')
240+
parser.add_argument('-concurrency', required=False,
241+
help='the number of benches to run in parallel, default=1',
242+
type=int, default=1)
243+
parser.add_argument('-hashmb', required=False, help='memory value in mb, default=64',
244+
type=int, default=64)
245+
parser.add_argument('-threads', required=False, help='number of threads, default=1',
246+
type=int, default=1)
247+
parser.add_argument('-limitvalue', required=False,
248+
help='a number that limits the engine search, default=15',
249+
type=int, default=15)
250+
parser.add_argument('-fenfile', required=False,
251+
help='Filename of FEN or EPD file, default=default',
252+
default='default')
253+
parser.add_argument('-limittype', required=False,
254+
help='the type of limit can be depth, perft, nodes and movetime, default=depth',
255+
type=str, default='depth')
256+
parser.add_argument('-evaltype', required=False,
257+
help='the type of eval to use can be mixed, classical or NNUE, default=mixed',
258+
type=str, default='mixed')
259+
parser.add_argument('-randomizefen', action='store_true',
260+
help='A flag to randomize position before using it in bench when position file is used.')
261+
parser.add_argument('-posperfile', required=False,
262+
help='the number of positions in the file to be used in the bench, default=50',
263+
type=int, default=50)
264+
parser.add_argument('-v', '--version', action='version', version=f'{__version__}')
265+
266+
args = parser.parse_args()
267+
268+
# Define engine files, name and options.
269+
engineinfo = define_engine(args.engine)
270+
271+
# Exit if engine file is not defined.
272+
if engineinfo['cmd'] is None:
273+
print('Error, engines are not properly defined!')
274+
return
275+
276+
tstart = time.perf_counter()
277+
278+
duel = Enhance(engineinfo, hashmb=args.hashmb, threads=args.threads,
279+
limitvalue=args.limitvalue, fenfile=args.fenfile,
280+
limittype=args.limittype, evaltype=args.evaltype,
281+
concurrency=args.concurrency,
282+
randomizefen=args.randomizefen,
283+
posperfile=args.posperfile)
284+
duel.run()
285+
286+
logging.info(f'total elapse time: {time.perf_counter() - tstart:0.2f}s')
287+
288+
289+
if __name__ == '__main__':
290+
main()

0 commit comments

Comments
 (0)