Source code for aido.optimization_helpers

from typing import Iterable, List, Literal, Tuple

import numpy as np
import torch

from aido.logger import logger
from aido.simulation_helpers import SimulationParameter, SimulationParameterDictionary


[docs] class OneHotEncoder(torch.nn.Module): """ OneHotEncoder is a module that performs one-hot encoding on discrete values. Attributes: logits (torch.Tensor): A set of unnormalized, real-valued scores for each category. These logits represent the model's confidence in each category prior to normalization. They can take any real value, including negatives, and are not probabilities themselves. Use the probabilities property to convert the logits to probabilities. TODO Restrict the learning rate of the logits since they converge much faster than Continuous parameters. """
[docs] def __init__(self, parameter: SimulationParameter): """ Args: parameter (dict): A dictionary containing the parameter information. """ super().__init__() self.discrete_values: list = parameter.discrete_values self.starting_value = torch.tensor(self.discrete_values.index(parameter.current_value)) self.logits = torch.nn.Parameter( torch.log(torch.tensor(parameter.probabilities, dtype=torch.float32)), requires_grad=True ) self._cost = parameter.cost if parameter.cost is not None else 0.0
[docs] def forward(self) -> torch.Tensor: """ Passes the probabilities of each entry """ return self.probabilities
@property def current_value(self) -> torch.Tensor: """ Returns the probability Tensor """ return self.probabilities @property def physical_value(self) -> torch.Tensor: """ Returns the value of the highest scoring entry """ return self.discrete_values[torch.argmax(self.current_value.clone().detach()).item()] @property def probabilities(self) -> torch.Tensor: """ Probabilities for each entry""" return torch.nn.functional.softmax(self.logits, dim=0) @property def cost(self) -> torch.Tensor: """ Costs associated to each entry """ return torch.dot(self.probabilities, torch.tensor(self._cost, device=self.probabilities.device))
[docs] class ContinuousParameter(torch.nn.Module):
[docs] def __init__(self, parameter: SimulationParameter): """ Initializes the optimization helper with the given parameters. Args: parameter (dict): A parameter dict with the format given by 'modules/simulation_helpers.py:SimulationParameterDictionary'. Attributes: starting_value (torch.Tensor): The initial value as a tensor. parameter (torch.nn.Parameter): The parameter wrapped in a PyTorch Parameter object. min_value (float): The minimum allowable value. max_value (float): The maximum allowable value. boundaries (torch.Tensor): A tensor containing the min and max values. sigma (numpy.ndarray): The standard deviation for the parameter. _cost (float): The cost associated with the parameter. """ super().__init__() self.reset(parameter) self.starting_value = torch.tensor(parameter.current_value) self.parameter = torch.nn.Parameter(self.starting_value.clone(), requires_grad=True) self.min_value = parameter.min_value if parameter.min_value is not None else -10E10 self.max_value = parameter.max_value if parameter.max_value is not None else +10E10 self.boundaries = torch.tensor(np.array([ (- self.sigma + self.min_value) / 1.1, (self.sigma + self.max_value) / 1.1 ], dtype="float32")) self._cost = parameter.cost if parameter.cost is not None else 0.0
def reset(self, parameter: SimulationParameter): self.sigma = np.array(parameter.sigma)
[docs] def forward(self) -> torch.Tensor: return torch.unsqueeze(self.parameter, 0)
@property def current_value(self) -> torch.Tensor: if torch.isnan(self.parameter.data): logger.debug(self.__dict__) return self.parameter @property def physical_value(self) -> float: return self.current_value.item() @property def cost(self) -> float: return self.physical_value * self._cost
[docs] class ParameterModule(torch.nn.ModuleDict): def __init__(self, parameter_dict: SimulationParameterDictionary): self.parameter_dict = parameter_dict super().__init__() for parameter in self.parameter_dict: if not parameter.optimizable: continue if parameter.discrete_values is not None: self[parameter.name] = OneHotEncoder(parameter) else: self[parameter.name] = ContinuousParameter(parameter)
[docs] def items(self) -> Iterable[Tuple[str, OneHotEncoder | ContinuousParameter]]: return super().items()
[docs] def values(self) -> Iterable[OneHotEncoder | ContinuousParameter]: return super().values()
def __getitem__(self, key: str) -> OneHotEncoder | ContinuousParameter: return super().__getitem__(key) def __call__(self) -> torch.Tensor: return super().__call__()
[docs] def forward(self) -> torch.Tensor: return torch.unsqueeze(torch.concat([parameter() for parameter in self.values()]), 0)
def continuous_tensors(self) -> torch.Tensor: tensor_list: List[torch.Tensor] = [] for parameter in self.values(): if isinstance(parameter, ContinuousParameter): tensor_list.append(parameter.current_value) return torch.stack(tensor_list) def current_values(self) -> dict: return {name: parameter.current_value for name, parameter in self.items()} def physical_values(self, format: Literal["list", "dict"] = "list") -> list | dict: if format == "list": return [parameter.physical_value for parameter in self.values()] elif format == "dict": return {name: parameter.physical_value for name, parameter in self.items()} @property def probabilities(self) -> dict[str, np.ndarray]: probability_dict = {} for name, parameter in self.items(): if isinstance(parameter, OneHotEncoder): probability_dict[name] = parameter.probabilities.detach().cpu().numpy() return probability_dict @property def constraints(self) -> torch.Tensor: """ A tensor of shape (P, 2) where P is the number of continuous parameters. In the second index, the order is the same as in the ContinuousParameter class (min, max) """ tensor_list: List[torch.Tensor] = [] for parameter in self.values(): if isinstance(parameter, ContinuousParameter): tensor_list.append(parameter.boundaries) return torch.stack(tensor_list) @property def cost_loss(self) -> torch.Tensor: return sum(parameter.cost for parameter in self.values()) @property def covariance(self) -> np.ndarray: return self.parameter_dict.covariance @covariance.setter def covariance(self, new_covariance: np.ndarray): self.parameter_dict.covariance = new_covariance
[docs] def adjust_covariance(self, direction: torch.Tensor, min_scale: float = 2.0): """ Stretches the box_covariance of the generator in the directon specified as input. Direction is a vector in parameter space """ direction = direction.cpu().detach().numpy() direction_normed = direction / (np.linalg.norm(direction) + 1e-4) scale_factor = min_scale * max(1, 4 * np.linalg.norm(direction)) adjustement_matrix = (scale_factor - 1) * np.outer(direction_normed, direction_normed) self.covariance = self.parameter_dict.sigma_array**2 + adjustement_matrix return self.covariance