Source code for slim_gsgp.algorithms.GSGP.representations.tree_utils

# MIT License
#
# Copyright (c) 2024 DALabNOVA
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
Utility functions for Tree Evaluation and Mutation in GSGP.
"""

from slim_gsgp.algorithms.GP.representations.tree import Tree
from slim_gsgp.algorithms.GP.representations.tree_utils import bound_value
import torch

def _execute_tree(individual, inputs, testing=False, logistic=False):
    """
    Calculate the semantics for the tree, storing it as an attribute.

    Parameters
    ----------
    individual : Tree
        The tree individual whose semantics are being calculated.
    inputs : array-like
        Input data for calculating semantics.
    testing : bool, optional
        Indicates if the calculation is for testing semantics. Defaults to `False`.
    logistic : bool, optional
        Indicates if a logistic function should be applied to tree outputs. Defaults to `False`.

    Returns
    -------
    None

    Notes
    -----
    The individual to be executed must have the `reconstruct` attribute set to True; otherwise,
    its structure will not be available for evaluation.
    """
    # if the calculation is for testing semantics
    if testing and individual.test_semantics is None:
        # if the individual is a base (gp) tree, use apply_tree to compute its semantics
        if isinstance(individual.structure, tuple):
            individual.test_semantics = (
                torch.sigmoid(apply_tree(individual, inputs))
                if logistic
                else apply_tree(individual, inputs)
            )
        # if not, use the operator (mutation or crossover) with the base trees to compute the semantics
        else:
            individual.test_semantics = individual.structure[0](
                *individual.structure[1:], testing=True
            )

    # if the calculation is for training semantics
    elif individual.train_semantics is None:
        # if the individual is a base (gp) tree, use apply_tree to compute its semantics
        if isinstance(individual.structure, tuple):
            individual.train_semantics = (
                torch.sigmoid(apply_tree(individual, inputs))
                if logistic
                else apply_tree(individual, inputs)
            )

        # if not, use the operator (mutation or crossover) with the base trees to compute the semantics
        else:
            individual.train_semantics = individual.structure[0](
                *individual.structure[1:], testing=False
            )

[docs] def apply_tree(tree, inputs): """ Evaluates the tree on input vectors. Parameters ---------- tree : Tree The tree structure to be evaluated. inputs : torch.Tensor Input vectors x and y. Returns ------- torch.Tensor Output of the evaluated tree. """ if isinstance(tree.structure, tuple): # If it's a function node function_name = tree.structure[0] if tree.FUNCTIONS[function_name]["arity"] == 2: left_subtree, right_subtree = tree.structure[1], tree.structure[2] left_subtree = Tree(left_subtree) right_subtree = Tree(right_subtree) left_result = left_subtree.apply_tree(inputs) right_result = right_subtree.apply_tree(inputs) output = tree.FUNCTIONS[function_name]["function"]( left_result, right_result ) else: left_subtree = tree.structure[1] left_subtree = Tree(left_subtree) left_result = left_subtree.apply_tree(inputs) output = tree.FUNCTIONS[function_name]["function"](left_result) return bound_value(output, -1000000000000.0, 10000000000000.0) else: # If it's a terminal node if tree.structure in list(tree.TERMINALS.keys()): output = inputs[:, tree.TERMINALS[tree.structure]] return output elif tree.structure in list(tree.CONSTANTS.keys()): output = tree.CONSTANTS[tree.structure](None) return output
[docs] def nested_depth_calculator(operator, depths): """ Calculate the depth of nested structures. To save computational effort, the new depth is calculated based on the operator used to generate the new tree. Parameters ---------- operator : callable The operator applied to the tree. depths : list of int List of depths of subtrees. Returns ------- int Maximum depth after applying the operator. """ if operator.__name__ == "tt_delta_sum": depths[0] += 2 depths[1] += 2 elif operator.__name__ == "tt_delta_mul": depths[0] += 3 depths[1] += 3 elif operator.__name__ == "ot_delta_sum_True": depths[0] += 3 elif operator.__name__ in ["ot_delta_sum_False", "ot_delta_mul_True"]: depths[0] += 4 elif operator.__name__ == "ot_delta_mul_False": depths[0] += 5 elif operator.__name__ == "geometric_crossover": depths = [n + 2 for n in depths] depths.append(depths[-1] + 1) return max(depths)
[docs] def nested_nodes_calculator(operator, nodes): """ Calculate the number of nodes in nested structures. Parameters ---------- operator : callable The operator applied to the tree. nodes : list of int List of node counts of subtrees. Returns ------- int Total number of nodes after applying the operator. """ extra_operators_nodes = ( [5, nodes[-1]] if operator.__name__ == "geometric_crossover" else ( [7] if operator.__name__ == "ot_delta_sum_True" else ( [11] if operator.__name__ == "ot_delta_mul_False" else ( [9] if operator.__name__ in ["ot_delta_sum_False", "ot_delta_mul_True"] else ( [6] if operator.__name__ == "tt_delta_mul" else ([4] if operator.__name__ == "tt_delta_sum" else [0]) ) ) ) ) ) return sum([*nodes, *extra_operators_nodes])