From 9a48035e49491875b192ce104d1e3f6a13d570ae Mon Sep 17 00:00:00 2001 From: Massimiliano Culpo Date: Mon, 20 Jun 2022 17:49:33 +0200 Subject: asp: refactor low level API to permit the injection of configuration This allows writing extension commands that can benchmark different configurations in clingo, or try different configurations for a single test. --- lib/spack/docs/conf.py | 1 + lib/spack/spack/cmd/solve.py | 16 +---- lib/spack/spack/solver/asp.py | 134 ++++++++++++++++++++----------------- lib/spack/spack/test/concretize.py | 4 +- 4 files changed, 78 insertions(+), 77 deletions(-) (limited to 'lib') diff --git a/lib/spack/docs/conf.py b/lib/spack/docs/conf.py index c29951154f..e139ac7774 100644 --- a/lib/spack/docs/conf.py +++ b/lib/spack/docs/conf.py @@ -200,6 +200,7 @@ nitpick_ignore = [ ("py:class", "_io.BufferedReader"), ("py:class", "unittest.case.TestCase"), ("py:class", "_frozen_importlib_external.SourceFileLoader"), + ("py:class", "clingo.Control"), # Spack classes that are private and we don't want to expose ("py:class", "spack.provider_index._IndexBase"), ("py:class", "spack.repo._PrependFileLoader"), diff --git a/lib/spack/spack/cmd/solve.py b/lib/spack/spack/cmd/solve.py index 48f0226022..fddb3cc4e2 100644 --- a/lib/spack/spack/cmd/solve.py +++ b/lib/spack/spack/cmd/solve.py @@ -42,13 +42,6 @@ def setup_parser(subparser): " solutions models found by asp program\n" " all all of the above", ) - subparser.add_argument( - "--models", - action="store", - type=int, - default=0, - help="number of solutions to search (default 0 for all)", - ) # Below are arguments w.r.t. spec display (like spack spec) arguments.add_common_arguments(subparser, ["long", "very_long", "install_status"]) @@ -170,10 +163,6 @@ def solve(parser, args): % (d, ", ".join(show_options + ("all",))) ) - models = args.models - if models < 0: - tty.die("model count must be non-negative: %d") - # Format required for the output (JSON, YAML or None) required_format = args.format @@ -195,7 +184,6 @@ def solve(parser, args): result = solver.solve( specs, out=output, - models=models, timers=args.timers, stats=args.stats, setup_only=setup_only, @@ -204,9 +192,7 @@ def solve(parser, args): _process_result(result, show, required_format, kwargs) else: for idx, result in enumerate( - solver.solve_in_rounds( - specs, out=output, models=models, timers=args.timers, stats=args.stats - ) + solver.solve_in_rounds(specs, out=output, timers=args.timers, stats=args.stats) ): if "solutions" in show: tty.msg("ROUND {0}".format(idx)) diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index a50b10133f..43a3d341f0 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -56,6 +56,35 @@ ASTType = None parse_files = None +#: Data class that contain configuration on what a +#: clingo solve should output. +#: +#: Args: +#: timers (bool): Print out coarse timers for different solve phases. +#: stats (bool): Whether to output Clingo's internal solver statistics. +#: out: Optional output stream for the generated ASP program. +#: setup_only (bool): if True, stop after setup and don't solve (default False). +OutputConfiguration = collections.namedtuple( + "OutputConfiguration", ["timers", "stats", "out", "setup_only"] +) + +#: Default output configuration for a solve +DEFAULT_OUTPUT_CONFIGURATION = OutputConfiguration( + timers=False, stats=False, out=None, setup_only=False +) + + +def default_clingo_control(): + """Return a control object with the default settings used in Spack""" + control = clingo.Control() + control.configuration.configuration = "tweety" + control.configuration.solve.models = 0 + control.configuration.solver.heuristic = "Domain" + control.configuration.solve.parallel_mode = "1" + control.configuration.solver.opt_strategy = "usc,one" + return control + + # backward compatibility functions for clingo ASTs def ast_getter(*names): def getter(node): @@ -520,6 +549,12 @@ class PyclingoDriver(object): self.out = llnl.util.lang.Devnull() self.cores = cores + # These attributes are part of the object, but will be reset + # at each call to solve + self.control = None + self.backend = None + self.assumptions = None + def title(self, name, char): self.out.write("\n") self.out.write("%" + (char * 76)) @@ -556,43 +591,31 @@ class PyclingoDriver(object): if choice: self.assumptions.append(atom) - def solve( - self, - setup, - specs, - nmodels=0, - reuse=None, - timers=False, - stats=False, - out=None, - setup_only=False, - ): + def solve(self, setup, specs, reuse=None, output=None, control=None): """Set up the input and solve for dependencies of ``specs``. Arguments: - setup (SpackSolverSetup): An object to set up the ASP problem. - specs (list): List of ``Spec`` objects to solve for. - nmodels (int): Number of models to consider (default 0 for unlimited). - reuse (None or list): list of concrete specs that can be reused - timers (bool): Print out coarse timers for different solve phases. - stats (bool): Whether to output Clingo's internal solver statistics. - out: Optional output stream for the generated ASP program. - setup_only (bool): if True, stop after setup and don't solve (default False). + setup (SpackSolverSetup): An object to set up the ASP problem. + specs (list): List of ``Spec`` objects to solve for. + reuse (None or list): list of concrete specs that can be reused + output (None or OutputConfiguration): configuration object to set + the output of this solve. + control (clingo.Control): configuration for the solver. If None, + default values will be used + + Return: + A tuple of the solve result, the timer for the different phases of the + solve, and the internal statistics from clingo. """ + output = output or DEFAULT_OUTPUT_CONFIGURATION # allow solve method to override the output stream - if out is not None: - self.out = out + if output.out is not None: + self.out = output.out timer = spack.util.timer.Timer() # Initialize the control object for the solver - self.control = clingo.Control() - self.control.configuration.configuration = "tweety" - self.control.configuration.solve.models = nmodels - self.control.configuration.solver.heuristic = "Domain" - self.control.configuration.solve.parallel_mode = "1" - self.control.configuration.solver.opt_strategy = "usc,one" - + self.control = control or default_clingo_control() # set up the problem -- this generates facts and rules self.assumptions = [] with self.control.backend() as backend: @@ -622,8 +645,8 @@ class PyclingoDriver(object): parse_files([path], visit) # If we're only doing setup, just return an empty solve result - if setup_only: - return Result(specs) + if output.setup_only: + return Result(specs), None, None # Load the file itself self.control.load(os.path.join(parent_dir, "concretize.lp")) @@ -682,18 +705,21 @@ class PyclingoDriver(object): # record the number of models the solver considered result.nmodels = len(models) + # record the possible dependencies in the solve + result.possible_dependencies = setup.pkgs + elif cores: result.control = self.control result.cores.extend(cores) - if timers: + if output.timers: timer.write_tty() print() - if stats: + if output.stats: print("Statistics:") pprint.pprint(self.control.statistics) - return result + return result, timer, self.control.statistics class SpackSolverSetup(object): @@ -732,6 +758,9 @@ class SpackSolverSetup(object): # If False allows for input specs that are not solved self.concretize_everything = True + # Set during the call to setup + self.pkgs = None + def pkg_version_rules(self, pkg): """Output declared versions of a package. @@ -964,7 +993,8 @@ class SpackSolverSetup(object): # virtual preferences self.virtual_preferences( - pkg.name, lambda v, p, i: self.gen.fact(fn.pkg_provider_preference(pkg.name, v, p, i)) + pkg.name, + lambda v, p, i: self.gen.fact(fn.pkg_provider_preference(pkg.name, v, p, i)), ) def condition(self, required_spec, imposed_spec=None, name=None, msg=None): @@ -1063,7 +1093,8 @@ class SpackSolverSetup(object): self.gen.h2("Default virtual providers") assert self.possible_virtuals is not None self.virtual_preferences( - "all", lambda v, p, i: self.gen.fact(fn.default_provider_preference(v, p, i)) + "all", + lambda v, p, i: self.gen.fact(fn.default_provider_preference(v, p, i)), ) def external_packages(self): @@ -1798,7 +1829,7 @@ class SpackSolverSetup(object): if missing_deps: raise spack.spec.InvalidDependencyError(spec.name, missing_deps) - pkgs = set(possible) + self.pkgs = set(possible) # driver is used by all the functions below to add facts and # rules to generate an ASP program. @@ -1835,7 +1866,7 @@ class SpackSolverSetup(object): self.flag_defaults() self.gen.h1("Package Constraints") - for pkg in sorted(pkgs): + for pkg in sorted(self.pkgs): self.gen.h2("Package rules: %s" % pkg) self.pkg_rules(pkg, tests=self.tests) self.gen.h2("Package preferences: %s" % pkg) @@ -2215,7 +2246,6 @@ class Solver(object): self, specs, out=None, - models=0, timers=False, stats=False, tests=False, @@ -2225,7 +2255,6 @@ class Solver(object): Arguments: specs (list): List of ``Spec`` objects to solve for. out: Optionally write the generate ASP program to a file-like object. - models (int): Number of models to search (default: 0 for unlimited). timers (bool): Print out coarse fimers for different solve phases. stats (bool): Print out detailed stats from clingo. tests (bool or tuple): If True, concretize test dependencies for all packages. @@ -2237,22 +2266,14 @@ class Solver(object): reusable_specs = self._check_input_and_extract_concrete_specs(specs) reusable_specs.extend(self._reusable_specs()) setup = SpackSolverSetup(tests=tests) - return self.driver.solve( - setup, - specs, - nmodels=models, - reuse=reusable_specs, - timers=timers, - stats=stats, - out=out, - setup_only=setup_only, - ) + output = OutputConfiguration(timers=timers, stats=stats, out=out, setup_only=setup_only) + result, _, _ = self.driver.solve(setup, specs, reuse=reusable_specs, output=output) + return result def solve_in_rounds( self, specs, out=None, - models=0, timers=False, stats=False, tests=False, @@ -2267,7 +2288,6 @@ class Solver(object): Arguments: specs (list): list of Specs to solve. - models (int): number of models to search (default: 0) out: Optionally write the generate ASP program to a file-like object. timers (bool): print timing if set to True stats (bool): print internal statistics if set to True @@ -2281,16 +2301,10 @@ class Solver(object): setup.concretize_everything = False input_specs = specs + output = OutputConfiguration(timers=timers, stats=stats, out=out, setup_only=False) while True: - result = self.driver.solve( - setup, - input_specs, - nmodels=models, - reuse=reusable_specs, - timers=timers, - stats=stats, - out=out, - setup_only=False, + result, _, _ = self.driver.solve( + setup, input_specs, reuse=reusable_specs, output=output ) yield result diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index d4f417239f..1560a085dd 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -1700,7 +1700,7 @@ class TestConcretize(object): with spack.config.override("concretizer:reuse", True): solver = spack.solver.asp.Solver() setup = spack.solver.asp.SpackSolverSetup() - result = solver.driver.solve(setup, [root_spec], reuse=reusable_specs, out=sys.stdout) + result, _, _ = solver.driver.solve(setup, [root_spec], reuse=reusable_specs) # The result here should have a single spec to build ('a') # and it should be using b@1.0 with a version badness of 2 # The provenance is: @@ -1731,7 +1731,7 @@ class TestConcretize(object): with spack.config.override("concretizer:reuse", True): solver = spack.solver.asp.Solver() setup = spack.solver.asp.SpackSolverSetup() - result = solver.driver.solve(setup, [root_spec], reuse=reusable_specs, out=sys.stdout) + result, _, _ = solver.driver.solve(setup, [root_spec], reuse=reusable_specs) concrete_spec = result.specs[0] assert concrete_spec.satisfies("%gcc@4.5.0") assert concrete_spec.satisfies("os=debian6") -- cgit v1.2.3-70-g09d2