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