"""
Module `lp_denorm_constraints`
==============================
This module provides functions to handle denormalized constraints in Linear
Programming (LP) problems, especially useful in machine learning contexts.
Denormalized constraints are required when working with values in LP problems
that have been normalized. This module facilitates the conversion back to the
original denormalized values.
This module contains a dictionary named `DENORMALIZED_CONSTRAINTS` that stores
the instructions to define the denormalized constraints. The keys of the dictionary
represent the name of the constraints to create while the values represent
lists of variables that define these constraints. Each element from those lists
defines a dictionary specifying how a variable should be added to the constraint.
Here's the macrostructure of the `DENORMALIZED_CONSTRAINTS` dictionary:
.. code-block:: python
    DENORMALIZED_CONSTRAINTS = {
        "<CONSTRAINT_NAME>": [...],
        ...,
    }
Each constraint contains a list with the following structure:
.. code-block:: python
    [
        {"feature": "<FEATURE_NAME>", "denorm": True/False, "coef": <NUMERIC_VALUE>},
        ...,
        {"operator": <"GTE", "LTE", "EQ">, "value": <NUMERIC_VALUE>},
    ],
For example, to define the constraint:
.. math::
    5\\cdot{\\text{x}} - 2\\cdot{\\text{y}} \\leq{22}
Where :math:`\\text{x}` and :math:`\\text{y}` represent optimization problem variables.
The above equation can be defined as follows:
.. code-block:: python
    DENORMALIZED_CONSTRAINTS = {
        "5_times_x_minus_2_times_y_leq_22": [
            {"feature": "x", "denorm": True, "coef": 5},
            {"feature": "y", "denorm": True, "coef": -2},
            {"operator": "LTE", "value": 22},
        ],
    }
Finally, to define the above constraint to the problem, use the function
`create_denorm_constraints`:
.. code-block:: python
    create_denorm_constraints(lp_problem, scalers, DENORMALIZED_CONSTRAINTS)
Where:
- `lp_problem`: instance of `pulp.LpProblem` that represents the optimization problem.
- `scalers`: contains a dictionary of `sklearn.preprocessing.MinMaxScaler`
  objects that represent the scalers for all variables that the optimization problem
  has.
.. important::
    For the above code to work, the variables that each constraint uses
    must exist on the `lp_problem` instance.
Functions
---------
- `denormalize_lpvar(variable)`: Converts a single LP variable back to its
  original scale.
- `process_term(coefficient, variable)`: Handles a term in a denormalized
  constraint, considering its coefficient and variable.
- `process_terms(terms)`: Processes a list of terms from a denormalized
  constraint.
- `create_denorm_constraints(constraints)`: Generates denormalized constraints
  for an LP problem using the above functions.
Primary usage is expected to be through the `create_denorm_constraints()`
function, which leverages the other functions to produce the denormalized
constraints for an LP problem.
Notes
-----
Denormalized constraints are defined after the creation of optimization
variables and differ from other modules in their handling of constraints
for optimization problems. Other modules typically append instructions for
constraint creation to a `.txt` file.
"""
from __future__ import annotations
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
import pulp
import sklearn
from wip.constants import constants
from wip.logging_config import logger
from wip.modules.ops import get_original_tag_name
from wip.modules.ops import inverse_transform_lpvar
# Dictionary with constraints to be added to the optimization problem.
# Each key is the name of the constraint (names must be unique).
# Each value is a list of terms, where each term is either a dictionary with the
# following keys:
# - feature: Name of the feature.
# - denorm: Whether the feature should be denormalized.
# - coef: Coefficient to multiply the feature by.
# Or a dictionary with the following keys:
# - operator: Operator to be applied to the constraint.
# - value: Value to be compared to the constraint.
# The terms are processed in order, and the final constraint is the sum of all
# the terms.
DENORMALIZED_CONSTRAINTS = {
    # Restrict "grupos de queima" 04 to 09, so that:
    # "TEMP1_I@08QU_QU_855I_GQ<XX>" >= "TEMP1_I@08QU_QU_855I_GQ<XX-1>"
    "diff_grupos_queima_05_04": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ05", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ04", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ06", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ05", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 0},
    ],
    "diff_grupos_queima_06_05": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ06", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ05", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ07", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ06", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 0},
    ],
    "diff_grupos_queima_07_06": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ07", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ06", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ08", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ07", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 0},
    ],
    "diff_grupos_queima_08_07": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ08", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ07", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ09", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ08", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 0},
    ],
    "diff_grupos_queima_09_08": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ09", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ08", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ10", "denorm": True, "coef": -1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ09", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 0},
    ],
    # Restrict "grupos de queima" 09 to 16 so that:
    # "TEMP1_I@08QU_QU_855I_GQ<XX>" <= "TEMP1_I@08QU_QU_855I_GQ<XX-1>"
    "grupo_queima_10_menor_igual_09": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ09", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ10", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": -20},
    ],
    "grupo_queima_11_menor_igual_10": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ10", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ11", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": 0},
    ],
    "grupo_queima_12_menor_igual_11": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ11", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ12", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": 0},
    ],
    "grupo_queima_13_menor_igual_12": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ12", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ13", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": 0},
    ],
    "grupo_queima_14_menor_igual_13": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ13", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ14", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": 0},
    ],
    "grupo_queima_15_menor_igual_14": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ14", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ15", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": 0},
    ],
    "grupo_queima_16_menor_igual_15": [
        {"feature": "TEMP1_I@08QU_QU_855I_GQ15", "denorm": True, "coef": 1},
        {"feature": "TEMP1_I@08QU_QU_855I_GQ16", "denorm": True, "coef": -1},
        {"operator": "GTE", "value": 0},
    ],
    "energia_prensa_min": [
        {"feature": "energia_prensa", "denorm": False, "coef": 1},
        {"operator": "GTE", "value": 2},
    ],
    # "POT_TOTAL_VENT_US8_EQ": [
    #     {"feature": "POTE1_I@08QU_PF_852I_01M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_02M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_03M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_04M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_05M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_06M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_07M1", "denorm": True, "coef": 1},
    #     {"feature": "POTE1_I@08QU_PF_852I_08M1", "denorm": True, "coef": 1},
    #     {"feature": "POT_TOTAL_VENT___US8", "denorm": True, "coef": -1},
    #     {"operator": "EQ", "value": 0},
    # ],
    "MIN_ROTA1_I@08PE_PN_840I_02M1": [
        {"feature": "ROTA1_I@08PE_PN_840I_02M1", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 900},
    ],
    "MIN_ROTA1_I@08PE_PN_840I_03M1": [
        {"feature": "ROTA1_I@08PE_PN_840I_03M1", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 900},
    ],
    "MIN_ROTA1_I@08PE_PN_840I_04M1": [
        {"feature": "ROTA1_I@08PE_PN_840I_04M1", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 900},
    ],
    "MIN_ROTA1_I@08PE_PN_840I_05M1": [
        {"feature": "ROTA1_I@08PE_PN_840I_05M1", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 900},
    ],
    "MIN_ROTA1_I@08PE_PN_840I_06M1": [
        {"feature": "ROTA1_I@08PE_PN_840I_06M1", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 900},
    ],
    "MIN_PESO1_I@08MO_BW_821I_02M1": [
        {"feature": "PESO1_I@08MO_BW_821I_02M1", "denorm": True, "coef": 1},
        {"operator": "GTE", "value": 300},
    ],
    # "MIN_POTE1_I@08QU_PF_852I_08M1": [
    #     {
    #         "feature": "PESO1_I@08MO_BW_821I_02M1",
    #         "denorm": True,
    #         "coef": 1
    #     },
    #     {
    #         "operator": "GTE",
    #         "value": 300
    #     },
    # ],
    "energia_forno_eq_CONS1_Y@08QU_VENT": [
        {"feature": "CONS1_Y@08QU_VENT", "denorm": True, "coef": 1},
        {"feature": "energia_forno", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_1_eq_GRAN_OCS_TM@08PE_BD_840I_01": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_01", "denorm": True, "coef": 1},
        {"feature": "rota_disco_1", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_2_eq_GRAN_OCS_TM@08PE_BD_840I_02": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_02", "denorm": True, "coef": 1},
        {"feature": "rota_disco_2", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_3_eq_GRAN_OCS_TM@08PE_BD_840I_03": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_03", "denorm": True, "coef": 1},
        {"feature": "rota_disco_3", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_4_eq_GRAN_OCS_TM@08PE_BD_840I_04": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_04", "denorm": True, "coef": 1},
        {"feature": "rota_disco_4", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_5_eq_GRAN_OCS_TM@08PE_BD_840I_05": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_05", "denorm": True, "coef": 1},
        {"feature": "rota_disco_5", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_6_eq_GRAN_OCS_TM@08PE_BD_840I_06": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_06", "denorm": True, "coef": 1},
        {"feature": "rota_disco_6", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_7_eq_GRAN_OCS_TM@08PE_BD_840I_07": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_07", "denorm": True, "coef": 1},
        {"feature": "rota_disco_7", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_8_eq_GRAN_OCS_TM@08PE_BD_840I_08": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_08", "denorm": True, "coef": 1},
        {"feature": "rota_disco_8", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_9_eq_GRAN_OCS_TM@08PE_BD_840I_09": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_09", "denorm": True, "coef": 1},
        {"feature": "rota_disco_9", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_10_eq_GRAN_OCS_TM@08PE_BD_840I_10": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_10", "denorm": True, "coef": 1},
        {"feature": "rota_disco_10", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_11_eq_GRAN_OCS_TM@08PE_BD_840I_11": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_11", "denorm": True, "coef": 1},
        {"feature": "rota_disco_11", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "rota_disco_12_eq_GRAN_OCS_TM@08PE_BD_840I_12": [
        {"feature": "GRAN_OCS_TM@08PE_BD_840I_12", "denorm": True, "coef": 1},
        {"feature": "rota_disco_12", "denorm": False, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "GANHO_PRENSA_EQ": [
        {"feature": "SE_PP", "denorm": False, "coef": 1},
        {"feature": "SE_PR", "denorm": False, "coef": -1},
        {"feature": "GANHO_PRENSA___US8", "denorm": True, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    "CONS_ENERGIA_VENTILADORES": [
        {"feature": "CONS_ESPEC_EE_VENT___US8", "denorm": True, "coef": 1},
        {"feature": "CONS1_Y@08QU_VENT", "denorm": True, "coef": -1},
        {"operator": "EQ", "value": 0},
    ],
    # "FLOTICOR_GT_0": [
    #     {
    #         "feature": "floticor",
    #         "denorm": True,
    #         "coef": 1,
    #     },
    #     {
    #         "operator": "GTE",
    #         "value": 3,
    #     },
    # ],
    # Definindo limites mínimos e máximos para rotação dos filtros 1 a 10
    # Os limites de rotação de todos os filtros serão definidos entre 0,7 e 1,1
    # Restrição: 0,7 ≤ ROTA1_I@08FI-FL-827I-01M1 ≤ 1,1
    # "min_ROTA1_I@08FI_FL_827I_01M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_01M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_01M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_01M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_02M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_02M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_02M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_02M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_03M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_03M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_03M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_03M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_04M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_04M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_04M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_04M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_05RM1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_05RM1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_05RM1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_05RM1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_06M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_06M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_06M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_06M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_07M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_07M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_07M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_07M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_08M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_08M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_08M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_08M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_09M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_09M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_09M1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_09M1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_ROTA1_I@08FI_FL_827I_10RM1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_10RM1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.70},
    # ],
    # "max_ROTA1_I@08FI_FL_827I_10RM1": [
    #     {"feature": "ROTA1_I@08FI_FL_827I_10RM1", "denorm": True, "coef": 1},
    #     {"operator": "LTE", "value": 0.95},
    # ],
    # "min_PESO1_I@08MO_BW_813I_04M1": [
    #     {"feature": "PESO1_I@08MO_BW_813I_04M1", "denorm": True, "coef": 1},
    #     {"operator": "GTE", "value": 0.80},
    # ]
}
[docs]def denormalize_lpvar(
    feature_name: str, lpvar: pulp.LpVariable, scalers: dict
) -> pulp.LpVariable | pulp.LpAffineExpression:
    """
    Denormalize an LP variable.
    Parameters
    ----------
    feature_name : str
        Name of the feature.
    lpvar : pulp.LpVariable
        LP variable to be denormalized.
    scalers : dict
        Dictionary of `sklearn.preprocessing.MinMaxScaler` scalers.
    Returns
    -------
    pulp.LpVariable | pulp.LpAffineExpression
        Denormalized LP variable, if a scaler is found.
        Otherwise, the original LP variable is returned.
    Examples
    --------
    Assuming that an optimization problem contains a normalized variable
    named `"TEMP1_I@08QU_QU_855I_GQ10"` that can range from 0 to 1.
    Calling this function to denormalize this variable results in the following
    `pulp.LpAffineExpression`:
    >>> lpvars = {
    ...     "TEMP1_I@08QU_QU_855I_GQ10": pulp.LpVariable("TEMP1_I@08QU_QU_855I_GQ10", lowBound=0, upBound=1)
    ... }
    >>> denormalize_lpvar("TEMP1_I@08QU_QU_855I_GQ10", lpvars["TEMP1_I@08QU_QU_855I_GQ10"], scalers)
    261.25*TEMP1_I@08QU_QU_855I_GQ10 + 1125.71
    In other words, the normalized variable gets multiplied by a factor,
    and then a constant is added to it. The resulting expression results
    in values that represent the original variable scale.
    """
    original_name = get_original_tag_name(feature_name)
    target_variables_names = {
        new_name: old_name for old_name, new_name in constants.TARGETS_IN_MODEL.items()
    }
    lpvar_scaler = scalers.get(original_name, None)
    # If name not found, we check whether it's a target variable, that get
    # renamed when building the optimization model.
    if lpvar_scaler is None:
        lpvar_scaler = scalers.get(target_variables_names[original_name], None)
    if lpvar_scaler is None:
        logger.error(
            "Failed to denormalize tag. Scaler for %s not found.", original_name
        )
        return lpvar
    return inverse_transform_lpvar(lpvar, lpvar_scaler) 
[docs]def process_term(term, lp_problem, scalers):
    """Process a term of a denormalized constraint.
    Parameters
    ----------
    term : dict
        Term of a denormalized constraint.
    lp_problem : pulp.LpProblem
        A `pulp.LpProblem` instance.
    scalers : dict
        Dictionary of `sklearn.preprocessing.MinMaxScaler` scalers.
    Returns
    -------
    pulp.LpVariable | pulp.LpAffineExpression | Callable
        The processed term that might consist of one of the following:
        - If the term is a feature, returns the LP variable.
        - If the term is an operator, returns a function that
          receives an LP Affine Expression and returns an LP constraint.
    Raises
    ------
    ValueError
        If the term is invalid. A term is considered invalid if it doesn't have
        a `"feature"` or `"operator"` key.
    """
    if "feature" in term.keys():
        feature_name = term["feature"]
        coef = term.get("coef", 1)
        _feature_name = (
            feature_name.replace("-", "_")
            .replace(" ", "_")
            .replace("+", "_")
            .replace("/", "_")
        )
        lpvars = lp_problem.variablesDict()
        lpvar = lpvars.get(_feature_name)
        if lpvar is None:
            lpvar = lpvars.get(
                constants.TARGETS_IN_MODEL.get(feature_name, feature_name)
            )
        if lpvar is None:
            logger.error(
                "Failed to process term. LP variable for %s not found.", feature_name
            )
            raise ValueError(f"LP variable for '{feature_name}' not found.")
        if term["denorm"]:
            lpvar = denormalize_lpvar(feature_name, lpvar, scalers)
        return coef * lpvar
    value_types = (
        int,
        float,
        pulp.LpAffineExpression,
        pulp.LpVariable,
        pulp.LpElement,
        pulp.LpConstraint,
    )
    operator = term.get("operator", "").upper()
    value = term.get("value")
    if not isinstance(value, value_types):
        raise ValueError(
            f"Term is missing a 'value' key or it has an invalid type: {term!r}"
        )
    if operator in ["GTE", "GT", ">", ">="]:
        return lambda lp_constraint: lp_constraint >= value
    if operator in ["LTE", "LT", "<", "<="]:
        return lambda lp_constraint: lp_constraint <= value
    if operator in ["E", "EQ", "==", "="]:
        return lambda lp_constraint: lp_constraint == value
    raise ValueError(f"Invalid or missing operator for term: {term!r}") 
[docs]def process_terms(terms, lp_problem, scalers):
    """Process a list of terms from a denormalized constraint.
    Parameters
    ----------
    terms : list
        A list of terms from a denormalized constraint.
    lp_problem : pulp.LpProblem
        A `pulp.LpProblem` instance.
    scalers : dict
        Dictionary of `sklearn.preprocessing.MinMaxScaler` scalers.
    Returns
    -------
    pulp.LpConstraint
        A `pulp.LpConstraint` instance.
    """
    lp_constraint = 0
    for term in terms:
        processed_term = process_term(term, lp_problem, scalers)
        if isinstance(processed_term, Callable):
            lp_constraint = processed_term(lp_constraint)
        else:
            lp_constraint += processed_term
    return lp_constraint 
[docs]def create_denorm_constraints(
    lp_problem: pulp.LpProblem,
    scalers: Dict[str, sklearn.preprocessing.MinMaxScaler],
    denormalized_constraints: Optional[
        Dict[str, List[Dict[str, str | bool | float]]]
    ] = None,
) -> pulp.LpProblem:
    """Create denormalized constraints.
    Parameters
    ----------
    lp_problem : pulp.LpProblem
        A `pulp.LpProblem` instance, that represents the optimization problem.
    scalers : Dict[str, sklearn.preprocessing.MinMaxScaler]
        Dictionary of `sklearn.preprocessing.MinMaxScaler` scalers for each
        existing variable.
    denormalized_constraints : Optional[Dict[str, List[Dict[str, str | bool | float]]]]
        Dictionary of constraint names and their corresponding equation terms.
        If None, the default `DENORMALIZED_CONSTRAINTS` dictionary is used.
        The `denormalized_constraints` dictionary must have the following structure:
        .. code-block:: python
            denormalized_constraints = {
                "<CONSTRAINT_NAME>": [
                    {"feature": "<FEATURE_NAME>", "denorm": True/False, "coef": <NUMERIC_VALUE>},
                    ...,
                    {"operator": <"GTE", "LTE", "EQ">, "value": <NUMERIC_VALUE>},
                ],
                ...
            }
    Returns
    -------
    pulp.LpProblem
        The `pulp.LpProblem` instance with the added constraints.
    Notes
    -----
    Some variable names are defined inside the optimization problem differently
    from its name inside the `scalers` dictionary. Therefore, this function
    contains a step that calls a function created to match these two different
    names that the variables have.
    These naming inconsistencies occur because PuLP doesn't allow some characters
    like "/", "-", "+" to be used as names when creating the optimization problem
    variables. Therefore, PuLP converts names like `"ROTA1_I@08PE-PN-840I-06M1"`
    to `"ROTA1_I@08PE_PN_840I_06M1"` when used to name variables.
    """
    denormalized_constraints = denormalized_constraints or DENORMALIZED_CONSTRAINTS
    for constraint_name, constraint in denormalized_constraints.items():
        lp_constraint = process_terms(constraint, lp_problem, scalers)
        lp_problem += lp_constraint, constraint_name
    return lp_problem 
[docs]def fan_consumption_constraint(prob: pulp.LpProblem) -> pulp.LpProblem:
    """
    Change the problem with constraints related to the fan consumption.
    This function adds constraints to the linear programming problem
    related to the energy consumption of fans. It ensures that the sum
    of the individual fan consumption equals the total fan consumption
    and that the total fan consumption doesn't exceed a defined maximum limit.
    Parameters
    ----------
    prob : pulp.LpProblem
        The linear programming problem to which the constraints will be added.
    scalers : dict
        A dictionary containing the scalers used to denormalize the linear
        programming variables.
    Returns
    -------
    pulp.LpProblem
        The modified linear programming problem with the new constraints added.
    See Also
    --------
    denormalize_lpvar : Utility function to denormalize the linear programming
                        variables using the provided scalers.
    pulp.LpProblem : Pulp's object representing a Linear Programming problem.
    Notes
    -----
    The function operates by iterating over predefined fan tag names and
    uses them to access and change the `LpProblem`. The maximum total fan
    consumption is hardcoded as 17. The tag names are specific and should be
    relevant to the context of the `LpProblem` being solved.
    Examples
    --------
    Here is a simple example of how to use the `fan_consumption_constraint` function:
    >>> import pulp
    >>> _prob = pulp.LpProblem("FanConsumptionProblem", pulp.LpMaximize)
    >>> _prob = fan_consumption_constraint(_prob)
    """
    fan_tag_names = [
        "CONS1_Y@08QU_PF_852I_01M1",
        "CONS1_Y@08QU_PF_852I_02M1",
        "CONS1_Y@08QU_PF_852I_03M1",
        "CONS1_Y@08QU_PF_852I_04M1",
        "CONS1_Y@08QU_PF_852I_05M1",
        "CONS1_Y@08QU_PF_852I_06M1",
        "CONS1_Y@08QU_PF_852I_07M1",
        "CONS1_Y@08QU_PF_852I_08M1",
    ]
    total_fan_ee = 0
    for tag_name in fan_tag_names:
        fan_lpvar = prob.variablesDict()[tag_name]
        total_fan_ee += fan_lpvar
    total_fan_ee_tag_name = "CONS1_Y@08QU_VENT"
    total_fan_lpvar = prob.variablesDict()[total_fan_ee_tag_name]
    prob += total_fan_lpvar == total_fan_ee, total_fan_ee_tag_name
    prob += total_fan_lpvar <= 18, "CONS1_Y@08QU_VENT_MAX"
    # prob += total_fan_lpvar_unscaled >= 14, "CONS1_Y@08QU_VENT_MIN"
    return prob