pygo/gomill/examples/gomill-clop

471 lines
16 KiB
Plaintext
Raw Normal View History

#!/usr/bin/env python
"""Gomill integration for CLOP.
Designed for use with CLOP 0.0.8, available from
http://remi.coulom.free.fr/CLOP/
"""
# The 'connection script' interface is as follows:
# - the command-line arguments are:
# - any arguments specified in the Script line
# - processor
# - seed
# - then pairs of arguments (parameter name, parameter value)
# - the connection script prints a single character to stdout:
# 'W' for candidate win, 'D' for draw, 'L' for loss
# any further output is ignored
# - if the output doesn't start with 'W', 'D', or 'L', it's treated as an error,
# and the complete stdout and stderr are reported
# (by convention, print "Error:" with a description)
# - the connection script's exit status is ignored
import os
import sys
from optparse import OptionParser
from gomill import compact_tracebacks
from gomill import competitions
from gomill import game_jobs
from gomill.competitions import (
Competition, CompetitionError, ControlFileError, Player_config)
from gomill.job_manager import JobFailed
from gomill.ringmasters import (
Ringmaster, RingmasterError, RingmasterInternalError)
from gomill.settings import *
from gomill.common import opponent_of
PARAMETER_TYPES = [
"LinearParameter",
"IntegerParameter",
"GammaParameter",
"IntegerGammaParameter",
]
parameter_settings = [
Setting('code', interpret_identifier),
Setting('type', interpret_enum(*PARAMETER_TYPES)),
Setting('min', interpret_float),
Setting('max', interpret_float),
]
class Parameter_config(Quiet_config):
"""Parameter (ie, dimension) description for use in control files."""
# positional or keyword
positional_arguments = ('code',)
# keyword-only
keyword_arguments = tuple(setting.name for setting in parameter_settings
if setting.name != 'code')
class Parameter_spec(object):
"""Internal description of a parameter spec from the configuration file.
Public attributes:
code -- identifier
"""
def format_for_clop(self):
"""Return the parameter configuration string for the .clop file."""
if self.is_integer:
fmt = "%s %s %d %d"
else:
fmt = "%s %s %f %f"
return fmt % (self.type, self.code, self.min, self.max)
def interpret_value(self, s):
"""Convert CLOP command-line parameter to an engine parameter.
Returns an int or float.
"""
if self.is_integer:
return int(s)
else:
return float(s)
def format_for_display(self, v):
return str(v)
class Clop_tuner(Competition):
def control_file_globals(self):
result = Competition.control_file_globals(self)
result.update({
'Parameter' : Parameter_config,
})
return result
global_settings = (Competition.global_settings +
competitions.game_settings + [
Setting('candidate_colour', interpret_colour),
Setting('parallel', interpret_int, default=1),
Setting('clop_H', interpret_float, default=3),
Setting('correlations', interpret_enum('all', 'none'), default='all'),
Setting('stop_on_error', interpret_bool, default=True),
])
special_settings = [
Setting('opponent', interpret_identifier),
Setting('parameters',
interpret_sequence_of_quiet_configs(Parameter_config)),
Setting('make_candidate', interpret_callable),
]
def parameter_spec_from_config(self, parameter_config):
"""Make a Parameter_spec from a Parameter_config.
Raises ControlFileError if there is an error in the configuration.
Returns a Parameter_spec with all attributes set.
"""
arguments = parameter_config.resolve_arguments()
interpreted = load_settings(parameter_settings, arguments)
pspec = Parameter_spec()
for name, value in interpreted.iteritems():
setattr(pspec, name, value)
pspec.is_integer = ("Integer" in pspec.type)
if pspec.is_integer:
if pspec.min != int(pspec.min):
raise ControlFileError("'min': should be an integer")
if pspec.max != int(pspec.max):
raise ControlFileError("'max': should be an integer")
return pspec
def initialise_from_control_file(self, config):
Competition.initialise_from_control_file(self, config)
competitions.validate_handicap(
self.handicap, self.handicap_style, self.board_size)
try:
specials = load_settings(self.special_settings, config)
except ValueError, e:
raise ControlFileError(str(e))
try:
self.opponent = self.players[specials['opponent']]
except KeyError:
raise ControlFileError(
"opponent: unknown player %s" % specials['opponent'])
self.parameter_specs = []
if not specials['parameters']:
raise ControlFileError("parameters: empty list")
seen_codes = set()
for i, parameter_spec in enumerate(specials['parameters']):
try:
pspec = self.parameter_spec_from_config(parameter_spec)
except StandardError, e:
code = parameter_spec.get_key()
if code is None:
code = i
raise ControlFileError("parameter %s: %s" % (code, e))
if pspec.code in seen_codes:
raise ControlFileError(
"duplicate parameter code: %s" % pspec.code)
seen_codes.add(pspec.code)
self.parameter_specs.append(pspec)
self.candidate_maker_fn = specials['make_candidate']
def get_clop_parameter_specs(self):
"""Describe the parameters in the format used in the .clop file.
Returns a list of strings.
"""
return [pspec.format_for_clop() for pspec in self.parameter_specs]
def interpret_clop_parameters(self, clop_parameters):
"""Convert the CLOP command-line parameters to engine parameters.
clop_parameters -- list of pairs of strings
(parameter name, parameter value)
Returns a list of engine parameters, suitable for passing to
make_candidate().
"""
engine_parameters = []
try:
if len(clop_parameters) != len(self.parameter_specs):
raise ValueError
for pspec, (name, value) in \
zip(self.parameter_specs, clop_parameters):
if name != pspec.code:
raise ValueError
engine_parameters.append(pspec.interpret_value(value))
return engine_parameters
except ValueError:
raise CompetitionError(
"couldn't interpret parameters: %s" % repr(clop_parameters))
def format_engine_parameters(self, engine_parameters):
return "; ".join(
"%s %s" % (pspec.code, pspec.format_for_display(v))
for pspec, v in zip(self.parameter_specs, engine_parameters))
def make_candidate(self, player_code, engine_parameters):
"""Make a player using the specified engine parameters.
Returns a game_jobs.Player.
"""
try:
candidate_config = self.candidate_maker_fn(*engine_parameters)
except Exception:
raise CompetitionError(
"error from make_candidate()\n%s" %
compact_tracebacks.format_traceback(skip=1))
if not isinstance(candidate_config, Player_config):
raise CompetitionError(
"make_candidate() returned %r, not Player" %
candidate_config)
try:
candidate = self.game_jobs_player_from_config(
player_code, candidate_config)
except Exception, e:
raise CompetitionError(
"bad player spec from make_candidate():\n"
"%s\nparameters were: %s" %
(e, self.format_engine_parameters(engine_parameters)))
return candidate
def get_game_for_parameters(self, clop_seed, clop_parameters):
"""Return the details of the next game to play.
clop_seed -- second command-line parameter passed by clop
clop_parameters -- remaining command-line parameters passed by clop.
This is like Competition.get_game(), but it never returns
NoGameAvailable.
"""
engine_parameters = self.interpret_clop_parameters(clop_parameters)
candidate = self.make_candidate("#%s" % clop_seed, engine_parameters)
job = game_jobs.Game_job()
job.game_id = clop_seed
if self.candidate_colour == 'b':
job.player_b = candidate
job.player_w = self.opponent
else:
job.player_b = self.opponent
job.player_w = candidate
job.board_size = self.board_size
job.komi = self.komi
job.move_limit = self.move_limit
job.handicap = self.handicap
job.handicap_is_free = (self.handicap_style == 'free')
job.use_internal_scorer = (self.scorer == 'internal')
job.internal_scorer_handicap_compensation = \
self.internal_scorer_handicap_compensation
job.sgf_event = self.competition_code
job.sgf_note = ("Candidate parameters: %s" %
self.format_engine_parameters(engine_parameters))
return job
class Clop_ringmaster(Ringmaster):
def __init__(self, *args, **kwargs):
Ringmaster.__init__(self, *args, **kwargs)
# clop uses .log, so we need something different
self.log_pathname = os.path.join(
self.base_directory, self.competition_code) + ".elog"
@staticmethod
def _get_competition_class(competition_type):
if competition_type == "clop_tuner":
return Clop_tuner
else:
raise ValueError
def ensure_output_directories(self):
if self.record_games:
try:
if not os.path.exists(self.sgf_dir_pathname):
os.mkdir(self.sgf_dir_pathname)
except EnvironmentError:
raise RingmasterError("failed to create SGF directory:\n%s" % e)
if self.write_gtp_logs:
try:
if not os.path.exists(self.gtplog_dir_pathname):
os.mkdir(self.gtplog_dir_pathname)
except EnvironmentError:
raise RingmasterError(
"failed to create GTP log directory:\n%s" % e)
def open_logfile(self):
try:
self.logfile = open(self.log_pathname, "a")
except EnvironmentError, e:
raise RingmasterError("failed to open log file:\n%s" % e)
def run_game_for_clop(self, seed, parameters):
"""Act as a CLOP connection script.
seed -- seed string passed by clop
parameters -- list of pairs of strings (parameter name, parameter value)
Returns the message to print.
"""
self._initialise_presenter()
try:
job = self.competition.get_game_for_parameters(seed, parameters)
self._prepare_job(job)
start_msg = "starting game %s: %s (b) vs %s (w)" % (
job.game_id, job.player_b.code, job.player_w.code)
self.log(start_msg)
response = job.run()
self.log("response from game %s" % response.game_id)
for warning in response.warnings:
self.warn(warning)
for log_entry in response.log_entries:
self.log(log_entry)
except (CompetitionError, JobFailed), e:
raise RingmasterError(e)
result = response.game_result
candidate_colour = self.competition.candidate_colour
if result.winning_colour == candidate_colour:
message = "W"
elif result.winning_colour == opponent_of(candidate_colour):
message = "L"
elif result.is_jigo:
message = "D"
else:
if self.competition.stop_on_error:
# Don't want the experiment to stop just because a single game
# failed (eg, went over the move limit), so treat it as a draw.
message = "D"
else:
raise RingmasterError("unexpected game result: %s" %
result.sgf_result)
return message
clop_template = """\
Name %(experiment_name)s
Script %(connection_pathname)s %(control_filename)s run-game
%(parameter_specs)s
%(processor_specs)s
Replications 1
DrawElo %(drawelo)s
H %(clop_H)s
Correlations %(correlations)s
StopOnError %(stop_on_error)s
"""
def do_setup(ringmaster, arguments, options):
"""Create the .clop file, and any needed directories."""
connection_pathname = os.path.abspath(__file__)
control_filename = os.path.basename(ringmaster.control_pathname)
clop_pathname = os.path.join(ringmaster.base_directory,
"%s.clop" % ringmaster.competition_code)
competition = ringmaster.competition
experiment_name = ringmaster.competition_code
parameter_specs = "\n".join(competition.get_clop_parameter_specs())
processor_specs = "\n".join(
"Processor par%d" % i for i in xrange(competition.parallel))
if competition.komi == int(competition.komi) or competition.stop_on_error:
drawelo = "100"
else:
drawelo = "0"
clop_H = competition.clop_H
correlations = competition.correlations
stop_on_error = "true" if competition.stop_on_error else "false"
with open(clop_pathname, "w") as f:
f.write(clop_template % locals())
ringmaster.ensure_output_directories()
def do_run_game(ringmaster, arguments, options):
"""Act as a CLOP connection script."""
try:
processor, seed = arguments[:2]
except ValueError:
raise RingmasterError("not enough connection script arguments")
parameter_args = arguments[2:]
parameters = []
i = 0
try:
while i < len(parameter_args):
parameters.append((parameter_args[i], parameter_args[i+1]))
i += 2
except LookupError:
raise RingmasterError("parameter without value: %s" % parameter_args[i])
ringmaster.set_display_mode('quiet')
ringmaster.open_logfile()
message = ringmaster.run_game_for_clop(seed, parameters)
print message
_actions = {
"setup" : (do_setup, False),
"run-game" : (do_run_game, True),
}
def main(argv):
usage = ("%prog <control file> <command> [connection script arguments]\n\n"
"commands: setup, run-game")
description = (
"`setup` generates a .clop file for use with clop-gui or clop-console. "
"Then `run-game` is used (behind the scenes) as the connection script.")
parser = OptionParser(usage=usage, description=description)
(options, args) = parser.parse_args(argv)
if len(args) == 0:
parser.error("no control file specified")
if len(args) == 1:
parser.error("no command specified")
command = args[1]
command_args = args[2:]
try:
action, takes_arguments = _actions[command]
except KeyError:
parser.error("no such command: %s" % command)
if command_args and not takes_arguments:
parser.error("too many arguments for %s" % command)
ctl_pathname = args[0]
try:
if not os.path.exists(ctl_pathname):
raise RingmasterError("control file %s not found" % ctl_pathname)
ringmaster = Clop_ringmaster(ctl_pathname)
action(ringmaster, command_args, options)
exit_status = 0
except RingmasterError, e:
print >>sys.stderr, "gomill-clop:", e
exit_status = 1
except KeyboardInterrupt:
exit_status = 3
except RingmasterInternalError, e:
print >>sys.stderr, "gomill-clop: internal error"
print >>sys.stderr, e
exit_status = 4
except:
print >>sys.stderr, "gomill-clop: internal error"
compact_tracebacks.log_traceback()
exit_status = 4
if exit_status != 0 and command == 'run-game':
# Make sure the problem runner sees an error response
print "Error"
sys.exit(exit_status)
if __name__ == "__main__":
main(sys.argv[1:])