"""Scaling.
The omlt.scaling module describes the interface for providing different scaling
expressions to the Pyomo model for the inputs and outputs of an ML model. An
implementation of a common scaling approach is included with `OffsetScaling`.
"""
import abc
from typing import Any
[docs]
class ScalingInterface(abc.ABC):
# pragma: no cover
[docs]
@abc.abstractmethod
def get_unscaled_output_expressions(self, scaled_output_vars):
"""Get unscaled outputs.
This method returns a list of expressions for the unscaled outputs from
the scaled outputs
"""
# pragma: no cover
def convert_to_dict(x: Any) -> dict[Any, Any]:
if isinstance(x, dict):
return dict(x)
return dict(enumerate(x))
[docs]
class OffsetScaling(ScalingInterface):
r"""OffsetScaling interface.
This scaling object represents the following scaling equations for inputs (x)
and outputs (y)
.. math::
\begin{align*}
x_i^{scaled} = \frac{(x_i-x_i^{offset})}{x_i^{factor}} \\
y_i^{scaled} = \frac{(y_i-y_i^{offset})}{y_i^{factor}}
\end{align*}
Parameters
----------
offset_inputs : array-like
Array of the values of the offsets for each input to the network
factor_inputs : array-like
Array of the scaling factors (division) for each input to the network
offset_outputs : array-like
Array of the values of the offsets for each output from the network
factor_outputs : array-like
Array of the scaling factors (division) for each output from the network
"""
def __init__(self, offset_inputs, factor_inputs, offset_outputs, factor_outputs):
super().__init__()
self.__x_offset = convert_to_dict(offset_inputs)
self.__x_factor = convert_to_dict(factor_inputs)
self.__y_offset = convert_to_dict(offset_outputs)
self.__y_factor = convert_to_dict(factor_outputs)
for k, v in self.__x_factor.items():
if v <= 0:
msg = (
"OffsetScaling only accepts positive values"
" for factor_inputs. Negative value found at"
f" index {k}."
)
raise ValueError(msg)
for k, v in self.__y_factor.items():
if v <= 0:
msg = (
"OffsetScaling only accepts positive values"
" for factor_outputs. Negative value found at"
f" index {k}."
)
raise ValueError(msg)
[docs]
def get_scaled_output_expressions(self, output_vars):
"""Get the scaled output expressions of the output variables."""
sorted_keys = sorted(output_vars.keys())
if (
sorted(self.__y_offset) != sorted_keys
or sorted(self.__y_factor) != sorted_keys
):
msg = (
"get_scaled_output_expressions called with output_vars"
" that do not have the same indices as offset_outputs"
" or factor_outputs.\n"
f"Keys in output_vars: {sorted_keys}\n"
f"Keys in offset_outputs: {sorted(self.__y_offset)}\n"
f"Keys in offset_factor: {sorted(self.__y_factor)}"
)
raise ValueError(msg)
y = output_vars
return {k: (y[k] - self.__y_offset[k]) / self.__y_factor[k] for k in y}
[docs]
def get_unscaled_output_expressions(self, scaled_output_vars):
"""Get the unscaled output expressions of the scaled output variables."""
sorted_keys = sorted(scaled_output_vars.keys())
if (
sorted(self.__y_offset) != sorted_keys
or sorted(self.__y_factor) != sorted_keys
):
msg = (
"get_scaled_output_expressions called with output_vars"
" that do not have the same indices as offset_outputs"
" or factor_outputs.\n"
f"Keys in output_vars: {sorted_keys}\n"
f"Keys in offset_outputs: {sorted(self.__y_offset)}\n"
f"Keys in offset_factor: {sorted(self.__y_factor)}"
)
raise ValueError(msg)
scaled_y = scaled_output_vars
return {
k: scaled_y[k] * self.__y_factor[k] + self.__y_offset[k] for k in scaled_y
}