diff --git a/qiskit/transpiler/passes/layout/_csp_custom_solver.py b/qiskit/transpiler/passes/layout/_csp_custom_solver.py new file mode 100644 index 000000000000..d700a60e4c85 --- /dev/null +++ b/qiskit/transpiler/passes/layout/_csp_custom_solver.py @@ -0,0 +1,63 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""A custom python-constraint solver used by the :class:`~.CSPLayout` pass""" +from time import time + +from qiskit.utils import optionals as _optionals + +# This isn't ideal usage because we will import constraint at import time +# but to ensure the CustomSolver class is defined we need to do this. +# If constraint is not installed this will not raise a missing library +# exception until CSPLayout is initialized +if _optionals.HAS_CONSTRAINT: + from constraint import RecursiveBacktrackingSolver + + class CustomSolver(RecursiveBacktrackingSolver): + """A wrap to RecursiveBacktrackingSolver to support ``call_limit``""" + + def __init__(self, call_limit=None, time_limit=None): + self.call_limit = call_limit + self.time_limit = time_limit + self.call_current = None + self.time_start = None + self.time_current = None + super().__init__() + + def limit_reached(self): + """Checks if a limit is reached.""" + if self.call_current is not None: + self.call_current += 1 + if self.call_current > self.call_limit: + return True + if self.time_start is not None: + self.time_current = time() - self.time_start + if self.time_current > self.time_limit: + return True + return False + + def getSolution(self, domains, constraints, vconstraints): + """Wrap RecursiveBacktrackingSolver.getSolution to add the limits.""" + if self.call_limit is not None: + self.call_current = 0 + if self.time_limit is not None: + self.time_start = time() + return super().getSolution(domains, constraints, vconstraints) + + def recursiveBacktracking(self, solutions, domains, vconstraints, assignments, single): + """Like ``constraint.RecursiveBacktrackingSolver.recursiveBacktracking`` but + limited in the amount of calls by ``self.call_limit``""" + if self.limit_reached(): + return None + return super().recursiveBacktracking( + solutions, domains, vconstraints, assignments, single + ) diff --git a/qiskit/transpiler/passes/layout/csp_layout.py b/qiskit/transpiler/passes/layout/csp_layout.py index f91faa7013fb..704c91c4de46 100644 --- a/qiskit/transpiler/passes/layout/csp_layout.py +++ b/qiskit/transpiler/passes/layout/csp_layout.py @@ -16,52 +16,13 @@ found, no ``property_set['layout']`` is set. """ import random -from time import time -from constraint import Problem, RecursiveBacktrackingSolver, AllDifferentConstraint from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.utils import optionals as _optionals -class CustomSolver(RecursiveBacktrackingSolver): - """A wrap to RecursiveBacktrackingSolver to support ``call_limit``""" - - def __init__(self, call_limit=None, time_limit=None): - self.call_limit = call_limit - self.time_limit = time_limit - self.call_current = None - self.time_start = None - self.time_current = None - super().__init__() - - def limit_reached(self): - """Checks if a limit is reached.""" - if self.call_current is not None: - self.call_current += 1 - if self.call_current > self.call_limit: - return True - if self.time_start is not None: - self.time_current = time() - self.time_start - if self.time_current > self.time_limit: - return True - return False - - def getSolution(self, domains, constraints, vconstraints): - """Wrap RecursiveBacktrackingSolver.getSolution to add the limits.""" - if self.call_limit is not None: - self.call_current = 0 - if self.time_limit is not None: - self.time_start = time() - return super().getSolution(domains, constraints, vconstraints) - - def recursiveBacktracking(self, solutions, domains, vconstraints, assignments, single): - """Like ``constraint.RecursiveBacktrackingSolver.recursiveBacktracking`` but - limited in the amount of calls by ``self.call_limit``""" - if self.limit_reached(): - return None - return super().recursiveBacktracking(solutions, domains, vconstraints, assignments, single) - - +@_optionals.HAS_CONSTRAINT.require_in_instance class CSPLayout(AnalysisPass): """If possible, chooses a Layout as a CSP, using backtracking.""" @@ -102,6 +63,9 @@ def run(self, dag): qubits = dag.qubits cxs = set() + from constraint import Problem, AllDifferentConstraint, RecursiveBacktrackingSolver + from qiskit.transpiler.passes.layout._csp_custom_solver import CustomSolver + for gate in dag.two_qubit_ops(): cxs.add((qubits.index(gate.qargs[0]), qubits.index(gate.qargs[1]))) edges = set(self.coupling_map.get_edges()) diff --git a/qiskit/utils/optionals.py b/qiskit/utils/optionals.py index 35c8201af262..916adda515b5 100644 --- a/qiskit/utils/optionals.py +++ b/qiskit/utils/optionals.py @@ -47,6 +47,10 @@ .. list-table:: :widths: 25 75 + * - .. py:data:: HAS_CONSTRAINT + - `python-constraint __ is a + constraint satisfaction problem solver, used in the :class:`~.CSPLayout` transpiler pass. + * - .. py:data:: HAS_CPLEX - The `IBM CPLEX Optimizer `__ is a high-performance mathematical programming solver for linear, mixed-integer and quadratic @@ -142,7 +146,6 @@ - `Z3 `__ is a theorem prover, used in the :class:`.CrosstalkAdaptiveSchedule` and :class:`.HoareOptimizer` transpiler passes. - External Command-Line Tools --------------------------- @@ -207,6 +210,12 @@ install="pip install qiskit-ignis", ) +HAS_CONSTRAINT = _LazyImportTester( + "constraint", + name="python-constraint", + install="pip install python-constraint", +) + HAS_CPLEX = _LazyImportTester( "cplex", install="pip install 'qiskit-terra[bip-mapper]'", diff --git a/releasenotes/notes/constraint-optional-b6a2b2ee21211ccd.yaml b/releasenotes/notes/constraint-optional-b6a2b2ee21211ccd.yaml new file mode 100644 index 000000000000..15e84b0cfa8b --- /dev/null +++ b/releasenotes/notes/constraint-optional-b6a2b2ee21211ccd.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + The ``python-constraint`` dependency, which is used solely by the + :class:`~.CSPLayout` transpiler pass, is no longer in the requirements list + for the Qiskit Terra package. This is because the :class:`~.CSPLayout` pass + is no longer used by default in any of the preset pass managers for + :func:`~.transpile`. While the pass is still available, if you're using it + you will need to manually install ``python-contraint`` or when you + install ``qiskit-terra`` you can use the ``csp-layout`` extra, for example:: + + pip install "qiskit-terra[csp-layout]" diff --git a/requirements-dev.txt b/requirements-dev.txt index 5e5af0f282e8..849ae998e6ac 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ setuptools-rust coverage>=4.4.0 hypothesis>=4.24.3 +python-constraint>=1.4 ipython<7.22.0 ipykernel<5.5.2 ipywidgets>=7.3.0 diff --git a/requirements.txt b/requirements.txt index b5e020bfb9b3..a7f1c84d2145 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ psutil>=5 scipy>=1.5 sympy>=1.3 dill>=0.3 -python-constraint>=1.4 python-dateutil>=2.8.0 stevedore>=3.0.0 symengine>=0.9 ; platform_machine == 'x86_64' or platform_machine == 'aarch64' or platform_machine == 'ppc64le' or platform_machine == 'amd64' or platform_machine == 'arm64' diff --git a/setup.py b/setup.py index 5a4ad0f9628c..1a1fdf3c4651 100755 --- a/setup.py +++ b/setup.py @@ -33,9 +33,6 @@ ) -csplayout_requirements = [ - "python-constraint>=1.4", -] visualization_extras = [ "matplotlib>=3.3", "ipywidgets>=7.3.0", @@ -49,6 +46,7 @@ "z3-solver>=4.7", ] bip_requirements = ["cplex", "docplex"] +csp_requirements = ["python-constraint>=1.4"] setup( @@ -85,11 +83,11 @@ "visualization": visualization_extras, "bip-mapper": bip_requirements, "crosstalk-pass": z3_requirements, - "csp-layout-pass": csplayout_requirements, + "csp-layout-pass": csp_requirements, # Note: 'all' does not include 'bip-mapper' because cplex is too fiddly and too little # supported on various Python versions and OSes compared to Terra. You have to ask for it # explicitly. - "all": visualization_extras + z3_requirements + csplayout_requirements, + "all": visualization_extras + z3_requirements + csp_requirements, }, project_urls={ "Bug Tracker": "https://github.com/Qiskit/qiskit-terra/issues",