From f87ece99d937f176dd612732d76b7e933b964178 Mon Sep 17 00:00:00 2001 From: Nolwen Date: Thu, 5 Sep 2024 11:44:58 +0200 Subject: [PATCH] Test python 3.12 - Enforce cffi>=1.17 (updated for python 3.12, avoid weird errors for dependencies relying on C code) - Update time limits to avoid errors with ubuntu + python 12: It seems ortools gets slower in its latest release 9.11 on ubuntu... - Fix LP_MRCPSP_GUROBI for python 3.12: we have a crash when using gurobi.Model.addConstrs with a quicksum on a list rather than on a generator. In other words, we need to get rid of []. See https://support.gurobi.com/hc/en-us/community/posts/28192464443281-model-addConstrs-breaking-sometimes-after-Python-version-change for more details. - Add a test dedicated to LP_MRCPSP_GUROBI. --- .github/workflows/build.yml | 8 +-- .../rcpsp/solver/rcpsp_lp_solver.py | 14 ++--- pyproject.toml | 1 + .../pickup_vrp/solvers/test_ortools_solver.py | 8 +-- tests/rcpsp/solver/test_rcpsp_gurobi.py | 51 +++++++++++++++++++ .../test_sequential_metasolver.py | 0 6 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 tests/rcpsp/solver/test_rcpsp_gurobi.py rename tests/rcpsp/{ => solver}/test_sequential_metasolver.py (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f55758fb..38721ebc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: fail-fast: false matrix: os: ["ubuntu-latest", "macos-12", "macos-latest", "windows-latest"] - python-version: ["3.8", "3.11"] + python-version: ["3.8", "3.12"] wo_gurobi: ["", "without gurobi"] include: - os: "ubuntu-latest" @@ -124,7 +124,7 @@ jobs: - coverage: false # generally no coverage to avoid multiple reports - coverage: true # coverage only for one entry of the matrix os: "ubuntu-latest" - python-version: "3.11" + python-version: "3.12" wo_gurobi: "" exclude: - os: "windows-latest" @@ -139,7 +139,7 @@ jobs: - os: "macos-latest" python-version: "3.8" - os: "macos-12" - python-version: "3.11" + python-version: "3.12" runs-on: ${{ matrix.os }} defaults: run: @@ -162,7 +162,7 @@ jobs: run: | python -m pip install -U pip wheelfile=$(ls ./dist/discrete_optimization*.whl) - pip install ${wheelfile} + pip install ${wheelfile} "cffi>=1.17" - name: Check import work without minizinc if DO_SKIP_MZN_CHECK set run: | export DO_SKIP_MZN_CHECK=1 diff --git a/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py b/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py index 33a4adc57..956bdb447 100644 --- a/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py +++ b/discrete_optimization/rcpsp/solver/rcpsp_lp_solver.py @@ -728,10 +728,8 @@ def init_model(self, **args): for j in variable_per_task ) self.model.addConstrs( - gurobi.quicksum( - [key[2] * self.x[key] for key in variable_per_task[s]] - + [-key[2] * self.x[key] for key in variable_per_task[j]] - ) + gurobi.quicksum(key[2] * self.x[key] for key in variable_per_task[s]) + - gurobi.quicksum(key[2] * self.x[key] for key in variable_per_task[j]) >= durations[j] for (j, s) in S ) @@ -781,11 +779,9 @@ def init_model(self, **args): if p_s.start_times is not None: constraints = self.model.addConstrs( gurobi.quicksum( - [ - self.x[k] - for k in self.variable_per_task[task] - if k[2] == p_s.start_times[task] - ] + self.x[k] + for k in self.variable_per_task[task] + if k[2] == p_s.start_times[task] ) == 1 for task in p_s.start_times diff --git a/pyproject.toml b/pyproject.toml index 6fb47cf39..796d30b60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ classifiers = [ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development", "Topic :: Scientific/Engineering", ] diff --git a/tests/pickup_vrp/solvers/test_ortools_solver.py b/tests/pickup_vrp/solvers/test_ortools_solver.py index 3c3ce4a15..9f8ceaaf6 100644 --- a/tests/pickup_vrp/solvers/test_ortools_solver.py +++ b/tests/pickup_vrp/solvers/test_ortools_solver.py @@ -85,7 +85,7 @@ def test_ortools_with_cb(random_seed): parameters_cost=[ParametersCost(dimension_name="Distance", global_span=True)], local_search_metaheuristic=LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH, first_solution_strategy=first_solution_strategy, - time_limit=5, + time_limit=10, use_cp=use_cp, use_lns=use_lns, use_cp_sat=use_cp_sat, @@ -105,7 +105,7 @@ def test_ortools_with_cb(random_seed): assert isinstance(fit, float) assert isinstance(sol, GPDPSolution) assert nb_iteration_tracker.nb_iteration > 0 - assert (end_time - start_time).total_seconds() < 5 + assert (end_time - start_time).total_seconds() < 10 def test_ortools_with_warm_start(random_seed): @@ -142,7 +142,7 @@ def test_ortools_with_warm_start(random_seed): parameters_cost=[ParametersCost(dimension_name="Distance", global_span=True)], local_search_metaheuristic=LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH, first_solution_strategy=first_solution_strategy, - time_limit=5, + time_limit=10, use_cp=use_cp, use_lns=use_lns, use_cp_sat=use_cp_sat, @@ -173,7 +173,7 @@ def test_ortools_with_warm_start(random_seed): parameters_cost=[ParametersCost(dimension_name="Distance", global_span=True)], local_search_metaheuristic=LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH, first_solution_strategy=first_solution_strategy, - time_limit=5, + time_limit=10, use_cp=use_cp, use_lns=use_lns, use_cp_sat=use_cp_sat, diff --git a/tests/rcpsp/solver/test_rcpsp_gurobi.py b/tests/rcpsp/solver/test_rcpsp_gurobi.py new file mode 100644 index 000000000..fa035b759 --- /dev/null +++ b/tests/rcpsp/solver/test_rcpsp_gurobi.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022 AIRBUS and its affiliates. +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +import pytest + +from discrete_optimization.generic_tools.cp_tools import ParametersCP +from discrete_optimization.rcpsp.rcpsp_parser import get_data_available, parse_file +from discrete_optimization.rcpsp.solver.cpsat_solver import CPSatRCPSPSolver +from discrete_optimization.rcpsp.solver.rcpsp_lp_solver import LP_MRCPSP_GUROBI + +try: + import gurobipy +except ImportError: + gurobi_available = False +else: + gurobi_available = True + + +@pytest.mark.skipif(not gurobi_available, reason="You need Gurobi to test this solver.") +@pytest.mark.parametrize( + "model", + ["j301_1.sm", "j1010_1.mm"], +) +def test_gurobi(model): + files_available = get_data_available() + file = [f for f in files_available if model in f][0] + rcpsp_problem = parse_file(file) + solver = LP_MRCPSP_GUROBI(problem=rcpsp_problem) + + result_storage = solver.solve() + + # test warm start + parameters_cp = ParametersCP.default() + parameters_cp.time_limit = 20 + start_solution = ( + CPSatRCPSPSolver(problem=rcpsp_problem) + .solve(parameters_cp=parameters_cp) + .get_best_solution_fit()[0] + ) + + # first solution is not start_solution + assert result_storage[0][0].rcpsp_schedule != start_solution.rcpsp_schedule + + # warm start at first solution + solver = LP_MRCPSP_GUROBI(problem=rcpsp_problem) + solver.init_model() + solver.set_warm_start(start_solution) + # force first solution to be the hinted one + result_storage = solver.solve() + assert result_storage[0][0].rcpsp_schedule == start_solution.rcpsp_schedule diff --git a/tests/rcpsp/test_sequential_metasolver.py b/tests/rcpsp/solver/test_sequential_metasolver.py similarity index 100% rename from tests/rcpsp/test_sequential_metasolver.py rename to tests/rcpsp/solver/test_sequential_metasolver.py