Source code for wip.files.lp_denorm_constraints

"""
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