Source code for gemseo.algos.opt.scipy_milp.scipy_milp

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
# Contributors:
#    INITIAL AUTHORS - initial API and implementation and/or initial
#                           documentation
#        :author: Simone Coniglio
"""SciPy linear programming library wrapper."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import Any
from typing import ClassVar
from typing import Final

from numpy import concatenate
from numpy import inf
from numpy import ones_like
from scipy.optimize import Bounds
from scipy.optimize import LinearConstraint
from scipy.optimize import milp

from gemseo.algos.design_space import DesignSpace
from gemseo.algos.design_space_utils import get_value_and_bounds
from gemseo.algos.opt.base_optimization_library import BaseOptimizationLibrary
from gemseo.algos.opt.base_optimization_library import OptimizationAlgorithmDescription
from gemseo.algos.opt.base_optimizer_settings import BaseOptimizerSettings
from gemseo.algos.opt.core.linear_constraints import build_constraints_matrices
from gemseo.algos.opt.scipy_milp.settings.scipy_milp_settings import SciPyMILP_Settings
from gemseo.algos.optimization_result import OptimizationResult
from gemseo.core.mdo_functions.mdo_linear_function import MDOLinearFunction
from gemseo.utils.compatibility.scipy import get_row
from gemseo.utils.compatibility.scipy import sparse_classes

if TYPE_CHECKING:
    from collections.abc import Mapping

    from gemseo.algos.optimization_problem import OptimizationProblem
    from gemseo.typing import RealArray


[docs] @dataclass class ScipyMILPAlgorithmDescription(OptimizationAlgorithmDescription): """The description of a the SciPy mixed-integer linear programming library.""" library_name: str = "SciPy Mixed-Integer Linear Programming" """The library name.""" handle_equality_constraints: bool = True """Whether the optimization algorithm handles equality constraints.""" handle_inequality_constraints: bool = True """Whether the optimization algorithm handles inequality constraints.""" handle_integer_variables: bool = True """Whether the optimization algorithm handles integer variables.""" Settings: type[SciPyMILP_Settings] = SciPyMILP_Settings """The option validation model for SciPy linear programming library."""
[docs] class ScipyMILP(BaseOptimizationLibrary): """SciPy Mixed Integer Linear Programming library interface. See BaseOptimizationLibrary. With respect to scipy milp function, this wrapper only allows continuous or integer variables. """ __DOC: Final[str] = "https://docs.scipy.org/doc/scipy/reference/" ALGORITHM_INFOS: ClassVar[dict[str, ScipyMILPAlgorithmDescription]] = { "Scipy_MILP": ScipyMILPAlgorithmDescription( algorithm_name="Branch & Cut algorithm", description="Mixed-integer linear programming", internal_algorithm_name="milp", website=f"{__DOC}scipy.optimize.milp.html", Settings=SciPyMILP_Settings, ), } _SUPPORT_SPARSE_JACOBIAN: ClassVar[bool] = True """Whether the library support sparse Jacobians.""" def __init__(self, algo_name: str = "Scipy_MILP") -> None: # noqa:D107 super().__init__(algo_name) def _run( self, problem: OptimizationProblem, **settings: Any ) -> tuple[Any, Any, Any, Any, Any, Any, Any]: # Get the starting point and bounds x_0, l_b, u_b = get_value_and_bounds(problem.design_space, False) # Replace infinite bounds with None bounds = Bounds(lb=l_b, ub=u_b, keep_feasible=True) # Build the functions matrices # N.B. use the non-processed functions to access the coefficients coefficients = problem.objective.original.coefficients if isinstance(coefficients, sparse_classes): obj_coeff = get_row(coefficients, 0).todense().flatten() else: obj_coeff = coefficients[0, :] lq_constraints = [] ineq_lhs, ineq_rhs = build_constraints_matrices( problem.constraints.get_originals(), MDOLinearFunction.ConstraintType.INEQ ) if ineq_lhs is not None: lq_constraints.append( LinearConstraint( ineq_lhs, -inf * ones_like(ineq_rhs), ineq_rhs + problem.tolerances.inequality, keep_feasible=True, ) ) eq_lhs, eq_rhs = build_constraints_matrices( problem.constraints.get_originals(), MDOLinearFunction.ConstraintType.EQ ) if eq_lhs is not None: lq_constraints.append( LinearConstraint( eq_lhs, eq_rhs - problem.tolerances.equality, eq_rhs + problem.tolerances.equality, keep_feasible=True, ) ) # Filter settings to get only the scipy.optimize.milp ones settings_ = self._filter_settings(settings, BaseOptimizerSettings) # Deactivate stopping criteria which are handled by GEMSEO settings["time_limit"] = inf # Pass the MILP to Scipy milp_result = milp( c=obj_coeff.real, bounds=bounds, constraints=lq_constraints, options=settings_, integrality=concatenate([ [ self._problem.design_space.get_type(variable_name) == DesignSpace.DesignVariableType.INTEGER ] * self._problem.design_space.get_size(variable_name) for variable_name in self._problem.design_space ]), ) # Gather the optimization results x_opt = x_0 if milp_result.x is None else milp_result.x # N.B. SciPy tolerance on bounds is higher than the DesignSpace one x_opt = problem.design_space.project_into_bounds(x_opt) output_functions, jacobian_functions = problem.get_functions( jacobian_names=(), no_db_no_norm=True, ) output_opt, jac_opt = problem.evaluate_functions( design_vector=x_opt, design_vector_is_normalized=False, output_functions=output_functions or None, jacobian_functions=jacobian_functions or None, ) return None, None, output_opt, jac_opt, x_0, x_opt, milp_result def _get_result( self, problem: OptimizationProblem, message: Any, status: Any, output_opt: Mapping[str, RealArray], jac_opt: Mapping[str, RealArray], x_0: RealArray, x_opt: RealArray, result: Any, ) -> OptimizationResult: """ Args: output_opt: The output values at optimum. jac_opt: The Jacobian values at optimum. x_0: The initial design value. x_opt: The optimal design value. result: A result specific to this library. """ # noqa: D205 D212 f_opt = output_opt[problem.objective.name] constraint_names = list(problem.constraints.original_to_current_names.keys()) constraint_values = {key: output_opt[key] for key in constraint_names} constraints_grad = {key: jac_opt[key] for key in constraint_names} is_feasible = problem.constraints.is_point_feasible(output_opt) return OptimizationResult( x_0=x_0, x_0_as_dict=problem.design_space.convert_array_to_dict(x_0), x_opt=x_opt, x_opt_as_dict=problem.design_space.convert_array_to_dict(x_opt), f_opt=f_opt, objective_name=problem.objective.name, status=result.status, constraint_values=constraint_values, constraints_grad=constraints_grad, optimizer_name=self._algo_name, message=result.message, n_obj_call=None, n_grad_call=None, n_constr_call=None, is_feasible=is_feasible, )