Module ortools_utils.explainability.positive_explanations

Expand source code
import logging
from typing import Dict

from ortools_utils.status import Status

logger = logging.getLogger(__name__)


def positive_explanations(solver, additional_condition) -> Dict:
    """Local explicability. The solver computes new objective values with an additional condition

    Returns one of the following:
    ```
    res = positive_explanations(my_solver, my_condition)

    # Adding this condition makes the model infeasible
    >> res = {"outcome": "infeasible"}

    # OR adding this condition lowers the optimization value at some point
    >> res = {"outcome": "less_optimal",
        "optimization_scores": {"old_values": [v1, v2...],
                                "new_values": [v'1, v'2...]},
        "objective_values": [{"id": obj1, "old_value": v1, "new_value": v'1}... ]
        }

    # OR another solution can be found with this additional condition without lowering the objective values
    >> res = {"outcome": "as_optimal",
        "changed_variables": [{"name": x_1_1, "old_value": v1, "new_value": v'1]}... }
    ```

    """
    model = solver.model
    # On ajoute comme nouvelle contrainte que variable ne doit pas prendre la valeur actuelle
    ref_contrainte = len(model.Proto().constraints)
    model.Add(additional_condition)

    # On résout avec cette nouvelle contrainte
    status, list_val, tab_val_obj = solver.try_solving(mute=True)

    if status != Status.FEASIBLE and status != Status.OPTIMAL:
        res = {"outcome": "infeasible"}

    elif list_val == solver.res_optimization:
        res = {"outcome": "as_optimal", "changed_variables": []}
        for var in model.VarList():
            if "is_respected" not in var.Name():
                if solver.Value(var) != solver.example_best_solution[var]:
                    res["changed_variables"].append({"name": var.Name(),
                                                     "old_value": solver.example_best_solution[var],
                                                     "new_value": solver.Value(var)})

    else:
        res = {"outcome": "less_optimal",
               "optimization_scores": {"old_value": solver.res_optimization,
                                       "new_value": list_val},
               "objective_values": []
               }

        for obj in model.objective().get_all_obj():
            res["objective_values"].append({"id": obj.get_id(), "old_value": obj.best_value(), "new_value": tab_val_obj[obj]})

    # On supprime cette nouvelle contrainte
    model.Proto().constraints[ref_contrainte].Clear()

    return res

Functions

def positive_explanations(solver, additional_condition) ‑> Dict[~KT, ~VT]

Local explicability. The solver computes new objective values with an additional condition

Returns one of the following:

res = positive_explanations(my_solver, my_condition)

# Adding this condition makes the model infeasible
>> res = {"outcome": "infeasible"}

# OR adding this condition lowers the optimization value at some point
>> res = {"outcome": "less_optimal",
    "optimization_scores": {"old_values": [v1, v2...],
                            "new_values": [v'1, v'2...]},
    "objective_values": [{"id": obj1, "old_value": v1, "new_value": v'1}... ]
    }

# OR another solution can be found with this additional condition without lowering the objective values
>> res = {"outcome": "as_optimal",
    "changed_variables": [{"name": x_1_1, "old_value": v1, "new_value": v'1]}... }
Expand source code
def positive_explanations(solver, additional_condition) -> Dict:
    """Local explicability. The solver computes new objective values with an additional condition

    Returns one of the following:
    ```
    res = positive_explanations(my_solver, my_condition)

    # Adding this condition makes the model infeasible
    >> res = {"outcome": "infeasible"}

    # OR adding this condition lowers the optimization value at some point
    >> res = {"outcome": "less_optimal",
        "optimization_scores": {"old_values": [v1, v2...],
                                "new_values": [v'1, v'2...]},
        "objective_values": [{"id": obj1, "old_value": v1, "new_value": v'1}... ]
        }

    # OR another solution can be found with this additional condition without lowering the objective values
    >> res = {"outcome": "as_optimal",
        "changed_variables": [{"name": x_1_1, "old_value": v1, "new_value": v'1]}... }
    ```

    """
    model = solver.model
    # On ajoute comme nouvelle contrainte que variable ne doit pas prendre la valeur actuelle
    ref_contrainte = len(model.Proto().constraints)
    model.Add(additional_condition)

    # On résout avec cette nouvelle contrainte
    status, list_val, tab_val_obj = solver.try_solving(mute=True)

    if status != Status.FEASIBLE and status != Status.OPTIMAL:
        res = {"outcome": "infeasible"}

    elif list_val == solver.res_optimization:
        res = {"outcome": "as_optimal", "changed_variables": []}
        for var in model.VarList():
            if "is_respected" not in var.Name():
                if solver.Value(var) != solver.example_best_solution[var]:
                    res["changed_variables"].append({"name": var.Name(),
                                                     "old_value": solver.example_best_solution[var],
                                                     "new_value": solver.Value(var)})

    else:
        res = {"outcome": "less_optimal",
               "optimization_scores": {"old_value": solver.res_optimization,
                                       "new_value": list_val},
               "objective_values": []
               }

        for obj in model.objective().get_all_obj():
            res["objective_values"].append({"id": obj.get_id(), "old_value": obj.best_value(), "new_value": tab_val_obj[obj]})

    # On supprime cette nouvelle contrainte
    model.Proto().constraints[ref_contrainte].Clear()

    return res