Source code for wip.files.range_complex_constraints

"""
This module defines a dictionary `range_complex_constraints`.

The `range_complex_constraints` dictionary outlines
the instructions to create specific constraints.
Its primary purpose is to formulate constraints for tags associated
with many machines executing the same process.
A case in point would be disk rotation tags, which
`range_complex_constraints` sets restrictions for.
These tags pertain to a pelletizing plant with 12 disks;
hence, the process includes 12 tag sets.
Despite minor differences in their names,
the tags convey the same meaning.
The dictionary is hence structured to enable
simultaneous specification of similar constraints.

Dictionary Structure
--------------------
The dictionary adopts the following structure:

- Keys represent the restriction names. Each key needs to define a string
  placeholder to be replaced by the ID of each tag from the specific process.
- The values are lists of dictionaries, with each dictionary in the list
  representing a different part of the constraint.
  The last element in each list specifies the constraint operator and constant.
  Each inner dictionary mandates the definition of the following keys:

    - "type": Requires the value "range", which determines the constraint type.
    - "feature": Denotes the tag name to be treated as a variable.
    - "start": Denotes the ID of the first tag from the specific process
      to which a restriction is applied.
    - "end": Denotes the ID of the last tag from the specific process
      to which a restriction is applied.
    - "coef": Specifies the coefficient that multiplies the variable
      to create a constraint equation element.

  The following keys are optional but important for specific use-cases:

    - "operator": When present, its value should be "norm", indicating
      the need for normalization.
    - "norm_feature": Identifies the tag scaler used for normalization.
    - "limit": Determines the value to be normalized.

  These optional keys are crucial when a specific part requires pre-normalization.
  In such instances, the `"limit"` key identifies the value that'll undergo normalization,
  while the `"norm_feature"` key indicates the feature used to normalize `"limit"`.

Developer Notes
---------------
While the concept of driving this module is logical, the implementation
is more complex than necessary for its intended purpose.
The dictionary keys traverse several functions,
leading to a significant increase in code lines and complexity
compared to the straightforward alternative of individually
specifying the constraints. The problem is further exacerbated
by lack of documentation and suboptimal namespace choices.

A recurring pattern in the codebase is the unnecessary complication
of simple tasks and the lack of enough documentation.
This issue poses a significant problem as it makes unconventional
code-indistinguishable from actual bugs. It escalates the risk of
new developers unintentionally introducing more bugs,
thereby adding to the codebase complexity and confusion.

.. versionadded:: 0.1.2

    Added the function `validate_range_complex_constraints` that
    validates the `range_complex_constraints` dictionary during
    this module's import.

"""
from typing import List, Dict


range_complex_constraints = {
    # Obs.: `taxa_alimentacao_disco_min_` e `taxa_alimentacao_disco_max_`
    #        foram convertidas na função:
    #        `wip.modules.constraints.constraint_taxa_alimentacao_disco`
    # "taxa_alimentacao_disco_min_{:02}": [
    #     {
    #         "type": "range",
    #         "feature": "PESO1_I@08PE-BW-840I-{:02}M1",
    #         "start": 1,
    #         "end": 12,
    #         "coef": 1,
    #     },
    #     {
    #         "type": "range",
    #         "feature": "FUNC1_D@08PE-BD-840I-{:02}M1",
    #         "start": 1,
    #         "end": 12,
    #         "operator": "norm",
    #         "coef": -1,
    #         "norm_feature": "PESO1_I@08PE-BW-840I-{:02}M1",
    #         "limit": 90,
    #     },
    #     {"operator": "GTE", "coef": 0},
    # ],
    # "taxa_alimentacao_disco_max_{:02}": [
    #     {
    #         "type": "range",
    #         "feature": "PESO1_I@08PE-BW-840I-{:02}M1",
    #         "start": 1,
    #         "end": 12,
    #         "coef": 1,
    #     },
    #     {
    #         "type": "range",
    #         "feature": "FUNC1_D@08PE-BD-840I-{:02}M1",
    #         "start": 1,
    #         "end": 12,
    #         "operator": "norm",
    #         "coef": -1,
    #         "norm_feature": "PESO1_I@08PE-BW-840I-{:02}M1",
    #         "limit": 140,
    #     },
    #     {"operator": "LTE", "coef": 0},
    # ],
    # "rotacao_disco_min_{:02}": [
    #     {"type": "range", "feature": "rota_disco_{}", "start": 1, "end": 12, "coef": 1},
    #     {
    #         "type": "range",
    #         "feature": "FUNC1_D@08PE-BD-840I-{:02}M1",
    #         "start": 1,
    #         "end": 12,
    #         "coef": -5,
    #     },
    #     {"operator": "GTE", "coef": 0},
    # ],
    # "rotacao_disco_max_{:02}": [
    #     {"type": "range", "feature": "rota_disco_{}", "start": 1, "end": 12, "coef": 1},
    #     {
    #         "type": "range",
    #         "feature": "FUNC1_D@08PE-BD-840I-{:02}M1",
    #         "start": 1,
    #         "end": 12,
    #         "coef": -6.7,
    #     },
    #     {"operator": "LTE", "coef": 0},
    # ],
    "tm_disco_min_{:02}": [
        {
            "type": "range",
            "feature": "GRAN_OCS_TM@08PE-BD-840I-{:02}",
            "start": 1,
            "end": 12,
            "coef": 1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08PE-BD-840I-{:02}M1",
            "start": 1,
            "end": 12,
            "coef": -1,
            "operator": "norm",
            "limit": 12,
            "norm_feature": "GRAN_OCS_TM@08PE-BD-840I-{:02}",
        },
        {"operator": "GTE", "coef": 0},
    ],
    "tm_disco_max_{:02}": [
        {
            "type": "range",
            "feature": "GRAN_OCS_TM@08PE-BD-840I-{:02}",
            "start": 1,
            "end": 12,
            "coef": 1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08PE-BD-840I-{:02}M1",
            "start": 1,
            "end": 12,
            "coef": -1,
            "operator": "norm",
            "limit": 14,
            "norm_feature": "GRAN_OCS_TM@08PE-BD-840I-{:02}",
        },
        {"operator": "LTE", "coef": 0},
    ],
    "potencia_moinho_min_{:02}": [
        {
            "type": "range",
            "feature": "POTE1_I@08MO-MO-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": 1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08MO-MO-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": -1,
            "operator": "norm",
            "limit": 5.48,
            "norm_feature": "POTE1_I@08MO-MO-821I-{:02}M1",
        },
        {"operator": "GTE", "coef": 0},
    ],
    "potencia_moinho_max_{:02}": [
        {
            "type": "range",
            "feature": "POTE1_I@08MO-MO-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": 1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08MO-MO-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": -1,
            "operator": "norm",
            "limit": 6,
            "norm_feature": "POTE1_I@08MO-MO-821I-{:02}M1",
        },
        {"operator": "LTE", "coef": 0},
    ],
    "alim_moinho_min_{:02}": [
        {
            "type": "range",
            "feature": "PESO1_I@08MO-BW-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": 1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08MO-MO-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": -1,
            "operator": "norm",
            "limit": 310,
            "norm_feature": "PESO1_I@08MO-BW-821I-{:02}M1",
        },
        {"operator": "GTE", "coef": 0},
    ],
    "alim_moinho_max_{:02}": [
        {
            "type": "range",
            "feature": "PESO1_I@08MO-BW-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": 1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08MO-MO-821I-{:02}M1",
            "start": 1,
            "end": 3,
            "coef": -1,
            "operator": "norm",
            "limit": 490,
            "norm_feature": "PESO1_I@08MO-BW-821I-{:02}M1",
        },
        {"operator": "LTE", "coef": 0},
    ],
    "equality_bv_filtro_{:02}_1_4": [
        {
            "type": "range",
            "feature": "FUNC1_D@08FI-FL-827I-{:02}",
            "start": 1,
            "end": 4,
            "coef": -1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08FI-BV-827I-{:02}M1",
            "start": 1,
            "end": 4,
            "coef": 1,
        },
        {"operator": "E", "coef": 0},
    ],
    "equality_bv_filtro_{:02}_6_9": [
        {
            "type": "range",
            "feature": "FUNC1_D@08FI-FL-827I-{:02}",
            "start": 6,
            "end": 9,
            "coef": -1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08FI-BV-827I-{:02}M1",
            "start": 6,
            "end": 9,
            "coef": 1,
        },
        {"operator": "E", "coef": 0},
    ],
    "equality_bv_filtro_{:02}_R": [
        {
            "type": "range",
            "feature": "FUNC1_D@08FI-FL-827I-{:02}R",
            "start": 5,
            "end": 10,
            "step": 5,
            "coef": -1,
        },
        {
            "type": "range",
            "feature": "FUNC1_D@08FI-BV-827I-{:02}RM1",
            "start": 5,
            "end": 10,
            "step": 5,
            "coef": 1,
        },
        {"operator": "E", "coef": 0},
    ],
}

OBLIGATORY_KEYS = ["type", "feature", "start", "end", "coef"]
OPTIONAL_KEYS = ["limit", "norm_feature"]


[docs]def validate_range_complex_constraints( _range_complex_constraints: Dict[str, List[Dict[str, object]]], ): """ Validate the structure of the `_range_complex_constraints` dictionary. This function checks whether all required keys are present in each element of the given dictionary. It also validates the optional keys if an "operator" key is present, indicating normalization is required. Parameters ---------- _range_complex_constraints : Dict[str, List[Dict[str, object]]] Dictionary where each key corresponds to a constraint name and the associated value is a list of dictionaries representing the constraints. Each dictionary in the list must have all keys defined in the `OBLIGATORY_KEYS` and, in case an "operator" key is present, it must also contain all keys defined in the `OPTIONAL_KEYS`. Raises ------ KeyError If any dictionary in the list of constraints associated with a constraint name is missing one of the obligatory keys, a KeyError is raised. The same exception is raised if an "operator" key is present, but one of the optional keys is missing. See Also -------- OBLIGATORY_KEYS : List[str] List of keys that must be present in each dictionary representing a constraint. OPTIONAL_KEYS : List[str] List of keys that must be present in each dictionary representing a constraint when an "operator" key is present. Examples -------- Here is an example of how to use this function: >>> test_range_complex_constraints = { ... "constraint1": [{"key1": "value1", "key2": "value2"}], ... "constraint2": [ ... {"key1": "value1", "operator": "value3", ... "limit": "value4", "norm_feature": "value5"} ... ] ... } >>> validate_range_complex_constraints(test_range_complex_constraints) The above code won't raise any exception as all dictionary values contain a list with a single dictionary. The function skips the last element that should represent the RHS of the inequality constraint, that in the above example also represents the only element in each list. """ for key, value in _range_complex_constraints.items(): # Excluding the last element as it represents the RHS of the # inequality constraint, and therefore doesn't require the # keys being checked. for element in value[:-1]: if any(inner_key not in element.keys() for inner_key in OBLIGATORY_KEYS): raise KeyError( f"One of the elements from {key!r} defined inside " "`range_complex_constraints` dictionary is missing " f"one of the obligatory keys: {OBLIGATORY_KEYS}. " f"Please review the {key!r} values inside " "`range_complex_constraints` dictionary." ) # If True, then it means that the element will have to be normalized. # Therefore, we check whether the optional keys are also present. if element.get("operator", None) and any( inner_key not in element.keys() for inner_key in OPTIONAL_KEYS ): raise KeyError( f"One of the elements from {key!r} defined inside " "`range_complex_constraints` dictionary is specifying " "that this element needs to be normalized, but it is missing " "one of the optional keys: `limit`, `norm_feature`. " )
# If the dictionary is defined, no exceptions are raised. validate_range_complex_constraints(range_complex_constraints)