diff --git a/.github/workflows/osa_mkdocs.yml b/.github/workflows/osa_mkdocs.yml new file mode 100644 index 00000000..174a2fb3 --- /dev/null +++ b/.github/workflows/osa_mkdocs.yml @@ -0,0 +1,25 @@ +name: MkDocs workflow +on: + push: + branches: ["main", "master"] + pull_request: + branches: ["main", "master"] +jobs: + mkdocs_deployment: + name: "[OSA] Deploying MkDocs" + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: "[OSA] Checking-out repository" + uses: actions/checkout@v4 + - name: "[OSA] Installing Python" + uses: actions/setup-python@v4 + with: + python-version: "3.12" + - name: "[OSA] Installing MkDocs dependencies" + run: pip install mkdocs mkdocs-material mkdocstrings[python] + - name: "[OSA] MkDocs documentation deploying" + run: mkdocs gh-deploy --force --config-file osa_mkdocs.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md new file mode 100644 index 00000000..24fc05ff --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# EPDE + +--- + +[![PyPi](https://badge.fury.io/py/epde.svg)](https://badge.fury.io/py/epde) +[![Downloads](https://static.pepy.tech/badge/epde)](https://pepy.tech/project/epde) +![License](https://img.shields.io/github/license/ITMO-NSS-team/EPDE?style=flat&logo=opensourceinitiative&logoColor=white&color=blue) +[![OSA-improved](https://img.shields.io/badge/improved%20by-OSA-yellow)](https://github.com/aimclub/OSA) + +Built with: + +![numpy](https://img.shields.io/badge/NumPy-013243.svg?style={0}&logo=NumPy&logoColor=white) +![pandas](https://img.shields.io/badge/pandas-150458.svg?style={0}&logo=pandas&logoColor=white) +![pytest](https://img.shields.io/badge/Pytest-0A9EDC.svg?style={0}&logo=Pytest&logoColor=white) +![scipy](https://img.shields.io/badge/SciPy-8CAAE6.svg?style={0}&logo=SciPy&logoColor=white) + +--- + +[![Logo of EPDE framework](docs/epde_logo.png)](docs/epde_logo.png) + +We use this repository as the main entry point for the EDPE framework. + +We are of opinion that partial differential equations (PDE) discovery is more that the regression in the prescribed differential terms space. We propose the novel method of evolutionary equation discovery. Apart from the differential equations we have the algrbraic expressions discovery algorithm that is now uses same abstractions. + +The project is maintained by the research team of the Natural Systems Simulation Lab, which is a part of the National Center for Cognitive Research of ITMO University. + +--- + +## Overview + +EPDE framework pioneers a novel evolutionary approach to discover governing equations, particularly partial differential equations, directly from data. It moves beyond traditional regression in predefined spaces, employing evolutionary algorithms to identify mathematical relationships within datasets. The core objective is to automate the discovery of these equations, enhancing surrogate modeling in natural sciences. EPDE features a multi-objective version for Pareto frontier analysis, a numerical differentiation scheme for noisy data, and a solver for visualizing the discovery process. By implementing evolutionary equation discovery, EPDE addresses real and synthetic data, contributing to data-driven differential equation-based model development as described in associated research articles. + +The main features of the framework are as follows: + +- We dont need to create prescribed library of terms - our 'building blocks' are single differential terms or simple functions +- We have the multi-objective version that allows to obtain Pareto frontier of the equations, which helps the expert to choose one from several equation. +- We use our own numerical differntiaion scheme, which allows to deal with high noise values +- We have solver to visualize the differential equations discovery process to add more control (https://github.com/ITMO-NSS-team/torch_DE_solver) + +--- + +## Table of Contents + +- [Overview](#overview) +- [Content](#content) +- [Algorithms](#algorithms) +- [Installation](#installation) +- [Getting Started](#getting-started) +- [Examples](#examples) +- [Documentation](#documentation) +- [License](#license) +- [Citation](#citation) + +--- + +## Content + +The EPDE framework employs evolutionary algorithms to discover governing equations, particularly partial differential equations, from data. It eschews predefined term libraries, instead using fundamental differential terms and functions as building blocks. The framework integrates data input, preprocessing, symbolic regression, equation solving, and fitness evaluation. A key aspect is multi-objective optimization, generating a Pareto frontier of equations balancing accuracy and complexity. Numerical differentiation handles noisy data, and a solver aids visualization. The system iteratively refines equation structures through mutation, crossover, and sparse regression, identifying significant terms and weights. This approach facilitates data-driven equation derivation and surrogate model development, applicable to both synthetic and real-world data. + +--- + +## Algorithms + +The EPDE framework employs an evolutionary algorithm combined with sparse regression to discover partial differential equations from data. The evolutionary algorithm iteratively refines the equation structure through mutation and crossover operations. Sparse regression, specifically LASSO, is used to select significant terms and determine their weights within the equation. A numerical differentiation scheme handles noisy data. Multi-objective optimization allows for generating a Pareto frontier of potential equations, balancing accuracy and complexity. These algorithms facilitate the automated discovery of underlying equations from data, even in the presence of noise and high dimensionality, supporting surrogate modeling and data-driven equation derivation. + +--- + +## Installation + +**Prerequisites:** requires Python >=3.8 + +Common installation: + +```sh +pip install epde +``` + +--- + +## Getting Started + +To get started with EPDE, you can install the package using pip: + +``` +pip install epde +``` + +Then, explore the examples provided in the `examples` directory. For instance, the `ODE discovery.ipynb` notebook demonstrates how to discover ordinary differential equations using the EPDE framework. It covers topics such as: + +- Generating synthetic data from known ODEs. +- Setting up the `EpdeSearch` object with appropriate parameters. +- Using trigonometric and grid tokens to enhance the search. +- Visualizing and interpreting the discovered equations. + +Also, there is an intro video about EPDE: + +[![Introducing EPDE](https://res.cloudinary.com/richarddedekind/image/upload/v1623953761/EDPE_front_dsyl9h.png)](https://www.youtube.com/watch?v=BSXGCeuTcdc) + +--- + +## Examples + +Examples of how this should work and how it should be used are available [here](https://github.com/ITMO-NSS-team/EPDE/tree/main/examples). + +--- + +## Documentation + +A detailed EPDE description is available [here](https://github.com/ITMO-NSS-team/EPDE/tree/main/docs). + +--- + +## License + +This project is protected under the Other. For more details, refer to the [LICENSE](https://github.com/ITMO-NSS-team/EPDE/tree/main/LICENSE.txt) file. + +--- + +## Citation + +@article{maslyaev2021partial, + title={Partial differential equations discovery with EPDE framework: application for real and synthetic data}, + author={Maslyaev, Mikhail and Hvatov, Alexander and Kalyuzhnaya, Anna V}, + journal={Journal of Computational Science}, + pages={101345}, + year={2021}, + publisher={Elsevier} +} + +@article{maslyaev2019discovery, + title={Discovery of the data-driven differential equation-based models of continuous metocean process}, + author={Maslyaev, Mikhail and Hvatov, Alexander}, + journal={Procedia Computer Science}, + volume={156}, + pages={367--376}, + year={2019}, + publisher={Elsevier} +} \ No newline at end of file diff --git a/epde/cache/cache.py b/epde/cache/cache.py index 5d915155..038e4597 100644 --- a/epde/cache/cache.py +++ b/epde/cache/cache.py @@ -37,16 +37,22 @@ def upload_simple_tokens(labels, cache, tensors, deriv_codes: List = None, grid_setting=False): """ - Uploads the basic factor into the cache with its value in ndimensional numpy.array - - Args: - labels: list or 1-d array with string name of coefficients - cache (`Cache`): keeping values of terms/factors of equations. - tensors (`numpy.ndarray`): values for coefficients, shape of array: (n, m, ...), where n is number of coefficients - grid_settings: optional, boolean argument, default - False - - Returns: - None + Uploads basic factors (tokens) into the cache, associating them with their numerical values. + + This function prepares the elementary building blocks for constructing more complex equation terms. + It iterates through provided labels and their corresponding tensor values, storing them within the cache. + The cache is later used to efficiently evaluate and manipulate these factors during the equation discovery process. + + Args: + labels (list or numpy.ndarray): A list of string names representing the coefficients or variables. + cache (Cache): The cache object used for storing and retrieving factor values. + tensors (numpy.ndarray): A multi-dimensional array containing the numerical values for each coefficient. + The shape should be (n, m, ...), where n is the number of coefficients. + deriv_codes (List, optional): List of derivative codes associated with each label. Defaults to None. + grid_setting (bool, optional): A flag indicating whether the labels are part of a grid setting. Defaults to False. + + Returns: + None """ if deriv_codes is not None and len(deriv_codes) != len(labels): print(deriv_codes, labels) @@ -65,11 +71,17 @@ def upload_simple_tokens(labels, cache, tensors, deriv_codes: List = None, def upload_grids(grids, cache): """ - Grids are saved into the base matrices of the cache - + Saves grid data into the cache for subsequent equation discovery. + + This method prepares grid data by converting it into a suitable format + and storing it within the cache. This allows the evolutionary algorithm + to efficiently access and utilize the grid data during the equation + search process. The grid data represents the independent variable space + over which the solution is defined. + Args: - grids (`list|tuple|numpy.ndarray`): value of grids - cache (`Cache`): object where grids wiil be stored + grids (`list|tuple|numpy.ndarray`): Grid data to be stored. Can be a list/tuple of grids or a single numpy array. + cache (`Cache`): The cache object where the grid data will be stored. Returns: None @@ -86,15 +98,15 @@ def upload_grids(grids, cache): def prepare_var_tensor(var_tensor, derivs_tensor, time_axis): """ - Method for transformation of the input data, the time axis is placed first - - Args: - var_tensor: numpy.array, - derivs_tensor: numpy.ndarray, - time_axis: - Returns: - result (`numpy.ndarray`): formed data for the algorithm - + Transforms the input variable and its derivatives to a format suitable for equation discovery. The time axis is moved to the first position to align the data for subsequent processing by the evolutionary algorithm. This rearrangement ensures that the temporal dependencies are properly considered when searching for the best-fitting differential equation. + + Args: + var_tensor (numpy.ndarray): The input variable tensor. + derivs_tensor (numpy.ndarray): The tensor containing the derivatives of the variable. + time_axis (int): The axis representing time in the input tensors. + + Returns: + numpy.ndarray: A reshaped tensor with the time axis moved to the first position, ready for equation discovery. """ initial_shape = var_tensor.shape print('initial_shape', initial_shape, 'derivs_tensor.shape', derivs_tensor.shape) @@ -114,6 +126,18 @@ def prepare_var_tensor(var_tensor, derivs_tensor, time_axis): return result def switch_format(inp: Union[torch.Tensor, np.ndarray], device = 'cpu'): + """ + Converts between NumPy arrays and PyTorch tensors to ensure compatibility between different computational stages within the EPDE framework. + + This function facilitates seamless data exchange between numerical solvers (often requiring NumPy arrays) and the evolutionary search process (leveraging PyTorch tensors for GPU acceleration and automatic differentiation). + + Args: + inp: The input NumPy array or PyTorch tensor. + device: The device to move the tensor to (only relevant when converting from NumPy to Torch). Defaults to 'cpu'. + + Returns: + Union[torch.Tensor, np.ndarray]: The converted NumPy array or PyTorch tensor. + """ if isinstance(inp, np.ndarray): return torch.from_numpy(inp).to(device) # TODO: add device selection elif isinstance(inp, torch.Tensor): @@ -125,12 +149,20 @@ def switch_format(inp: Union[torch.Tensor, np.ndarray], device = 'cpu'): def download_variable(var_filename, deriv_filename, time_axis): """ - + Downloads variable data and its derivatives, then prepares a tensor of tokens. + + This function loads variable data and its corresponding derivatives from specified files, + processes them, and combines them into a tensor suitable for equation discovery. + The resulting tensor serves as input for the evolutionary process of identifying + the underlying differential equation. + Args: - var_filename: str, - deriv_filename: str, + var_filename (str): The filename of the variable data. + deriv_filename (str): The filename of the derivative data. + time_axis (int): The axis representing the time dimension. + Returns: - + np.ndarray: A tensor of tokens representing the processed variable and derivative data. """ var = np.load(var_filename) derivs = np.load(deriv_filename) @@ -139,15 +171,41 @@ def download_variable(var_filename, deriv_filename, time_axis): class Cache(object): - """Class for keeping values of terms/factors of equations. - + """ + Class for efficiently storing and retrieving intermediate calculation results, enhancing performance by avoiding redundant computations. + + Args: max_allowed_tensors (`int`): limitation on the number of allowed tensors to load into the cache. memory_default (`dict`): key - name of tensor (tuple - (name_of_term, params)), value - derivative. Objects without changes after evolutional step memory_normalized (`dict`): key - name of tensor (tuple - (name_of_term, params)), value - derivative. Objects with normalize memory_structural (`dict`): key - name of tensor (tuple - (name_of_term, params)), value - derivative. NOT USED ДОПИСАТЬ ПРОСМОТРЕВ КОД """ + def __init__(self, device = 'cpu'): + """ + Initializes the MemoryAnalyzer class. + + Sets up the memory analyzer to track and manage memory usage during the equation discovery process. This initialization prepares the data structures needed to store memory consumption data for different types of tensors, allowing for efficient memory management and optimization of the search for governing equations. By tracking memory usage, the system can avoid memory overflow and improve the efficiency of the equation discovery process. + + Args: + device (str): The device being used for computation (e.g., 'cpu', 'cuda'). + + Fields: + _device (str): The device being used for computation. + max_allowed_tensors (None): Maximum number of tensors allowed (currently not set). + memory_default (dict): Stores default memory usage for torch and numpy tensors. + memory_normalized (dict): Stores normalized memory usage for torch and numpy tensors. + memory_structural (dict): Stores structural memory usage for torch and numpy tensors. + memory_anns (dict): Stores memory annotations. + mem_prop_set (bool): Flag indicating if memory properties have been set. + base_tensors (list): Stores non-normalized tensors. + structural_and_base_merged (dict): Stores merged structural and base tensor information. + _deriv_codes (list): Stores derivative codes and corresponding cache labels. + + Returns: + None + """ self._device = device self.max_allowed_tensors = None @@ -164,6 +222,30 @@ def __init__(self, device = 'cpu'): # standard form ('dy/dx1', (1.0,)) def attrs_from_dict(self, attributes, except_attrs: dict = {}): + """ + Populates the cache object's attributes from a dictionary, excluding specified keys to maintain a consistent and manageable cache state. + + This method initializes the object's attributes from the provided dictionary, + optionally excluding keys specified in the `except_attrs` dictionary. + If `except_attrs` contains keys with non-None values, those keys + and values are explicitly set as attributes on the object after + processing the main `attributes` dictionary. This ensures that certain + attributes, if provided, always take precedence, allowing for fine-grained + control over the cache's configuration. + + Args: + attributes: A dictionary containing attribute names and their values, + used to populate the cache object's state. + except_attrs: A dictionary of attributes to exclude from + the `attributes` dictionary during initial population. + If a key exists in this dictionary and its value is not None, it + will be explicitly set as an attribute after the initial population + step. + + Returns: + None. The method modifies the object's attributes in place, updating + the cache object's internal state. + """ except_attrs['obj_type'] = None self.__dict__ = {key : item for key, item in attributes.items() if key not in except_attrs.keys} @@ -172,6 +254,19 @@ def attrs_from_dict(self, attributes, except_attrs: dict = {}): self.__dict__[key] = elem def use_structural(self, use_base_data=True, label=None, replacing_data=None): + """ + Uses structural data, potentially replacing base data, to refine equation discovery. + + This method allows the user to specify whether to use the initial data as a starting point for structural exploration or to replace it entirely with new data. This is crucial for exploring different equation structures and identifying the best fit for the observed data. + + Args: + use_base_data: A boolean flag. If True, the method uses the initial data stored in the cache as a basis for structural data. If False, it uses the `replacing_data` instead. + label: An optional label. If None, the method applies the structural changes to all base data. If a tuple, it applies the changes only to the data associated with the given label. + replacing_data: Optional data to replace the base data. If `label` is None, this must be a dictionary where keys are memory keys (tuples) and values are NumPy arrays. If `label` is a tuple, this must be a NumPy array representing the replacement data for that specific label. + + Returns: + None + """ assert use_base_data or replacing_data is not None, 'Structural data must be declared with base data or by additional tensors.' # print('Called `use_structural`, expect caches to alter') if label is None: @@ -217,6 +312,22 @@ def use_structural(self, use_base_data=True, label=None, replacing_data=None): @property def g_func(self): # , g_func: Union[Callable, type(None)] = None try: + """ + Calculates and returns the result of the g-function. + + This method dynamically selects and applies the g-function. If a grid cache + is available, it computes the g-function based on the cached data, + facilitating efficient equation discovery by reusing previously computed + values. Otherwise, it returns the pre-computed g-function, ensuring + that the equation discovery process can proceed even without cached data. + + Args: + self: The instance of the Cache class. + + Returns: + The result of the g-function, which can be either a NumPy array + or a list, depending on whether the grid cache is utilized. + """ assert '0' in self.memory_default['numpy'].keys() # Check if we are working with the grid cache return self._g_func(self.get_all()[1]) except TypeError: @@ -225,15 +336,51 @@ def g_func(self): # , g_func: Union[Callable, type(None)] = None @g_func.setter def g_func(self, function: Union[Callable, np.ndarray, list]): + """ + Sets the function to be used for calculating the 'g' term. + + The 'g' term represents a component of the discovered differential equation. + Setting this function allows the algorithm to explore different mathematical + representations for this component during the equation discovery process. + + Args: + function (Callable | np.ndarray | list): The function to be used for the 'g' term. + It can be a callable (e.g., a Python function), a NumPy array representing + discrete values, or a list of values. + + Returns: + None + """ self._g_func = function def add_base_matrix(self, label): + """ + Adds a label to the list of base tensors, ensuring it exists in the memory. + + This function is crucial for tracking the fundamental matrices used in constructing more complex equation terms. By maintaining a record of these base matrices, the system can efficiently manage and reuse them during the equation discovery process. + + Args: + label (str): The label of the base matrix to add. This label must correspond to a key in the 'numpy' section of the default memory. + + Returns: + None + """ assert label in self.memory_default['numpy'].keys() self.base_tensors.append(label) def set_boundaries(self, boundary_width: Union[int, list, tuple]): """ - Setting the number of unaccounted elements at the edges + Sets the boundary width, defining the region near the edges of the data that is excluded from equation discovery. + + This is crucial for mitigating boundary effects and ensuring the discovered equations accurately represent the underlying dynamics within the domain, rather than being influenced by artificial constraints at the edges. + + Args: + boundary_width (Union[int, list, tuple]): The width of the boundary region. If an integer is provided, the same width is applied to all dimensions. If a list or tuple is provided, it specifies the width for each dimension. + + Raises: + AssertionError: If the cache is not initialized for grid data. + IndexError: If the boundary width is too large for the data shape. + TypeError: If the boundary_width is not an int, list, or tuple. """ assert '0' in self.memory_default['numpy'].keys(), 'Boundaries should be specified for grid cache.' shape = self.get('0')[1].shape @@ -250,15 +397,20 @@ def set_boundaries(self, boundary_width: Union[int, list, tuple]): def memory_usage_properties(self, obj_test_case=None, mem_for_cache_frac=None, mem_for_cache_abs=None): """ - Method for setting of memory using in algorithm's process - - Args: - obj_test_case (`ndarray`): referntial tensor to evaluate memory consuption by tensors equation search - mem_for_cache_frac (`int`): memory available for cache (in fraction of RAM). The default - None. - mem_for_cache_abs (`int`): memory available for cache (in byte). The default - None. + Method for configuring memory usage for equation discovery. - Returns: - None + This method sets the available memory for caching tensors, which influences the complexity of equations that can be explored. + + Args: + obj_test_case (`ndarray`): A sample tensor used to estimate the memory footprint of potential equation terms. + mem_for_cache_frac (`int`): The fraction of total RAM to allocate for the cache (as a percentage). Defaults to None. + mem_for_cache_abs (`int`): The absolute amount of memory (in bytes) to allocate for the cache. Defaults to None. + + Returns: + None + + Why: + Configuring memory usage allows the algorithm to efficiently manage resources when searching for differential equations. By setting memory limits, we control the size and number of tensors that can be stored, which directly impacts the complexity of equations that can be discovered. """ assert not (mem_for_cache_frac is None and mem_for_cache_abs is None), 'Avalable memory space not defined' assert obj_test_case is not None or len(self.memory_default['numpy']) > 0, 'Method needs sample of stored matrix to evaluate memory allocation' @@ -282,6 +434,18 @@ def memory_usage_properties(self, obj_test_case=None, mem_for_cache_frac=None, m print('The memory can not containg any tensor even if it is entirely free (This message can not appear)') def clear(self, full=False): + """ + Clears the stored derivative codes and cached tensors. + + This method is crucial for managing memory and ensuring that the equation discovery process remains efficient. By clearing the cache, we prevent the accumulation of unnecessary data, which can slow down the search for governing equations. + + Args: + self: The instance of the Cache class. + full (bool, optional): If True, clears all cached tensors, including base tensors. If False, only derivative codes are cleared, and base tensors are retained. Defaults to False. + + Returns: + None + """ self._deriv_codes = [] print('Clearing cache') if full: @@ -311,9 +475,24 @@ def clear(self, full=False): def add(self, label, tensor, normalized: bool = False, structural: bool = False, deriv_code = None, indication: bool = False): - ''' - Method for addition of a new tensor into the cache. Returns True if there was enough memory and the tensor was save, and False otherwise. - ''' + """ + Adds a tensor to the cache, categorizing it as normalized or default based on the provided flags. + + This method attempts to store the given tensor in the appropriate memory cache (normalized or default) based on its properties and available space. + The addition is skipped if the cache already contains tensor with provided label. + The method ensures that the cache does not exceed its maximum allowed tensor limit. + + Args: + label (str): A unique identifier for the tensor. + tensor (torch.Tensor or numpy.ndarray): The tensor to be added to the cache. + normalized (bool, optional): Indicates whether the tensor is normalized. Defaults to False. + structural (bool, optional): Indicates whether the tensor is structural. Defaults to False. + deriv_code (object, optional): Derivative code associated with the tensor. Defaults to None. + indication (bool, optional): Enables print statements for debugging. Defaults to False. + + Returns: + bool: True if the tensor was successfully added to the cache, False otherwise. + """ # print(deriv_code) if deriv_code is not None: self._deriv_codes.append((deriv_code, label)) @@ -360,6 +539,20 @@ def add(self, label, tensor, normalized: bool = False, structural: bool = False, return False def delete_entry(self, entry_label): + """ + Deletes a specific data entry from all cache levels (default, structural, and normalized). + + This ensures data consistency across different representations used during the equation discovery process. By removing the entry from all levels, we prevent outdated or inconsistent data from influencing the search for governing differential equations. + + Args: + entry_label (str): The label of the data entry to delete. + + Returns: + None + + Raises: + ValueError: If the specified `entry_label` is not found in the default memory. This indicates a potential issue with data management or a request to delete a non-existent entry. + """ print(f'Deleting {entry_label} from cache!') if entry_label not in self.memory_default["numpy"].keys(): raise ValueError('deleted element already not in memory') @@ -374,6 +567,25 @@ def delete_entry(self, entry_label): pass def get(self, label, normalized=False, structural=False, saved_as=None, torch_mode: bool = False, deriv_code = None): + """ + Retrieves a matrix from the cache based on the provided label and flags. + + This method is central to accessing and managing the discovered equation components. + It allows retrieval of matrices in different formats (numpy or torch), and with different scaling + (normalized or structural), ensuring that the correct representation is used in the equation discovery process. + The method also handles on-the-fly format conversion if the matrix is available in a different format. + + Args: + label: The label of the matrix to retrieve. + normalized: A boolean indicating whether to retrieve the normalized version of the matrix. + structural: A boolean indicating whether to retrieve the structurally scaled version of the matrix. + saved_as: The format the matrix was saved as. + torch_mode: A boolean indicating whether to return a torch tensor or a numpy array. + deriv_code: Derivative code to look up the label. + + Returns: + The retrieved matrix, either as a numpy array or a torch tensor. + """ assert not (normalized and structural), 'The added matrix can not be simultaneously normalized and scaled' type_key, other, other_bool = ('torch', 'numpy', False) if torch_mode else ('numpy', 'torch', True) if deriv_code is not None: @@ -406,6 +618,31 @@ def get(self, label, normalized=False, structural=False, saved_as=None, torch_mo return self.memory_default[type_key][label] def get_all(self, normalized=False, structural=False, mode: str = 'numpy'): + """ + Returns all stored keys and their corresponding tensors. + + This method retrieves data from both primary and secondary memory formats (NumPy and Torch), + ensuring all available data is included regardless of the current primary format. It also + handles necessary format conversions to maintain consistency. + + Args: + normalized (bool, optional): If True, retrieve data from the normalized memory. Defaults to False. + structural (bool, optional): If True, retrieve data from the structural memory. Defaults to False. + mode (str, optional): The primary memory format ('numpy' or 'torch'). Defaults to 'numpy'. + + Returns: + tuple[list, list]: A tuple containing: + - A list of keys representing the identified equation terms. + - A list of tensors representing the corresponding data for each term, + ensuring data from both memory formats are included and converted to + the appropriate format. + + Why: + This method is essential for accessing all discovered equation terms and their + corresponding data, regardless of the storage format. It ensures that the + evolutionary algorithm has a complete view of the candidate equations and + their performance, facilitating the equation discovery process. + """ other = 'torch' if mode == 'numpy' else 'numpy' if normalized: processed_mem = self.memory_normalized[mode] @@ -431,12 +668,21 @@ def get_all(self, normalized=False, structural=False, mode: str = 'numpy'): return keys, tensors def __contains__(self, obj): - ''' - Valid input type: - 'label' (checked in unnormalized data); ('label1', normalized), where normalized is bool (T if norm, else F); - np.ndarray of values (checked in unnormalized data); (np.ndarray, normalized), where normalized is bool - (T if norm, else F) and np.ndarray is np.ndarray of tensor values. Does not support scaled vals - ''' + """ + Checks if a given object is present in the cache, considering both normalized and unnormalized data. This is essential for efficiently reusing previously computed results when exploring the solution space of differential equations. + + Args: + obj (str, tuple, list, np.ndarray, torch.Tensor): The object to check for presence in the cache. + It can be a label (string), a tuple/list containing a label and a boolean indicating normalization, + a NumPy array, a PyTorch tensor, or a tuple/list containing a NumPy array/PyTorch tensor and a boolean + indicating normalization. + + Returns: + bool: True if the object is found in the cache, False otherwise. + + Raises: + NotImplementedError: If the input object's format is not supported. + """ if (type(obj) == tuple or type(obj) == list) and type(obj[0]) == str: return (obj in self.memory_default['numpy'].keys()) or (obj in self.memory_default['torch'].keys()) elif (type(obj) == tuple or type(obj) == list) and type(obj[0]) == tuple and type(obj[1]) == bool: @@ -477,6 +723,22 @@ def __contains__(self, obj): # for key in self.memory_default.keys() def prune_tensors(self, pruner, mem_to_process: list = ['default', 'structural', 'normalized'], torch_mode: bool = False): + """ + Prunes stored tensors to reduce memory footprint based on the provided pruner. + + This method iterates through specified memory types (default, structural, normalized) + and applies the provided pruner to each tensor associated with a key. This helps in + reducing the computational cost by removing unnecessary information, thereby optimizing + the search for governing differential equations. + + Args: + pruner: The pruner object used to prune the tensors. + mem_to_process: A list of memory types to process (default, structural, normalized). + torch_mode: A boolean indicating whether to use torch mode or numpy mode. + + Returns: + None. The method modifies the memory tensors in place. + """ mode = 'torch' if torch_mode else 'numpy' mem_arranged = {'default': self.memory_default, 'structural': self.memory_structural, @@ -491,6 +753,25 @@ def prune_tensors(self, pruner, mem_to_process: list = ['default', 'structural', @property def consumed_memory(self): + """ + Return the total memory consumed by NumPy arrays across different memory scopes. + + This property calculates the total memory footprint of NumPy arrays + stored within the cache. It aggregates the memory usage from the + 'memory_default', 'memory_normalized', and 'memory_structural' + dictionaries, focusing specifically on 'numpy' entries. This is + crucial for monitoring memory usage during the equation discovery + process, especially when dealing with large datasets or complex + equation structures. By tracking memory consumption, we can optimize + the performance and prevent memory-related issues during the search + for governing differential equations. + + Args: + self: The Cache instance. + + Returns: + int: The total memory consumed by NumPy arrays, in bytes. + """ memsize = np.sum([value.nbytes for _, value in self.memory_default['numpy'].items()]) memsize += np.sum([value.nbytes for _, value in self.memory_normalized['numpy'].items()]) for label, merged_state in self.structural_and_base_merged.items(): @@ -499,6 +780,27 @@ def consumed_memory(self): def upload_complex_token(label: str, params_values: OrderedDict, evaluator, tensor_cache: Cache, grid_cache: Cache): + """ + Uploads a complex token's evaluated tensor to the cache. + + This method constructs a tensor by evaluating a predefined function + (identified by the label) over a grid of input values. These input + grids are retrieved from the grid cache, and the resulting tensor + is stored in the tensor cache, enabling later use for equation discovery. + + Args: + label (str): The label identifying the evaluation function to use for tensor generation. + params_values (OrderedDict): An ordered dictionary of parameter values to be + passed to the evaluation function. + evaluator: An object containing the evaluation functions. + tensor_cache (Cache): The cache for storing the resulting tensors. + grid_cache (Cache): The cache containing the grids of input values. + + Returns: + None: This method does not return any value. The generated tensor is stored in `tensor_cache`. + + WHY: This method is crucial for constructing the building blocks of potential differential equations. By evaluating predefined functions over a grid of values, it generates tensors that represent different terms or operations within an equation. These tensors are then used in the equation discovery process to find the best-fitting model for the given data. + """ try: evaluation_function = evaluator.evaluation_functions[label] except TypeError: diff --git a/epde/cache/ctrl_cache.py b/epde/cache/ctrl_cache.py index 345c18b6..525b62e0 100644 --- a/epde/cache/ctrl_cache.py +++ b/epde/cache/ctrl_cache.py @@ -3,8 +3,38 @@ import torch class ControlNNContainer(): + """ + A container class for managing a neural network, its arguments, and the device it runs on. + + Class Methods: + - __init__: Initializes the ControlNNContainer with network arguments, a neural network, and a device. + - create_shallow_nn: Creates a shallow neural network. + + Attributes: + net_args: Stores the network arguments provided during initialization. + net: Stores the neural network, either the provided one or a newly created shallow network. + """ + def __init__(self, output_num: int = 1, args: List[Tuple[Union[int, List]]] = [(0, [None,]),], net: torch.nn.Sequential = None, device: str = 'cpu'): + """ + Initializes the ControlNNContainer with network configuration, a neural network, and the computation device. + + This setup is crucial for managing and executing neural networks within the equation discovery process. The neural network architecture and its computational environment are configured to efficiently evaluate candidate equation structures. + + Args: + output_num: The number of output neurons in the network. Defaults to 1. + args: A list of tuples, where each tuple defines the input and any associated parameters for a network layer. Defaults to `[(0, [None,])]`. + net: A `torch.nn.Sequential` neural network. If `None`, a default shallow network is created based on the provided `args`. Defaults to `None`. + device: The device ('cpu' or 'cuda') on which the neural network will be executed. Defaults to 'cpu'. + + Returns: + None + + Initializes: + net_args (List[Tuple[Union[int, List]]]): Stores the network arguments for later use. + net (torch.nn.Sequential): Stores the initialized neural network, either the provided one or a newly created shallow network. + """ self.net_args = args self.net = net if isinstance(net, torch.nn.Sequential) else self.create_shallow_nn(len(self.net_args), output_num, device) diff --git a/epde/control/constr.py b/epde/control/constr.py index f99e6610..d6d7f991 100644 --- a/epde/control/constr.py +++ b/epde/control/constr.py @@ -9,28 +9,47 @@ from epde.supplementary import BasicDeriv class ConstrLocation(): + """ + Objects to contain the indices of the control training contraint location. + + Class Methods: + - __init__ + - get_boundary_indices + - apply + + Attributes: + - domain_shape: shape of the domain, for which the control problem is solved. + - axis: axis, along that the boundary conditions are selected. + - loc: position along axis, where "bounindices = self.get_boundary_indices(self.domain_indixes, axis, loc) + - device: string, matching the device, used for computation. + """ + def __init__(self, domain_shape: Tuple[int], axis: int = None, loc: int = None, indices: List[np.ndarray] = None, device: str = 'cpu'): - ''' - Objects to contain the indices of the control training contraint location. - - Args: - domain_shape (`Tuple[int]`): shape of the domain, for which the control problem is solved. - - axis (`int`): axis, along that the boundary conditions are selected. Shall be introduced - only for constraints on the boundary. - Optional, the default value (`None`) matches the entire domain. - - - loc (`int`): position along axis, where "bounindices = self.get_boundary_indices(self.domain_indixes, axis, loc) - self.flat_idxdary" is located. Shall be introduced only for constraints on the boundary. - Optional, the default value (`None`) matches the entire domain. For example, -1 will correspond - to the end of the domain along axis. - - device (`str`): string, matching the device, used for computation. Uses default torch designations. - Optional, defaults to `cpu` for CPU computations. + """ + Object to store indices representing a specific location within a domain where constraints are applied. - ''' + This object facilitates the application of constraints at specific points or boundaries within the domain. + It computes and stores the flat indices corresponding to the specified location, enabling efficient access + and manipulation of these locations during the training process. This is needed to apply constraints only + on a part of the domain. + + Args: + domain_shape (`Tuple[int]`): Shape of the domain for which the control problem is solved. + + axis (`int`, optional): Axis along which the boundary conditions are selected. Required for boundary constraints. + Defaults to `None`, which matches the entire domain. + + loc (`int`, optional): Position along the axis where the boundary is located. Required for boundary constraints. + Defaults to `None`, which matches the entire domain. For example, -1 corresponds to the end of the domain along the axis. + + indices (`List[np.ndarray]`, optional): Explicit list of indices to use for the location. Defaults to `None`. If provided, axis and loc are ignored + + device (`str`, optional): Device used for computation (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + + Returns: + None + """ self._device = device self._initial_shape = domain_shape @@ -48,38 +67,53 @@ def __init__(self, domain_shape: Tuple[int], axis: int = None, loc: int = None, @staticmethod def get_boundary_indices(domain_indices: np.ndarray, axis: int, loc: Union[int, Tuple[int]]) -> np.array: - ''' - Method of obtaining domain indices for specified position, i.e. all 0-th elements along an axis, or the last - elements along a specific axis. + """ + Method of extracting indices corresponding to specific boundaries of a domain. This is useful for defining constraints or boundary conditions when solving differential equations. - Args: - domain_indices (`np.ndarray`): an array representing the indices of a grid. The subarrays contain index - values 0, 1, … varying only along the corresponding axis. For furher details inspect `np.indices(...)` - function. - - axis (`int`): index of the axis, along which the elements are taken, - - loc (`int` or `tuple` of `int`): positions along the specified axis, which are taken. Can be tuple to - accomodate for multiple elements along axis. - - Returns: - `np.ndarray` of indicies, where the conditions are estimated. - ''' + Args: + domain_indices (`np.ndarray`): An array representing the indices of a grid. The subarrays contain index + values 0, 1, … varying only along the corresponding axis. For further details inspect `np.indices(...)` + function. + + axis (`int`): Index of the axis along which the elements are taken. + + loc (`int` or `tuple` of `int`): Positions along the specified axis, which are taken. Can be a tuple to + accommodate for multiple elements along the axis. + + Returns: + `np.ndarray`: A NumPy array of indices, representing the specified boundary locations. + + Why: + This method is used to identify the indices that define the boundaries of the domain. These indices are crucial for applying boundary conditions when solving differential equations, ensuring that the solution adheres to the physical constraints of the problem. + """ return np.stack([np.take(domain_indices[idx], indices = loc, axis = axis).reshape(-1) for idx in np.arange(domain_indices.shape[0])]) def apply(self, tensor: torch.Tensor, flattened: bool = True, along_axis: int = None): - ''' - Get `tensor` values at the locations, specified by the object indexing. The resulting tensor will be flattened. - - Args: - tensor (`torch.Tensor`): the filtered tensor. - - flattened (`bool`): marker, of will the tensor be flattened. Optional, default `True`, and - `False` is not yet implemented. - - along_axis (`int`): axis, for which the filtering is taken. - ''' + """ + Applies the constraint location to extract specific values from a tensor. + + This method leverages pre-computed indices to efficiently gather elements from the input tensor + along a specified axis. The extracted elements are then returned as a new tensor. + This functionality is crucial for applying constraints defined by the evolutionary process + to candidate solutions represented as tensors. + + Args: + tensor (`torch.Tensor`): The input tensor from which values will be extracted. + + flattened (`bool`): A flag indicating whether the resulting tensor should be flattened. + Defaults to `True`. Currently, only flattened output is supported. + + along_axis (`int`): The axis along which the constraint location should be applied. + Specifies the dimension from which elements are selected based on the pre-computed indices. + + Returns: + `torch.Tensor`: A new tensor containing the extracted values from the input tensor, + gathered according to the constraint location and flattened if `flattened` is `True`. + + Raises: + NotImplementedError: If `flattened` is set to `False`, as only flattened output is currently supported. + """ if flattened: shape = [1,] * tensor.ndim shape[along_axis] = -1 @@ -94,8 +128,34 @@ class ControlConstraint(ABC): ''' Abstract class for constraints declaration in the control optimization problems. ''' + def __init__(self, val : Union[float, torch.Tensor], deriv_method: BasicDeriv, indices: ConstrLocation, device: str = 'cpu', deriv_axes: List = [None,], nn_output: int = 0, **kwargs): + """ + Initializes a `ControlConstraint` object, defining a constraint on a specific location within the computational domain. + + This constraint is used to enforce desired behaviors or conditions at particular points or regions during the equation discovery process. + + Args: + val (Union[float, torch.Tensor]): The target value for the constraint. This could be a fixed value or a tensor. + deriv_method (BasicDeriv): The method used to calculate derivatives, influencing how the constraint is enforced. + indices (ConstrLocation): Specifies the location (index) within the domain where the constraint applies. + device (str, optional): The device ('cpu' or 'cuda') to use for computations. Defaults to 'cpu'. + deriv_axes (List, optional): The axes along which derivatives are computed for the constraint. Defaults to `[None]`. + nn_output (int, optional): The index of the neural network output to which this constraint applies. Defaults to 0. + **kwargs: Additional keyword arguments. + + Returns: + None + + Class Fields: + _val (Union[float, torch.Tensor]): The target value of the constraint. + _indices (ConstrLocation): The location (index) of the constraint. + _axes (List): The axes along which derivatives are computed. + _nn_output (int): The index of the neural network output. + _deriv_method (BasicDeriv): The derivative calculation method. + _device (str): The device used for computations. + """ self._val = val self._indices = indices self._axes = deriv_axes @@ -106,19 +166,80 @@ def __init__(self, val : Union[float, torch.Tensor], deriv_method: BasicDeriv, i @abstractmethod def __call__(self, fun_nn: Union[torch.nn.Sequential, torch.Tensor], arg_tensor: torch.Tensor) -> Tuple[bool, torch.Tensor]: + """ + Evaluates the constraint discrepancy. This method serves as an abstract interface for evaluating how well a candidate equation (represented by `fun_nn`) satisfies a given constraint at a specific point (`arg_tensor`). Since constraints are problem-specific, concrete constraint classes must implement this method to define the evaluation logic. + + Args: + fun_nn: The neural network or tensor representing the function. This is the candidate equation being evaluated. + arg_tensor: The argument tensor. This is the point at which the constraint is being evaluated. + + Returns: + A tuple containing a boolean and a tensor. The boolean indicates whether the constraint is satisfied, and the tensor represents the discrepancy value. The discrepancy value quantifies the degree to which the constraint is violated. + + Raises: + NotImplementedError: Always raised, as this is an abstract method. + """ raise NotImplementedError('Trying to call abstract constraint discrepancy evaluation.') @abstractmethod def loss(self, fun_nn: torch.nn.Sequential, arg_tensor: torch.Tensor) -> torch.Tensor: + """ + Evaluates the constraint violation for a given neural network and input. + + This abstract method is intended to be overridden by concrete constraint + implementations to define how the constraint discrepancy is calculated. + It serves as a placeholder and raises a NotImplementedError if called directly. + This ensures that all specific constraint types provide their own evaluation logic + during the equation discovery process. + + Args: + fun_nn: The neural network function representing a candidate solution. + arg_tensor: The input tensor at which to evaluate the constraint. + + Returns: + torch.Tensor: This method always raises an error, so no meaningful + return value exists. + """ raise NotImplementedError('Trying to call abstract constraint discrepancy evaluation.') class ControlConstrEq(ControlConstraint): ''' Class for equality constrints of type $c(u^(n)) = f(u) - val = 0$ . ''' + def __init__(self, val : Union[float, torch.Tensor], deriv_method: BasicDeriv, # grid: torch.Tensor, indices: ConstrLocation, device: str = 'cpu', deriv_axes: List = [None,], nn_output: int = 0, tolerance: float = 1e-7, estim_func: Callable = None): + """ + Initializes the constraint equation condition. + + This method sets up a condition that enforces a constraint on the discovered differential equation. + It configures the value the equation should satisfy, the method for calculating derivatives, + the locations where the constraint applies, and other parameters necessary for evaluating the constraint + during the equation discovery process. + + Args: + val: The target value for the constraint equation. This could be a constant or a tensor. + deriv_method: The method used to compute derivatives within the constraint. + indices: Specifies where the constraint is applied (e.g., specific points or regions). + device: The device ('cpu' or 'cuda') used for computations. Defaults to 'cpu'. + deriv_axes: The axes along which derivatives are calculated. Defaults to [None]. + nn_output: The index of the neural network output used in the constraint. Defaults to 0. + tolerance: Numerical tolerance for comparisons. Defaults to 1e-7. + estim_func: An optional function to estimate values within the constraint. Defaults to None. + + Returns: + None + + Class Fields: + _eps (float): The tolerance for numerical computations. + _estim_func (Callable): An optional estimation function. + + Why: + This initialization is crucial for defining the constraints that guide the equation discovery process. + By specifying the desired value, differentiation method, and location, we ensure that the discovered + equations adhere to known physical laws or observed behaviors. + """ print(f'Initializing condition with {deriv_method} method of differentiation.') super().__init__(val, deriv_method, indices, device, deriv_axes, nn_output) # grid, self._eps = tolerance @@ -128,15 +249,40 @@ def __init__(self, val : Union[float, torch.Tensor], deriv_method: BasicDeriv, # @singledispatchmethod def __call__(self, function, arg_tensor) -> Tuple[bool, torch.Tensor]: - ''' - Calculate the fullfilment of the equality constraint condition and the discrepancy between - the observed value of constraint and the desired value. - ''' + """ + Calculates the fulfillment of an equality constraint condition. It also quantifies the discrepancy between the observed constraint value and the desired value. This helps in evaluating candidate equation structures during the equation discovery process. + + Args: + function: The function representing the equality constraint. + arg_tensor: The input tensor to the function. + + Returns: + A tuple containing: + - A boolean indicating whether the constraint is satisfied. + - A tensor representing the discrepancy between the observed and desired values. + + Raises: + NotImplementedError: If the input `function` is not of a supported type (np.ndarray or torch.nn.Sequential). + """ raise NotImplementedError(f'Incorrect type of arguments passed into the call method. \ Got {type(function)} instead of np.ndarrays of torch.nn.Sequentials.') @__call__.register def _(self, function: np.ndarray, arg_tensor): + """ + Enforces a constraint by comparing the derivative of a function to a target value, ensuring the discovered equation adheres to specific conditions. + + This method calculates the derivative of a given function (representing a term in the equation) with respect to specified variables, and then compares the result to a predefined value. This comparison determines if the candidate equation structure satisfies the constraint within a given tolerance. This is crucial for guiding the search towards valid and physically meaningful equation forms. + + Args: + function: The function to differentiate (NumPy array), representing a term in the candidate equation. + arg_tensor: The tensor containing the arguments to the function, corresponding to the variables in the equation. + + Returns: + tuple: A tuple containing two elements: + - A boolean tensor indicating whether the constraint is satisfied (torch.Tensor). + - A tensor representing the difference between the calculated derivative and the target value (torch.Tensor). This difference quantifies the constraint violation. + """ if isinstance(self._deriv_method, AutogradDeriv): raise RuntimeError('Trying to call autograd differentiation of numpy npdarray. Use FDDeriv instead.') @@ -165,6 +311,24 @@ def _(self, function: np.ndarray, arg_tensor): @__call__.register def _(self, function: torch.nn.Sequential, arg_tensor): + """ + Evaluates the constraint represented by a differential operator applied to a function. + + This method computes the constraint violation by evaluating the derivative of a given function (e.g., a neural network) with respect to specified axes and indices. + It then compares the result to a target value, effectively checking if the constraint, derived from the governing differential equation, is satisfied. + The method returns a tuple indicating whether the constraint is satisfied within a given tolerance and the difference between the target value and the evaluated derivative. + This difference quantifies the constraint violation, guiding the optimization process towards solutions that better satisfy the underlying differential equation. + + Args: + function: The function to evaluate (e.g., a neural network). + arg_tensor: The input tensor to the function. + + Returns: + tuple: A tuple containing: + - A boolean tensor indicating whether the constraint is satisfied at each point. + - A tensor representing the difference between the target value and the function's output, + quantifying the constraint violation. + """ if isinstance(self._deriv_method, FDDeriv): raise RuntimeError('Trying to call finite differences to get derivatives of ANN, while ANN eval is not supported.\ Use Autograd instead.') @@ -191,17 +355,16 @@ def _(self, function: torch.nn.Sequential, arg_tensor): constr_enf) # val_transformed - to_compare def loss(self, function: Union[torch.nn.Sequential, np.ndarray], arg_tensor: torch.Tensor) -> torch.Tensor: - ''' - Return value of the loss function term, created by the condition. - - Args: - fun_nn (`torch.nn.Sequential`): artificial neural network, approximating the function used in the condition. - - arg_tensor (`torch.Tensor`): tensor, used as the argument of the network, passed as `fun_nn`. - - Returns: - `torch.Tensor` with norm of the contraint discrepancy to be used in the combined loss. - ''' + """ + Return the magnitude of the constraint violation, which quantifies the discrepancy between the predicted and actual behavior based on the learned representation. This value serves as a penalty term in the overall loss function, guiding the learning process to satisfy the imposed constraints. + + Args: + function (`torch.nn.Sequential` or `np.ndarray`): The function, represented either as a neural network or a numerical array, that approximates a part of the constraint equation. + arg_tensor (`torch.Tensor`): The input tensor to be evaluated by the function. + + Returns: + `torch.Tensor`: The norm of the constraint discrepancy, representing the magnitude of the violation, to be incorporated into the combined loss. + """ _, discrepancy = self(function, arg_tensor) return torch.norm(discrepancy) @@ -210,9 +373,38 @@ class ControlConstrNEq(ControlConstraint): ''' Class for constrints of type $c(u, x) = f(u, x) - val `self._sign` 0$ ''' + def __init__(self, val : Union[float, torch.Tensor], deriv_method: BasicDeriv, # grid: torch.Tensor, indices: ConstrLocation, device: str = 'cpu', sign: str = '>', deriv_axes: List = [None,], nn_output: int = 0, tolerance: float = 1e-7, estim_func: Callable = None): + """ + Initializes the constraint object. + + This method sets up the constraint by storing essential parameters required for its evaluation. + These parameters define the constraint's value, the method for calculating derivatives, + the location of the constraint, the device for computation, the sign of the constraint, + the axes along which derivatives are computed, the relevant neural network output, + the tolerance for constraint satisfaction, and an optional estimation function. + It is a crucial step in defining the constraints that guide the equation discovery process. + + Args: + val: The value associated with the condition. + deriv_method: The method used for calculating derivatives. + indices: The indices related to the condition's location. + device: The device to use for computations (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + sign: The sign of the condition (e.g., '>', '<', '='). Defaults to '>'. + deriv_axes: The axes along which to calculate derivatives. Defaults to [None]. + nn_output: The index of the neural network output to use. Defaults to 0. + tolerance: The tolerance level for satisfying the condition. Defaults to 1e-7. + estim_func: An optional estimation function. Defaults to None. + + Returns: + None + + Class Fields: + _sign: The sign of the condition (e.g., '>', '<', '='). + _estim_func: An optional estimation function. + """ print(f'Initializing condition with {deriv_method} method of differentiation.') super().__init__(val, deriv_method, indices, device, deriv_axes, nn_output) # grid, self._sign = sign @@ -220,15 +412,45 @@ def __init__(self, val : Union[float, torch.Tensor], deriv_method: BasicDeriv, # @singledispatchmethod def __call__(self, function, arg_tensor) -> Tuple[bool, torch.Tensor]: - ''' - Calculate the fullfilment of the inequality constraint condition and the discrepancy between - the observed value of constraint and the desired value. - ''' + """ + Calculates the fulfillment of an inequality constraint condition and the discrepancy between the observed and desired constraint values. This method serves as a base for specialized implementations handling different constraint representations within the equation discovery process. + + Args: + function: The constraint function. + arg_tensor: The tensor of arguments to the constraint function. + + Returns: + A tuple containing: + - A boolean indicating whether the inequality constraint is satisfied. + - A torch.Tensor representing the discrepancy between the observed and desired constraint values. + """ raise NotImplementedError(f'Incorrect type of arguments passed into the call method. \ Got {type(function)} instead of np.ndarrays of torch.nn.Sequentials.') @__call__.register def _(self, function: np.ndarray, arg_tensor) -> Tuple[bool, torch.Tensor]: + """ + Evaluates the constraint based on the provided function and arguments. + + This method calculates constraint enforcement by comparing the derivative + of a given function with a predefined value. It leverages a specified + derivative method to compute the derivative, then compares it against + a stored value, considering the constraint's sign ('>' or '<'). This + comparison determines whether the constraint is satisfied. + + Args: + function (np.ndarray): The function to evaluate the derivative of. + arg_tensor (torch.Tensor): The argument tensor to apply the function to. + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing: + - A boolean tensor indicating whether the constraint is satisfied. + - A ReLU-activated tensor representing the constraint enforcement. + + WHY: This method is crucial for verifying whether the discovered equation + satisfies the imposed constraints, ensuring the identified model aligns + with known physical laws or domain-specific requirements. + """ if isinstance(self._deriv_method, AutogradDeriv): raise RuntimeError('Trying to call autograd differentiation of numpy npdarray. Use FDDeriv instead.') @@ -258,6 +480,25 @@ def _(self, function: np.ndarray, arg_tensor) -> Tuple[bool, torch.Tensor]: @__call__.register def _(self, function: torch.nn.Sequential, arg_tensor) -> Tuple[bool, torch.Tensor]: + """ + Evaluates a constraint by comparing a derivative with a target value. + + This method computes the derivative of a given function with respect to + an input tensor, and then compares it against a predefined value. + The comparison result, indicating constraint satisfaction, and a measure + of constraint enforcement are returned. This process helps to identify + equation structures that accurately represent the underlying dynamics + of the system. + + Args: + function: The function to take the derivative of. + arg_tensor: The input tensor with respect to which the derivative is taken. + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing: + - A boolean tensor indicating whether the constraint is satisfied. + - A tensor representing the constraint enforcement (ReLU of the difference). + """ to_compare = self._deriv_method.take_derivative(u = function, args=self._indices.apply(arg_tensor, along_axis = 0), # correct along_axis argument axes=self._axes, component = self._nn_output) @@ -285,17 +526,21 @@ def _(self, function: torch.nn.Sequential, arg_tensor) -> Tuple[bool, torch.Tens #torch.less(val_transformed, to_compare), torch.nn.functional.relu(to_compare - val_transformed) def loss(self, function: Union[torch.nn.Sequential, np.ndarray], arg_tensor: torch.Tensor) -> torch.Tensor: - ''' - Return value of the loss function term, created by the condition. - - Args: - fun_nn (`torch.nn.Sequential`): artificial neural network, approximating the function used in the condition. - - arg_tensor (`torch.Tensor`): tensor, used as the argument of the network, passed as `fun_nn`. - - Returns: - `torch.Tensor` with norm of the contraint discrepancy to be used in the combined loss. - ''' + """ + Computes the norm of the constraint discrepancy for a given function and argument tensor. + + This function quantifies how well a candidate equation satisfies the imposed constraints, + contributing to the overall loss function that guides the equation discovery process. + A lower loss indicates a better fit to the constraints. + + Args: + function (`torch.nn.Sequential` or `np.ndarray`): The function (approximated by a neural network or represented directly) + that is evaluated within the constraint. + arg_tensor (`torch.Tensor`): The tensor representing the input values at which the constraint is evaluated. + + Returns: + `torch.Tensor`: The norm of the constraint discrepancy, representing the magnitude of the violation of the constraint. + """ _, discrepancy = self(function, arg_tensor) return torch.norm(discrepancy) @@ -303,16 +548,27 @@ def loss(self, function: Union[torch.nn.Sequential, np.ndarray], arg_tensor: tor class ConditionalLoss(): ''' Class for the loss, used in the control function opimizaton procedure. Conrains terms of the loss - function in `self._cond` attribute. + function in `self._cond` attribute. ''' - def __init__(self, conditions: List[Tuple[Union[float, ControlConstraint, int]]]): - ''' - Initialize the conditional loss with the terms, partaking in evaluating inflicted control and - quality of the equation solution with the current control. - Args: - conditions (`list` of triplet `tuple` as (`float`, `ControlConstraint`, `int`)) - ''' + def __init__(self, conditions: List[Tuple[Union[float, ControlConstraint, int]]]): + """ + Initialize the conditional loss with the terms for evaluating control strategies. + + This loss function incorporates conditions based on control constraints and solution quality, + allowing the evolutionary algorithm to prioritize controls that satisfy specific criteria + while optimizing the equation's fit to the data. + + Args: + conditions (`list` of triplet `tuple` as (`float`, `ControlConstraint`, `int`)): + A list of conditions, where each condition is a tuple containing: + - A floating-point value representing a threshold or target. + - A `ControlConstraint` object defining a constraint on the control. + - An integer indicating the type of condition (e.g., penalty for constraint violation). + + Returns: + None + """ self._cond = conditions def __call__(self, models: List[torch.nn.Sequential], args: list): # Introduce prepare control input: get torch tensors from solver & autodiff them @@ -321,6 +577,18 @@ def __call__(self, models: List[torch.nn.Sequential], args: list): # Introduce p ''' temp = [] for cond in self._cond: + """ + Calculates the total conditional loss based on provided models and arguments. + + This method iterates through the defined conditional loss components, computes the loss for each component using the specified model and arguments, and then returns the sum of these losses. This aggregated loss represents the overall discrepancy between the model predictions and the observed data, guiding the optimization process towards identifying the differential equation that best describes the system. + + Args: + models (List[torch.nn.Sequential]): A list of PyTorch models used in the loss calculation. Each model corresponds to a specific term in the conditional loss. + args (list): A list of arguments, where each element corresponds to the input required by the respective model. + + Returns: + torch.Tensor: The summed value of all conditional loss components, representing the total loss. + """ temp.append(cond[0] * cond[1].loss(models[cond[2]], args[cond[2]])) return torch.stack(temp, dim=0).sum(dim=0).sum(dim=0) diff --git a/epde/control/control.py b/epde/control/control.py index 08fb2f89..26ada10e 100644 --- a/epde/control/control.py +++ b/epde/control/control.py @@ -24,18 +24,90 @@ from epde.supplementary import FDDeriv, AutogradDeriv class ControlExp(): + ''' + Represents a control experiment. + + This class provides a framework for conducting control experiments, + allowing for different control strategies and optimization methods. + + Class Methods: + - __init__: Initializes the ConditionalControlNetTrainer. + - create_best_equations: Creates the best equations based on the provided optimal equations. + - create_ode_bop: Creates a BOPElement for ODE integration. + - set_solver_params: None + - get_solver_adapter: Returns the appropriate solver adapter based on the configuration. + - finite_diff_calculation: Calculate finite-differecnce approximation of gradient in respect to the specified parameter. + - feedback: Performs feedback control to optimize a system. + - time_based: Performs a time-based simulation or optimization. + ''' + def __init__(self, loss : ConditionalLoss, device: str = 'cpu'): + """ + Initializes the ConditionalControlNetTrainer. + + This class prepares the training environment for discovering differential equations. It sets up the necessary components, + including the loss function and the device for computation, to optimize the control network. + + Args: + loss: The conditional loss function to be minimized during the equation discovery process. + device: The device to run the training on (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + + Fields: + _device: The device used for training. + _state_net: The state network (initialized to None). + _best_control_net: The best control network found during training (initialized to None). + loss: The conditional loss function. + + Returns: + None. + + Why: This initialization is a crucial step in setting up the training process for the control network, which is used to identify the underlying differential equations from the given data by minimizing the specified loss function. + """ self._device = device self._state_net = None self._best_control_net = None self.loss = loss def create_best_equations(self, optimal_equations: Union[list, ParetoLevels]): + """ + Creates the best equation by combining optimal equations using an evolutionary algorithm. + + This method refines a set of optimal equations by combining them and + evaluating their performance, ultimately aiming to identify the equation + that best balances accuracy and complexity. It uses parallel processing + to efficiently explore the search space of possible combinations. + + Args: + optimal_equations: A list or ParetoLevels object containing the optimal + equations to combine. + + Returns: + The result of calling `create_best` method on the + `ExperimentCombiner` instance, after initialization with provided equations and passing the `pool` to it. + The `create_best` method selects the highest complexity values and creates the best equation variant. + """ res_combiner = ExperimentCombiner(optimal_equations) return res_combiner.create_best(self._pool) @staticmethod def create_ode_bop(key, var, term, grid_loc, value, device: str = 'cpu'): + """ + Creates a BOPElement, sets its grid location, and assigns a value to it, preparing it for integration within the equation discovery process. + + This method constructs a BOPElement, sets its grid location, and assigns a value to it. + The BOPElement is configured for use in ODE integration. This is a crucial step in representing the equation terms at specific grid points, enabling the framework to evaluate the equation's fitness against the observed data. + + Args: + key: The key associated with the BOPElement. + var: The variable associated with the BOPElement. + term: The term associated with the BOPElement. + grid_loc: The grid location for the BOPElement. + value: The value to be assigned to the BOPElement. + device: The device to which the BOPElement's tensors will be moved (default: 'cpu'). + + Returns: + BOPElement: The created and configured BOPElement. + """ bop = BOPElement(axis = 0, key = key, term = term, power = 1, var = var, device = device) bop_grd_np = np.array([[grid_loc,]]) bop.set_grid(torch.from_numpy(bop_grd_np).type(torch.FloatTensor).to(device)) @@ -63,6 +135,20 @@ def set_solver_params(self, use_pinn: bool = True, mode: str = 'autograd', compi } def get_solver_adapter(self, net: torch.nn.Sequential = None): + """ + Returns the appropriate solver adapter based on the configuration. + + This method determines which solver adapter to use for simulating the system's dynamics. If `use_pinn` is True, a `SolverAdapter` is returned, configured with parameters from `solver_params`. Otherwise, an `OdeintAdapter` is returned, using the specified ODE solving method. The choice depends on whether a neural network-based solver is employed or a traditional ODE solver is preferred. + + Args: + net: The neural network to be used by the SolverAdapter. + + Returns: + The solver adapter, either a SolverAdapter or an OdeintAdapter. + + Why: + This method allows the framework to switch between different numerical integration approaches based on user configuration. The `SolverAdapter` leverages neural networks, while the `OdeintAdapter` relies on traditional ODE solvers. This flexibility enables the framework to handle a wider range of problems and leverage different computational resources. + """ if self._use_pinn: adapter = SolverAdapter(net = net, use_cache = False, device = self._device) # Edit solver forms of functions of dependent variable to Callable objects. @@ -85,10 +171,27 @@ def get_solver_adapter(self, net: torch.nn.Sequential = None): @staticmethod def finite_diff_calculation(system, adapter, loc, control_loss, state_net: torch.nn.Sequential, bc_operators, grids: list, solver_params: dict, eps: float): - ''' - Calculate finite-differecnce approximation of gradient in respect to the specified parameter. - - ''' + """ + Calculates the finite difference approximation of the gradient with respect to a specified parameter of the control neural network. + + This method perturbs a parameter of the control neural network by a small amount `eps` and calculates the corresponding change in the loss function. + This approximation is used to estimate the sensitivity of the loss function to changes in the control parameters, + which is essential for optimizing the control strategy. + + Args: + system: The EPDE system to be solved. + adapter: The solver adapter used to solve the EPDE system. + loc (List[Union[str, Tuple[int]]]): A list specifying the location of the parameter to be modified within the control NN's state dictionary. + control_loss: The loss function used to evaluate the performance of the control strategy. + state_net (torch.nn.Sequential): The state neural network. + bc_operators: The boundary condition operators. + grids (list): The grids used for solving the EPDE system and evaluating the control loss. + solver_params (dict): The parameters for the EPDE solver. + eps (float): The perturbation size used for finite difference approximation. + + Returns: + torch.Tensor: The finite difference approximation of the gradient. + """ # Calculating loss in p[i]+eps: ctrl_dict_prev = global_var.control_nn.net.state_dict() ctrl_nn_dict = eps_increment_diff(input_params=ctrl_dict_prev, @@ -160,6 +263,31 @@ def feedback(self, bc_operators: List[Union[dict, float]], grids: List[Union[np. opt_params: List[float] = [0.01, 0.9, 0.999, 1e-8], control_net: torch.nn.Sequential = None, fig_folder: str = None, LV_exp: bool = True, eps: float = 1e-2, solver_params: dict = {}): + """ + Performs feedback control to optimize a system by iteratively solving a differential equation and refining a control strategy. + + This method alternates between solving the full system and an abridged version to efficiently adjust control parameters based on finite differences. It aims to find the optimal control strategy that minimizes a defined loss function, effectively steering the system towards a desired behavior. The iterative refinement leverages both the full system dynamics and a simplified representation to balance accuracy and computational cost. + + Args: + bc_operators: List of boundary condition operators, each a dict or float. These define the constraints on the solution at the boundaries of the domain. + grids: List of grids (np.ndarray or torch.Tensor) on which to solve the system. These represent the spatial or temporal discretization of the problem domain. + n_control: Number of control variables. Defaults to 1. This specifies the dimensionality of the control input. + epochs: Number of training epochs. Defaults to 1e2. This determines the number of iterations for the optimization process. + state_net: Initial state network (torch.nn.Sequential). Defaults to None. This provides an initial guess for the solution of the differential equation. + opt_params: Optimization parameters (learning rate, beta1, beta2, epsilon). Defaults to [0.01, 0.9, 0.999, 1e-8]. These control the behavior of the optimizer used to update the control parameters. + control_net: Initial control network (torch.nn.Sequential). Defaults to None. This represents the initial control strategy. + fig_folder: Folder to save figures. Defaults to None. If provided, visualizations of the training process will be saved here. + LV_exp: Flag to indicate if it is a Lotka-Volterra experiment. Defaults to True. This is a specific flag for a particular type of experiment. + eps: Epsilon value for finite difference calculations. Defaults to 1e-2. This determines the step size used to approximate derivatives. + solver_params: Dictionary of solver parameters for full and abridged solves. Defaults to {}. These parameters configure the numerical solver used to solve the differential equation. + + Returns: + tuple: A tuple containing: + - The optimized state network (torch.nn.Sequential). This is the refined solution to the differential equation. + - The optimized control network (torch.nn.Sequential). This is the refined control strategy. + - The control predictions (torch.Tensor). These are the control actions predicted by the optimized control network. + - A list of loss values over training history (list). This provides insight into the convergence of the optimization process. + """ def modify_bc(operator: dict, scale: Union[float, torch.Tensor]) -> dict: noised_operator = deepcopy(operator) noised_operator['bnd_val'] = torch.normal(operator['bnd_val'], scale).to(self._device) @@ -343,6 +471,33 @@ def time_based(self, bc_operators: List[Union[dict, float]], grids: List[Union[n opt_params: List[float] = [0.01, 0.9, 0.999, 1e-8], control_net: torch.nn.Sequential = None, fig_folder: str = None, LV_exp: bool = True, eps: float = 1e-2, solver_params: dict = {}): + """ + Performs a time-based simulation or optimization. + + This method is intended to perform a time-based simulation or + optimization of a system, but the implementation is not yet + available. It sets solver parameters, gets a solver adapter, + and then raises a NotImplementedError. This method would be used to simulate the system's behavior over time, potentially optimizing control parameters to achieve desired outcomes based on the discovered equations. + + Args: + bc_operators: Boundary condition operators. + grids: Spatial grids for the simulation. + n_control: Number of control variables. Defaults to 1. + epochs: Number of training epochs. Defaults to 1e2. + state_net: Neural network for the state. Defaults to None. + opt_params: Optimization parameters. Defaults to [0.01, 0.9, 0.999, 1e-8]. + control_net: Neural network for the control. Defaults to None. + fig_folder: Folder to save figures. Defaults to None. + LV_exp: Flag for Lotka-Volterra experiment. Defaults to True. + eps: A small constant. Defaults to 1e-2. + solver_params: Parameters for the solver. Defaults to {}. + + Returns: + None. + + Raises: + NotImplementedError: This method is not yet implemented. + """ self.set_solver_params(**solver_params['full']) adapter = self.get_solver_adapter(None) solver_form = self.system diff --git a/epde/control/optim.py b/epde/control/optim.py index 17600bdc..7fa815b0 100644 --- a/epde/control/optim.py +++ b/epde/control/optim.py @@ -8,23 +8,124 @@ import torch class FirstOrderOptimizerNp(ABC): + """ + Base class for first-order optimization algorithms using NumPy. + + This class serves as a foundation for implementing first-order optimization + algorithms that rely on gradient information to update parameters. It + provides a common interface for subclasses to implement the optimization + logic using NumPy arrays. + + Attributes: + learning_rate: The learning rate for the optimizer. + params: The parameters to be optimized. + """ + behavior = 'None' def __init__(self, parameters: np.ndarray, optimized: np.ndarray): + """ + Initializes the abstract optimizer. + + This method serves as a blueprint for initializing specific optimization algorithms. + Subclasses must override this method to implement their own initialization logic. + It raises a NotImplementedError if called directly, ensuring that the abstract class is not instantiated. + + Args: + parameters: The parameters to be optimized, represented as a NumPy array. + optimized: The optimized values, represented as a NumPy array. + + Raises: + NotImplementedError: Always raised, as this is an abstract method. + + Returns: + None. + + Why: This abstract initializer ensures that concrete optimizer implementations define their own initialization logic, + tailored to the specific optimization algorithm and equation discovery process. + """ raise NotImplementedError('Calling __init__ of an abstract optimizer') def step(self, gradient: np.ndarray): + """ + Performs a single optimization step. + + This method is an abstract method and should be implemented by subclasses. + It updates the parameters of the model based on the calculated gradient. + This is a crucial part of the equation discovery process, as it refines the model's parameters to better match the observed data by minimizing the error between the model's predictions and the actual data. + + Args: + gradient: The gradient of the loss function with respect to the model's parameters. + + Returns: + None. + + Raises: + NotImplementedError: Always raised, as this is an abstract method. + """ raise NotImplementedError('Calling step of an abstract optimizer') class AdamOptimizerNp(FirstOrderOptimizerNp): + """ + Implements the Adam optimization algorithm using NumPy. + + The Adam optimizer is a stochastic gradient descent method + that computes adaptive learning rates for each parameter. + + Attributes: + _alpha: The learning rate. + _beta_1: The exponential decay rate for the first moment estimates. + _beta_2: The exponential decay rate for the second moment estimates. + _eps: A small constant for numerical stability. + """ + behavior = 'Gradient' def __init__(self, optimized: np.ndarray, parameters: np.ndarray = np.array([0.001, 0.9, 0.999, 1e-8])): - ''' - parameters[0] - alpha, parameters[1] - beta_1, parameters[2] - beta_2 - parameters[3] - eps - ''' + """ + Initializes the Adam optimizer with provided parameters. + + The Adam optimizer is initialized with the optimized variable and a set of hyperparameters + that control the optimization process. These parameters include the learning rate (alpha), + exponential decay rates for the first and second moment estimates (beta_1 and beta_2), + and a small constant for numerical stability (epsilon). + + Args: + optimized (np.ndarray): The variable to be optimized. + parameters (np.ndarray, optional): An array containing the optimization hyperparameters: + - alpha (float): The learning rate. + - beta_1 (float): Exponential decay rate for the first moment estimates. + - beta_2 (float): Exponential decay rate for the second moment estimates. + - eps (float): A small constant for numerical stability. + Defaults to np.array([0.001, 0.9, 0.999, 1e-8]). + + Returns: + None + """ self.reset(optimized, parameters) def reset(self, optimized: np.ndarray, parameters: np.ndarray): + """ + Resets the optimizer's internal state to begin a fresh optimization run. + + This method reinitializes the moment, second moment, and + second moment max accumulators to zero, updates the parameters, + and resets the time step to zero. This ensures that the optimization + process starts from a clean slate, preventing any influence from + previous optimization attempts. + + Args: + optimized (np.ndarray): The array to match the shape for accumulators. + parameters (np.ndarray): The new parameter values to be used by the optimizer. + + Returns: + None. + + Class Fields Initialized: + _moment (np.ndarray): First moment vector, initialized to zeros with the same shape as 'optimized'. + _second_moment (np.ndarray): Second moment vector, initialized to zeros with the same shape as 'optimized'. + _second_moment_max (np.ndarray): Maximum of second moment vector, initialized to zeros with the same shape as 'optimized'. + parameters (np.ndarray): The parameter values to be used by the optimizer. + time (int): The current time step, initialized to 0. + """ self._moment = np.zeros_like(optimized) self._second_moment = np.zeros_like(optimized) self._second_moment_max = np.zeros_like(optimized) @@ -32,6 +133,18 @@ def reset(self, optimized: np.ndarray, parameters: np.ndarray): self.time = 0 def step(self, gradient: np.ndarray, optimized: np.ndarray): + """ + Performs a single Adam optimization step to refine parameter values based on the calculated gradient. + + This method updates the optimized parameters, leveraging the Adam algorithm's momentum and adaptive learning rate features. This helps to efficiently navigate the search space of possible equation coefficients, ultimately aiming to discover a differential equation that accurately describes the observed data. + + Args: + gradient (np.ndarray): The gradient of the loss function with respect to the parameters. This indicates the direction of steepest ascent in the loss landscape. + optimized (np.ndarray): The current values of the parameters being optimized. These represent the coefficients within the candidate differential equation. + + Returns: + np.ndarray: The updated values of the optimized parameters after applying the Adam update. These updated values represent a refined set of coefficients for the candidate differential equation, potentially leading to a better fit with the observed data. + """ self.time += 1 self._moment = self.parameters[1] * self._moment + (1-self.parameters[1]) * gradient self._second_moment = self.parameters[2] * self._second_moment +\ @@ -41,27 +154,139 @@ def step(self, gradient: np.ndarray, optimized: np.ndarray): return optimized - self.parameters[0]*moment_cor/(np.sqrt(second_moment_cor)+self.parameters[3]) class FirstOrderOptimizer(ABC): + """ + Base class for first-order optimization algorithms. + + This class provides a foundation for implementing various first-order + optimization algorithms. It includes abstract methods for resetting and + performing optimization steps, which must be implemented by subclasses. + """ + behavior = 'Gradient' def __init__(self, optimized: List[torch.Tensor], parameters: list): + """ + Initializes the abstract optimizer. + + This method serves as a blueprint for concrete optimizer implementations. + It ensures that subclasses define their own initialization logic, tailored + to the specific optimization strategy they employ for equation discovery. + Since the base class doesn't implement a specific optimization, it raises + a NotImplementedError to enforce proper subclassing. + + Args: + optimized: The list of optimized tensors. These tensors represent the + variables or parameters within the discovered equations that are + being adjusted to improve the fit to the data. + parameters: The list of parameters. These parameters define the search + space and influence the evolutionary process of equation discovery. + + Raises: + NotImplementedError: Always raised, as this is an abstract method and + must be implemented by a concrete subclass to define the + optimization procedure. + + Returns: + None. + """ raise NotImplementedError('Calling __init__ of an abstract optimizer') def reset(self, optimized: Dict[str, torch.Tensor], parameters: np.ndarray): + """ + Resets the optimizer. + + This abstract method should be implemented by subclasses to reset the optimizer's internal state. + This is crucial for re-evaluating equation candidates with a clean slate, ensuring fair comparison and preventing bias from previous optimization steps. + It raises a NotImplementedError if called directly. + + Args: + optimized (Dict[str, torch.Tensor]): A dictionary containing optimized tensors. + parameters (np.ndarray): A NumPy array containing the parameters. + + Returns: + None. + + Raises: + NotImplementedError: Always raised when calling the base class method. + """ raise NotImplementedError('Calling reset method of an abstract optimizer') def step(self, gradient: Dict[str, torch.Tensor], optimized: Dict[str, torch.Tensor], *args, **kwargs) -> Dict[str, torch.Tensor]: + """ + Performs a single optimization step. + + This method is an abstract method and should be implemented by subclasses. + It updates the optimized parameters based on the given gradients. This is a crucial step in the equation discovery process, as it refines the candidate equations based on how well they fit the observed data. + + Args: + gradient: The gradients of the parameters to be optimized. + optimized: The current values of the parameters being optimized. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + The updated values of the optimized parameters. + + Raises: + NotImplementedError: If the method is called directly from the base class. + """ raise NotImplementedError('Calling step of an abstract optimizer') class AdamOptimizer(FirstOrderOptimizer): + """ + Implements the Adam optimization algorithm. + + The Adam optimizer is a stochastic gradient descent method + that computes adaptive learning rates for each parameter. + + Attributes: + _alpha: The learning rate. + _beta_1: The exponential decay rate for the first moment estimates. + _beta_2: The exponential decay rate for the second moment estimates. + _eps: A small constant for numerical stability. + """ + behavior = 'Gradient' def __init__(self, optimized: List[torch.Tensor], parameters: list = [0.001, 0.9, 0.999, 1e-8]): - ''' - parameters[0] - alpha, parameters[1] - beta_1, parameters[2] - beta_2 - parameters[3] - eps - ''' + """ + Initializes the Adam optimizer with specified hyperparameters. + + This optimizer adapts learning rates for each parameter, + enhancing the discovery of differential equations by fine-tuning + the optimization process based on equation complexity. + + Args: + optimized (List[torch.Tensor]): List of torch.Tensors to be optimized. + parameters (list, optional): List of hyperparameters for Adam optimizer. + parameters[0] - alpha (float): The learning rate. Defaults to 0.001. + parameters[1] - beta_1 (float): Exponential decay rate for the first moment estimates. Defaults to 0.9. + parameters[2] - beta_2 (float): Exponential decay rate for the second moment estimates. Defaults to 0.999. + parameters[3] - eps (float): Term added to the denominator to improve numerical stability. Defaults to 1e-8. + + Returns: + None + """ self.reset(optimized, parameters) def reset(self, optimized: Dict[str, torch.Tensor], parameters: np.ndarray): + """ + Resets the optimizer's internal state to begin a fresh optimization run. + + This method reinitializes the first and second moment estimates, effectively clearing the optimizer's memory of previous iterations. It also sets the initial parameters and resets the time step counter. This ensures that the optimization process starts from a clean slate, preventing any influence from prior optimization attempts. + + Args: + optimized (Dict[str, torch.Tensor]): A dictionary of optimized tensors, used to determine the shape and device of the moment estimates. + parameters (np.ndarray): The initial parameters to be optimized, replacing any previously held parameters. + + Returns: + None. + + Initializes: + _moment (list): A list of tensors representing the first moment estimates, initialized to zero tensors with the same shape and device as the optimized tensors. + _second_moment (list): A list of tensors representing the second moment estimates, initialized to zero tensors with the same shape and device as the optimized tensors. + parameters (np.ndarray): The parameters to be optimized, assigned from the input. + time (int): The current time step, initialized to 0, indicating the start of a new optimization sequence. + """ self._moment = [torch.zeros_like(param_subtensor) for param_subtensor in optimized.values()] self._second_moment = [torch.zeros_like(param_subtensor) for param_subtensor in optimized.values()] self.parameters = parameters @@ -69,6 +294,24 @@ def reset(self, optimized: Dict[str, torch.Tensor], parameters: np.ndarray): def step(self, gradient: Dict[str, torch.Tensor], optimized: Dict[str, torch.Tensor], *args, **kwargs) -> Dict[str, torch.Tensor]: + """ + Performs a single optimization step using the Adam algorithm. + + This method refines the equation parameters based on the calculated gradients + and the Adam optimization parameters. It maintains and updates the first + and second moments of the gradients, applies bias correction, and then + updates the equation parameters. This step is crucial for iteratively improving + the fit of the discovered equations to the observed data by adjusting the + equation's coefficients and structure. + + Args: + gradient: A dictionary containing the gradients for each parameter. + optimized: A dictionary containing the parameters to be optimized. + + Returns: + A dictionary containing the updated optimized parameters after the + optimization step. + """ self.time += 1 self._moment = [self.parameters[1] * self._moment[tensor_idx] + (1-self.parameters[1]) * grad_subtensor @@ -85,20 +328,76 @@ def step(self, gradient: Dict[str, torch.Tensor], optimized: Dict[str, torch.Ten for tensor_idx, subtensor_key in enumerate(optimized.keys())]) class CoordDescentOptimizer(FirstOrderOptimizer): + """ + The CoordDescentOptimizer class implements a coordinate descent optimization algorithm. + + It iteratively updates parameters by optimizing one parameter at a time, + keeping others fixed. + + Attributes: + loc (int): The location of the parameter to update. + """ + behavior = 'Coordinate' def __init__(self, optimized: List[torch.Tensor], parameters: list = [0.001,]): - ''' - parameters[0] - alpha, parameters[1] - beta_1, parameters[2] - beta_2 - parameters[3] - eps - ''' + """ + Initializes the CoordDescentOptimizer with optimization parameters. + + The optimizer uses a coordinate descent approach, adjusting individual parameters + to minimize the loss function. This initialization sets up the optimizer + with the parameters that control the step size and other optimization dynamics. + + Args: + optimized (List[torch.Tensor]): A list of PyTorch tensors to be optimized. + parameters (list, optional): A list of optimization parameters. Defaults to [0.001]. + - parameters[0] (float): Alpha, the step size or learning rate. + - parameters[1] (float): Beta_1, parameter for momentum (if applicable). + - parameters[2] (float): Beta_2, parameter for adaptive learning rates (if applicable). + - parameters[3] (float): Epsilon, a small value to prevent division by zero (if applicable). + + Returns: + None + """ self.reset(optimized, parameters) def reset(self, optimized: Dict[str, torch.Tensor], parameters: np.ndarray): + """ + Resets the optimizer's state with new parameters and sets the time to zero. This is done to prepare the optimizer for a new phase of the evolutionary search process, ensuring a clean slate for evaluating candidate solutions. + + Args: + optimized (Dict[str, torch.Tensor]): A dictionary of optimized tensors (unused). + parameters (np.ndarray): The new parameter values to be used by the optimizer. + + Returns: + None. + + Initializes: + parameters (np.ndarray): The parameter values of the object. + time (int): The current time step, initialized to 0. + """ self.parameters = parameters self.time = 0 def step(self, gradient: Dict[str, torch.Tensor], optimized: Dict[str, torch.Tensor], *args, **kwargs) -> Dict[str, torch.Tensor]: + """ + Performs a coordinate descent step to refine a single parameter. + + This method updates a specific parameter by moving it along the + negative gradient direction. This iterative refinement of individual + parameters contributes to the overall optimization process, + ultimately aiming to discover the underlying differential equation. + + Args: + gradient: A dictionary containing the gradients for each parameter. + optimized: A dictionary containing the current optimized parameter values. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. Must contain 'loc' key + specifying the location of the parameter to update. + + Returns: + A dictionary containing the updated optimized parameter values. + """ self.time += 1 assert 'loc' in kwargs.keys(), 'Missing location of parameter value shift in coordinate descent.' loc = kwargs['loc'] diff --git a/epde/control/utils.py b/epde/control/utils.py index a03170ef..cc6b2232 100644 --- a/epde/control/utils.py +++ b/epde/control/utils.py @@ -8,21 +8,33 @@ def prepare_control_inputs(model: Union[torch.nn.Sequential, List[np.ndarray]], grid: torch.Tensor, args: List[Tuple[Union[int, List]]], diff_method: BasicDeriv = None) -> torch.Tensor: - ''' - Recompute the control ANN arguments tensor from the solutions of - controlled equations $L \mathbf{u}(t, \mathbf{x}, \mathbf{c}) = 0$, - calculating necessary derivatives, as `args` - - Args: - model (`torch.nn.Sequential`): solution of the controlled equation $\mathbf{u}(\mathbf{u})$. - - grid (`torch.Tensor`): tensor of the grids m x n, where m - number of points in the domain, n - number of NN inputs. - - args (`List[Tuple[Union[int, List]]]`) - list of arguments of derivative operators. - - Returns: - `torch.Tensor`: tensor of arguments for the control ANN. - ''' + """ + Recompute the arguments for the control ANN by evaluating derivatives of the solution with respect to the input grid. + + This function calculates the necessary derivatives of the solution `model` on the given `grid` + according to the specifications in `args`. These derivatives then form the input to the control ANN. + This process allows the control ANN to learn the relationship between the solution and its derivatives, + enabling the discovery of the underlying differential equation. + + Args: + model (`torch.nn.Sequential` or `List[np.ndarray]`): The solution of the controlled equation. + Can be a neural network or a list of numpy arrays. + + grid (`torch.Tensor`): A tensor representing the grid points where the solution is evaluated. + Shape: (m, n), where m is the number of points and n is the number of input dimensions. + + args (`List[Tuple[Union[int, List]]]`): A list of tuples specifying the derivative operators. + Each tuple contains the component index (int) and the axes (list of ints) with respect to which + the derivative is taken. + + diff_method (`BasicDeriv`, optional): The method used for calculating derivatives. + Defaults to `AutogradDeriv`. + + Returns: + `torch.Tensor`: A tensor containing the arguments for the control ANN. + Each column represents a different derivative, and each row corresponds to a grid point. + Shape: (m, k), where m is the number of grid points and k is the number of derivatives. + """ if diff_method is None: diff_method = AutogradDeriv @@ -52,6 +64,20 @@ def prepare_control_inputs(model: Union[torch.nn.Sequential, List[np.ndarray]], def eps_increment_diff(input_params: OrderedDict, loc: List[Union[str, Tuple[int]]], forward: bool = True, eps = 1e-4): # input_keys: list, prev_loc: List = None, if forward: + """ + Incrementally modifies a parameter in the input dictionary by a small value (epsilon). + + This method perturbs a specific parameter within a nested dictionary structure by either adding or subtracting a small value (epsilon). The modification is done in-place. This perturbation is crucial for exploring the parameter space and evaluating the sensitivity of the discovered equations to slight variations in parameter values. By making these small adjustments, the algorithm can refine its search for the optimal equation structure and parameterization that best fits the observed data. + + Args: + input_params: The input dictionary containing the parameters to be modified. + loc: A list specifying the location of the parameter to be modified within the input dictionary. The first element is the key for the outer dictionary, and the remaining elements form a tuple used as a key for the inner dictionary. + forward: A boolean indicating whether to increment (True) or decrement (False) the parameter. Defaults to True. + eps: The small value (epsilon) to add or subtract. Defaults to 1e-4. + + Returns: + OrderedDict: The modified input dictionary with the parameter at the specified location incremented or decremented. + """ input_params[loc[0]][tuple(loc[1:])] += eps else: input_params[loc[0]][tuple(loc[1:])] -= 2*eps diff --git a/epde/decorators.py b/epde/decorators.py index 9d9c3d04..ae6cc421 100644 --- a/epde/decorators.py +++ b/epde/decorators.py @@ -17,13 +17,47 @@ class ResetEquationStatus: + """ + Resets the status of equation parts (input, output, right part). + + Attributes: + reset_input (bool): Whether to reset the input. + reset_output (bool): Whether to reset the output. + reset_right_part (bool): Whether to reset the right part. + """ + def __init__(self, reset_input: bool = False, reset_output: bool = False, reset_right_part: bool = True): + """ + Initializes the ResetEquationStatus object. + + This object determines which parts of an equation should be reset during the equation simplification process. + Resetting parts of the equation allows the algorithm to explore different equation structures and avoid getting stuck in local optima. + + Args: + reset_input (bool): Whether to reset the input part of the equation. Defaults to False. + reset_output (bool): Whether to reset the output part of the equation. Defaults to False. + reset_right_part (bool): Whether to reset the right-hand side of the equation. Defaults to True. + + Returns: + None. + """ self.reset_input = reset_input self.reset_output = reset_output self.reset_right_part = reset_right_part def __call__(self, method): + """ + Wraps a method to reset the state of input and output equation objects after execution. + + This decorator is applied to methods involved in equation discovery. After the method executes, it resets the state of the input arguments (typically equation components or data) and the resulting equation(s). This ensures a clean slate for subsequent evolutionary steps, preventing state accumulation from influencing the search process and maintaining the integrity of each generation. The reset operation is applied recursively to elements within lists, tuples, or sets. + + Args: + method: The method to be wrapped. + + Returns: + The wrapped method. + """ @wraps(method) def wrapper(obj, *args, **kwargs): result = method(obj, *args, **kwargs) @@ -58,20 +92,64 @@ def wrapper(obj, *args, **kwargs): class HistoryExtender(): - ''' - - Extend histroy log of the complex structure + """ + The `HistoryExtender` class manages and extends the history of a symbolic regression process. It stores past populations, expressions, and their associated fitness scores, enabling the algorithm to revisit promising areas of the search space and avoid premature convergence. The class provides methods for adding new populations to the history, retrieving the best individuals from the history, and managing the size of the historical archive. This allows for a more comprehensive exploration of the solution space and can lead to the discovery of more accurate and robust models. + + + Extend histroy log of the complex structure + + ''' + """ - ''' def __init__(self, action_log_entry: str = '', state_writing_points='n'): + """ + Initializes a HistoryExtender instance, configuring how historical states are managed during the equation discovery process. + + This configuration determines when the state of the evolutionary process + is recorded, allowing for detailed analysis and potential rollback + during the search for the optimal equation. + + Args: + action_log_entry (str): A descriptive string to log actions performed. Defaults to ''. + state_writing_points (str): Specifies points in the evolutionary algorithm's cycle + where state should be saved. Valid values are: + - 'n': Never write state. + - 'ba': Write state before and after each generation. + - 'b': Write state before each generation. + - 'a': Write state after each generation. + Defaults to 'n'. + + Returns: + None. + + Class Fields: + action_log_entry (str): Stores the action log entry for logging purposes. + state_writing_points (str): Stores the state writing points configuration, + controlling the frequency of state saving. + """ assert (state_writing_points == 'n' or state_writing_points == 'ba' or state_writing_points == 'b' or state_writing_points == 'a') self.action_log_entry = action_log_entry self.state_writing_points = state_writing_points def __call__(self, method): + """ + Wraps a method to track changes in objects relevant to differential equation discovery. + + This wrapper adds history entries to objects involved in the execution of the wrapped method, + specifically those possessing `_history` and `add_history` attributes. These entries, + derived from the class's `action_log_entry` and object states, capture the evolution + of objects during equation discovery, aiding in understanding how different operations + affect the search for the best-fitting differential equation. This is important to track the changes of the objects and debug the process of equation discovery. + + Args: + method: The method to wrap. + + Returns: + The wrapped method. + """ @wraps(method) def wrapper(obj, *args, **kwargs): def historized(h_obj): @@ -102,15 +180,51 @@ def historized(h_obj): class BoundaryExclusion(): """ - Class for modifing test function to ignore (i.e. considered zeros) values at the bounderies - - Attributes: - boundary_width (`int|list|tuple`): the number of unaccounted elements at the edges + Class designed to modify a test function, effectively treating values at specified boundaries as zero. This allows the function to disregard boundary values during calculations or evaluations. + + + Attributes: + boundary_width (`int|list|tuple`): the number of unaccounted elements at the edges """ + def __init__(self, boundary_width=0): + """ + Initializes a BoundaryExclusion object. + + This object defines an exclusion zone around the boundaries of the domain. + The boundary width determines the size of this zone, which is used to avoid + evaluating equation terms too close to the edges of the data domain. This + helps to improve the accuracy and stability of the equation discovery process + by mitigating boundary effects. + + Args: + boundary_width (int): The width of the boundary exclusion zone. Defaults to 0. + + Returns: + None + + Attributes: + boundary_width (int): The width of the boundary exclusion zone. + """ self.boundary_width = boundary_width def __call__(self, func): + """ + Applies a mask to the output of a function, effectively excluding boundary regions. + + This method acts as a decorator. It wraps a given function, modifying its + output by applying a mask. This mask sets values within a specified + `boundary_width` of the grid edges to zero. This is useful to exclude boundary effects from consideration when discovering underlying equations. + + Args: + func (callable): The function to be decorated. This function should + accept a tuple of grids as input and return a numerical array + representing the function's output on those grids. + + Returns: + callable: A wrapped function that, when called, applies the boundary + exclusion mask to the original function's output. + """ @wraps(func) def wrapper(grids, boundary_width: Union[int, list] = 0): assert len(grids) == grids[0].ndim diff --git a/epde/eq_mo_objectives.py b/epde/eq_mo_objectives.py index ef28cd61..e492a4f6 100644 --- a/epde/eq_mo_objectives.py +++ b/epde/eq_mo_objectives.py @@ -13,64 +13,81 @@ def generate_partial(obj_function, equation_key): + """ + Generates a partially applied function, fixing the equation key. + + This is useful for creating specialized versions of a function + that operate on a specific equation derived within the EPDE framework. + By pre-filling the equation key, subsequent calls to the function + can focus on other parameters, simplifying the overall workflow + when dealing with a set of predefined equation structures. + + Args: + obj_function: The function to partially apply. + equation_key: The equation key to pre-fill. + + Returns: + A partial function with the equation_key argument pre-filled. + """ return partial(obj_function, equation_key=equation_key) def equation_fitness(system, equation_key): - ''' - Evaluate the quality of the system of PDEs, using the individual values of fitness function for equations. - - Parameters: - ----------- - system - ``epde.structure.main_structures.SoEq`` object - The system, that is to be evaluated. - - Returns: - ---------- - error : float. - The value of the error metric. - ''' + """ + Evaluate the fitness of a specific equation within the PDE system. + + This method retrieves the pre-calculated fitness value for a given equation. + It's used to assess how well each individual equation in the system contributes + to the overall model accuracy. By examining individual equation fitness, the + algorithm can identify and refine the most relevant equations for describing + the underlying dynamics. + + Args: + system (``epde.structure.main_structures.SoEq``): The system of equations being evaluated. + equation_key (str): The identifier (key) of the equation within the system to evaluate. + + Returns: + float: The fitness value of the specified equation. + """ assert system.vals[equation_key].fitness_calculated, 'Trying to call fitness before its evaluation.' res = system.vals[equation_key].fitness_value return res def equation_complexity_by_terms(system, equation_key): - ''' - Evaluate the complexity of the system of PDEs, evaluating a number of terms for each equation. - In the evaluation, we consider only terms with non-zero weights, and the target term with the free - coefficient are not included in the final metric due to their ubiquty in the equations. - - Parameters: - ----------- - system - ``epde.structure.main_structures.SoEq`` object - The system, that is to be evaluated. - - Returns: - ---------- - discrepancy : list of integers. - The values of the error metric: list entry for each of the equations. - ''' + """ + Evaluate the complexity of a specific equation within the PDE system by counting the number of non-zero terms. This provides a measure of the equation's complexity, which is useful for guiding the equation discovery process by favoring simpler, more parsimonious models. + + Args: + system (``epde.structure.main_structures.SoEq``): The system of equations being analyzed. + equation_key (int): Index of equation in system + + Returns: + int: The number of terms with non-zero weights in the specified equation. + """ return np.count_nonzero(system.vals[equation_key].weights_internal) def equation_complexity_by_factors(system, equation_key): - ''' - Evaluate the complexity of the system of PDEs, evaluating a number of factors in terms for each - equation. In the evaluation, we consider only terms with non-zero weights and target, while - the free coefficient is not included in the final metric. Also, the real-valued factors are - not considered in the result. - - Parameters: - ----------- - system - ``epde.structure.main_structures.SoEq`` object - The system, that is to be evaluated. - - Returns: - ---------- - discrepancy : list of integers. - The values of the error metric: list entry for each of the equations. - ''' + """ + Evaluate the complexity of a single equation within the system based on its constituent factors. + This function quantifies the equation's complexity by summing the complexities of individual terms, + considering their structure and the significance of their weights. The complexity is evaluated + by considering the number of derivatives within each term. + + Args: + system (``epde.structure.main_structures.SoEq``): The system of equations to be evaluated. + equation_key (int): The index of the equation within the system to evaluate. + + Returns: + int: The complexity score for the specified equation. + + Why: + This method helps to assess the complexity of candidate equation structures, guiding the search + towards simpler and more interpretable models that still accurately represent the underlying dynamics. + By penalizing overly complex equations, the method promotes parsimony and reduces the risk of overfitting + the data. + """ # eq_compl = 0 # for idx, term in enumerate(system.vals[equation_key].structure): @@ -98,16 +115,68 @@ def equation_complexity_by_factors(system, equation_key): def equation_terms_stability(system, equation_key): + """ + Calculates the stability of the coefficients within a given equation. + + This function retrieves the pre-calculated stability of the coefficients + for a specified equation within the system. It relies on the system + having already computed the stability during the equation discovery + process. This information is crucial for assessing the reliability and + robustness of the identified equation structure. + + Args: + system: The system object containing equation values and their + associated stability metrics. + equation_key: The key identifying the equation for which to retrieve + the coefficient stability. + + Returns: + The stability of the coefficients for the specified equation. + """ assert system.vals[equation_key].stability_calculated res = system.vals[equation_key].coefficients_stability return res def equation_aic(system, equation_key): + """ + Retrieves the pre-calculated Akaike Information Criterion (AIC) for a specified equation. + + This function accesses the stored AIC value, which quantifies the trade-off between the goodness of fit and the complexity of the equation. + A lower AIC indicates a better balance, aiding in the selection of the most suitable equation form to represent the underlying dynamics. + + Args: + system: The system object holding the equation and its associated results, including the pre-calculated AIC. + equation_key: The identifier for the equation within the system. + + Returns: + float: The pre-calculated AIC value for the specified equation. + """ assert system.vals[equation_key].aic_calculated res = system.vals[equation_key].aic return res def complexity_deriv(term_list: list): + """ + Calculates a complexity score reflecting the number of operations in a symbolic expression. + + This function iterates through a list of terms, summing up the complexity + contributed by each term based on its `deriv_code` attribute, which + represents the operations involved in the term. The final score is then + scaled by a 'power' parameter, likely representing the overall influence + or weight of the expression. This complexity score is used to guide the + search for simpler and more parsimonious equation structures during the + equation discovery process. + + Args: + term_list: A list of terms, where each term is expected to have + a `deriv_code` attribute (representing operations) and a `param` + method to access parameters like 'power'. + + Returns: + float: The calculated complexity score, scaled by the 'power' + parameter of the last factor. This score reflects the overall + complexity of the symbolic expression. + """ total = 0 for factor in term_list: if factor.deriv_code == [None]: diff --git a/epde/evaluators.py b/epde/evaluators.py index 836a0ccd..cad11ec4 100644 --- a/epde/evaluators.py +++ b/epde/evaluators.py @@ -17,20 +17,114 @@ from epde.supplementary import factor_params_to_str class EvaluatorTemplate(ABC): + """ + An abstract base class for evaluators. + + This class defines the basic structure for evaluators, + providing a call method that must be implemented by subclasses. + + Class Methods: + - __call__: Call method for the abstract class. + """ + def __init__(self): + """ + Initializes the evaluator. + + This method prepares the evaluator for assessing the fitness of candidate equation structures. + It sets up the necessary internal state, such as data handling configurations or result storage, + required for subsequent evaluation steps. + + Args: + self: The object instance. + + Returns: + None. + """ pass @abstractmethod def __call__(self, factor, structural: bool = False, grids: list = None, torch_mode: bool = False, **kwargs): + """ + Abstract call method for evaluating equation fitness. + + This method must be implemented by subclasses to define how the fitness + of a candidate equation is evaluated based on the provided data and + settings. It raises a NotImplementedError if called directly, ensuring + that concrete evaluator classes provide their own implementation. + + Args: + factor: The factor to be used in the evaluation. + structural: A boolean indicating whether the method should consider + structural properties during evaluation. Defaults to False. + grids: A list of grids to be used for evaluation. Defaults to None. + torch_mode: A boolean indicating whether to use torch mode for + computation. Defaults to False. + **kwargs: Additional keyword arguments to be passed to the + evaluation function. + + Returns: + None. Subclasses should return a fitness score or a tuple of scores. + + Raises: + NotImplementedError: Always raised, indicating that the method + must be implemented by a subclass. + + WHY: This abstract method enforces that each specific evaluation strategy + (implemented in subclasses) defines its own way of calculating the + fitness of a candidate equation, which is a crucial step in the + equation discovery process. + """ raise NotImplementedError( 'Trying to call the method of an abstract class') class CustomEvaluator(EvaluatorTemplate): + """ + A custom evaluator class for NumPy and Torch evaluation functions. + + This class allows users to define and apply custom evaluation functions, + supporting both NumPy and Torch implementations. It can handle single + evaluation functions or dictionaries of functions, and it provides a + flexible interface for evaluating functions with different parameters. + + Class Methods: + - __init__ + - __call__ + + Attributes: + _evaluation_functions_np: NumPy evaluation function(s). + _evaluation_functions_torch: Torch evaluation function(s). + _single_function_token: A boolean token indicating whether a single evaluation function or a dictionary of functions is used. + eval_fun_params_labels: Labels for the evaluation function parameters. + """ + def __init__(self, evaluation_functions_np: Union[Callable, dict] = None, evaluation_functions_torch: Union[Callable, dict] = None, eval_fun_params_labels: Union[list, tuple, set] = ['power']): + """ + Initializes the CustomEvaluator with NumPy and Torch evaluation functions. + + This setup is crucial for assessing the fitness of candidate equation structures generated during the evolutionary search. The evaluator determines how well a given equation matches the observed data, guiding the optimization process towards more accurate and representative models. + + Args: + evaluation_functions_np: NumPy evaluation function(s). Can be a single callable or a dictionary of callables. + evaluation_functions_torch: Torch evaluation function(s). Can be a single callable or a dictionary of callables. + eval_fun_params_labels: Labels for the evaluation function parameters. + + Raises: + ValueError: If both `evaluation_functions_np` and `evaluation_functions_torch` are None, as at least one evaluation function is required to assess equation fitness. + + Returns: + None + + Class Fields Initialized: + _evaluation_functions_np (Union[Callable, dict]): NumPy evaluation function(s). + _evaluation_functions_torch (Union[Callable, dict]): Torch evaluation function(s). + _single_function_token (bool): A boolean token indicating whether a single evaluation function or a dictionary of functions is used. + eval_fun_params_labels (Union[list, tuple, set]): Labels for the evaluation function parameters. + """ self._evaluation_functions_np = evaluation_functions_np self._evaluation_functions_torch = evaluation_functions_torch @@ -49,6 +143,28 @@ def __call__(self, factor, structural: bool = False, func_args: List[Union[torch if torch_mode: # TODO: rewrite torch_mode_explicit = True if not self._single_function_token and factor.label not in self._evaluation_functions_np.keys(): + """ + Evaluates the function associated with a given factor, using either NumPy or Torch implementations. + + This method selects the appropriate evaluation function based on the provided arguments and the evaluator's configuration. + It then applies this function to the provided arguments, using either pre-computed grids or a new set of arguments. + This evaluation is a core step in exploring the search space of potential equation structures. + + Args: + factor: The factor object containing the function label, parameters, and grids. + structural: A boolean flag indicating whether the evaluation is structural. Defaults to False. + func_args: A list of arguments to be passed to the evaluation function. + If None, the factor's grids are used. Defaults to None. + torch_mode: A boolean flag indicating whether to use the Torch evaluation + functions. Defaults to False. + **kwargs: Additional keyword arguments. + + Returns: + np.ndarray: The result of evaluating the function on the given arguments. + + WHY: This method is used to calculate the value of a symbolic expression (factor) for a given set of input values. + The result of this evaluation is then used to assess the fitness of the expression in representing the underlying dynamics of the system. + """ raise KeyError( 'The label of the token function does not match keys of the evaluator functions') if func_args is not None: @@ -89,27 +205,19 @@ def __call__(self, factor, structural: bool = False, func_args: List[Union[torch def simple_function_evaluator(factor, structural: bool = False, grids=None, torch_mode: bool = False, **kwargs): - ''' - - Example of the evaluator of token values, that can be used for uploading values of stored functions from cache. Cases, when - this approach can be used, include evaluating derivatives, coordinates, etc. - - - Parameters - ---------- - - factor : epde.factor.Factor object, - Object, that represents a factor from the equation terms, for that we want to calculate the values. - - structural : bool, - Mark, if the evaluated value will be used for discovering equation structure (True), or calculating coefficients (False) - - Returns - ---------- - value : numpy.ndarray - Vector of the evaluation of the token values, that can be used as target, or feature during the LASSO regression. - - ''' + """ + Evaluates the value of a factor, potentially retrieving it from a cache. This is useful for reusing previously computed values of equation terms, such as derivatives or coordinates, during the equation discovery process. The method prioritizes using pre-computed values to enhance computational efficiency. + + Args: + factor (epde.factor.Factor): The factor object representing the term to evaluate. + structural (bool, optional): Indicates whether the value is used for structural discovery (True) or coefficient calculation (False). Defaults to False. + grids (optional): Grid values for prediction with an ANN, if applicable. Defaults to None. + torch_mode (bool, optional): Flag indicating whether to use torch tensors. Defaults to False. + **kwargs: Additional keyword arguments. + + Returns: + numpy.ndarray: The evaluated value of the factor, which can be used as a target or feature in regression. + """ for param_idx, param_descr in factor.params_description.items(): if param_descr['name'] == 'power': @@ -149,104 +257,510 @@ def simple_function_evaluator(factor, structural: bool = False, grids=None, grid_eval_fun_torch = lambda *grids, **kwargs: torch.pow(grids[int(kwargs['dim'])], kwargs['power']) def phased_sine_np(*grids, **kwargs): + """ + Generates a phased sine wave using NumPy. + + This method constructs a basis function consisting of a phased sine wave, + parameterized by frequency, phase, and power. These basis functions are + essential components for representing more complex functions within the + equation discovery process. By combining multiple phased sine waves with + different parameters, the framework can approximate a wide range of + potential solutions to the differential equation being sought. + + Args: + *grids: Variable number of grid arrays, one for each dimension. These grids + define the coordinate system on which the sine wave is evaluated. + **kwargs: Keyword arguments containing 'freq' (frequencies for each dimension), + 'phase' (phases for each dimension), and 'power' (the exponent to raise + the sine wave to). These parameters control the shape and characteristics + of the generated sine wave. + + Returns: + np.ndarray: A NumPy array representing the phased sine wave. This array + can then be used as a building block in the equation discovery process. + """ coordwise_elems = [kwargs['freq'][dim] * 2*np.pi*(grids[dim] + kwargs['phase'][dim]) for dim in range(len(grids))] return np.power(np.sin(np.sum(coordwise_elems, axis = 0)), kwargs['power']) def phased_sine_torch(*grids, **kwargs): + """ + Generates a coordinate-wise phased sine wave using PyTorch. + + This method constructs a sine wave by combining coordinate-wise elements, + modulated by specified frequencies and phases, and then applies a power exponent. + This is useful for creating basis functions with controlled spatial variations, + allowing the discovery process to represent complex functions as combinations of simpler, + spatially-varying sinusoidal components. + + Args: + *grids: Variable number of grid tensors, one for each dimension. These grids + define the spatial coordinates over which the sine wave is generated. + **kwargs: Keyword arguments containing: + - 'freq' (torch.Tensor): Frequencies for each dimension. + - 'phase' (torch.Tensor): Phases for each dimension. + - 'power' (float): The exponent to raise the sine to. + + Returns: + torch.Tensor: The resulting phased sine wave tensor. This tensor represents + the function value at each point defined by the input grids. + """ coordwise_elems = [kwargs['freq'][dim] * 2*torch.pi*(grids[dim] + kwargs['phase'][dim]) for dim in range(len(grids))] return torch.pow(torch.sin(torch.sum(coordwise_elems, axis = 0)), kwargs['power']) def phased_sine_1d_np(*grids, **kwargs): + """ + Generates a 1D phased sine wave using NumPy to represent a basis function. + + This function constructs a 1D sine wave, a fundamental component in representing more complex functions within the equation search space. + The sine wave is defined by its frequency, phase, and power, allowing for flexible adaptation to different equation structures. + + Args: + *grids: A tuple containing the grid along which the sine wave is calculated. + **kwargs: A dictionary containing the keyword arguments: + freq (float): The frequency of the sine wave. + phase (float): The phase of the sine wave. + power (float): The power to which the sine wave is raised. + + Returns: + np.ndarray: A NumPy array representing the 1D phased sine wave. + """ coordwise_elems = kwargs['freq'] * 2*np.pi*(grids[0] + kwargs['phase']/kwargs['freq']) return np.power(np.sin(coordwise_elems), kwargs['power']) def phased_sine_1d_torch(*grids, **kwargs): + """ + Computes a 1D phased sine wave using PyTorch to represent a component within a larger equation. + + This method generates a 1D sine wave with adjustable frequency, phase, and power, + serving as a fundamental building block for constructing more complex equation terms. + By combining such elements, the framework can explore a wide range of potential equation structures. + + Args: + *grids: A tuple containing a single PyTorch tensor representing the 1D grid coordinates. + **kwargs: A dictionary containing the keyword arguments: + freq: The frequency of the sine wave. + phase: The phase of the sine wave. + power: The power to which the sine wave is raised. + + Returns: + torch.Tensor: A PyTorch tensor containing the 1D phased sine wave. + """ coordwise_elems = kwargs['freq'] * 2*torch.pi*(grids[0] + kwargs['phase']/kwargs['freq']) return torch.pow(torch.sin(coordwise_elems), kwargs['power']) def const_eval_fun_np(*grids, **kwargs): + """ + Evaluates a constant function, creating a grid filled with a specified value. + + This function is used to initialize or set a baseline value across the entire + computational domain when constructing more complex equation structures. + It leverages NumPy for efficient array creation. + + Args: + *grids: One or more NumPy arrays representing the grid(s). The first grid + is used to determine the shape and data type of the output array. + **kwargs: Keyword arguments. Must contain the 'value' key, which + specifies the constant value to fill the array with. + + Returns: + np.ndarray: A NumPy array with the same shape and data type as the + first input grid, filled with the specified constant value. + """ return np.full_like(a=grids[0], fill_value=kwargs['value']) def const_eval_fun_torch(*grids, **kwargs): + """ + Evaluates a constant function using PyTorch to provide a foundational building block for more complex equation discovery. + + This function generates a tensor of constant values, matching the shape and data type of the input grid. This is useful for creating constant terms within candidate differential equations during the equation search process. + + Args: + *grids: Variable number of input grids (tensors). The shape and + data type of the first grid are used to create the output tensor. + **kwargs: Keyword arguments. Must contain the 'value' key, which + specifies the constant value to fill the output tensor with. + + Returns: + torch.Tensor: A tensor with the same shape and data type as the + first input grid, filled with the specified constant value. + """ return torch.full_like(a=grids[0], fill_value=kwargs['value']) def const_grad_fun_np(*grids, **kwargs): + """ + Returns a zero-filled NumPy array with the same shape and data type as the first input grid. + + This function is designed to provide a constant gradient of zero, which can be useful as a baseline or a starting point + when searching for more complex gradient functions within the equation discovery process. By providing a zero gradient, + it allows the evolutionary algorithm to explore other potential terms and structures in the differential equation. + + Args: + *grids: One or more NumPy arrays representing the input grids. Only the first grid's shape and data type are used to create the zero-filled array. + **kwargs: Arbitrary keyword arguments. These are not used in the function. + + Returns: + np.ndarray: A NumPy array filled with zeros, having the same shape and data type as the first input grid. + """ return np.zeros_like(a=grids[0]) def const_grad_fun_torch(*grids, **kwargs): + """ + Creates a zero-filled tensor matching the shape of the input grid. + + This function is used to initialize gradient tensors during the equation discovery process. + By providing a zero-filled tensor with the correct dimensions, it ensures that the optimization + process starts with a clean slate for gradient calculations, avoiding any potential bias + from pre-existing values. + + Args: + *grids: One or more input tensors. The shape and data type of the first + tensor in `grids` are used to create the output tensor of zeros. + **kwargs: Additional keyword arguments. These are not used in the function. + + Returns: + torch.Tensor: A tensor of zeros with the same shape and data type as the + first input tensor in `grids`. + """ return torch.zeros_like(a=grids[0]) def get_velocity_common(*grids, **kwargs): + """ + Calculates velocity components alpha and beta based on input grids and polynomial coefficients. + + These components are crucial for modeling the dynamics of the system by representing velocity profiles + derived from the identified differential equation. The method employs polynomial and exponential functions + to map the input grids to velocity components, using coefficients optimized during the equation discovery process. + + Args: + *grids: Variable number of grid arrays. The first grid is used in polynomial calculations, + and the second grid is used in exponential and polynomial calculations. + **kwargs: Keyword arguments representing polynomial coefficients (p1 to p15) obtained during the equation discovery process. + These coefficients fine-tune the velocity component calculations. + + Returns: + tuple: A tuple containing two arrays, alpha and beta, representing the calculated velocity components. + These components are essential outputs for further analysis or simulation within the identified model. + """ a = [kwargs['p' + str(idx*3+1)] * grids[0]**2 + kwargs['p' + str(idx*3 + 2)] * grids[0] + kwargs['p' + str(idx*3 + 3)] for idx in range(5)] alpha = np.exp(a[0] * grids[1] + a[1]); beta = a[2] * grids[1]**2 + a[3] * grids[1] + a[4] return alpha, beta def velocity_heating_eval_fun(*grids, **kwargs): - ''' - Assumption of the velocity field for two-dimensional heat equation with convetion. - ''' + """ + Calculates the velocity field based on spatial grids, which is used to model convection in heat transfer problems. The velocity field is a crucial component in simulating how heat is transported by the movement of a fluid or gas. + + Args: + *grids: Spatial grids representing the domain of the heat equation. + **kwargs: Additional keyword arguments passed to `get_velocity_common`. + + Returns: + The calculated velocity field as a product of two intermediate fields, `alpha` and `beta`. This field is then used to simulate the convective heat transfer. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return alpha * beta def vhef_grad_1(*grids, **kwargs): + """ + Computes a gradient-like expression used to evaluate candidate differential equations. + + This function calculates a weighted product of input grids, using velocity parameters + (alpha and beta) to refine the expression. These parameters, derived from the input grids, + act as coefficients that scale the contribution of each grid to the overall gradient. + This gradient approximation is used within the evolutionary algorithm to assess how well + a candidate equation fits the observed data. + + Args: + *grids: Variable number of grid-like arrays representing different terms in the equation. + **kwargs: Keyword arguments to be passed to `get_velocity_common` for velocity parameter calculation. + + Returns: + The computed gradient-like value, representing a component of the overall equation's gradient. + This value is calculated as the product of the square of the first grid, the second grid, and the + alpha and beta velocity parameters. This result contributes to the fitness evaluation of the + candidate equation. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0]**2 * grids[1] * alpha * beta def vhef_grad_2(*grids, **kwargs): + """ + Computes a component of the equation search gradient using velocity fields. + + This function calculates a term contributing to the overall gradient used in the equation discovery process. It leverages velocity fields derived from the input grids to weight the product of the first two grids. This weighting helps to emphasize regions where the velocity fields indicate significant changes or flows, thereby guiding the search towards equation terms that capture these dynamics. + + Args: + *grids: Variable number of grid arrays representing different state variables or spatial coordinates. These grids are used to compute velocity components and contribute to the final result. + **kwargs: Keyword arguments passed to the `get_velocity_common` function, influencing the velocity field calculation. + + Returns: + The weighted product of the first two grids, `alpha`, and `beta`, representing a component of the overall gradient. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0] * grids[1] * alpha * beta def vhef_grad_3(*grids, **kwargs): + """ + Calculates a component of the gradient used in the equation discovery process. + + This function computes a gradient term by scaling the second input grid + with velocity components `alpha` and `beta`. These velocity components + are derived from the input grids, representing potential relationships + between variables, and additional keyword arguments. The resulting gradient + contributes to the overall fitness evaluation of candidate equations. + + Args: + *grids: Variable number of grid arrays representing different terms in the equation. + **kwargs: Keyword arguments passed to the `get_velocity_common` function, + potentially influencing the calculation of velocity components. + + Returns: + numpy.ndarray: The calculated gradient, representing a component of the + overall equation gradient, obtained by element-wise multiplication + of the second grid, `alpha`, and `beta`. This gradient is used to + evaluate the fitness of candidate equations within the EPDE framework. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[1] * alpha * beta def vhef_grad_4(*grids, **kwargs): + """ + Computes a gradient-related term for equation discovery. + + This method calculates a component used in the equation discovery process, + specifically a term resembling a gradient. It utilizes input grids, + representing potential variables or features, and combines them using + coefficients derived from `get_velocity_common`. This term contributes + to the overall fitness evaluation of candidate equations. + + Args: + *grids: Variable number of grid-like arguments. These represent the + spatial or temporal data upon which the equation is being discovered. + **kwargs: Keyword arguments passed to `get_velocity_common`. These + arguments control aspects of the coefficient calculation, influencing + the weighting of different terms in the equation. + + Returns: + The computed gradient-related value. This value represents a component + of a potential term in the discovered equation, calculated as the square + of the first grid multiplied by the alpha and beta values obtained from + `get_velocity_common`. This value contributes to the overall equation's + representation and fitness. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0]**2 * alpha * beta def vhef_grad_5(*grids, **kwargs): + """ + Calculates a gradient used in the equation discovery process. + + This method refines the search for governing equations by calculating a gradient + based on input grids and shared velocity components. This gradient guides the + evolutionary algorithm towards equation structures that better fit the data. + + Args: + *grids: Variable number of grid arrays representing different data fields. + **kwargs: Keyword arguments passed to `get_velocity_common` to control + the computation of shared velocity components. + + Returns: + The calculated gradient, which is the product of the first grid, alpha, + and beta. This gradient is used to evaluate and refine candidate equations. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0] * alpha * beta def vhef_grad_6(*grids, **kwargs): + """ + Computes a scalar value reflecting the interplay between different velocity field characteristics. + + This function leverages velocity grid data to compute two intermediate values, `alpha` and `beta`, using `get_velocity_common`. The product of these values provides a combined measure reflecting the relationships within the velocity fields. This can be used to quantify certain aspects of the underlying dynamics. + + Args: + *grids: Variable number of grid arguments representing velocity fields, passed to `get_velocity_common`. + **kwargs: Keyword arguments passed to `get_velocity_common` to configure the computation of `alpha` and `beta`. + + Returns: + float: The product of `alpha` and `beta`, representing a combined measure derived from the velocity fields. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return alpha * beta def vhef_grad_7(*grids, **kwargs): + """ + Calculates a component of the equation discovery process by combining grid data with velocity components. + + This method computes a weighted value based on the input grids and velocity components, + contributing to the overall equation discovery process. It uses `get_velocity_common` + to obtain intermediate velocity components (alpha and beta) and then combines these + with the input grids to produce a component that reflects the relationships + between spatial variations and underlying dynamics. + + Args: + *grids: Variable number of grid-like arrays representing spatial data. + **kwargs: Keyword arguments passed to `get_velocity_common` to configure + velocity component calculation. + + Returns: + The computed gradient-like value, representing a component of the discovered equation, + calculated as the product of the squares of the first two grids and the alpha + component returned by `get_velocity_common`. This value reflects the influence + of spatial variations on the identified equation. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0]**2 * grids[1]**2 * alpha def vhef_grad_8(*grids, **kwargs): + """ + Calculates a component used in the equation discovery process. + + This method computes a value derived from the input grids and common velocity components (alpha, beta). + It multiplies the first grid by the square of the second grid and the alpha component + obtained from the velocity calculation. This result contributes to the overall equation construction + by representing a potential term within the differential equation. + + Args: + *grids: Variable number of grid-like arrays representing different terms or variables in the equation. + **kwargs: Keyword arguments passed to `get_velocity_common` to influence velocity calculation. + + Returns: + The calculated gradient-like quantity, representing a component of the discovered equation. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0] * grids[1]**2 * alpha def vhef_grad_9(*grids, **kwargs): + """ + Computes a term that contributes to the overall equation discovery process by evaluating the importance of the gradient. + + This method calculates a value that represents a component of the gradient within a potential differential equation. It uses provided grids to estimate gradient-related quantities (alpha and beta) and then combines these with the square of one of the grids. This term helps the evolutionary algorithm to prioritize equation structures that incorporate gradient information effectively. + + Args: + *grids: Variable number of grid arguments representing different data dimensions or variables. These grids are used to calculate gradient-related quantities. + **kwargs: Keyword arguments passed to the `get_velocity_common` function, allowing for customization of the gradient estimation process. + + Returns: + float: A value representing the contribution of the gradient (specifically, the square of the second grid multiplied by alpha) to the overall equation. This value is used to guide the search for the best-fitting differential equation. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[1]**2 * alpha def vhef_grad_10(*grids, **kwargs): + """ + Computes a gradient-like quantity based on input grids. + + This method calculates a value that approximates the gradient, + derived from the input grids and velocity parameters. + It squares the first grid, multiplies it by the second grid, + and scales the result by a velocity parameter (alpha). + This computation helps in identifying potential relationships + between spatial variations and underlying dynamics. + + Args: + *grids: Variable number of grid arrays representing spatial data. + **kwargs: Keyword arguments passed to `get_velocity_common` to determine velocity parameters. + + Returns: + The computed gradient-like value, scaled by the velocity parameter. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0]**2 * grids[1] * alpha def vhef_grad_11(*grids, **kwargs): + """ + Calculates a gradient based on input grids and common velocity components. + + This method computes a gradient by multiplying the first two input grids + with a combined velocity component derived from all input grids. This + gradient is a component of the discovered differential equation. + + Args: + *grids: Variable number of grid arrays representing different terms in the equation. + **kwargs: Keyword arguments passed to `get_velocity_common` to influence velocity calculation. + + Returns: + np.ndarray: The calculated gradient, representing a component of the discovered equation, + which is the product of the first two grids and the alpha component of the common velocity. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0] * grids[1] * alpha def vhef_grad_12(*grids, **kwargs): + """ + Computes a gradient-like quantity using a combination of input grids and shared velocity components. + + This function estimates a gradient-related value by multiplying the second input grid with a shared velocity component (alpha) derived from all input grids. This approach helps to approximate spatial derivatives, which are essential for constructing differential equation models. + + Args: + *grids: Variable number of grid-like arguments, representing spatial data. These grids are used to estimate the shared velocity components. + **kwargs: Arbitrary keyword arguments. These are passed to the `get_velocity_common` function, allowing for customization of the velocity component calculation. + + Returns: + The product of the second grid and the alpha velocity component, representing an approximation of a spatial derivative. This value contributes to the overall equation discovery process by providing gradient-related information. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[1] * alpha def vhef_grad_13(*grids, **kwargs): + """ + Calculates a scaled representation of a velocity component based on input grids. + + This method computes a value related to a velocity component, derived from the input grids using `get_velocity_common`. + The result is then scaled by the square of the first grid. This scaling emphasizes regions where the first grid has a larger magnitude, + effectively highlighting areas of interest within the data. + + Args: + *grids: Variable number of grid-like arrays. These grids are used to + calculate velocity-related quantities. + **kwargs: Keyword arguments passed to the `get_velocity_common` function, influencing the velocity component calculation. + + Returns: + float: The calculated and scaled velocity component. This value represents a weighted measure of velocity, + emphasizing regions where the first grid's magnitude is significant. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0]**2 * alpha def vhef_grad_14(*grids, **kwargs): + """ + Calculates a gradient used in the equation discovery process. + + This method computes a gradient by multiplying the first input grid with a + velocity component derived from all input grids. This gradient is a component + used within the evolutionary algorithm to evaluate potential equation structures. + It leverages shared velocity information across multiple grids to refine the search + for the underlying differential equation. + + Args: + *grids: Variable number of grid arrays representing different data fields. + **kwargs: Keyword arguments passed to `get_velocity_common`, influencing + the calculation of common velocity components. + + Returns: + np.ndarray: The gradient, computed as the product of the first grid and + the alpha velocity component. This gradient contributes to the fitness + evaluation of candidate equations. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return grids[0] * alpha def vhef_grad_15(*grids, **kwargs): + """ + Estimates the gradient of a field based on provided grid data. + + This function calculates the gradient by analyzing velocity fields derived from the input grids. + It uses `get_velocity_common` to extract alpha and beta velocity components, which are then used to approximate the gradient. + The alpha component is returned as an estimate of the gradient. This is useful for understanding the spatial changes + within the field represented by the grids, which is a key step in discovering underlying differential equations. + + Args: + *grids: Variable number of grid arguments passed to `get_velocity_common`. These grids provide the spatial data for gradient estimation. + **kwargs: Keyword arguments passed to `get_velocity_common`. These arguments configure the velocity field calculation. + + Returns: + The alpha velocity component, representing the estimated gradient of the field. + """ alpha, beta = get_velocity_common(*grids, **kwargs) return alpha diff --git a/epde/globals.py b/epde/globals.py index 3ac96dba..95086a69 100644 --- a/epde/globals.py +++ b/epde/globals.py @@ -22,11 +22,17 @@ def init_caches(set_grids: bool = False, device = 'cpu'): """ - Initialization global variables for keeping input data, values of grid and useful tensors such as evaluated terms - + Initializes global caches for storing tensors, initial data, and grid information. + + These caches are used to store and manage intermediate calculations and data + during the equation discovery process, improving efficiency by avoiding redundant computations. + The grid cache is optional and is used when working with data defined on a grid. + Args: - set_grids (`bool`): flag about using grid data - + set_grids (bool, optional): A flag indicating whether to initialize the grid cache. + Defaults to False. + device (str, optional): Device to store tensors. Defaults to 'cpu'. + Returns: None """ @@ -41,23 +47,80 @@ def init_caches(set_grids: bool = False, device = 'cpu'): def set_time_axis(axis: int): """ - Setting global of time axis + Sets the global time axis for data processing. + + This ensures that all subsequent operations within the framework + correctly interpret the temporal dimension of the input data. + + Args: + axis (int): The index of the axis representing time. + + Returns: + None + + Why: + The time axis is a crucial parameter for various data processing + steps, such as calculating derivatives or integrating solutions. + Setting it globally ensures consistency across different modules + and functions within the framework. """ global time_axis time_axis = axis def init_eq_search_operator(operator): + """ + Initializes the global equality search operator. + + This method sets the global variable `eq_search_operator` to the provided operator. This operator will be used to evaluate the equality of equation structures during the evolutionary search process. + + Args: + operator: The operator to be used for equality searches. + + Returns: + None. + """ global eq_search_operator eq_search_operator = operator def init_sys_search_operator(operator): + """ + Initializes the global system search operator. + + This method sets the global variable `sys_search_operator` to the + provided operator. This global variable will be used by the evolutionary algorithm + to explore the search space of possible differential equations. By initializing this operator, + the system ensures that all search processes utilize a consistent and pre-defined search strategy + for discovering equation structures. + + Args: + operator: The search operator to be used by the system. + + Returns: + None. + """ global sys_search_operator sys_search_operator = operator def delete_cache(): + """ + Deletes the global tensor and grid caches used for storing intermediate calculations. + + This method removes the global variables `tensor_cache` and `grid_cache` to + free up memory and ensure a clean state for subsequent equation discoveries. + If either cache does not exist, a message is printed, indicating that it was + not previously initialized. This is done to manage memory effectively during + the equation discovery process, especially when dealing with large datasets + or complex equation structures. + + Args: + None + + Returns: + None + """ global tensor_cache, grid_cache try: del tensor_cache @@ -70,18 +133,75 @@ def delete_cache(): class TrainHistory(object): + """ + A class to store the history of training metrics. + + Attributes: + history (list): A list to store historical data. + _idx (int): An index to track the current position. + """ + def __init__(self): + """ + Initializes a new TrainHistory instance. + + The TrainHistory class stores the equation structures, losses, and other relevant information during the training process. + This initialization prepares the object to record the evolutionary search for differential equations. + + Args: + None + + Returns: + None + """ self.reset() def reset(self): + """ + Resets the training history and index to their initial states. + + This method clears the stored training history and sets the current index to zero, + effectively starting a new training cycle. This is useful when restarting or + re-initializing the training process to ensure that previous training data does + not influence the new run. + + Args: + self: The object instance. + + Returns: + None. + """ self.history = [] self._idx = 0 def add(self, element): + """ + Adds a new element to the training history, associating it with a unique index. This is crucial for tracking the evolution of solutions during the equation discovery process, allowing to analyze how different equation structures perform over time and to identify promising candidates for further refinement. + + Args: + element: The element to add to the history (e.g., an equation, a set of parameters, or a performance metric). + + Returns: + None + """ self.history.append((self._idx, element)) self._idx += 1 def reset_hist(): + """ + Resets the global training history. + + This method reinitializes the global 'history' variable with a new + instance of the TrainHistory class, effectively clearing the + previous training history. This is crucial for starting a new equation discovery run + without being influenced by the results of previous runs. + + Args: + None + + Returns: + None + """ global history history = TrainHistory() @@ -90,6 +210,7 @@ class VerboseManager: """ Manager for output in text form """ + plot_DE_solutions : bool show_iter_idx : bool iter_fitness : bool @@ -101,19 +222,30 @@ def init_verbose(plot_DE_solutions : bool = False, show_iter_idx : bool = True, show_iter_fitness : bool = False, show_iter_stats : bool = False, show_ann_loss : bool = False, show_warnings : bool = False): """ - Method for initialized of manager for output in text form - + Initializes the verbosity settings for the evolutionary algorithm's output. + + This configuration controls what information is displayed during the equation discovery process, + allowing users to monitor progress and diagnose potential issues. By adjusting these flags, + users can tailor the output to focus on specific aspects of the search, such as iteration number, + fitness, or statistical properties of the population. This helps in understanding how the algorithm + is exploring the search space and identifying promising equation structures. + Args: - plot_DE_solutions (`bool`): optional - display solutions of a differential equation, default - False + plot_DE_solutions (`bool`): optional + Display solutions of a differential equation, default - False. Useful for visualizing the behavior of discovered equations. show_iter_idx (`bool`): optional - display the index of each iteration EA, default - False + Display the index of each iteration of the evolutionary algorithm, default - True. Helps track the progress of the search. show_iter_fitness (`bool`): optional - display the fitness of each iteration EA, default - False + Display the fitness of each iteration of the evolutionary algorithm, default - False. Provides insight into the quality of solutions found over time. show_iter_stats (`bool`): optional - display statistical properties of the population in each iteration EA, default - False + Display statistical properties of the population in each iteration of the evolutionary algorithm, default - False. Allows monitoring of population diversity and convergence. + show_ann_loss (`bool`): optional + Display loss of approximation by artificial neural network, default - False. show_warnings (`bool`): optional - display warnings arising during the operation of the algorithm, default - False + Display warnings arising during the operation of the algorithm, default - False. Can help identify potential issues or unexpected behavior. + + Returns: + `None` """ global verbose if not show_warnings: @@ -123,9 +255,18 @@ def init_verbose(plot_DE_solutions : bool = False, show_iter_idx : bool = True, def reset_control_nn(n_control: int = 1, ann: torch.nn.Sequential = None, ctrl_args: list = [(0, [None,]),], device: str = 'cpu'): - ''' - Use of bad practices, link control nn to the token family. - ''' + """ + Resets the global control neural network (`control_nn`) with a new configuration. This is used to adjust the parameters and architecture of the neural network responsible for generating control signals within the EPDE framework. By resetting the control network, the search process can explore different control strategies and adapt to the evolving equation structures. + + Args: + n_control (int, optional): The number of control signals to generate. Defaults to 1. + ann (torch.nn.Sequential, optional): A custom neural network to use as the control network. If None, a default network is created. Defaults to None. + ctrl_args (list, optional): Arguments for the control network. Defaults to `[(0, [None,])]`. + device (str, optional): The device (e.g., 'cpu', 'cuda') on which to run the control network. Defaults to 'cpu'. + + Returns: + None. The function modifies the global `control_nn` variable. + """ global control_nn control_nn = ControlNNContainer(output_num = n_control, args = ctrl_args, @@ -138,12 +279,28 @@ def reset_data_repr_nn(data: List[np.ndarray], grids: List[np.ndarray], train: b epochs_max=1e3, predefined_ann: torch.nn.Sequential = None, batch_frac=0.5, learining_rate=1e-6, device = 'cpu', use_fourier: bool = True, fourier_params: dict = {'L' : [4,], 'M' : [3,]}): - ''' - Represent the data with ANN, suitable to be used as the initial guess of the candidate equations solutions - during the equation search, employing solver-based fitness function. - - Possible addition: add optimization in Sobolev space, using passed derivatives, incl. higher orders. - ''' + """ + Represent the data with an artificial neural network (ANN) to provide an informed initial guess for candidate equation solutions during the equation search. This is achieved by training the ANN to approximate the provided data, enabling a more efficient and accurate exploration of the solution space. + + The ANN learns a continuous representation of the data, which can then be used to evaluate the fitness of candidate equations using solver-based methods. + + Args: + data (List[np.ndarray]): A list of numpy arrays representing the data to be approximated. + grids (List[np.ndarray]): A list of numpy arrays representing the grid coordinates corresponding to the data. + train (bool, optional): Whether to train the ANN. If False, a predefined ANN must be provided. Defaults to True. + derivs (List[Union[int, List, Union[np.ndarray]]], optional): List of derivatives to be included in the loss function. Defaults to None. + penalised_derivs (List[Union[int, List]], optional): List of derivatives to be penalised in the loss function. Defaults to None. + epochs_max (float, optional): The maximum number of training epochs. Defaults to 1e3. + predefined_ann (torch.nn.Sequential, optional): A pre-defined ANN to use instead of creating a new one. Defaults to None. + batch_frac (float, optional): The fraction of data to use in each batch during training. Defaults to 0.5. + learining_rate (float, optional): The learning rate for the optimizer. Defaults to 1e-6. + device (str, optional): The device to use for training (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + use_fourier (bool, optional): Whether to use Fourier features in the input layer of the ANN. Defaults to True. + fourier_params (dict, optional): Parameters for the Fourier features. Defaults to {'L' : [4,], 'M' : [3,]}. + + Returns: + None. The trained ANN is stored in the global variable `solution_guess_nn`. + """ global solution_guess_nn diff --git a/epde/integrate/bop.py b/epde/integrate/bop.py index 2302f23a..02152d54 100644 --- a/epde/integrate/bop.py +++ b/epde/integrate/bop.py @@ -12,6 +12,23 @@ def get_max_deriv_orders(system_sf: List[Union[Dict[str, Dict]]], variables: List[str] = ['u',]) -> dict: + """ + Computes the maximum derivative orders for each variable in a system of equations. + + This method analyzes a system of symbolic forms representing equations to determine the highest derivative order present for each variable with respect to each axis. This information is crucial for constructing suitable numerical schemes and discretizations when solving or analyzing the identified differential equations. + + Args: + system_sf: A list of symbolic forms, where each element represents an equation. + Each equation can be a dictionary or a list. + variables: A list of variable names to analyze. Defaults to ['u']. + + Returns: + dict: A dictionary where keys are variable names and values are NumPy arrays + representing the maximum derivative orders for each axis. + For example, {'u': array([2., 0.])} indicates that the maximum + derivative order of variable 'u' is 2 along the first axis and 0 + along the second axis. + """ def count_factor_order(factor_code, deriv_ax): if factor_code is None or isinstance(factor_code, tuple): return 0 @@ -84,9 +101,64 @@ def _(equation_sf: list, variables=['u',]): return max_orders class BOPElement(object): + """ + Represents a boundary operator element. + + This class defines a boundary operator element, encapsulating its properties + and methods for application and manipulation within a boundary value problem. + + Methods: + - __init__ + - set_grid + - operator_form + - values + - __call__ + """ + def __init__(self, axis: int, key: str, coeff: float = 1., term: list = [None], power: Union[Union[List[int], int]] = 1, var: Union[List[int], int] = 1, rel_location: float = 0., device = 'cpu'): + """ + Initializes a Constraint object for equation discovery. + + This method sets up a constraint within the equation search space, + guiding the evolutionary algorithm towards solutions that satisfy + specific conditions. These constraints help to narrow down the search + and ensure that the discovered equations adhere to known physical laws + or domain-specific knowledge. + + Args: + axis: The axis along which the constraint is applied. + key: A string identifier for the constraint. + coeff: The coefficient of the constraint term (default: 1.0). + term: A list representing the terms in the constraint (default: [None]). + power: The power of the constraint term (default: 1). It can be either a list of integers or a single integer. + var: The variable associated with the constraint (default: 1). It can be either a list of integers or a single integer. + rel_location: The relative location of the constraint (default: 0.0). + device: The device to use for computation (default: 'cpu'). + + Fields: + axis: The axis along which the constraint is applied (int). + key: A string identifier for the constraint (str). + coefficient: The coefficient of the constraint term (float). + term: A list representing the terms in the constraint (list). + power: The power of the constraint term (Union[Union[List[int], int]]). + variables: The variable associated with the constraint (Union[List[int], int]). + location: The relative location of the constraint (float). + grid: Placeholder for grid information, initialized to None. + status: A dictionary tracking the status of boundary condition setup, + initialized with 'boundary_location_set' and 'boundary_values_set' + both set to False. + _device: The device to use for computation (str). + + Returns: + None. + + Why: + Constraints are essential for guiding the equation discovery process. + They allow incorporating prior knowledge and physical laws, leading + to more accurate and physically meaningful discovered equations. + """ self.axis = axis self.key = key self.coefficient = coeff @@ -102,11 +174,52 @@ def __init__(self, axis: int, key: str, coeff: float = 1., term: list = [None], self._device = device def set_grid(self, grid: torch.Tensor): + """ + Sets the spatial grid for the element. + + This method updates the internal grid representation, which defines the + spatial domain over which the element is defined. Setting the grid also + marks the boundary location as set, indicating that the element's + spatial context is fully defined. This is a crucial step for subsequent + operations that rely on spatial information. + + Args: + grid (torch.Tensor): The new grid to be used. + + Returns: + None. + + Class Fields: + grid (torch.Tensor): The grid representing the spatial domain. + status (dict): A dictionary storing the status of various flags, + including 'boundary_location_set'. + """ self.grid = grid self.status['boundary_location_set'] = True @property def operator_form(self): + """ + Represents the term in a structured format suitable for equation discovery. + + This representation facilitates the application of evolutionary algorithms + by organizing the term's components (coefficient, term itself, power, and variables) + into a dictionary. This structured format is essential for evaluating and + manipulating equation candidates during the search process. + + Returns: + tuple: A tuple containing the key and a dictionary representing the operator form. + The dictionary includes the coefficient ('coeff'), term, power ('pow'), + and variables ('var') of this element. + + Args: + None + + Returns: + tuple: A tuple where the first element is the key of the term and the second + element is a dictionary containing the 'coeff', term, 'pow', and 'var' + representing the operator form of the term. + """ form = { 'coeff': self.coefficient, self.key: self.term, @@ -117,6 +230,22 @@ def operator_form(self): @property def values(self): + """ + Returns the coefficient values, evaluating them on the grid if necessary. + + This property provides a way to access the coefficient values, ensuring they are + evaluated on the spatial grid if the coefficient is defined as a function. This + dynamic evaluation is crucial for representing spatially varying coefficients + within the discovered differential equations. + + Args: + self: The object instance. + + Returns: + torch.Tensor: The value of the coefficient, as a PyTorch tensor. If the coefficient + is a function, the tensor represents the function evaluated on the grid. + Otherwise, it returns the pre-computed value. + """ if isinstance(self._values, FunctionType): assert self.grid_set, 'Tring to evaluate variable coefficent without a proper grid.' res = self._values(self.grids) @@ -127,6 +256,24 @@ def values(self): @values.setter def values(self, vals): + """ + Sets the coefficient values for the basis function. These values determine the contribution of this basis function to the overall equation. + + The input is processed to ensure compatibility with the computational backend. Specifically, if the input is a NumPy array, it's converted to a PyTorch tensor and moved to the appropriate device for efficient computation. This ensures that all coefficients are in a consistent format for equation evaluation and optimization. + + Args: + vals (Union[Callable, int, float, torch.Tensor, np.ndarray]): The values to set for the coefficients. Can be a function, integer, float, PyTorch tensor, or NumPy array. + + Returns: + None + + Raises: + TypeError: If the input `vals` is not of a supported type. + + Class Fields Initialized: + _values (torch.Tensor or function): The values of the coefficients, stored as a PyTorch tensor or a function. + vals_set (bool): A flag indicating whether the values have been set. + """ if isinstance(vals, (FunctionType, int, float, torch.Tensor)): self._values = vals self.vals_set = True @@ -138,6 +285,24 @@ def values(self, vals): f'Incorrect type of coefficients. Must be a type from list {VAL_TYPES}.') def __call__(self, values: VAL_TYPES = None) -> dict: + """ + Applies the boundary operator to construct a boundary condition for the problem. + + This method prepares the boundary condition by either using provided values or + setting them internally. It determines the boundary's spatial configuration + based on the grid or location information, then formulates the boundary + operator and value. This is crucial for defining the constraints of the + differential equation being solved. + + Args: + values: The boundary values. If None and boundary values have not been + previously set, a ValueError is raised. + + Returns: + dict: A dictionary containing the boundary location ('bnd_loc'), + boundary operator ('bnd_op'), boundary value ('bnd_val'), + variables ('variables'), and type ('type'). + """ if not self.vals_set and values is not None: self.values = values self.status['boundary_values_set'] = True @@ -174,21 +339,69 @@ def __call__(self, values: VAL_TYPES = None) -> dict: 'variables' : self.variables, 'type' : 'operator'} class PregenBOperator(object): + """ + Represents a boundary operator (BOperator) that pre-generates boundary conditions. + + Class Methods: + - demonstrate_required_ords + - conditions + - max_deriv_orders + - generate_default_bc + """ + def __init__(self, system: SoEq, system_of_equation_solver_form: list): #, device = 'cpu' self.system = system self.equation_sf = [eq for eq in system_of_equation_solver_form] self.variables = list(system.vars_to_describe) def demonstrate_required_ords(self): + """ + Demonstrates how to specify the maximum derivative orders for each equation in the system. + + This method prepares a list associating each main variable with its corresponding maximum derivative order. This information is crucial for the evolutionary process to explore suitable equation structures by limiting the order of derivatives considered for each variable, thereby guiding the search for governing differential equations. + + Args: + self: The instance of the class. + + Returns: + list: A list of tuples, where each tuple contains a main variable and its maximum derivative order. + """ linked_ords = list(zip([eq.main_var_to_explain for eq in self.system], self.max_deriv_orders)) @property def conditions(self): + """ + Returns the boolean conditions used to define the search space. + + These conditions constrain the possible forms of the equation, + guiding the search towards more plausible or physically meaningful solutions. + + Returns: + list: The list of boolean conditions. + """ return self._bconds @conditions.setter def conditions(self, conds: List[BOPElement]): + """ + Initializes the boundary conditions required for solving the discovered differential equation. + + This method sets up the boundary conditions necessary for numerical solvers to accurately compute solutions. It ensures that the provided boundary conditions are of the correct type and number, matching the order of derivatives present in the discovered equation. + + Args: + conds: A list of BOPElement objects, each representing a boundary condition. The order of these conditions must align with the derivative orders of the equation. + + Returns: + None + + Raises: + ValueError: If the number of provided boundary conditions does not match the expected number based on the equation's derivative orders. + NotImplementedError: If an attempt is made to initialize a boundary operator in-place, as this functionality is not yet implemented. + + Class Fields Initialized: + _bconds (list): A list of initialized boundary condition objects, ready for use by the numerical solver. + """ self._bconds = [] if len(conds) != int(sum([value.sum() for value in self.max_deriv_orders.values()])): raise ValueError( @@ -203,10 +416,42 @@ def conditions(self, conds: List[BOPElement]): @property def max_deriv_orders(self): + """ + Gets the maximum derivative orders for each variable in the equation. + + This property determines the highest derivative order present for each variable within the symbolic equation, which is crucial for understanding the complexity and structure of the identified differential equation. Knowing the maximum derivative orders helps in selecting appropriate numerical solvers and interpreting the equation's behavior. + + Args: + self: The instance of the class. + + Returns: + dict: A dictionary where keys are the variables and values are their + maximum derivative orders. + """ return get_max_deriv_orders(self.equation_sf, self.variables) def generate_default_bc(self, vals: Union[np.ndarray, dict] = None, grids: List[np.ndarray] = None, allow_high_ords: bool = False, required_bc_ord: List[int] = None): + """ + Generates default boundary conditions based on the provided parameters. + + This method constructs boundary condition operators (BOPElement) to enforce constraints on the solution at the domain boundaries. + It determines the location and derivative order of these conditions based on the provided parameters and the underlying grid. + This ensures that the discovered differential equations adhere to the physical constraints of the problem. + + Args: + vals: Values for the boundary conditions. If None, default values are used. + grids: List of grid tensors. If None, grids are retrieved from the cache. + allow_high_ords: A boolean flag to allow higher order derivatives. (Not fully implemented) + required_bc_ord: A dictionary specifying the required derivative orders for each variable. + If None, the `max_deriv_orders` attribute is used. + + Returns: + list: A list of BOPElement objects representing the generated boundary conditions. + + Initializes: + conditions (list): A list of BOPElement objects representing the generated boundary conditions. + """ # Implement allow_high_ords - selection of derivatives from if required_bc_ord is None: required_bc_ord = self.max_deriv_orders @@ -266,11 +511,54 @@ def get_boundary_ind(tensor_shape, axis, rel_loc): class BoundaryConditions(object): + """ + Represents boundary conditions for a physical simulation. + + Attributes: + dirichlet (dict): Dirichlet boundary conditions. + neumann (dict): Neumann boundary conditions. + robin (dict): Robin boundary conditions. + """ + def __init__(self, grids=None, partial_operators: dict = []): + """ + Initializes the OperatorCollection object. + + The OperatorCollection stores spatial information and precomputed operators + required for efficient boundary condition application. This precomputation + is done once and used many times during the PDE approximation. + + Args: + grids: The grids to store. + partial_operators (dict): A dictionary of partial operators. + + Fields: + grids_set (bool): A boolean indicating whether grids are set. + grids: The grids stored, if provided. + operators (dict): The partial operators stored. + + Returns: + None. + """ self.grids_set = (grids is not None) if grids is not None: self.grids = grids self.operators = partial_operators def form_operator(self): + """ + Forms a list of boundary conditions for each operator. + + This method retrieves boundary conditions associated with each operator + defined in the system. These conditions are essential for solving the discovered + differential equations numerically. + + Args: + None + + Returns: + list[list]: A list of lists, where each inner list represents the boundary + conditions for a specific operator. The operators and their associated + boundary conditions are obtained from the `self.operators` dictionary. + """ return [list(bcond()) for bcond in self.operators.values()] diff --git a/epde/integrate/interface.py b/epde/integrate/interface.py index 373417f1..6d86fafe 100644 --- a/epde/integrate/interface.py +++ b/epde/integrate/interface.py @@ -9,10 +9,46 @@ import epde.globals as global_var def make_eval_func(eval_func, eval_func_kwargs): + """ + Creates a wrapper function for evaluation. + + This method generates a lambda function that calls the provided + evaluation function with the given keyword arguments. This allows + for pre-configuring the evaluation function with specific settings + without modifying its original signature. + + Args: + eval_func: The evaluation function to be wrapped. + eval_func_kwargs: Keyword arguments to be passed to the evaluation function. + + Returns: + A lambda function that calls `eval_func` with the provided keyword arguments. + """ return lambda *args: eval_func(*args, **eval_func_kwargs) class SystemSolverInterface(object): def __init__(self, system_to_adapt: SoEq, coeff_tol: float = 1.e-9, device = 'cpu'): + """ + Initializes the SystemDescription object. + + This method prepares the SystemDescription object for describing a system, + extracting its variables and setting up necessary parameters. + + Args: + system_to_adapt: The system to be adapted and described. + coeff_tol: Tolerance for coefficients. + device: The device to use for computation (e.g., 'cpu', 'cuda'). + + Fields: + variables (list): A list of variables to describe, extracted from the input system. + adaptee (SoEq): The system to be adapted, stored for later use. + grids (None): Placeholder for grids, initialized to None. + coeff_tol (float): Tolerance for coefficients, used in various calculations. + _device (str): The device to use for computation. + + Returns: + None. + """ self.variables = list(system_to_adapt.vars_to_describe) self.adaptee = system_to_adapt self.grids = None @@ -23,6 +59,26 @@ def __init__(self, system_to_adapt: SoEq, coeff_tol: float = 1.e-9, device = 'cp @staticmethod def _term_solver_form(term, grids, default_domain, variables: List[str] = ['u',], device = 'cpu') -> dict: + """ + Transforms a symbolic term into a solver-compatible dictionary format. + + This method processes a symbolic term, extracting derivative information + and coefficient values to create a dictionary suitable for use by a numerical solver. + It handles both constant coefficients and coefficients that are functions of the grid. + + Args: + term: The symbolic term to be transformed. + grids: The grid points on which the term is evaluated. + default_domain: A boolean indicating whether to use the default domain. + variables: A list of variable names. Defaults to ['u']. + + Returns: + dict: A dictionary containing the transformed term information, with the following keys: + - 'coeff': The coefficient tensor or scalar value. + - 'term': A list of derivative orders. + - 'pow': A list of derivative powers. + - 'var': A list of derivative variable indices. + """ deriv_orders = [] deriv_powers = [] deriv_vars = [] @@ -101,9 +157,42 @@ def _term_solver_form(term, grids, default_domain, variables: List[str] = ['u',] @singledispatchmethod def set_boundary_operator(self, operator_info): + """ + Sets the boundary operator. + + Args: + operator_info: Information about the boundary operator. + + Returns: + None. + + Raises: + NotImplementedError: This method is not implemented. + """ raise NotImplementedError() def _equation_solver_form(self, equation, variables, grids=None, mode = 'NN') -> dict: + """ + Forms the equation for the solver. + + This method takes an equation, variables, and grids, and transforms + them into a dictionary suitable for the solver. It iterates through + the terms of the equation, calculates their contributions, and + organizes them into a structured format. + + Args: + equation: The equation to be solved. + variables: The variables involved in the equation. + grids: The grids on which the equation is defined. Defaults to None, + in which case the grids from the `self` object are used. + mode: The mode of operation ('NN', 'autograd', or 'mat'). Defaults to 'NN'. + + Returns: + dict: A dictionary representing the equation in a solver-friendly format. + The dictionary contains terms of the equation as keys, and each term + is represented by a dictionary containing its coefficient, term + structure, power, and variable information. + """ assert mode in ['NN', 'autograd', 'mat'], 'Incorrect mode passed. Form available only \ for "NN", "autograd "and "mat" methods' @@ -156,6 +245,28 @@ def adjust_shape(tensor, mode = 'NN'): def use_grids(self, grids=None): # if grids is None and self.grids is None: + """ + Uses provided grids or retrieves them from the global grid cache. + + If grids are not provided and the instance's grids are also None, + it retrieves all grids from the global grid cache. If grids are + provided, it checks if the number of provided grids matches the + number of grids in the global grid cache. If the provided grids + are NumPy arrays, they are converted to PyTorch tensors and moved + to the device specified by `self._device`. + + Args: + grids: A list of grids to use. If None, grids are retrieved from + the global grid cache. + + Returns: + None. + + Class Fields Initialized: + grids (list of torch.Tensor): The grids used by the instance. + Initialized with the provided grids or retrieved from the + global grid cache. + """ _, self.grids = global_var.grid_cache.get_all(mode = 'torch') elif grids is not None: if len(grids) != len(global_var.grid_cache.get_all(mode = 'torch')[1]): @@ -167,6 +278,21 @@ def use_grids(self, grids=None): # def form(self, grids=None, mode = 'NN'): + """ + Forms the equations into a solver-friendly format. + + Transforms the equations stored in the adaptee into a format suitable + for a numerical solver, associating each main variable with its + corresponding equation form. + + Args: + grids: Optional grid data to be used in the equation solving process. + mode: String indicating the solving mode, defaults to 'NN'. + + Returns: + A list of tuples, where each tuple contains the main variable to + explain and its corresponding equation form ready for the solver. + """ self.use_grids(grids=grids) equation_forms = [] diff --git a/epde/integrate/numeric_integration.py b/epde/integrate/numeric_integration.py index 90d9e86a..e10934a9 100644 --- a/epde/integrate/numeric_integration.py +++ b/epde/integrate/numeric_integration.py @@ -16,9 +16,18 @@ from epde.integrate.interface import SystemSolverInterface def get_terms_der_order(equation: Dict, variable_idx: int) -> np.ndarray: - ''' - Get the highest orders of the ``variable_idx``-th variable derivative in the equation terms. - ''' + """ + Get the highest derivative orders of a specified variable within each term of the equation. + This information is crucial for determining the complexity and structure of the equation, + which guides the search for the optimal equation form. + + Args: + equation (Dict): A dictionary representing the equation, where keys are term indices and values are dictionaries containing term information (variables, derivatives, powers). + variable_idx (int): The index of the variable for which to find the highest derivative orders. + + Returns: + np.ndarray: A NumPy array containing the highest derivative order of the specified variable in each term of the equation. + """ term_max_ord = np.zeros(len(equation)) for term_idx, term_dict in enumerate(equation.values()): @@ -44,6 +53,19 @@ def get_terms_der_order(equation: Dict, variable_idx: int) -> np.ndarray: return term_max_ord def get_higher_order_coeff(equation: Dict, orders: np.ndarray, var: int) -> Tuple[List]: + """ + Extracts coefficients corresponding to the highest derivative order with respect to a specified variable from the equation. + + This function identifies terms in the equation associated with the highest derivative order and isolates them as 'denominator terms'. The remaining terms are considered 'numerator terms'. The 'denominator terms' are then transformed by effectively nullifying the influence of the specified variable, preparing them for further analysis or simplification of the equation. + + Args: + equation: A dictionary representing the equation, where keys are term indices and values are dictionaries containing term information about each term. + orders: A NumPy array containing the derivative order of each term in the equation, corresponding to the term indices in the equation dictionary. + var: The variable index with respect to which the higher-order coefficients are extracted. + + Returns: + A list containing two lists: the first list contains the transformed 'denominator terms' (higher-order coefficients), and the second list contains the remaining 'numerator terms'. + """ def transform_term(term: Dict, deriv_key: list, var: int) -> Dict: term_filtered = copy.deepcopy(term) if (isinstance(term['var'], int) and term['var'] == var) or (isinstance(term['var'], list) @@ -67,6 +89,24 @@ def transform_term(term: Dict, deriv_key: list, var: int) -> Dict: return [denom_terms, numer_terms] def get_eq_order(equation, variables: List[str]): + """ + Identifies the variable with the highest derivative order in a given equation. + + This function is crucial for determining the complexity and structure of the + differential equation. By identifying the variable with the highest derivative, + we can understand which variable's dynamics are most influential in the equation. + This information is essential for equation discovery and simplification. + + Args: + equation: The equation to analyze, represented as a symbolic expression or string. + variables: A list of variable names present in the equation. + + Returns: + A tuple containing: + - The index of the variable with the highest derivative order in the `variables` list. + - A NumPy array representing the derivative orders of each term in the equation + with respect to the identified variable. + """ eq_var = 0; eq_orders = np.zeros(len(equation)) for var_idx in range(len(variables)): orders = get_terms_der_order(equation=equation, variable_idx=var_idx) @@ -75,13 +115,21 @@ def get_eq_order(equation, variables: List[str]): return eq_var, eq_orders def replace_operator(term: Dict, variables: List): - ''' - - Variables have to be in form of [(0, [None]), (0, [0,]), (0, [0, 0]), (0, [0, 0, 0]), (1, [None,]), ... ] - where the list elements are factors, taken as derivatives: (variable, differentiations), and the index in list - matches the index of dynamics operator output. - - ''' + """ + Replaces symbolic variable representations within a term with numerical indices based on a provided variable list. + + This transformation is crucial for efficiently representing and manipulating equations within the system, + allowing for streamlined processing during equation discovery and optimization. By converting symbolic + variables into numerical indices, the system can leverage optimized numerical operations for tasks such as + equation evaluation and comparison. + + Args: + term (Dict): A dictionary representing a term in the equation, containing information about the variable, its power, and any derivatives. + variables (List): A list of tuples, where each tuple represents a variable and its differentiation order, used to map symbolic variables to numerical indices. + + Returns: + Dict: A modified term dictionary with symbolic variables replaced by their corresponding numerical indices from the `variables` list. + """ term_ = copy.deepcopy(term) if isinstance(term_['var'], list) and len(term_['var']) > 1: for arg_idx, deriv_ord in enumerate(term_['term']): @@ -100,7 +148,49 @@ def replace_operator(term: Dict, variables: List): return term_ class ImplicitEquation(object): + """ + Represents an implicit equation system for solving differential equations. + + This class takes a system of equations, a grid, and a list of variables + to set up the problem for numerical solution. It parses the equations, + determines the order of each variable, and constructs dynamics operators. + + Class Methods: + - __init__ + - parse_cond + - __call__ + - grid_dict + - create_first_ord_eq + - merge_coeff + - term_callable + + Attributes: + grid_dict: A NumPy array representing the grid. + _dynamics_operators: A list of dynamics operators, each corresponding to a variable. + _var_order: A list of tuples, where each tuple contains a variable and its derivative order. + _vars_with_eqs: A dictionary mapping an index to a tuple containing a variable and its order within the system of equations. + """ + def __init__(self, system: List, grid: np.ndarray, variables: List[str]): + """ + Initializes the `ImplicitEquation` with a system of equations, a grid, and a list of variables. + + This constructor prepares the system for analysis by determining the order of each variable within the equations and constructing corresponding dynamics operators. These operators are essential for representing the relationships between variables and their derivatives, enabling the system to be solved or analyzed. + + Args: + system: A list of equations representing the system. + grid: A NumPy array representing the grid. + variables: A list of variable names. + + Fields: + grid_dict: A NumPy array representing the grid. + _dynamics_operators: A list of dynamics operators, each corresponding to a variable. + _var_order: A list of tuples, where each tuple contains a variable and its derivative order. + _vars_with_eqs: A dictionary mapping an index to a tuple containing a variable and its order within the system of equations. + + Returns: + None + """ self.grid_dict = grid # print(f'Solved system is {system}') @@ -128,6 +218,24 @@ def __init__(self, system: List, grid: np.ndarray, variables: List[str]): self._dynamics_operators.append(operator) def parse_cond(self, conditions: List[Union[BOPElement, dict]]): + """ + Parses boundary conditions to create a condition vector aligned with dynamic operators. + + This method processes a list of boundary conditions, which can be specified + as `BOPElement` objects or dictionaries. It maps these conditions to a + numerical vector, ensuring each dynamic operator has a corresponding + boundary value. This is crucial for setting up the problem before solving. + + Args: + conditions: A list of boundary conditions. Each condition can be + either a `BOPElement` or a dictionary. `BOPElement` should contain + information about variable index and term, dictionary should contain + 'bnd_op' and 'bnd_val' keys. + + Returns: + A NumPy array representing the condition values for each dynamic operator. + The array has the same length as the list of dynamic operators. + """ cond_val = np.full(shape = len(self._dynamics_operators), fill_value=np.inf) for cond in conditions: if isinstance(cond, BOPElement): @@ -153,6 +261,25 @@ def parse_cond(self, conditions: List[Union[BOPElement, dict]]): return cond_val def __call__(self, t, y): + """ + Evaluates the dynamics operators for a given state and time. + + This method calculates the values of the dynamics operators based on the + provided state `y` and time `t`. It iterates through each operator, + calculates the numerator and denominator using the `term_callable` method, + and computes the resulting value. A `ZeroDivisionError` is raised if any + denominator is close to zero. This evaluation is crucial for assessing + the equation's behavior at specific points in the search space, guiding + the evolutionary algorithm towards better solutions. + + Args: + t (float): The time value. + y (np.ndarray): The state vector. + + Returns: + np.ndarray: An array containing the evaluated values of the dynamics + operators. + """ values = np.empty(len(self._dynamics_operators)) for idx, operator in enumerate(self._dynamics_operators): if operator[0] is None: @@ -167,25 +294,86 @@ def __call__(self, t, y): @property def grid_dict(self): + """ + Returns a dictionary containing the grid values, rounded for discrete representation. This is useful for tasks such as visualization or when the equation discovery process benefits from discrete representations of the data domain. + + Args: + None + + Returns: + dict: A dictionary where keys represent grid points and values are the corresponding rounded values. + """ return self._grid_rounded @grid_dict.setter def grid_dict(self, grid_points): + """ + Generates a dictionary mapping rounded grid points to their indices. + + This method calculates the grid step size, rounds the grid points to + a precision determined by the grid step, and creates a dictionary + where the keys are the rounded grid point values and the values are + their corresponding indices in the original grid points array. This + dictionary is used to efficiently find the grid index closest to a + given value during the equation discovery process. + + Args: + grid_points: A NumPy array of grid point values. + + Returns: + dict: A dictionary where keys are rounded grid point values and + values are their indices. + + Initializes: + _grid_step (float): The step size between grid points, calculated + as the difference between the second and first grid points. + _grid_rounded (dict): A dictionary mapping rounded grid point values + to their indices in the original `grid_points` array. + """ self._grid_step = grid_points[1] - grid_points[0] digits = np.floor(np.log10(self._grid_step/2.)-1) self._grid_rounded = {np.round(grid_val, -int(digits)): idx for idx, grid_val in np.ndenumerate(grid_points)} def create_first_ord_eq(self, var: int) -> List[Tuple]: - ''' - Example of order: np.array([3., 0., 0.]) for third ord eq. - ''' + """ + Creates a first-order equation term for a specified variable. + + This is a fundamental building block for constructing more complex differential equations. + It ensures that each term adheres to the defined structure, which is essential for the evolutionary process. + + Args: + var (int): The index of the variable to create the first-order term for. + + Returns: + List[Tuple]: A list containing a dictionary representing the first-order equation term. + The dictionary includes the coefficient, term, power, and variable index. + """ return [{'coeff' : -1., 'term' : [None,], 'pow' : 1, 'var' : var},] def merge_coeff(self, coeff: np.ndarray, t: float): + """ + Merges coefficients based on time, leveraging the temporal grid. + + This method retrieves a coefficient value for a given time `t`. It first attempts to + directly access the coefficient from the internal grid dictionary. If the time `t` + is not explicitly present in the grid, it interpolates the coefficient value + between the nearest neighboring grid points. This ensures a continuous representation + of the coefficient over time, even when data is only available at discrete points. + This is important to correctly represent coefficient values at any given point in time + during the equation discovery process. + + Args: + coeff (np.ndarray): Coefficient array representing the coefficient values at grid points. + t (float): Time value for which to retrieve/interpolate the coefficient. + + Returns: + float: The coefficient value at time `t`, either retrieved directly from the grid + or interpolated between neighboring grid points. + """ try: return self.grid_dict[t] except KeyError: @@ -199,6 +387,23 @@ def merge_coeff(self, coeff: np.ndarray, t: float): return val def term_callable(self, term: Dict, t, y): + """ + Evaluates a single term in the equation based on its definition. + + This method calculates the value of a term in the equation, considering + its coefficient, variables, and powers. It handles different types of + coefficients (callables, neural networks, arrays, or constants) and + variables (indices or tuples of indices). It also supports callable or + neural network powers. This flexibility is crucial for representing a wide range of equation structures that the evolutionary algorithm might discover. + + Args: + term: A dictionary defining the term, containing keys 'coeff', 'var', and 'pow'. + t: The current time value. + y: The current state vector. + + Returns: + float: The calculated value of the term. + """ def call_ann_token(token_nn: torch.nn.Sequential, arguments: list, t: float, y: np.ndarray): return token_nn[torch.from_numpy(y[tuple(arguments)]).reshape((-1, 1))].detach().numpy() # Hereby, the ANN does not explicitly depend on time @@ -251,12 +456,68 @@ def call_ann_token(token_nn: torch.nn.Sequential, arguments: list, class OdeintAdapter(object): + """ + Adapts the odeint solver for use with EPDE framework. + + This class provides an interface to solve systems of ordinary differential + equations using the odeint solver. It handles the conversion of EPDE + systems into a format suitable for odeint and manages the solution process. + + Class Methods: + - __init__ + - solve_system + - build_ode_problem + - solve + """ + def __init__(self, method: str = 'Radau'): + """ + Initializes the solver adapter with a specified numerical integration method. + + This constructor configures the adapter to use a particular method for solving the discovered differential equations. + The choice of the integration method can significantly impact the accuracy and efficiency of the equation solving process, + allowing the framework to explore a wider range of equation candidates and refine the discovered models. + + Args: + method (str): The numerical integration method to be used (default: 'Radau'). + + Returns: + None + + Class Fields: + _solve_method (str): The solving method to be used. Initialized with the value of the `method` parameter. + """ self._solve_method = method pass # TODO: implement hyperparameters setup, according to the problem specifics def solve_epde_system(self, system: Union[SoEq, dict], grids: list=None, boundary_conditions=None, mode='NN', data=None, vars_to_describe = ['u'], *args, **kwargs): + """ + Solves a system of equations, preparing it for the numerical solver. + + This method takes a system of equations, either in a structured format + or as a list of solver forms, and prepares it for solution. It sets up + the necessary boundary conditions, either using provided conditions or + generating default ones based on the input data and grids. This ensures + the system is properly configured before being passed to the solver. + + Args: + system (Union[SoEq, dict]): The system of equations to solve. Can be a SoEq object or a list of solver forms. + grids (list, optional): The spatial and temporal grids on which to solve the system. Defaults to None. + boundary_conditions (optional): Boundary conditions for the system. If None, default conditions are generated. Defaults to None. + mode (str, optional): The mode for adapting the system. Defaults to 'NN'. + data (optional): Data used for generating default boundary conditions. Defaults to None. + vars_to_describe (list, optional): List of variables to describe in the solution. Defaults to ['u']. + *args: Additional positional arguments. + **kwargs: Additional keyword arguments. + + Returns: + The solution to the EPDE system. + + WHY: This method prepares the system of equations for the solver by handling different input formats, + setting up boundary conditions, and extracting relevant variables. This ensures that the solver receives + a properly configured system, leading to accurate and reliable solutions. + """ if isinstance(system, SoEq): system_interface = SystemSolverInterface(system_to_adapt=system) system_solver_forms = system_interface.form(grids = grids, mode = mode) @@ -283,6 +544,23 @@ def solve_epde_system(self, system: Union[SoEq, dict], grids: list=None, boundar def solve(self, equations, domain: Union[Domain, np.ndarray], boundary_conditions: List[BOPElement] = None, vars: List[str] = ['x',], *args, **kwargs): + """ + Solves the implicit equation defined by the given equations, domain, and boundary conditions using numerical integration. + + This method leverages `scipy.integrate.solve_ivp` to find a numerical solution to the system of ordinary differential equations defined by `equations`. The solution is computed over the specified `domain`, starting from the provided `boundary_conditions`. The method returns the solution trajectory evaluated at points within the domain. This is a crucial step in the equation discovery process, as it allows us to evaluate how well a candidate equation fits the observed data by comparing the numerical solution to the data. + + Args: + equations: A list of equations that define the implicit equation. + domain: The domain over which to solve the equation. It can be a + `Domain` object or a NumPy array. + boundary_conditions: A list of boundary conditions for the equation. + vars: A list of variable names used in the equations. + + Returns: + A tuple containing: + - An integer status code (0 for success). + - A NumPy array representing the solution of the equation. + """ if not isinstance(equations, list): raise RuntimeError('Equations have to be passed as a list.') self._implicit_equation = ImplicitEquation(equations, domain, vars) @@ -299,6 +577,21 @@ def solve(self, equations, domain: Union[Domain, np.ndarray], return 0, solution.y.T def loadSpectralSolver(path_to_so: str): + """ + Loads a spectral solver library from a shared object file. + + This method loads a spectral solver library (a .so file) using `ctypes.PyDLL`. + It configures the argument and return types for the solver function within the library + to ensure compatibility with the evolutionary algorithm's data structures. + This is necessary for evaluating candidate equations by numerically solving them + and comparing the solutions to the observed data. + + Args: + path_to_so: The path to the shared object file (.so) containing the spectral solver. + + Returns: + The loaded solver library as a `ctypes.PyDLL` object, or `None` if loading fails. + """ try: # TODO: # Using PyDLL (particularly .so file), that is inported via ctypes is a kludge @@ -311,7 +604,52 @@ def loadSpectralSolver(path_to_so: str): return None class SpectralAdapter(object): + """ + Adapts a spectral solver to a unified interface. + + This class serves as an adapter for spectral solvers, providing a consistent + interface for solving equations using different spectral methods. It handles + the setup, execution, and result retrieval from the underlying solver. + + Class Methods: + - __init__ + - set_parameters + - solve + - get_result + - get_named_result + - get_raw_result + - is_finished + - has_errors + - get_error_message + - get_solver_statistics + - get_solution + - get_named_solution + - get_raw_solution + - get_internal_data + - set_internal_data + """ + def __init__(self, path_to_so: str = None, **kwargs): + """ + Initializes the SpectralSolverWrapper, preparing the solver for equation discovery. + + This involves loading the spectral solver library and configuring it + for subsequent operations such as basis function evaluation and + spectral coefficient calculation, which are crucial steps in + transforming data into a spectral representation suitable for + equation learning. + + Args: + path_to_so: Path to the spectral solver shared object file. + **kwargs: Arbitrary keyword arguments to be stored as parameters. + + Returns: + None + + Class Fields: + params (dict): A dictionary storing arbitrary keyword arguments passed during initialization. + spectral_solver: The loaded spectral solver object. + """ self.params = kwargs if path_to_so is None: @@ -323,6 +661,30 @@ def __init__(self, path_to_so: str = None, **kwargs): def solve_epde_system(self, system: Union[SoEq, dict], grids: list=None, boundary_conditions=None, mode='NN', data=None, vars_to_describe = ['u'], *args, **kwargs): + """ + Solves an EPDE system using the specified solver to find the best equation representation. + + This method takes a system of equations and transforms it into a format suitable + for the solver. It then uses the solver to find the solution that best fits + the data, effectively identifying the underlying differential equation. Default + boundary conditions are generated if none are provided. This is done to ensure + the solver has all the necessary information to find a valid solution. + + Args: + system: The system of equations to solve. Can be a SoEq object or a list + of solver forms. + grids: The spatial and temporal grids on which to solve the system. + boundary_conditions: The boundary conditions for the system. If None, + default boundary conditions are generated. + mode (str): The mode of operation for the solver. Defaults to 'NN'. + data: The data to use for solving the system. Defaults to None. + vars_to_describe (list): A list of variables to describe. Defaults to ['u']. + *args: Additional positional arguments. + **kwargs: Additional keyword arguments. + + Returns: + The solution to the EPDE system. + """ if isinstance(system, SoEq): system_interface = SystemSolverInterface(system_to_adapt=system) system_solver_forms = system_interface.form(grids = grids, mode = mode) @@ -346,6 +708,29 @@ def solve_epde_system(self, system: Union[SoEq, dict], grids: list=None, boundar def solve(self, equations, domain: Union[Domain, np.ndarray], boundary_conditions: List[BOPElement] = None, vars: List[str] = ['x',], *args, **kwargs): + """ + Solves a system of equations using a spectral solver. + + This method leverages a spectral solver to find solutions that satisfy the given equations within the specified domain and boundary conditions. + It prepares the domain by converting it into a suitable grid format for the solver. + The method returns the solution obtained from the spectral solver. + + Args: + equations: The system of equations to solve. + domain: The domain over which to solve the equations. Can be a + Domain object or a numpy array. + boundary_conditions: A list of boundary conditions to apply. Defaults to None. + vars: A list of variable names. Defaults to ['x']. + + Returns: + tuple: A tuple containing: + - An integer status code (0 for success). + - The solution obtained from the spectral solver. + + Why: This method automates the process of finding solutions for differential equations, + which is a crucial step in discovering governing equations from data. + By using a spectral solver, it efficiently finds solutions that fit the observed data within the given constraints. + """ if not isinstance(equations, list): raise RuntimeError('Incorrect type of equations passed into odeint solver.') diff --git a/epde/integrate/pinn_integration.py b/epde/integrate/pinn_integration.py index 009909d6..78a413f6 100644 --- a/epde/integrate/pinn_integration.py +++ b/epde/integrate/pinn_integration.py @@ -128,6 +128,26 @@ } def solver_formed_grid(training_grid=None, grid_var_keys=None, device = 'cpu'): + """ + Forms a grid for solver input. + + Transforms training grid data into a PyTorch tensor, preparing it for use + within the solver by ensuring it has the correct format and resides on the + appropriate device. This transformation is crucial for efficient + numerical computation within the equation discovery process. The grid + represents the independent variable space over which the solution is + evaluated. + + Args: + training_grid: The training grid data. If None, it's retrieved from cache. + grid_var_keys: Keys associated with the grid variables. If None and + training_grid is provided, keys are retrieved from cache. + device: The device to place the resulting tensor on (e.g., 'cpu', 'cuda'). + + Returns: + torch.Tensor: A tensor representing the formed grid, transposed and + placed on the specified device. The tensor's data type is float32. + """ if training_grid is None: keys, training_grid = global_var.grid_cache.get_all(mode = 'torch') elif grid_var_keys is None: @@ -140,7 +160,45 @@ def solver_formed_grid(training_grid=None, grid_var_keys=None, device = 'cpu'): class SolverAdapter(object): + """ + A base class for solver adapters, providing a common interface for different solver implementations. + + Class Methods: + - solve_epde_system: Solves the EPDE system based on the provided equations and domain. + - solve: Solves the equation using the specified method. + - create_domain: Creates a Domain object from a list of variables and grids. + - change_parameter: Changes a specified parameter within the object's parameter dictionaries. + - set_training_params: Sets training parameters for the solver. + - set_plotting_params: Sets plotting parameters for the solver. + - set_early_stopping_params: Sets the early stopping parameters for the solver. + - set_cache_params: Sets the cache parameters for the solver. + - set_optimizer_params: Sets the optimizer parameters for the solver. + - set_compiling_params: Sets the compiling parameters for the solver. + - get_net: Generates a neural network based on the specified mode. + - set_net: Sets the network attribute of the object. + - mode: Returns the current compilation mode. + - __init__: Initializes the class instance. + """ + def __init__(self, net=None, use_cache: bool = True, device: str = 'cpu'): + """ + Initializes the SolverAdapter for equation discovery. + + The SolverAdapter manages the optimization and training process for identifying differential equations. + This initialization configures the core components required for the search, including the network, + device for computation, and default parameters for various stages like compiling, optimization, + caching, early stopping, plotting, and training. These parameters can be adjusted later to fine-tune + the discovery process. The adapter needs to be initialized with the network and other parameters + to properly conduct the search for differential equations. + + Args: + net: The network to be used. + use_cache: A boolean indicating whether to use cache. + device: The device to run the computations on. + + Returns: + None. + """ self._device = device self.set_net(net) @@ -169,15 +227,58 @@ def __init__(self, net=None, use_cache: bool = True, device: str = 'cpu'): @property def mode(self): + """ + Returns the current compilation mode. + + This method retrieves the compilation mode, which influences the trade-off between compilation speed and the ability to explore a wider range of equation structures. Different modes might prioritize faster iteration during the equation discovery process or focus on a more thorough search for potentially complex solutions. + + Args: + self: The object instance. + + Returns: + str: The current compilation mode. + """ return self._compiling_params['mode'] def set_net(self, net: torch.nn.Sequential): + """ + Sets the neural network model. + + This method allows to update the neural network used for approximating solutions of differential equations. + By setting a new network, the solver can explore different solution spaces and potentially find better approximations. + + Args: + net (torch.nn.Sequential): The new neural network to be assigned. + + Returns: + None. + + Class Fields: + net (torch.nn.Sequential): The neural network model. + """ # if self.net is not None and self.net = net @staticmethod def get_net(equations, mode: str, domain: Domain, use_fourier = True, fft_params: dict = {'L' : [4,], 'M' : [3,]}, device: str = 'cpu'): + """ + Generates a neural network suitable for solving differential equations, adapting the network structure based on the specified mode. + + Args: + equations: The equations to be solved. + mode: The mode of operation ('mat', 'autograd', or 'NN'). 'mat' returns a simplified model, while others return a full neural network. + domain: The domain of the problem. + use_fourier: Flag to use Fourier transform in the network. + fft_params: Parameters for the Fourier transform. + device: The device to use for computation ('cpu' or 'cuda'). + + Returns: + A neural network model. Returns a mat_model if mode is 'mat', otherwise returns a solution network created by create_solution_net. + + Why: + The method creates different network architectures based on the chosen mode. This allows the framework to explore different solution strategies, from simplified models for initial exploration to more complex neural networks capable of capturing intricate relationships within the data when discovering underlying equations. + """ if mode == 'mat': return mat_model(domain, equations) elif mode in ['autograd', 'NN']: @@ -189,6 +290,30 @@ def set_compiling_params(self, mode: str = None, lambda_operator: float = None, lambda_bound : float = None, normalized_loss_stop: bool = None, h: float = None, inner_order: str = None, boundary_order: str = None, weak_form: List[Callable] = None, tol: float = None, derivative_points: int = None): + """ + Sets the compilation parameters to tailor the equation discovery process. + + This method configures the solver's behavior by updating its internal + compilation parameters. It selectively updates parameters if they are + provided, ensuring that the solver uses the specified settings during + the equation search. This allows users to fine-tune the search process + based on their specific problem and data characteristics. + + Args: + mode: The mode of compilation. + lambda_operator: The lambda operator value. + lambda_bound: The lambda bound value. + normalized_loss_stop: Flag for normalized loss stopping. + h: The step size. + inner_order: The order of the inner approximation. + boundary_order: The order of the boundary approximation. + weak_form: The weak form of the equation. + tol: The tolerance value. + derivative_points: The number of derivative points. + + Returns: + None + """ compiling_params = {'mode' : mode, 'lambda_operator' : lambda_operator, 'lambda_bound' : lambda_bound, 'normalized_loss_stop' : normalized_loss_stop, 'h' : h, 'inner_order' : inner_order, 'boundary_order' : boundary_order, 'weak_form' : weak_form, 'tol' : tol, @@ -206,6 +331,33 @@ def set_compiling_params(self, mode: str = None, lambda_operator: float = None, def set_optimizer_params(self, optimizer: str = None, params: Dict[str, float] = None, gamma: float = None, decay_every: int = None): + """ + Sets the optimizer parameters for the equation discovery process. + + This method configures the optimization algorithm used to refine the equation coefficients. + It updates the internal dictionary of optimizer parameters with the provided values, if they are not None. + It also performs validation on the optimizer name and sets default parameters if an optimizer is specified. + This ensures that the equation discovery process uses a valid and properly configured optimization strategy. + + Args: + optimizer: The name of the optimizer to use (e.g., 'Adam', 'SGD'). + params: A dictionary of optimizer-specific parameters. + gamma: The learning rate decay factor. + decay_every: The frequency of learning rate decay. + + Returns: + None. + + Class Fields Initialized: + _optimizer_params (Dict[str, Any]): A dictionary storing the optimizer parameters, + including the optimizer name, parameters, gamma, and decay frequency. + This dictionary is updated by this method. + + Why: + Configuring the optimizer is crucial for effectively searching the space of possible equation coefficients. + This method allows users to specify the optimization algorithm and its parameters, + enabling them to fine-tune the equation discovery process for optimal performance. + """ optim_params = {'optimizer' : optimizer, 'params' : params, 'gamma' : gamma, 'decay_every' : decay_every} @@ -230,6 +382,20 @@ def set_cache_params(self, cache_verbose: bool = None, cache_model: Sequential = 'model_randomize_parameter' : model_randomize_parameter, 'clear_cache' : clear_cache} for param_key, param_vals in cache_params.items(): + """ + Sets the cache parameters for the solver. + + This method configures the solver's caching behavior, allowing for optimization of repeated equation evaluations. By adjusting parameters like verbosity, cached models, and randomization, the efficiency of the equation discovery process can be significantly improved. This is particularly useful when exploring a large space of potential equation structures. + + Args: + cache_verbose: Verbosity level for caching. + cache_model: The model to be cached (e.g., a neural network). + model_randomize_parameter: Parameter to randomize the model. + clear_cache: Flag indicating whether to clear the cache. + + Returns: + None. + """ if param_vals is not None: try: if param_vals == 'None': @@ -242,6 +408,25 @@ def set_cache_params(self, cache_verbose: bool = None, cache_model: Sequential = def set_early_stopping_params(self, eps: float = None, loss_window: int = None, no_improvement_patience: int = None, patience: int = None, abs_loss: float = None, normalized_loss: bool = None, randomize_parameter: float = None, info_string_every: int = None, verbose: bool = None): + """ + Sets the early stopping parameters for the solver. + + This method configures the early stopping criteria used during the equation discovery process. By adjusting parameters like patience, loss tolerance, and verbosity, the search can be fine-tuned to stop when satisfactory progress is no longer being made, thus saving computational resources. + + Args: + eps: The minimum difference between two consecutive loss values to be considered an improvement. + loss_window: The number of loss values to consider when calculating the average loss. + no_improvement_patience: The number of iterations to wait without improvement before stopping. + patience: The number of iterations to wait before stopping. + abs_loss: The absolute loss value to reach before stopping. + normalized_loss: Whether to use normalized loss for early stopping. + randomize_parameter: The parameter to randomize. + info_string_every: The frequency at which to print early stopping information. + verbose: Whether to print early stopping information. + + Returns: + None + """ early_stopping_params = {'eps' : eps, 'loss_window' : loss_window, 'no_improvement_patience' : no_improvement_patience, 'patience' : patience, 'abs_loss' : abs_loss, 'normalized_loss' : normalized_loss, 'randomize_parameter' : randomize_parameter, 'info_string_every' : info_string_every, @@ -259,6 +444,23 @@ def set_early_stopping_params(self, eps: float = None, loss_window: int = None, def set_plotting_params(self, save_every: int = None, print_every: int = None, title: str = None, img_dir: str = None): + """ + Sets the parameters that control how the solver's progress and results are visualized. + + This method configures the plotting behavior of the solver by updating its internal plotting parameters. + It allows users to specify how frequently plots are saved and information is printed, as well as + the directory where plot images are stored. This is essential for monitoring the solver's progress + and analyzing the discovered equations. + + Args: + save_every: The interval (in iterations) at which to save plots. + print_every: The interval (in iterations) at which to print plotting information. + title: The title to use for the plots. (Not used in the method's body) + img_dir: The directory to save the plot images. + + Returns: + None. + """ plotting_params = {'save_every' : save_every, 'print_every' : print_every, 'print_every' : print_every, 'img_dir' : img_dir} for param_key, param_vals in plotting_params.items(): @@ -273,6 +475,24 @@ def set_plotting_params(self, save_every: int = None, print_every: int = None, t def set_training_params(self, epochs: int = None, info_string_every: int = None, mixed_precision: bool = None, save_model: bool = None, model_name: str = None): + """ + Sets training parameters for the solver. + + This method configures the training process by allowing users to specify parameters such as the number of epochs, + information printing frequency, mixed precision usage, model saving behavior, and the model's name. + These parameters influence how the solver refines its equation discovery process. By adjusting these parameters, + users can fine-tune the training to achieve optimal equation discovery and model performance. + + Args: + epochs: The number of training epochs. + info_string_every: The frequency of printing training information. + mixed_precision: Whether to use mixed precision training. + save_model: Whether to save the trained model. + model_name: The name to use when saving the model. + + Returns: + None + """ training_params = {'epochs' : epochs, 'info_string_every' : info_string_every, 'mixed_precision' : mixed_precision, 'save_model' : save_model, 'model_name' : model_name} @@ -287,6 +507,33 @@ def set_training_params(self, epochs: int = None, info_string_every: int = None, print(f'Parameter {param_key} can not be passed into the solver.') def change_parameter(self, parameter: str, value, param_dict_key: str = None): + """ + Changes a specified parameter within the solver's configuration. + + This method facilitates dynamic adjustment of solver settings, enabling users + to fine-tune the equation discovery process without directly modifying the code. + It updates parameters related to compiling, optimization, caching, early stopping, + plotting, and training, allowing for experimentation with different configurations + to improve the accuracy and efficiency of equation discovery. + + Args: + parameter (str): The name of the parameter to change. + value: The new value for the parameter. If None, it's converted to the string 'None'. + param_dict_key (str, optional): An optional key specifying which parameter dictionary to target directly. + If provided, the method only searches within that specific dictionary. + Valid keys are 'compiling_params', 'optimizer_params', 'cache_params', + 'early_stopping_params', 'plotting_params', and 'training_params'. Defaults to None. + + Returns: + None: The method modifies the object's internal parameter dictionaries directly. + + Why: + This method allows users to modify solver parameters, which is essential for + adapting the equation discovery process to different datasets and problem settings. + By changing parameters, users can influence the search strategy, optimization + behavior, and other aspects of the solver, ultimately affecting the quality + and speed of the discovered equations. + """ setters = {'compiling_params' : (BASE_COMPILING_PARAMS, self.set_compiling_params), 'optimizer_params' : (BASE_OPTIMIZER_PARAMS, self.set_optimizer_params), 'cache_params' : (BASE_CACHE_PARAMS, self.set_cache_params), @@ -309,6 +556,24 @@ def change_parameter(self, parameter: str, value, param_dict_key: str = None): @staticmethod def create_domain(variables: List[str], grids : List[Union[np.ndarray, torch.Tensor]], device: str = 'cpu') -> Domain: + """ + Creates a `Domain` object representing the problem's spatial or temporal domain. + + This method constructs the domain by associating variable names with their corresponding grid values, + ensuring that the number of variables matches the number of provided grids. The grid values are converted + to a consistent format (PyTorch tensors) and placed on the specified device. This domain representation + is crucial for defining the space in which the differential equations are solved. + + Args: + variables: A list of variable names (strings) that define the dimensions of the domain. + grids: A list of grids, where each grid is a NumPy array or a PyTorch tensor + representing the values for the corresponding variable. These grids define the + discretization of each dimension in the domain. + device: The device to move the grids to (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + + Returns: + Domain: A Domain object representing the domain defined by the variables and grids. + """ assert len(variables) == len(grids), f'Number of passed variables {len(variables)} does not \ match number of grids {len(grids)}.' if isinstance(grids[0], np.ndarray): @@ -328,6 +593,37 @@ def solve_epde_system(self, system: Union[SoEq, dict], grids: list=None, boundar mode='NN', data=None, use_cache: bool = False, use_fourier: bool = False, fourier_params: dict = None, use_adaptive_lambdas: bool = False, to_numpy: bool = False, grid_var_keys = None, *args, **kwargs): + """ + Solves the EPDE system based on the provided equations and domain. + + This method orchestrates the solution of a system of equations by first discretizing it over a specified domain. + It then applies necessary boundary conditions and employs a chosen solving mode (e.g., 'NN' for neural network-based solver) + to find the solution. This process is essential for identifying the underlying differential equations from data + by comparing the solutions obtained with different equation structures. + + Args: + system: The system of equations to solve. Can be a SoEq object, + a dictionary of equations, or a list of equations. + grids: The grids on which to discretize the equations. If None, + grids are retrieved from the global grid cache. + boundary_conditions: The boundary conditions to apply. If None, + default boundary conditions are generated based on the data and grids. + mode: The solving mode (e.g., 'NN'). + data: Data used for generating default boundary conditions if + `boundary_conditions` is None. + use_cache: Whether to use cached results if available. + use_fourier: Whether to use Fourier features. + fourier_params: Parameters for the Fourier features. + use_adaptive_lambdas: Whether to use adaptive lambda parameters. + to_numpy: Whether to convert the solution to a NumPy array. + grid_var_keys: Keys for the grid variables. If None, keys are retrieved from the global grid cache. + *args: Additional positional arguments passed to the solver. + **kwargs: Additional keyword arguments passed to the solver. + + Returns: + The solution to the EPDE system. The type of the returned object + depends on the solver and the `to_numpy` flag. + """ solver_device(device = self._device) if isinstance(system, SoEq): @@ -369,6 +665,33 @@ def solve(self, equations: Union[List, SoEq, SolverEquation], domain: Domain, boundary_conditions = None, mode = 'NN', use_cache: bool = False, use_fourier: bool = False, fourier_params: dict = None, # epochs = 1e3, use_adaptive_lambdas: bool = False, to_numpy = False, *args, **kwargs): + """ + Solves the given equation within the specified domain, aiming to find the best possible solution by optimizing the network parameters. + + This method orchestrates the solution process, taking into account boundary conditions, network configurations, and optimization strategies to minimize the loss function. It leverages techniques like caching, Fourier features, and adaptive lambdas to enhance the solution's accuracy and efficiency. + + Args: + equations: The equation(s) to be solved. It can be a single + `SolverEquation` object or a list of equations to be added to a + `SolverEquation` object. + domain: The domain over which the equation is solved. + boundary_conditions: Boundary conditions for the equation. Defaults to None. + mode: Specifies the solution mode ('NN', 'autograd', or 'mat'). Defaults to 'NN'. + use_cache: A boolean indicating whether to use caching. Defaults to False. + use_fourier: A boolean indicating whether to use Fourier features. Defaults to False. + fourier_params: Parameters for the Fourier features. Defaults to None. + use_adaptive_lambdas: A boolean indicating whether to use adaptive + lambdas. Defaults to False. + to_numpy: A boolean indicating whether to convert the solution to a + NumPy array. Defaults to False. + *args: Additional positional arguments. + **kwargs: Additional keyword arguments. + + Returns: + tuple: A tuple containing the final loss value and the solution. + The solution can be a PyTorch tensor or a NumPy array, depending + on the `mode` and `to_numpy` parameters. + """ if isinstance(equations, SolverEquation): equations_prepared = equations diff --git a/epde/interface/equation_translator.py b/epde/interface/equation_translator.py index eea4246b..99968738 100755 --- a/epde/interface/equation_translator.py +++ b/epde/interface/equation_translator.py @@ -15,6 +15,17 @@ from functools import singledispatch def float_convertable(obj): + """ + Checks if a given object can be interpreted as a floating-point number. + + This function is crucial for ensuring that data used in equation discovery is of the correct type, preventing errors during the evaluation of equation fitness. By verifying the convertibility of data points to floats, the system avoids attempting mathematical operations on incompatible data types, which would lead to inaccurate or invalid results in the equation search process. + + Args: + obj: The object to check for float convertibility. + + Returns: + bool: True if the object can be converted to a float without error, False otherwise. + """ try: float(obj) return True @@ -23,10 +34,54 @@ def float_convertable(obj): @singledispatch def translate_equation(text_form, pool, all_vars): + """ + Translates an equation from a text form into an executable representation. + + This is the base implementation that raises a `NotImplementedError`. + Specific text formats (e.g., SymPy, LaTeX) should have their own + specialized implementations registered using `singledispatch`. + + Args: + text_form: The equation in a text-based format. + pool: A pool of resources or variables available for translation. + all_vars: A collection of all variables used in the equation. + + Returns: + NotImplementedError: Always raises an exception indicating that + the translation is not implemented for the given text form. + + WHY: This base case ensures that all equation types must be explicitly + handled by a dedicated translation function, preventing unexpected + behavior when encountering an unsupported format during the equation + discovery process. + """ raise NotImplementedError(f'Equation shall be translated from {type(text_form)}') @translate_equation.register def _(text_form : str, pool, all_vars: List[str], use_pic: bool = False): + """ + Parses a text-based equation into a system of equations suitable for symbolic regression. + + This method takes a string representation of an equation, parses it into individual terms, + and constructs an `Equation` object representing the mathematical relationship. This `Equation` + is then encapsulated within a `SoEq` (System of Equations) object, which provides a structure + for further analysis and optimization within the EPDE framework. The parsing process involves + identifying factors, handling numerical coefficients, and extracting relevant metadata. + + Args: + text_form: The string representation of the equation to be parsed. + pool: A pool object for parallel processing, enabling efficient computation of equation terms. + all_vars: A list of all variable names present in the equation, used for creating symbolic + representations of the terms. + use_pic: A boolean flag indicating whether to use a default multiobjective function. + + Returns: + SoEq: The created system of equations object, ready for symbolic regression and analysis. + + WHY: This method transforms a human-readable equation string into a structured, symbolic representation + that the EPDE framework can then use to perform symbolic regression, identify governing equations, + and build predictive models. + """ parsed_text_form = parse_equation_str(text_form) term_list = [] weights = np.empty(len(parsed_text_form) - 1) @@ -70,6 +125,25 @@ def _(text_form : str, pool, all_vars: List[str], use_pic: bool = False): @translate_equation.register def _(text_form : dict, pool, all_vars: List[str], use_pic: bool = False): + """ + Generates a system of equations (SoEq) from a text-based equation form. + + This method parses a dictionary of equation strings, converts them into Equation objects, + and combines them into a SoEq object to represent a complete equation system. It prepares + the equations for the evolutionary search process by creating metaparameters and setting up + the multiobjective function, which guides the search towards optimal equation structures. + This is a crucial step in translating human-readable equations into a format suitable for + automated discovery and refinement. + + Args: + text_form: A dictionary where keys are variable names and values are equation strings. + pool: A pool object (likely for parallel processing or resource management). + all_vars: A list of all variable names used in the equations. + use_pic: A boolean flag indicating whether to use a default multiobjective function. + + Returns: + SoEq: A system of equations object representing the parsed equations. + """ equations = [] for var_key, eq_text_form in text_form.items(): parsed_text_form = parse_equation_str(eq_text_form) @@ -117,11 +191,22 @@ def _(text_form : dict, pool, all_vars: List[str], use_pic: bool = False): def parse_equation_str(text_form): ''' - - Example input: '0.0 * d^3u/dx2^3{power: 1} * du/dx2{power: 1} + 0.0 * d^3u/dx1^3{power: 1} + - 0.015167810810763344 * d^2u/dx1^2{power: 1} + 0.0 * d^3u/dx2^3{power: 1} + 0.0 * du/dx2{power: 1} + - 4.261009307104081e-07 = d^2u/dx1^2{power: 1} * du/dx1{power: 1}' - + Parses a string representation of an equation into a structured format. + + This function is essential for transforming symbolic equations into a processable format. + It splits the equation into left-hand side and right-hand side components, + further breaking down each side into individual terms. This structured representation + facilitates subsequent analysis and manipulation of the equation's components. + + Args: + text_form (str): A string representing the equation, e.g., + '0.0 * d^3u/dx2^3{power: 1} * du/dx2{power: 1} + 0.0 * d^3u/dx1^3{power: 1} + + 0.015167810810763344 * d^2u/dx1^2{power: 1} + 0.0 * d^3u/dx2^3{power: 1} + 0.0 * du/dx2{power: 1} + + 4.261009307104081e-07 = d^2u/dx1^2{power: 1} * du/dx1{power: 1}' + + Returns: + list: A list containing the terms from the left-hand side and right-hand side of the equation. + Each side is further split into a list of terms. ''' left, right = text_form.split(' = ') left = left.split(' + ') @@ -132,6 +217,21 @@ def parse_equation_str(text_form): def parse_term_str(term_form): + """ + Parses a term string into a structured representation suitable for equation discovery. + + This function takes a string representing a term in a differential equation and + transforms it into a format that the EPDE framework can use for symbolic manipulation + and evaluation. The parsed representation facilitates the automated search for + equation structures that best fit the observed data. + + Args: + term_form (str): The term string to parse. + + Returns: + None: The function's return type is currently None, but it would ideally return + a structured representation of the parsed term (e.g., a tree or a list of tokens). + """ pass @@ -139,6 +239,22 @@ def parse_factor(factor_form, pool, all_vars): # В проект: работы # print(factor_form) label_str, params_str = tuple(factor_form.split('{')) if '}' not in params_str: + """ + Parses a factor from its string representation to construct a component of the overall equation. + + This method dissects a factor's string representation, extracting its label and parameters. + It then instantiates a corresponding factor object from a predefined pool of factor families + and configures its 'power' parameter, effectively defining a term within a larger equation. + This process is crucial for building equation structures that can be optimized to fit observed data. + + Args: + factor_form (str): The string representation of the factor, e.g., "factor_label{param1=value1,param2=value2}". + pool (FactorPool): The pool of factor families to search for the appropriate family. + all_vars (list): A list of all variables. + + Returns: + Factor: The created factor object, representing a component of the equation. + """ raise ValueError('Missing brackets, denoting parameters part of factor text form. Possible explanation: passing wrong argument') params_str = parse_params_str(params_str.replace('}', '')) # print(label_str, params_str) @@ -149,6 +265,23 @@ def parse_factor(factor_form, pool, all_vars): # В проект: работы def parse_params_str(param_str): + """ + Parses a comma-separated string of parameters into a dictionary to configure equation discovery process. + + This function is essential for setting up the parameters of the evolutionary search, + allowing users to specify configurations like population size, mutation rates, and + other optimization settings via a string format. The input string should follow + the format "key1:value1,key2:value2,...", where values are automatically converted + to integers or floats based on their format. + + Args: + param_str (str): The string containing the parameters. + + Returns: + dict: A dictionary where keys are the parameter names (strings) and values are + the corresponding parameter values (integers or floats). These parameters + influence the behavior of the equation discovery algorithms. + """ assert isinstance(param_str, str), 'Passed parameters are not in string format' params_split = param_str.split(',') params_parsed = dict() @@ -159,11 +292,30 @@ def parse_params_str(param_str): return params_parsed class CoeffLessEquation(): + """ + Represents a coefficient-less equation in a linear program. + + This class is used to store equations where the coefficients are implicitly 1. + """ + def __init__(self, lp_terms : Union[list, tuple, dict], rp_term : Union[list, tuple, dict], pool, all_vars, use_pic: bool = False): - ''' - ``lp_terms'' - ''' + """ + Initializes a `CoeffLessEquation` object, translating symbolic terms into a numerical representation suitable for equation discovery. It prepares the equation for coefficient optimization by structuring the terms and setting up the optimization environment. + + Args: + lp_terms (Union[list, tuple, dict]): Left-hand side terms of the equation. Can be a list/tuple or a dictionary where keys are variables and values are lists/tuples of terms. + rp_term (Union[list, tuple, dict]): Right-hand side term(s) of the equation. Must match the structure of `lp_terms`. + pool: A pool of variables and constants used in the equation. + all_vars: A list of all variable names used in the equation. + use_pic (bool, optional): A flag to use PIC (physics-informed calibration). Defaults to False. + + Returns: + None + + Why: + This initialization process is crucial for converting symbolic representations of equations into a numerical format that can be processed by optimization algorithms. By translating the terms and setting up the optimization environment, the method prepares the equation for coefficient fitting, which is a key step in discovering the underlying differential equation from data. + """ if isinstance(lp_terms, dict): if not len(lp_terms.keys()) == len(rp_term.keys()): raise KeyError(f'Number of left parts {lp_terms.keys()} mismatches right parts {rp_term.keys()}.') diff --git a/epde/interface/interface.py b/epde/interface/interface.py index 082f8609..55ad9427 100644 --- a/epde/interface/interface.py +++ b/epde/interface/interface.py @@ -55,17 +55,40 @@ class InputDataEntry(object): """ - Class for keeping input data - - Attributes: - var_name (`str`): name of input data dependent variable - data_tensor (`np.ndarray`): value of the input data - names (`list`): keys for derivatides - d_orders (`list`): keys for derivatides on `int` format for `solver` - derivatives (`np.ndarray`): values of derivatives - deriv_properties (`dict`): settings of derivatives + Represents a single entry of input data. This class is designed to hold and manage individual data points used within the equation discovery process. + + + Attributes: + var_name (`str`): name of input data dependent variable + data_tensor (`np.ndarray`): value of the input data + names (`list`): keys for derivatides + d_orders (`list`): keys for derivatides on `int` format for `solver` + derivatives (`np.ndarray`): values of derivatives + deriv_properties (`dict`): settings of derivatives """ + def __init__(self, var_name: str, var_idx: int, data_tensor: Union[List[np.ndarray], np.ndarray]): + """ + Initializes a DataTensor object, associating a variable with its corresponding data. + + This method creates a `DataTensor` instance, linking a variable name and index to its numerical representation. + The data tensor's dimensionality is also inferred and validated to ensure consistency. This association is crucial + for representing the data in a structured format suitable for equation discovery. + + Args: + var_name (str): The name of the variable. + var_idx (int): The index of the variable. + data_tensor (Union[List[np.ndarray], np.ndarray]): The data tensor, which can be a NumPy array or a list of NumPy arrays. + + Returns: + None + + Class Fields: + var_name (str): The name of the variable associated with the data tensor. + var_idx (int): The index of the variable associated with the data tensor. + ndim (int): The dimensionality of the data tensor. + data_tensor (Union[List[np.ndarray], np.ndarray]): The actual data tensor, which can be a single NumPy array or a list of NumPy arrays. + """ self.var_name = var_name self.var_idx = var_idx if isinstance(data_tensor, np.ndarray): @@ -81,16 +104,20 @@ def __init__(self, var_name: str, var_idx: int, data_tensor: Union[List[np.ndarr def set_derivatives(self, preprocesser: PreprocessingPipe, deriv_tensors: Union[list, np.ndarray] = None, max_order: Union[list, tuple, int] = 1, grid: list = []): """ - Method for setting derivatives ot calculate derivatives from data - - Args: - preprocesser (`PreprocessingPipe`): operator for preprocessing data (smooting and calculating derivatives) - deriv_tensor (`np.ndarray`): values of derivatives - max_order (`list`|`tuple`|`int`): order for derivatives - grid: value of grid - - Returns: - None + Method for associating derivative information with the input data. + + This method prepares the data for the equation discovery process by calculating or assigning derivative values. + It ensures that the data is properly formatted and that the necessary derivative information is available for + the subsequent steps of equation learning. + + Args: + preprocesser (`PreprocessingPipe`): Operator for preprocessing data, including smoothing and derivative calculation. + deriv_tensors (`np.ndarray`, optional): Pre-calculated derivative values. If None, derivatives are computed using the preprocesser. + max_order (`list`|`tuple`|`int`): The maximum order of derivatives to be calculated or used. + grid (`list`): Grid values corresponding to the data points, used for derivative calculation. + + Returns: + None """ deriv_names, deriv_orders = define_derivatives(self.var_name, dimensionality=self.ndim, max_order=max_order) @@ -140,6 +167,21 @@ def use_global_cache(self): # , var_idx: int, deriv_codes: list deriv_codes = [(var_idx, code) for code in deriv_codes] try: + """ + Stores precomputed derivative tensors in a global cache for efficient reuse. + + This method stores the calculated derivatives and the original data tensor + in a global cache, indexed by variable name and derivative order. This + avoids redundant computations when the same derivatives are needed + multiple times during the equation discovery process. The method also + prepares the data by adjusting the time axis for consistency. + + Args: + self: The InputDataEntry instance containing the data and derivatives. + + Returns: + None. The method updates the global tensor and initial data caches. + """ upload_simple_tokens(self.names, global_var.tensor_cache, derivs_stacked, deriv_codes=deriv_codes) upload_simple_tokens([self.var_name,], global_var.tensor_cache, [self.data_tensor,], @@ -153,20 +195,21 @@ def use_global_cache(self): # , var_idx: int, deriv_codes: list @staticmethod def latex_form(label, **params): - ''' - Parameters - ---------- - label : str - label of the token, for which we construct the latex form. - **params : dict - dictionary with parameter labels as keys and tuple of parameter values - and their output text forms as values. - - Returns - ------- - form : str - LaTeX-styled text form of token. - ''' + """ + Constructs a LaTeX-formatted representation of a token, incorporating its label and any associated parameters like power. + + This ensures proper display of mathematical expressions, especially when dealing with derivatives and exponents. + + Args: + label (str): The label of the token to be formatted. + **params (dict): A dictionary containing parameter labels as keys and tuples of (value, LaTeX-formatted string) as values. Crucially expects a 'power' key. + + Returns: + str: The LaTeX-formatted string representation of the token. + + Why: + This method is essential for generating human-readable and mathematically correct representations of the discovered equation terms. The LaTeX format ensures that derivatives, fractions, and exponents are displayed properly, aiding in the interpretation and validation of the identified equations. + """ if '/' in label: label = label[:label.find('x')+1] + '_' + label[label.find('x')+1:] label = label.replace('d', r'\partial ').replace('/', r'}{') @@ -177,6 +220,31 @@ def latex_form(label, **params): return label def create_derivs_family(self, max_deriv_power: int = 1): + """ + Creates a family of tokens representing the derivatives of this token. + + This method generates a `TokenFamily` instance to manage the derivatives + of the current token, configuring its LaTeX representation, status flags, + derivative parameters, and evaluation function. This is crucial for + representing and manipulating derivative terms within the equation discovery process. + + Args: + max_deriv_power: The maximum order of derivative to include in the family. + + Returns: + None. + + Initializes: + _derivs_family (TokenFamily): A TokenFamily instance representing the derivatives + of the current token. It is initialized with the token type as + 'deriv of {self.var_name}', the variable name, and the + `family_of_derivs` flag set to True. The latex form constructor is set using `self.latex_form`. + The status is set to `demands_equation=True`, `unique_specific_token=False`, + `unique_token_type=False`, `s_and_d_merged=False`, and `meaningful=True`. + The parameters are set using `self.names`, an OrderedDict with 'power' + ranging from 1 to `max_deriv_power`, a default power of 0, and `self.d_orders`. + The evaluator is set to `simple_function_evaluator`. + """ self._derivs_family = TokenFamily(token_type=f'deriv of {self.var_name}', variable = self.var_name, family_of_derivs=True) @@ -189,13 +257,57 @@ def create_derivs_family(self, max_deriv_power: int = 1): self._derivs_family.set_evaluator(simple_function_evaluator) def create_polynomial_family(self, max_power): + """ + Creates a family of polynomial tokens for representing the input data. + + This method generates a set of polynomial tokens based on the input variable name + and a specified maximum power. These tokens are then stored as the polynomial family, + which will be used to construct candidate differential equations. + + Args: + max_power: The maximum power to which the input variable will be raised in the polynomial tokens. + + Returns: + None. The polynomial family is stored internally for later use in equation discovery. + """ polynomials = DataPolynomials(self.var_name, max_power = max_power) self._polynomial_family = polynomials.token_family def get_families(self): + """ + Returns the polynomial and derivative families associated with this data entry. + + These families define the basis functions and derivative operators used to construct candidate differential equations. + + Args: + None + + Returns: + list: A list containing the polynomial family and the derivative family. + """ return [self._polynomial_family, self._derivs_family] def matched_derivs(self, max_order = 1): + """ + Returns a list of derivatives matched to a maximum order for equation discovery. + + This method filters the derivatives associated with the variable, + selecting those with an order less than or equal to `max_order`. + These derivatives are then prepared for use in the equation discovery process. + The derivatives are stacked using `prepare_var_tensor` to prepare them for use. + This ensures that only relevant terms, up to the specified order, + are considered when constructing candidate equations. + + Args: + max_order: The maximum order of derivatives to include. Defaults to 1. + + Returns: + A list of lists, where each inner list contains: + - The variable index (`self.var_idx`). + - The derivative order (`key`). + - The derivative tensor (`derivs_stacked[idx, ...]`). + Only derivatives with an order less than or equal to `max_order` are included. + """ derivs_stacked = prepare_var_tensor(self.data_tensor, self.derivatives, time_axis=global_var.time_axis) # print(f'Creating matched derivs: {[[self.var_idx, key, len(key) <= max_order] for idx, @@ -205,28 +317,45 @@ def matched_derivs(self, max_order = 1): if len(key) <= max_order] def simple_selector(sorted_neighbors, number_of_neighbors=4): + """ + Selects the most relevant neighbors from a ranked list to refine equation discovery. + + This function takes a list of neighbors, assumed to be sorted by relevance + (e.g., based on some error metric), and returns a subset of the top neighbors. + This selection is crucial for focusing the search on the most promising equation candidates + and improving the efficiency of the equation discovery process. + + Args: + sorted_neighbors (list): A list of neighbors, sorted by relevance. + number_of_neighbors (int, optional): The number of top neighbors to select. Defaults to 4. + + Returns: + list: A list containing the first `number_of_neighbors` elements from the + `sorted_neighbors` list, representing the most relevant neighbors. + """ return sorted_neighbors[:number_of_neighbors] class EpdeSearch(object): """ Intialization of the epde search object. Here, the user can declare the properties of the - search mechainsm by defining evolutionary search strategy. + search mechainsm by defining evolutionary search strategy. - Attributes: - multiobjective_mode (`bool`): set mode of multiobjective optimization during equation search - preprocessor_set (`bool`): flag about using defined algorithm for preprocessing input data - director (`OptimizationPatternDirector`): optional - Pre-defined director, responsible for construction of multi-objective evolutionary optimization - strategy; shall not be interfered with unless for very specific tasks. - director_params (`dict`): optionals - Contains parameters for evolutionary operator builder / construction director, that - can be passed to individual operators. Keys shall be 'variation_params', 'mutation_params', - 'pareto_combiner_params', 'pareto_updater_params'. - search_conducted (`bool`): flag that the equation was searched - optimizer_init_params (`dict`): parameters for optimization algorithm initialization - optimizer_exec_params (`dict`): parameters for execution algorithm of optimization - optimizer (`OptimizationPatternDirector`): the strategy of the evolutionary algorithm + Attributes: + multiobjective_mode (`bool`): set mode of multiobjective optimization during equation search + preprocessor_set (`bool`): flag about using defined algorithm for preprocessing input data + director (`OptimizationPatternDirector`): optional + Pre-defined director, responsible for construction of multi-objective evolutionary optimization + strategy; shall not be interfered with unless for very specific tasks. + director_params (`dict`): optionals + Contains parameters for evolutionary operator builder / construction director, that + can be passed to individual operators. Keys shall be 'variation_params', 'mutation_params', + 'pareto_combiner_params', 'pareto_updater_params'. + search_conducted (`bool`): flag that the equation was searched + optimizer_init_params (`dict`): parameters for optimization algorithm initialization + optimizer_exec_params (`dict`): parameters for execution algorithm of optimization + optimizer (`OptimizationPatternDirector`): the strategy of the evolutionary algorithm """ + def __init__(self, multiobjective_mode: bool = True, use_pic = False, use_default_strategy: bool = True, director=None, director_params: dict = {'variation_params': {}, 'mutation_params': {}, 'pareto_combiner_params': {}, 'pareto_updater_params': {}}, @@ -237,51 +366,78 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = False, use_defaul division_fractions=3, rectangular: bool = True, params_filename: str = None, device: str = 'cpu'): """ - Args: - multiobjective_mode (`bool`): optional, default True - Flag, if the multiobjective MOEADD-based optimization is to be held. If False, singleobjective - evolutionary optimization will be executed. - use_default_strategy (`bool`): optional, default True - True (base and recommended value), if the default evolutionary strategy will be used, - False if the user-defined strategy will be passed further. Otherwise, the search will - not be conducted. - time_axis (`int`): optional - Indicator of time axis in data and grids. Used in normalization for regressions. - define_domain (`bool`): optional - Indicator, showing that if the domain will be set in the initialization of the search objects. - For more details view ``epde_search.set_domain_properties`` method. - function_form (`callable`): optional - Auxilary function, used in the weak derivative definition. Default function is negative square function - with maximum values in the center of the domain. - boundary (`int|tuple/list of integers`): optional - Boundary width for the domain. Boundary points will be ignored for the purposes of equation discovery - use_solver (`bool`): optional - Allow use of the automaic partial differential solver to evaluate fitness of the candidate solutions. - verbose_params (`dict`): optional - Description, of algorithm details, that will be demonstrated to the user. Usual - coordinate_tensors (`list of np.ndarrays`): optional - Values of the coordinates on the grid nodes with studied functions values. In case of 1D-problem, - that will be ``numpy.array``, while the parameter for higher dimensionality problems can be set from - ``numpy.meshgrid`` function. With None, the tensors will be created as ranges with step of 1 between - nodes. Defalut value: None. - memory_for_cache (`int|float`): optional - Rough estimation of the memory, which can be used for cache of pre-evaluated tensors during the equation - prune_domain (`bool`): optional - If ``True``, subdomains with no dynamics will be pruned from data. Default value: ``False``. - pivotal_tensor_label (`str`): optional - Indicator, according to which token data will be pruned. Default value - ``'du/dt'``, where - ``t`` is selected as a time axis from ``time_axis`` parameter. - pruner (`object`): optional - Pruner object, which will remove subdomains with no dynamics i.e. with derivative - identically equal to zero. - threshold (`float`): optional - Pruner parameter, indicating the boundary of interval in which the pivotal tensor values are - considered as zeros. Default value: 1e-2 - division_fractions (`int`): optional - Number of subdomains along each axis, defining the division of the domain for pruning. - Default value: 3 - rectangular(`bool`): optional - A line of subdomains along an axis can be removed if all values inside them are identical to zero. + Initializes the EPDE search object with specified parameters. + + This method sets up the search environment, including the evolutionary strategy, + domain properties, and optimization mode (single or multi-objective). It prepares + the system for discovering differential equations from data by configuring the + search space and optimization algorithms. + + Args: + multiobjective_mode (`bool`): optional, default True + Flag indicating whether to use multi-objective optimization (MOEADD). If False, + single-objective optimization is used. Multi-objective optimization allows to find + a set of equations that are good in terms of different criteria, such as accuracy and complexity. + use_pic (`bool`): optional, default False + Flag indicating whether to use Physics Informed learning paradigm. + use_default_strategy (`bool`): optional, default True + If True, the default evolutionary strategy is used. If False, a user-defined + strategy must be provided via the `director` argument. The evolutionary strategy + defines how the search for equations is conducted. + director (`object`): optional + A custom director object that implements a specific evolutionary strategy. + Required if `use_default_strategy` is False. + director_params (`dict`): optional + Parameters for the director object, such as variation, mutation, and Pareto + combination/update parameters. + time_axis (`int`): optional, default 0 + The index of the time axis in the input data and coordinate tensors. Used for + calculating time derivatives. + define_domain (`bool`): optional, default True + If True, the domain properties (coordinate tensors, memory cache, boundary) are + initialized. Disabling this is useful when the domain is already defined. + function_form (`callable`): optional + An auxiliary function used in the weak derivative definition. The default is a + negative square function centered in the domain. + boundary (`int` or `tuple/list of integers`): optional, default 0 + The width of the boundary region to exclude from the equation discovery process. + use_solver (`bool`): optional, default False + If True, an automatic partial differential equation solver is used to evaluate the + fitness of candidate solutions. This can improve the accuracy of the discovered equations. + verbose_params (`dict`): optional + A dictionary of parameters controlling the verbosity of the algorithm's output. + coordinate_tensors (`list of np.ndarrays`): optional + The values of the coordinates at the grid nodes where the function values are known. + For 1D problems, this is a NumPy array. For higher-dimensional problems, this can + be generated using `numpy.meshgrid`. If None, tensors are created as ranges with a + step of 1 between nodes. + memory_for_cache (`int` or `float`): optional, default 15 + An estimate of the memory (in MB) that can be used to cache pre-evaluated tensors. + Caching can speed up the equation discovery process. + prune_domain (`bool`): optional, default False + If True, subdomains with no dynamics are pruned from the data. This can improve + the efficiency of the search. + pivotal_tensor_label (`str`): optional + The label of the tensor used to identify subdomains with no dynamics. The default + is 'du/dt', where 't' is the time axis. + pruner (`object`): optional + A custom pruner object that removes subdomains with no dynamics. + threshold (`float`): optional, default 1e-2 + The threshold for determining whether a pivotal tensor value is considered zero. + Used by the pruner. + division_fractions (`int`): optional, default 3 + The number of subdomains along each axis used for domain pruning. + rectangular (`bool`): optional, default True + If True, entire lines of subdomains along an axis can be removed if all values + within them are zero. + params_filename (`str`): optional + Path to a JSON file containing evolutionary parameters. If None, default parameters + are used based on the optimization mode. + device (`str`): optional, default 'cpu' + The device to use for computations (e.g., 'cpu' or 'cuda'). + + Returns: + None """ self._device = device self.multiobjective_mode = multiobjective_mode @@ -331,17 +487,15 @@ def __init__(self, multiobjective_mode: bool = True, use_pic = False, use_defaul def set_memory_properties(self, example_tensor, mem_for_cache_frac=None, mem_for_cache_abs=None): """ - Setting properties for using memory to cache - - Args: - example_tensor (`ndarray`): referntial tensor to evaluate memory consuption by tensors equation search - mem_for_cache_frac (`int`): optional - memory available for cache (in fraction of RAM). The default - None. - mem_for_cache_abs (`int`): optional - memory available for cache (in byte). The default - None. - - Returns: - None + Sets the memory constraints for caching intermediate tensor results during the equation search. This is crucial for balancing computational speed and memory usage, especially when dealing with large datasets or complex equation spaces. By limiting the memory footprint of the tensor cache, the search process can avoid memory overflow errors and maintain efficiency. + + Args: + example_tensor (`ndarray`): A representative tensor used to estimate the memory consumption of other tensors during the equation search process. + mem_for_cache_frac (`float`, optional): The fraction of total RAM to allocate for caching tensors. Defaults to None. + mem_for_cache_abs (`int`, optional): The absolute amount of memory (in bytes) to allocate for caching tensors. Defaults to None. + + Returns: + None """ if global_var.grid_cache is not None: if mem_for_cache_frac is not None: @@ -359,53 +513,34 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {} neighborhood_selector: Callable = simple_selector, neighborhood_selector_params: tuple = (4,)): """ - Setting the parameters of the multiobjective evolutionary algorithm. declaration of - the default values is held in the initialization of EpdeSearch object. - - Args: - population_size (`int`): optional - The size of the population of solutions, created during MO - optimization, default 6. - solution_params (`dict`): optional - Dictionary, containing additional parameters to be sent into the newly created solutions. - delta (`float`): optional - parameter of uniform spacing between the weight vectors; *H = 1 / delta* - should be integer - a number of divisions along an objective coordinate axis. - neighbors_number (`int`): *> 0*, optional - number of neighboring weight vectors to be considered during the operation - of evolutionary operators as the "neighbors" of the processed sectors. - nds_method (`callable`): optional, default ``moeadd.moeadd_supplementary.fast_non_dominated_sorting`` - Method of non-dominated sorting of the candidate solutions. The default method is implemented according to the article - *K. Deb, A. Pratap, S. Agarwal, and T. Meyarivan, “A fast and elitist - multiobjective genetic algorithm: NSGA-II,” IEEE Trans. Evol. Comput., - vol. 6, no. 2, pp. 182–197, Apr. 2002.* - ndl_update (`callable`): optional, defalut ``moeadd.moeadd_supplementary.NDL_update`` - Method of adding a new solution point into the objective functions space, introduced - to minimize the recalculation of the non-dominated levels for the entire population. - The default method was taken from the *K. Li, K. Deb, Q. Zhang, and S. Kwong, “Efficient non-domination level - update approach for steady-state evolutionary multiobjective optimization,” - Dept. Electr. Comput. Eng., Michigan State Univ., East Lansing, - MI, USA, Tech. Rep. COIN No. 2014014, 2014.* - neighborhood_selector (`callable`): optional - Method of finding "close neighbors" of the vector with proximity list. - The baseline example of the selector, presented in - ``moeadd.moeadd_stc.simple_selector``, selects n-adjacent ones. - subregion_mating_limitation (`float`): optional - The probability of mating selection to be limited only to the selected - subregions (adjacent to the weight vector domain).:math:`\delta \in [0., 1.) - neighborhood_selector_params (`tuple|list`): optional - Iterable, which will be passed into neighborhood_selector, as - an arugument. *None*, is no additional arguments are required inside - the selector. - training_epochs (`int`): optional - Maximum number of iterations, during that the optimization will be held. - Note, that if the algorithm converges to a single Pareto frontier, - the optimization is stopped. - PBI_penalty (`float`): optional - The penalty parameter, used in penalty based intersection - calculation, defalut value is 1. - - Returns: - None + Sets the parameters for the multi-objective evolutionary algorithm used to discover differential equations. Default values are set during the initialization of the `EpdeSearch` object. These parameters control the search process, influencing the diversity and convergence of solutions representing potential equation structures. + + Args: + population_size (`int`): optional + The size of the population of solutions (equation candidates) created during the multi-objective optimization. Default is 6. + solution_params (`dict`): optional + A dictionary containing additional parameters to be passed to newly created solutions. This allows for customization of individual equation candidates. + delta (`float`): optional + Parameter controlling the uniform spacing between weight vectors. `H = 1 / delta` should be an integer, representing the number of divisions along an objective coordinate axis. + neighbors_number (`int`): *> 0*, optional + The number of neighboring weight vectors considered during evolutionary operations. This influences the exploration of the solution space by considering similar equation structures. + nds_method (`callable`): optional, default ``moeadd.moeadd_supplementary.fast_non_dominated_sorting`` + The method used for non-dominated sorting of candidate solutions. The default method is implemented according to the article *K. Deb, A. Pratap, S. Agarwal, and T. Meyarivan, “A fast and elitist multiobjective genetic algorithm: NSGA-II,” IEEE Trans. Evol. Comput., vol. 6, no. 2, pp. 182–197, Apr. 2002.* + ndl_update_method (`callable`): optional, default ``moeadd.moeadd_supplementary.NDL_update`` + The method for adding a new solution point into the objective functions space, minimizing the recalculation of non-dominated levels for the entire population. The default method is taken from *K. Li, K. Deb, Q. Zhang, and S. Kwong, “Efficient non-domination level update approach for steady-state evolutionary multiobjective optimization,” Dept. Electr. Comput. Eng., Michigan State Univ., East Lansing, MI, USA, Tech. Rep. COIN No. 2014014, 2014.* + neighborhood_selector (`callable`): optional + The method for finding "close neighbors" of a vector with a proximity list. The baseline example, presented in ``moeadd.moeadd_stc.simple_selector``, selects n-adjacent ones. + subregion_mating_limitation (`float`): optional + The probability of mating selection being limited only to selected subregions (adjacent to the weight vector domain). Value should be in the range :math:`\delta \in [0., 1.)`. + neighborhood_selector_params (`tuple|list`): optional + An iterable passed to the `neighborhood_selector` as an argument. Use `None` if no additional arguments are required. + training_epochs (`int`): optional + The maximum number of iterations during which the optimization will be performed. The optimization stops if the algorithm converges to a single Pareto frontier. + PBI_penalty (`float`): optional + The penalty parameter used in penalty-based intersection calculation. Default value is 1. + + Returns: + None """ self.optimizer_init_params = {'weights_num': population_size, 'pop_size': population_size, 'delta': delta, 'neighbors_number': neighbors_number, @@ -418,19 +553,28 @@ def set_moeadd_params(self, population_size: int = 6, solution_params: dict = {} def set_singleobjective_params(self, population_size: int = 4, solution_params: dict = {}, sorting_method: Callable = simple_sorting, training_epochs: int = 50): """ - Setting parameters for singelobjective optimization. - - Args: - population_size (`int`): optional, default - 4 - Size of population. - solution_params (`dict`): - Parameters, guiding candidate solution creation. - sorting_method(`callable`): optional, default - `simple_sorting` - Method for sorting of individs in population. - trainig_epochs (`int`): optional, default - 50 - Maximum number of iterations, during that the optimization will be held. - Returns: - None + Sets the parameters required for the single-objective evolutionary search. + + This configuration step is crucial for tailoring the search process to the specific problem, + allowing control over the population size, solution generation, sorting strategies, and training duration. + These parameters influence how the algorithm explores the space of possible differential equations + and converges towards an optimal solution. + + Args: + population_size (`int`): optional, default - 4 + Size of population. A larger population may improve exploration but increases computational cost. + solution_params (`dict`): + Parameters guiding the creation of candidate solutions (equation structures). + These parameters define the building blocks and constraints for generating differential equations. + sorting_method(`callable`): optional, default - `simple_sorting` + Method for sorting individuals in the population based on their fitness. + The sorting method determines how the evolutionary algorithm selects promising equation structures for reproduction and refinement. + training_epochs (`int`): optional, default - 50 + Maximum number of iterations for the optimization process. + This parameter limits the computational time spent searching for the best equation. + + Returns: + None """ self.optimizer_init_params = {'pop_size' : population_size, 'solution_params': solution_params, 'sorting_method' : sorting_method} @@ -441,23 +585,25 @@ def domain_pruning(self, pivotal_tensor_label = None, pruner = None, threshold : float = 1e-5, division_fractions = 3, rectangular : bool = True): """ - Method for select only subdomains with variable dynamics. - - Args: - pivotal_tensor_label (`np.ndarray`): - Pattern that guides the domain pruning will be cutting areas, where values of the - `pivotal_tensor` are closed to zero. - pruner (`DomainPruner`): - Custom object for selecting domain region by pruning out areas with no dynamics. - threshold (`float`): optional, default - 1e-5 - The boundary at which values are considered zero. - division_fractions (`int`): optional, default - 3 - Number of fraction for each axis (if this is integer than all axis are dividing by same fractions). - rectangular (`bool`): default - True - Flag indecating that area is rectangle. - - Returns: - None + Method for refining the search domain by focusing on regions exhibiting significant dynamic behavior. + + This method identifies and isolates areas within the domain where the dynamics, as indicated by the pivotal tensor, are most pronounced. By discarding regions with minimal change, the search for governing equations becomes more efficient and accurate. + + Args: + pivotal_tensor_label (`np.ndarray`): + Pattern that guides the domain pruning will be cutting areas, where values of the + `pivotal_tensor` are closed to zero. + pruner (`DomainPruner`): + Custom object for selecting domain region by pruning out areas with no dynamics. + threshold (`float`): optional, default - 1e-5 + The boundary at which values are considered zero. + division_fractions (`int`): optional, default - 3 + Number of fraction for each axis (if this is integer than all axis are dividing by same fractions). + rectangular (`bool`): default - True + Flag indecating that area is rectangle. + + Returns: + None """ if pruner is not None: @@ -482,16 +628,20 @@ def domain_pruning(self, pivotal_tensor_label = None, pruner = None, def _create_caches(self, coordinate_tensors, memory_for_cache): """ - Creating caches for keeping tensors during EPDE search. - - Args: - coordinate_tensors (`np.ndarray|list`): - Grid values, passed as a single `np.ndarray` or a list of `np.ndarray`'s. - memory_for_cache (`int`): - allowed amount of memory for data storage + Creates and initializes caches for storing tensors, which are essential for efficient equation discovery. - Returns: - None + The caches are used to store coordinate tensors and other intermediate results, + allowing for faster access and manipulation during the evolutionary search process. + This pre-allocation of memory optimizes performance by reducing the overhead of repeated memory allocation and deallocation. + + Args: + coordinate_tensors (`np.ndarray|list`): + Grid values, passed as a single `np.ndarray` or a list of `np.ndarray`'s. These represent the independent variables of the differential equation. + memory_for_cache (`float`): + Fraction of available memory to be used for caching tensors. This parameter controls the trade-off between memory usage and computational speed. + + Returns: + None """ global_var.init_caches(set_grids=True, device=self._device) example = coordinate_tensors if isinstance(coordinate_tensors, np.ndarray) else coordinate_tensors[0] @@ -501,20 +651,32 @@ def _create_caches(self, coordinate_tensors, memory_for_cache): def set_boundaries(self, boundary_width: Union[int, list]): """ - Setting the number of unaccountable elements at the edges into cache with saved grid. + Sets the boundary conditions for the computational grid. This ensures that the equation discovery process accounts for edge effects and avoids overfitting to boundary artifacts. + + Args: + boundary_width (Union[int, list]): The width of the boundary region to be excluded from consideration during equation discovery. Can be an integer for uniform width or a list for variable widths along different dimensions. + + Returns: + None """ global_var.grid_cache.set_boundaries(boundary_width=boundary_width) def _upload_g_func(self, function_form: Union[Callable, np.ndarray, list] = None): """ - Loading testing function connected to the weak derivative notion. - - Args: - function_form (`callable`, or `np.ndarray`, or `list[np.ndarray]`) - Test function, default using inverse polynomial with max in the domain center. - - Returns: - None + Loads and prepares a test function for weak derivative calculations. This function is crucial for evaluating the fitness of candidate equations by projecting them onto a suitable function space. + + Args: + function_form (`callable`, or `np.ndarray`, or `list[np.ndarray]`, optional): + The test function to be used. It can be a callable function, a NumPy array, or a list of NumPy arrays. If `None`, a default inverse polynomial function with a maximum at the domain center is used. Defaults to `None`. + + Returns: + None + + Raises: + NameError: If the grid cache has not been initialized. + + Why: + This method sets up the test function used in the weak formulation of derivatives. The weak formulation allows the framework to handle noisy or incomplete data by integrating the equation against a test function, effectively smoothing the derivatives. The choice of test function can influence the accuracy and stability of the equation discovery process. """ if isinstance(function_form, (np.ndarray, list)): global_var.grid_cache.g_func = function_form @@ -546,41 +708,39 @@ def set_domain_properties(self, coordinate_tensors, memory_for_cache, boundary_w pivotal_tensor_label=None, pruner=None, threshold: float = 1e-5, division_fractions: int = 3, rectangular: bool = True): """ - Setting properties for processing considered domain, such as removing areas with no dynamics, - and setting bounderes. Can be used for uploading test function. - - Parameters - ---------- - coordinate_tensors : list|np.ndarrays, optional - Values of the coordinates on the grid nodes with studied functions values. In case of 1D-problem, - that will be ``numpy.array``, while the parameter for higher dimensionality problems can be set from - ``numpy.meshgrid`` function. - memory_for_cache : int - Allowed amount of memory (in percentage) for data storage. - boundary_width : int|list - The number of unaccountable elements at the edges of the domain. - function_form : callable, optional - Testing function connected to the weak derivative notion, the default value is None, that - corresponds with the product of normalized inverse square functions of the coordinates, - centered at the middle of the domain. - prune_domain : bool - Flag, enabling area cropping by removing subdomains with constant values, default - False. - pivotal_tensor_label : np.ndarray - Pattern that guides the domain pruning, the default is None. - pruner : DomainPruner - Object for selecting domain region, the default is None. - threshold : float, optional - The boundary at which values are considered zero, the default is 1e-5. - division_fractions : int, optional - Number of fraction for each axis (if this is integer than all axis are dividing by - same fractions), the default is 3. - rectangular : bool, optional - Flag indecating that crop subdomains are rectangle, default - True. - - Returns - ------- - None. - + Sets the properties of the domain to be processed, including boundary widths, a test function, and whether to prune the domain. This configuration is crucial for tailoring the search space and improving the efficiency of the equation discovery process by focusing on dynamically relevant regions. + + Parameters + ---------- + coordinate_tensors : list|np.ndarrays, optional + Values of the coordinates on the grid nodes with studied functions values. In case of 1D-problem, + that will be ``numpy.array``, while the parameter for higher dimensionality problems can be set from + ``numpy.meshgrid`` function. + memory_for_cache : int + Allowed amount of memory (in percentage) for data storage. + boundary_width : int|list + The number of unaccountable elements at the edges of the domain. + function_form : callable, optional + Testing function connected to the weak derivative notion, the default value is None, that + corresponds with the product of normalized inverse square functions of the coordinates, + centered at the middle of the domain. + prune_domain : bool + Flag, enabling area cropping by removing subdomains with constant values, default - False. + pivotal_tensor_label : np.ndarray + Pattern that guides the domain pruning, the default is None. + pruner : DomainPruner + Object for selecting domain region, the default is None. + threshold : float, optional + The boundary at which values are considered zero, the default is 1e-5. + division_fractions : int, optional + Number of fraction for each axis (if this is integer than all axis are dividing by + same fractions), the default is 3. + rectangular : bool, optional + Flag indecating that crop subdomains are rectangle, default - True. + + Returns + ------- + None. """ self._create_caches(coordinate_tensors=coordinate_tensors, memory_for_cache=memory_for_cache) if prune_domain: @@ -590,27 +750,25 @@ def set_domain_properties(self, coordinate_tensors, memory_for_cache, boundary_w def set_preprocessor(self, preprocessor_pipeline: PreprocessingPipe = None, default_preprocessor_type: str = 'poly', preprocessor_kwargs: dict = {}): - ''' - Specification of preprocessor, devoted to smoothing the raw input data and - calculating the derivatives. - - Parameters - ---------- - preprocessor_pipeline : PreprocessingPipe, optional - Pipeline of operators, aimed on preparing all necessary data for equation discovery. - default_preprocessor_type : str, optional - Key for selection of pre-defined preprocessors: **'poly'** matches Savitsky-Golay filtering, 'ANN' if for - neural network data approximation and further finite-difference differentiation, 'spectral' for - spectral differentiation. The default is 'poly'. - preprocessor_kwargs : dict, optional - Keyword arguments for preprocessor setup and operation. The default is an empty dictionary, corresponding to - all default parameters of the preprocessors. - - Returns - ------- - None. - - ''' + """ + Specifies the preprocessor to be used for data preparation and derivative calculation. + The preprocessor smooths the input data and computes derivatives, which are essential + for constructing candidate equation terms. + + Args: + preprocessor_pipeline (PreprocessingPipe, optional): A custom pipeline of preprocessing + operators. If provided, this pipeline will be used directly. Defaults to None. + default_preprocessor_type (str, optional): A key to select a pre-defined preprocessing + pipeline. Options include 'poly' (Savitsky-Golay filtering), 'ANN' (neural network + approximation with finite-difference differentiation), 'spectral' (spectral differentiation), + and 'FD' (finite-difference differentiation). Defaults to 'poly'. + preprocessor_kwargs (dict, optional): Keyword arguments to configure the selected + preprocessor. If not provided, default parameters for the chosen preprocessor type + will be used. Defaults to {}. + + Returns: + None + """ if preprocessor_pipeline is None: setup = PreprocessorSetup() builder = ConcretePrepBuilder() @@ -639,13 +797,27 @@ def create_pool(self, data: Union[np.ndarray, list, tuple], variable_names=['u', data_fun_pow: int = 1, deriv_fun_pow: int = 1, grid: list = None, data_nn: torch.nn.Sequential = None, fourier_layers: bool = True, fourier_params: dict = {'L' : [4,], 'M' : [3,]}): - ''' - Create pool of tokens to represent elementary functions, that can be included in equations. + """ + Create a pool of token families representing elementary functions and their derivatives. - Args: - data : np.ndarray | list of np.ndarrays | tuple of np.ndarrays - - ''' + This pool serves as the building block for constructing candidate equations, allowing the search algorithm to explore different combinations of functions and derivatives to identify the governing equation. + + Args: + data (np.ndarray | list of np.ndarrays | tuple of np.ndarrays): Input data as a single array or a list/tuple of arrays, where each array represents a different variable. + variable_names (list, optional): Names of the variables corresponding to the data arrays. Defaults to ['u']. + derivs (list, optional): Pre-computed derivatives for each variable. If None, derivatives are computed internally. Defaults to None. + max_deriv_order (int, optional): Maximum order of derivatives to compute. Defaults to 1. + additional_tokens (list, optional): List of additional token families or prepared tokens to include in the pool. Defaults to []. + data_fun_pow (int, optional): Maximum power of the input data to include in the pool. Defaults to 1. + deriv_fun_pow (int, optional): Maximum power of the derivatives to include in the pool. Defaults to 1. + grid (list, optional): Grid on which the data is defined. If None, the global grid cache is used. Defaults to None. + data_nn (torch.nn.Sequential, optional): Pre-trained neural network for data representation. Defaults to None. + fourier_layers (bool, optional): Whether to use Fourier layers in the neural network. Defaults to True. + fourier_params (dict, optional): Parameters for the Fourier layers. Defaults to {'L' : [4,], 'M' : [3,]}. + + Returns: + None + """ grid = grid if grid is not None else global_var.grid_cache.get_all()[1] self.pool_params = {'variable_names' : variable_names, 'max_deriv_order' : max_deriv_order, @@ -712,22 +884,16 @@ def create_pool(self, data: Union[np.ndarray, list, tuple], variable_names=['u', family.chech_constancy() def save_derivatives(self, variable:str, deriv:np.ndarray): - ''' - Pass the derivatives of a variable as a np.ndarray. - - Parameters - ---------- - variable : str - Key for the variable to have the derivatives set. - deriv : np.ndarray - Arrays of derivatives. Have to be shaped as (n, m), where n is the number of passed derivatives - (for example, when you differentiate the dataset once for the first axis, and up to the second order for - the second, and you have no mixed derivatives, *n = 3*), and m is the number of data points in the domain. - - Returns - ------- - None. - ''' + """ + Passes the computed derivatives of a variable to the internal storage. These derivatives are essential components for constructing candidate equations. + + Args: + variable (str): Key identifying the variable for which the derivatives are being stored. + deriv (np.ndarray): Array of derivatives. It should be shaped as (n, m), where n represents the number of derivative terms (e.g., first and second-order derivatives along different axes), and m is the number of data points. + + Returns: + None + """ try: self._derivatives except AttributeError: @@ -736,6 +902,17 @@ def save_derivatives(self, variable:str, deriv:np.ndarray): @property def saved_derivaties(self): + """ + Return the precomputed derivatives of the input data. + + These derivatives are essential for constructing candidate equations. If they haven't been computed yet, a warning is issued, as equation discovery relies on these values. + + Args: + self: The object instance. + + Returns: + The precomputed derivatives, or None if they haven't been calculated. + """ try: return self._derivatives except AttributeError: @@ -749,61 +926,66 @@ def fit(self, data: Union[np.ndarray, list, tuple] = None, equation_terms_max_nu population: List[SoEq] = None, data_nn = None, fourier_layers: bool = False, fourier_params: dict = {'L' : [4,], 'M' : [3,]}): """ - Fit epde search algorithm to obtain differential equations, describing passed data. - - Parameters - ---------- - data : np.ndarray | list | tuple, optional - Values of modeled variables. If the variable is single (i.e. deriving a single equation), - it can be passed as the numpy.ndarray or as the list/tuple with a single element; - multiple variables are not supported yet, use older interfaces. Default value is None, but it - shall be used only for retraining, when the pool argument is passed. - equation_terms_max_number : int, optional - The maximum number of terms, present in the derived equations, the default is 6. - equation_factors_max_number : int, optional - The maximum number of factors (token functions; real-valued coefficients are not counted here), - present in terms of the equaton, the default is 1. - variable_names : list | str, optional - Names of the independent variables, passed into search mechanism. Length of the list must correspond - to the number of np.ndarrays, sent with in ``data`` parameter. In case of system of differential equation discovery, - all variables shall be named here, default - ``['u',]``, representing a single variable *u*. - eq_sparsity_interval : tuple, optional - The left and right boundaries of interval with sparse regression values. Undirectly influences the - number of active terms in the equation, the default is ``(1e-4, 2.5)``. - derivs : list or list of lists of np.ndarrays, optional - Pre-computed values of derivatives. If ``None`` is passed, the derivatives are calculated in the - method. Recommended to use, if the computations of derivatives take too long. For further information - about using data, prepared in advance, check ``epde.preprocessing.derivatives.preprocess_derivatives`` - function, default - None. - max_deriv_order : int | list | tuple, optional - Highest order of calculated derivatives, the default is 1. - additional_tokens : list of TokenFamily or Prepared_tokens, optional - Additional tokens, that would be used to construct the equations among the main variables and their - derivatives. Objects of this list must be of type ``epde.interface.token_family.TokenFamily`` or - of ``epde.interface.prepared_tokens.Prepared_tokens`` subclasses types. The default is None. - field_smooth : bool, optional - Parameter, if the input variable fields shall be smoothed to avoid the errors. If the data is - assumed to be noiseless, shall be set to False, otherwise - True, the default - False. - memory_for_cache : int | float, optional - Limit for the cache (in fraction of the memory) for precomputed tensor values to be stored: - if int, will be considered as the percentage of the entire memory, and if float, - then as a fraction of memory, the default is 5. - data_fun_pow : int, optional - Maximum power of token, the default is 1. - optimizer : SimpleOptimizer | MOEADDOptimizer, optional - Pre-defined optimizer, that will be used during evolution. Shall correspond with the mode - (single- and multiobjective). The default is None, matching no use of pre-defined optimizer. - pool : TFPool, optional - Pool of tokens, that can be explicitly passed. The default is None, matching no use of passed pool. - population : Population | ParetoLevels, optional - Population of candidate equatons, that can be optionally passed in explicit form. The type of objects - must match the optimization algorithm: epde.optimizers.single_criterion.optimizer.Population for - single-objective mode and epde.optimizers.moeadd.moeadd.ParetoLevels for multiobjective optimization. - The default is None, specifing no passed population. - - Returns - ------- - None. + Fit the EPDE search algorithm to identify differential equations that best describe the provided data. + + This method orchestrates the search process, leveraging evolutionary algorithms and optimization techniques + to explore the space of possible equation structures. It initializes the search pool, configures the + optimizer, and executes the optimization loop to find equations that accurately represent the underlying + dynamics of the data. + + Parameters + ---------- + data : np.ndarray | list | tuple, optional + Values of modeled variables. If the variable is single (i.e. deriving a single equation), + it can be passed as the numpy.ndarray or as the list/tuple with a single element; + multiple variables are not supported yet, use older interfaces. Default value is None, but it + shall be used only for retraining, when the pool argument is passed. + equation_terms_max_number : int, optional + The maximum number of terms, present in the derived equations, the default is 6. + equation_factors_max_number : int, optional + The maximum number of factors (token functions; real-valued coefficients are not counted here), + present in terms of the equaton, the default is 1. + variable_names : list | str, optional + Names of the independent variables, passed into search mechanism. Length of the list must correspond + to the number of np.ndarrays, sent with in ``data`` parameter. In case of system of differential equation discovery, + all variables shall be named here, default - ``['u',]``, representing a single variable *u*. + eq_sparsity_interval : tuple, optional + The left and right boundaries of interval with sparse regression values. Undirectly influences the + number of active terms in the equation, the default is ``(1e-4, 2.5)``. + derivs : list or list of lists of np.ndarrays, optional + Pre-computed values of derivatives. If ``None`` is passed, the derivatives are calculated in the + method. Recommended to use, if the computations of derivatives take too long. For further information + about using data, prepared in advance, check ``epde.preprocessing.derivatives.preprocess_derivatives`` + function, default - None. + max_deriv_order : int | list | tuple, optional + Highest order of calculated derivatives, the default is 1. + additional_tokens : list of TokenFamily or Prepared_tokens, optional + Additional tokens, that would be used to construct the equations among the main variables and their + derivatives. Objects of this list must be of type ``epde.interface.token_family.TokenFamily`` or + of ``epde.interface.prepared_tokens.Prepared_tokens`` subclasses types. The default is None. + data_fun_pow : int, optional + Maximum power of token, the default is 1. + optimizer : SimpleOptimizer | MOEADDOptimizer, optional + Pre-defined optimizer, that will be used during evolution. Shall correspond with the mode + (single- and multiobjective). The default is None, matching no use of pre-defined optimizer. + pool : TFPool, optional + Pool of tokens, that can be explicitly passed. The default is None, matching no use of passed pool. + population : List[SoEq], optional + Population of candidate equatons, that can be optionally passed in explicit form. The type of objects + must match the optimization algorithm: epde.optimizers.single_criterion.optimizer.Population for + single-objective mode and epde.optimizers.moeadd.moeadd.ParetoLevels for multiobjective optimization. + The default is None, specifing no passed population. + data_nn : TYPE, optional + DESCRIPTION. The default is None. + fourier_layers : bool, optional + DESCRIPTION. The default is False. + fourier_params : dict, optional + DESCRIPTION. The default is {'L' : [4,], 'M' : [3,]}. + + Returns + ------- + None. + The method modifies the internal state of the EPDE search object, storing the identified equations. """ # TODO: ADD EXPLICITLY SENT POPULATION PROCESSING if additional_tokens is None: @@ -846,6 +1028,29 @@ def fit(self, data: Union[np.ndarray, list, tuple] = None, equation_terms_max_nu def _create_optimizer(multiobjective_mode: bool, optimizer_init_params: dict, opt_strategy_director: OptimizationPatternDirector, population: List[SoEq] = None, use_pic: bool = False): + """ + Creates and configures an optimizer instance tailored for equation discovery. + + This method selects and configures an optimizer based on whether the search + involves multiple objectives (e.g., accuracy and complexity) or a single + objective. It then sets up the optimization strategy to guide the search + for the best equation structure. + + Args: + multiobjective_mode: A boolean indicating whether to use multi-objective optimization. + optimizer_init_params: A dictionary containing initialization parameters for the optimizer. + opt_strategy_director: An `OptimizationPatternDirector` instance to set the optimization strategy. + population: A list of `SoEq` objects representing the initial population (optional). + use_pic: A boolean indicating whether to use a specific configuration for best solution values. + + Returns: + The created and configured optimizer instance. + + Why: + This method is responsible for setting up the optimization process, which is a crucial step in discovering + differential equations from data. The choice of optimizer and its configuration directly impact the + effectiveness and efficiency of the equation search. + """ if multiobjective_mode: optimizer_init_params['passed_population'] = population optimizer = MOEADDOptimizer(**optimizer_init_params) @@ -868,6 +1073,23 @@ def _create_optimizer(multiobjective_mode: bool, optimizer_init_params: dict, @property def _resulting_population(self): + """ + Returns the population of equation candidates after the evolutionary search process. + + This population represents the set of equations that the algorithm has identified as potentially + describing the underlying dynamics of the system. The structure of the returned list depends + on whether the search was conducted in multiobjective mode. + + Returns: + list: The resulting population. If in multiobjective mode, returns the Pareto levels, + representing a set of non-dominated solutions balancing multiple objectives. + Otherwise, returns the population from the optimizer, representing the final + set of equation candidates after the search. + + Raises: + AttributeError: If the search has not been conducted yet (``self.fit`` method not called). + The population is only available after the evolutionary search has been completed. + """ if not self.search_conducted: raise AttributeError('Pareto set of the best equations has not been discovered. Use ``self.fit`` method.') if self.multiobjective_mode: @@ -877,18 +1099,23 @@ def _resulting_population(self): def equations(self, only_print : bool = True, only_str = False, num = 1): """ - Method for print or getting results of searching differential equation - - Parameters - ---------- - only_print : `bool`, optional - Flag about action (print ot get) for results, the default is True. - Num : `int`, optional - Number of results for return or printing, the default is 1. - - Returns: - None, when `only_print` == True - resulting equations from population, when `only_print` == False + Method for retrieving or displaying the discovered differential equations. + + This method provides access to the final set of equations identified by the algorithm. + It allows the user to either print these equations for immediate inspection or retrieve them + as a data structure for further analysis or use in other parts of a workflow. The number of + equations returned or printed can be controlled. This is useful to inspect the best solutions + found by the algorithm. + + Args: + only_print (bool, optional): If True, the equations are printed to the console. + If False, the equations are returned as a list. Defaults to True. + only_str (bool, optional): If True, returns only string representation of equations. Defaults to False. + num (int, optional): The maximum number of equations to return or print. Defaults to 1. + + Returns: + None: If `only_print` is True. + list: A list of `num` best equations found by the algorithm. If `only_str` is True, returns list of strings. """ if self.multiobjective_mode: if only_print: @@ -917,12 +1144,18 @@ def equations(self, only_print : bool = True, only_str = False, num = 1): return self._resulting_population[:num] def solver_forms(self, grids: list = None, num: int = 1): - ''' - Method returns solver forms of the equations in a form of Python list. - - Returns: - system form, suitable for solver - ''' + """ + Converts the discovered equation systems into a solver-compatible format. + + This method prepares the identified equation systems for numerical solution by transforming them into a format readily usable by numerical solvers. This involves converting the symbolic representation of the equations into a form that can be efficiently evaluated. The number of returned systems is controlled by the `num` parameter. + + Args: + grids (list, optional): Spatial and temporal grids on which the solution is to be evaluated. Defaults to None. + num (int, optional): The number of top-performing equation systems to convert. Defaults to 1. + + Returns: + list: A list of equation systems, each formatted for use with a numerical solver. If multi-objective mode is enabled, the list contains sub-lists, each representing a Pareto-optimal set of systems. + """ forms = [] if self.multiobjective_mode: for level in self._resulting_population[:min(num, len(self._resulting_population))]: @@ -937,24 +1170,38 @@ def solver_forms(self, grids: list = None, num: int = 1): @property def cache(self): + """ + Returns the cached grid and tensor to avoid redundant computations. + + The grid cache stores previously computed grids, and the tensor cache + stores corresponding tensor representations. If a grid has already been + computed and stored, this method retrieves it along with its tensor, + preventing the need for recomputation. This optimization is crucial + for improving the efficiency of the equation discovery process. + + Args: + self: The object instance. + + Returns: + Tuple[Any, Any]: A tuple containing the grid cache and the tensor cache. + Returns (global_var.grid_cache, global_var.tensor_cache) if + global_var.grid_cache is not None, otherwise (None, global_var.tensor_cache). + """ if global_var.grid_cache is not None: return global_var.grid_cache, global_var.tensor_cache else: return None, global_var.tensor_cache def get_equations_by_complexity(self, complexity : Union[float, list]): - ''' - Get equations with desired complexity. Works best with ``EpdeSearch.visualize_solutions(...)`` - - Parameters - ---------- - complexity : float | list of floats - The complexity metric of the desited equation. For systems of equations shall be passed as the list of complexities. - - Returns - ------- - list of ``epde.structure.main_structures.SoEq objects``. - ''' + """ + Retrieve equations that strike a balance between accuracy and simplicity, as quantified by their complexity. This is particularly useful for exploring the trade-off between model fit and parsimony when analyzing the solution space. + + Args: + complexity (float | list of floats): The desired complexity level(s) for the equations. For systems of equations, provide a list of complexity values corresponding to each equation in the system. + + Returns: + list of ``epde.structure.main_structures.SoEq objects``: A list of equation objects that match the specified complexity criteria. These equations represent potential solutions that balance model fit and simplicity. + """ return self.optimizer.pareto_levels.get_by_complexity(complexity) def predict(self, system : SoEq, boundary_conditions: BoundaryConditions = None, grid : list = None, data = None, @@ -962,42 +1209,60 @@ def predict(self, system : SoEq, boundary_conditions: BoundaryConditions = None, cache_params: dict = {}, early_stopping_params: dict = {}, plotting_params: dict = {}, training_params: dict = {}, use_cache: bool = False, use_fourier: bool = False, fourier_params: dict = None, net = None, use_adaptive_lambdas: bool = False): - ''' - Predict state by automatically solving discovered equation or system. Employs solver implementation, adapted from - https://github.com/ITMO-NSS-team/torch_DE_solver. - - Parameters - ---------- - system : SoEq - Object, containing the system (or a single equation as a system of one equation) to solve. - boundary_conditions : BoundaryConditions, optional - Boundary condition objects, should match the order of differential equations due to no internal checks. - Over/underdefined solution can happen, if the number of conditions is incorrect. The default value is None, - matching automatic construction of the required Dirichlet BC from data. - grid : list of np.ndarrays, optional - Grids, defining Cartesian coordinates, on which the equations will be solved. The default is None, specifing - the use of grids, stored in cache during equation learning. - data : TYPE, optional - Dataset, from which the boundary conditions can be automatically created. The default is None, making use of - the training datasets, stored in cache during equation training. - system_file : str, optional - Filename for the pickled equation/system of equations. If passed, **system** can be None. The default is None, meaning no equation. - solver_kwargs : dict, optional - Parameters of the solver. The default is {'use_cache' : True}, with that no - mode : TYPE, optional - Key, defining used method of the automatic DE solution. Supported methods: 'NN', 'mat' and 'autodiff'. The default is 'NN'. - - Raises - ------ - ValueError - DESCRIPTION. - - Returns - ------- - TYPE - DESCRIPTION. - - ''' + """ + Predicts the state by solving the discovered equation or system of equations. This leverages a solver implementation adapted from https://github.com/ITMO-NSS-team/torch_DE_solver to find a numerical solution. This is a crucial step in the EPDE workflow, allowing us to validate the discovered equations against the original data and generate predictions. + + Parameters + ---------- + system : SoEq + Object containing the system (or a single equation as a system of one equation) to solve. + boundary_conditions : BoundaryConditions, optional + Boundary condition objects, should match the order of differential equations due to no internal checks. + Over/underdefined solution can happen if the number of conditions is incorrect. The default value is None, + matching automatic construction of the required Dirichlet BC from data. + grid : list, optional + Grids, defining Cartesian coordinates, on which the equations will be solved. The default is None, specifying + the use of grids stored in cache during equation learning. + data : TYPE, optional + Dataset, from which the boundary conditions can be automatically created. The default is None, making use of + the training datasets stored in cache during equation training. + system_file : str, optional + Filename for the pickled equation/system of equations. If passed, **system** can be None. The default is None, meaning no equation. + mode : str, optional + Key, defining used method of the automatic DE solution. Supported methods: 'NN', 'mat' and 'autodiff'. The default is 'NN'. + compiling_params : dict, optional + Parameters for the equation compiling stage. The default is {}. + optimizer_params : dict, optional + Parameters for the optimization algorithm used in the solver. The default is {}. + cache_params : dict, optional + Parameters for caching intermediate results. The default is {}. + early_stopping_params : dict, optional + Parameters for early stopping during the solver's training. The default is {}. + plotting_params : dict, optional + Parameters for plotting the solution. The default is {}. + training_params : dict, optional + Parameters for the solver's training process. The default is {}. + use_cache : bool, optional + Flag indicating whether to use cached results. The default is False. + use_fourier : bool, optional + Flag indicating whether to use Fourier features. The default is False. + fourier_params : dict, optional + Parameters for the Fourier features. The default is None. + net : torch.nn.Module, optional + A pre-trained neural network to use as a solver. The default is None. + use_adaptive_lambdas : bool, optional + Flag indicating whether to use adaptive lambdas. The default is False. + + Raises + ------ + ValueError + If no system is provided, either directly or via a file. + + Returns + ------- + solution_model : torch.nn.Module + The trained model that represents the solution to the discovered equation. + """ if system is not None: print('Using explicitly sent system of equations.') @@ -1038,8 +1303,19 @@ def predict(self, system : SoEq, boundary_conditions: BoundaryConditions = None, def visualize_solutions(self, dimensions:list = [0, 1], **visulaizer_kwargs) -> None: ''' - Plot discovered equation, using matplotlib tools. By default the method plots only the Pareto-optimal - equations from the population. Furthermore, the annotate of the candidate equations are made with LaTeX toolkit. + Plot the discovered equations using matplotlib. + + By default, the method plots only the Pareto-optimal equations from the population. + Annotations of the candidate equations are rendered using LaTeX. This visualization + helps in understanding the trade-offs between different equation structures and their + fit to the data, allowing for informed selection of the most appropriate model. + + Args: + dimensions (list, optional): The dimensions to plot. Defaults to [0, 1]. + **visulaizer_kwargs: Keyword arguments to pass to the plotting function. + + Returns: + None ''' if self.multiobjective_mode: self.optimizer.plot_pareto(dimensions=dimensions, **visulaizer_kwargs) @@ -1048,7 +1324,42 @@ def visualize_solutions(self, dimensions:list = [0, 1], **visulaizer_kwargs) -> class ExperimentCombiner(object): + """ + Combines results from multiple experiments to find the best solutions. + + Class Methods: + - create_best: + """ + def __init__(self, candidates: Union[ParetoLevels, List[SoEq], List[ParetoLevels]]): + """ + Initializes the ComplexityHandler, preparing complexity data for equation discovery. + + This method takes a list of candidate solutions, extracts their complexities, + and organizes them to facilitate efficient searching of the equation space. + The extracted complexities are stored alongside their corresponding candidate + solutions, and then sorted to enable efficient comparison during the + equation discovery process. + + Args: + candidates: A list of candidate solutions, which can be either + ParetoLevels objects, SoEq objects, or a list of ParetoLevels objects. + + Returns: + None. + + Class Fields: + complexity_matched (List[Tuple[Any, List[Any]]]): A list of tuples, where each tuple contains + a candidate solution and a list of its complexities. + ordered_complexities (List[List[Any]]): A list of lists, where each inner list contains + the sorted unique complexities for a specific complexity metric. + + WHY: + This initialization is crucial for efficiently exploring the equation space. By + extracting and organizing complexities, the system can effectively compare + different candidate solutions based on their complexity profiles, guiding the + search towards simpler and more accurate equations. + """ self.complexity_matched = self.get_complexities(candidates) complexity_sets = [set() for i in self.complexity_matched[0][1]] for eq, complexity in self.complexity_matched: @@ -1058,10 +1369,52 @@ def __init__(self, candidates: Union[ParetoLevels, List[SoEq], List[ParetoLevels @singledispatchmethod def get_complexities(self, candidates) -> list: + """ + Calculate the complexity of each candidate equation. + + This method assesses the complexity of candidate equations, + providing a basis for balancing model accuracy with parsimony + during the equation discovery process. More complex equations + might fit the data better but are also more prone to overfitting. + + Args: + candidates: A list of candidate equations. + + Returns: + A list of complexities, where each element corresponds to the + complexity of the respective candidate equation. + + Raises: + NotImplementedError: Always raised, indicating that the method + should be implemented in a subclass. + """ raise NotImplementedError('Incorrect type of equations to parse') @get_complexities.register def _(self, candidates: list) -> list: + """ + Processes a list of candidate solutions to extract complexity information. + + This method prepares candidate solutions for further analysis by extracting + relevant complexity measures. The type of the candidate solutions determines + how the complexity is assessed. It supports `ParetoLevels` and `SoEq` types. + The extracted complexity information is used to evaluate and compare different + equation candidates during the evolutionary search process. + + Args: + candidates: A list of candidate solutions. The type of the elements + determines the processing logic. + + Returns: + A list containing complexity information extracted from the candidates. + The format of the returned list depends on the type of the candidates: + - If candidates are of type `ParetoLevels`, returns a list of complexities. + - If candidates are of type `SoEq`, returns a list of tuples, where each + tuple contains a candidate and its complexity objectives. + + Raises: + ValueError: If the type of the candidate is not supported. + """ if isinstance(candidates[0], ParetoLevels): return reduce(lambda x, y: x.append(y), [self.get_complexities(pareto_level) for pareto_level in candidates], []) @@ -1076,11 +1429,39 @@ def _(self, candidates: list) -> list: @get_complexities.register def _(self, candidates: ParetoLevels) -> list: + """ + Combines complexities from all Pareto levels into a single list. + + This method aggregates the complexities associated with each Pareto level + present in the provided `candidates` object. By consolidating these + complexities, the method facilitates a comprehensive evaluation of the + candidate solutions discovered during the equation search process. This + aggregation is crucial for assessing the overall complexity of the + identified equations and selecting the most parsimonious models. + + Args: + candidates (ParetoLevels): A ParetoLevels object containing the Pareto levels to process. + + Returns: + list: A list containing the combined complexities from all Pareto levels. + """ eqs = reduce(lambda x, y: x.append(y), [self.get_complexities(level) for level in candidates.levels], []) return eqs def create_best_for_complexity(self, complexity: tuple, pool: TFPool): + """ + Creates a compound equation by selecting the best candidate equations for each dimension's complexity level. + + This method aims to construct an equation that accurately represents the system by choosing the most suitable equation for each dimension based on the provided complexity constraints. It iterates through the specified complexity levels, identifies the best candidate equation for each, and combines them into a single, comprehensive equation. This approach allows the framework to adapt to varying levels of complexity across different dimensions of the problem. + + Args: + complexity: A tuple representing the desired complexity levels for each dimension. A value of `None` indicates that the best equation should be selected regardless of complexity for that dimension. + pool: A TFPool object (not used in the provided code). + + Returns: + The compound equation created from the best candidate equations, representing the discovered relationship between variables. + """ vars_to_describe = self.complexity_matched[0][0].vars_to_describe # Get dependent variables best_eqs = [] @@ -1101,10 +1482,63 @@ def create_best_for_complexity(self, complexity: tuple, pool: TFPool): return compound_equation def create_best(self, pool: TFPool): + """ + Creates the best program variant based on the highest complexities. + + It retrieves the highest complexity value for each program in the + ordered complexities list and then uses these values to create the + best program variant. This is done to prioritize more complex and potentially more accurate models + identified during the equation discovery process. + + Args: + pool: The TFPool object used for program creation. + + Returns: + The best program variant created based on the highest complexities. + """ best_qualities_compl = [complexities[-1] for complexities in self.ordered_complexities] return self.create_best_for_complexity(best_qualities_compl, pool) class EpdeMultisample(EpdeSearch): + """ + Multisample EPDE class for equation discovery from multiple datasets. + + This class extends the EPDE framework to handle multiple data samples, + allowing for equation discovery across different datasets. It provides + methods for setting domain properties, uploading test functions, setting + data samples, and fitting the EPDE search algorithm. + + Class Methods: + - __init__ + - set_domain_properties + - _upload_g_func + - set_samples + - fit + + Attributes: + - use_default_strategy: True (base and recommended value), if the default evolutionary strategy will be used, + False if the user-defined strategy will be passed further. Otherwise, the search will + not be conducted. + - time_axis: Indicator of time axis in data and grids. Used in normalization for regressions. + - function_form: Auxilary function, used in the weak derivative definition. Default function is negative square function + with maximum values in the center of the domain. + - boundary: Boundary width for the domain. Boundary points will be ignored for the purposes of equation discovery + - use_solver: Allow use of the automaic partial differential solver to evaluate fitness of the candidate solutions. + - dimensionality: Dimensionality of the problem. ! Currently you should pass value, reduced by one ! + - verbose_params: Description, of algorithm details, that will be demonstrated to the user. Usual + - memory_for_cache: Rough estimation of the memory, which can be used for cache of pre-evaluated tensors during the equation + - prune_domain: If ``True``, subdomains with no dynamics will be pruned from data. Default value: ``False``. + - pivotal_tensor_label: Indicator, according to which token data will be pruned. Default value - ``'du/dt'``, where + ``t`` is selected as a time axis from ``time_axis`` parameter. + - pruner: Pruner object, which will remove subdomains with no dynamics i.e. with derivative + identically equal to zero. + - threshold: Pruner parameter, indicating the boundary of interval in which the pivotal tensor values are + considered as zeros. Default value: 1e-2 + - division_fractions: Number of subdomains along each axis, defining the division of the domain for pruning. + Default value: 3 + - rectangular: A line of subdomains along an axis can be removed if all values inside them are identical to zero. + """ + def __init__(self, data_samples : List[List], multiobjective_mode: bool = True, use_default_strategy: bool = True, director=None, director_params: dict = {'variation_params': {}, 'mutation_params': {}, @@ -1115,42 +1549,34 @@ def __init__(self, data_samples : List[List], multiobjective_mode: bool = True, pivotal_tensor_label=None, pruner=None, threshold: float = 1e-2, division_fractions=3, rectangular: bool = True, params_filename: str = None): """ - Args: - use_default_strategy (`bool`): optional - True (base and recommended value), if the default evolutionary strategy will be used, - False if the user-defined strategy will be passed further. Otherwise, the search will - not be conducted. - time_axis (`int`): optional - Indicator of time axis in data and grids. Used in normalization for regressions. - function_form (`callable`): optional - Auxilary function, used in the weak derivative definition. Default function is negative square function - with maximum values in the center of the domain. - boundary (`int|tuple/list of integers`): optional - Boundary width for the domain. Boundary points will be ignored for the purposes of equation discovery - use_solver (`bool`): optional - Allow use of the automaic partial differential solver to evaluate fitness of the candidate solutions. - dimensionality (`int`): optional - Dimensionality of the problem. ! Currently you should pass value, reduced by one ! - verbose_params (`dict`): optional - Description, of algorithm details, that will be demonstrated to the user. Usual - memory_for_cache (`int|float`): optional - Rough estimation of the memory, which can be used for cache of pre-evaluated tensors during the equation - prune_domain (`bool`): optional - If ``True``, subdomains with no dynamics will be pruned from data. Default value: ``False``. - pivotal_tensor_label (`str`): optional - Indicator, according to which token data will be pruned. Default value - ``'du/dt'``, where - ``t`` is selected as a time axis from ``time_axis`` parameter. - pruner (`object`): optional - Pruner object, which will remove subdomains with no dynamics i.e. with derivative - identically equal to zero. - threshold (`float`): optional - Pruner parameter, indicating the boundary of interval in which the pivotal tensor values are - considered as zeros. Default value: 1e-2 - division_fractions (`int`): optional - Number of subdomains along each axis, defining the division of the domain for pruning. - Default value: 3 - rectangular(`bool`): optional - A line of subdomains along an axis can be removed if all values inside them are identical to zero. + Initializes the EpdeMultisample class, configuring the equation discovery process across multiple data samples. + + This class extends the functionality of the base EPDE framework to handle multiple datasets, + allowing for more robust and generalizable equation discovery. It preprocesses and stacks + grids from multiple samples to define the problem domain. + + Args: + data_samples (List[List]): A list of data samples, where each sample is a list containing grids and corresponding data. + multiobjective_mode (bool): Whether to use multiobjective optimization. Defaults to True. + use_default_strategy (bool): If True, uses the default evolutionary strategy. If False, a user-defined strategy is expected. Defaults to True. + director: An optional director object to manage the evolutionary process. + director_params (dict): Parameters for the director, including variation, mutation, pareto combiner, and pareto updater settings. + time_axis (int): The index of the time axis in the data. Defaults to 0. + function_form (callable): An auxiliary function used in the weak derivative definition. + boundary (int|tuple/list of integers): The boundary width for the domain; boundary points are ignored during equation discovery. Defaults to 0. + use_solver (bool): Whether to use an automatic partial differential equation solver to evaluate candidate solutions. Defaults to False. + verbose_params (dict): Parameters controlling the verbosity of the algorithm's output. Defaults to {'show_iter_idx' : True}. + memory_for_cache (int|float): An estimate of the memory to use for caching pre-evaluated tensors. Defaults to 5. + prune_domain (bool): If True, subdomains with no dynamics are removed from the data. Defaults to False. + pivotal_tensor_label (str): The label of the tensor used to identify subdomains with dynamics. Defaults to None. + pruner: An optional pruner object to remove subdomains with no dynamics. + threshold (float): The threshold for determining when pivotal tensor values are considered zero for pruning. Defaults to 1e-2. + division_fractions (int): The number of subdomains along each axis for pruning. Defaults to 3. + rectangular (bool): If True, entire lines of subdomains with zero values can be removed. Defaults to True. + params_filename (str): Path to the file with custom parameters. Defaults to None. + + Returns: + None """ super().__init__(multiobjective_mode = multiobjective_mode, use_default_strategy = use_default_strategy, director = director, director_params = director_params, time_axis = time_axis, @@ -1184,38 +1610,39 @@ def set_domain_properties(self, coordinate_tensors, memory_for_cache, boundary_w pivotal_tensor_label=None, pruner=None, threshold: float = 1e-5, division_fractions: int = 3, rectangular: bool = True): """ - Setting properties for processing considered domain, such as removing areas with no dynamics, - and setting bounderes. Can be used for uploading test function. In enseble equation learning can not - take coordinates as the argument. - - Parameters - ---------- - memory_for_cache : int - Allowed amount of memory (in percentage) for data storage. - boundary_width : int|list - The number of unaccountable elements at the edges of the domain. - function_form : callable, optional - Testing function connected to the weak derivative notion, the default value is None, that - corresponds with the product of normalized inverse square functions of the coordinates, - centered at the middle of the domain. - prune_domain : bool - Flag, enabling area cropping by removing subdomains with constant values, default - False. - pivotal_tensor_label : np.ndarray - Pattern that guides the domain pruning, the default is None. - pruner : DomainPruner - Object for selecting domain region, the default is None. - threshold : float, optional - The boundary at which values are considered zero, the default is 1e-5. - division_fractions : int, optional - Number of fraction for each axis (if this is integer than all axis are dividing by - same fractions), the default is 3. - rectangular : bool, optional - Flag indecating that crop subdomains are rectangle, default - True. - - Returns - ------- - None. - + Sets the properties of the domain for equation discovery. + + This method configures the domain by setting boundaries, pruning regions with minimal dynamics, + and defining a test function. These properties are crucial for focusing the equation search + on the most relevant areas of the domain and for evaluating candidate equations effectively. + + Args: + coordinate_tensors (list): + List of tensors representing the coordinates of the domain. + memory_for_cache (int): + Allowed amount of memory (in percentage) for data storage. + boundary_width (int | list): + The number of unaccountable elements at the edges of the domain. + function_form (callable, optional): + Testing function connected to the weak derivative notion. The default value is None, which + corresponds to the product of normalized inverse square functions of the coordinates, + centered at the middle of the domain. + prune_domain (bool, optional): + Flag enabling area cropping by removing subdomains with constant values. Default is False. + pivotal_tensor_label (np.ndarray, optional): + Pattern that guides the domain pruning. The default is None. + pruner (DomainPruner, optional): + Object for selecting domain region. The default is None. + threshold (float, optional): + The boundary at which values are considered zero. The default is 1e-5. + division_fractions (int, optional): + Number of fractions for each axis (if this is an integer, then all axes are divided by the + same fraction). The default is 3. + rectangular (bool, optional): + Flag indicating that crop subdomains are rectangular. Default is True. + + Returns: + None """ # raise NotImplementedError('In ensemble mode the domain is set in `set_samples` method.') @@ -1230,16 +1657,22 @@ def set_domain_properties(self, coordinate_tensors, memory_for_cache, boundary_w def _upload_g_func(self, function_form: Union[Callable, np.ndarray, list] = None, boundary_width: int = None): """ - Loading testing function connected to the weak derivative notion. In contrast to a single equation - discovery approach the testing function is not immediately stored in cache, but saved to be used - later and applied to equations. - - Args: - function_form (`callable`, or `np.ndarray`, or `list[np.ndarray]`) - Test function, default using inverse polynomial with max in the domain center. - - Returns: - None + Loading a test function for weak derivative calculations. + + This function prepares a test function, crucial for evaluating equation fitness using weak derivatives. + Unlike single-equation approaches, the test function is stored for later application across multiple equation candidates, + allowing for a more comprehensive and efficient search for the best model. + + Args: + function_form (`callable`, `np.ndarray`, or `list`): + The test function itself. If a callable is provided, it's used directly (or a default is created if None). + If an array or list of arrays is given, it's used as the test function directly. + boundary_width (`int`, optional): + Specifies the width of the boundary region to exclude when evaluating the test function. + Defaults to the boundary width defined in the global grid cache. + + Returns: + None: The test function is stored internally for later use in equation evaluation. """ boundary_width = boundary_width if boundary_width is not None else global_var.grid_cache.boundary_width if isinstance(function_form, (np.ndarray, list)): @@ -1271,6 +1704,39 @@ def uniformize(data): def set_samples(self, data_samples: List[List], sample_derivs: List[List[np.ndarray]] = None, var_names: List[str] = ['u',], max_deriv_orders: Union[int, list[int]] = 1, additional_tokens: list = [], data_fun_pow: int = 1, deriv_fun_pow: int = 1): + """ + Sets the data samples and their derivatives to construct a pool of equation terms. + + This method prepares the data by extracting grid and data values from the input samples. + It then organizes this information to create a comprehensive pool of tokens, + which are the building blocks for constructing candidate differential equations. + This setup is crucial for the subsequent symbolic regression process, where the + algorithm searches for the best equation structure that fits the data. + The method supports both single and multiple dependent variables, accommodating + various data formats such as NumPy arrays or lists/tuples of NumPy arrays. + + Args: + data_samples: A list of lists, where each inner list contains the grid + and data values for a sample. The data values can be a NumPy array + or a list/tuple of NumPy arrays for multiple dependent variables. + sample_derivs: A list of lists containing the derivatives of the + data samples. Each inner list corresponds to a sample and + contains NumPy arrays representing the derivatives. Defaults to None. + var_names: A list of strings representing the names of the dependent + variables. Defaults to ['u']. + max_deriv_orders: An integer or a list of integers specifying the + maximum order of derivatives to be considered for each variable. + Defaults to 1. + additional_tokens: A list of additional tokens to be included in the + token pool. Defaults to []. + data_fun_pow: An integer representing the power to which the data + is raised when creating tokens. Defaults to 1. + deriv_fun_pow: An integer representing the power to which the + derivatives are raised when creating tokens. Defaults to 1. + + Returns: + None. + """ if isinstance(data_samples[0][1], np.ndarray): data_comb = [sample[1] for sample in data_samples] print('Samples are np.ndarrays somehow') @@ -1331,60 +1797,59 @@ def fit(self, samples: List[Tuple], equation_terms_max_number=6, equation_factor pool: TFPool = None, population: Union[ParetoLevels, Population] = None): """ Fit epde search algorithm to obtain differential equations, describing passed data. - - Parameters - ---------- - data : np.ndarray | list | tuple, optional - Values of modeled variables. If the variable is single (i.e. deriving a single equation), - it can be passed as the numpy.ndarray or as the list/tuple with a single element; - multiple variables are not supported yet, use older interfaces. Default value is None, but it - shall be used only for retraining, when the pool argument is passed. - equation_terms_max_number : int, optional - The maximum number of terms, present in the derived equations, the default is 6. - equation_factors_max_number : int, optional - The maximum number of factors (token functions; real-valued coefficients are not counted here), - present in terms of the equaton, the default is 1. - variable_names : list | str, optional - Names of the independent variables, passed into search mechanism. Length of the list must correspond - to the number of np.ndarrays, sent with in ``data`` parameter. In case of system of differential equation discovery, - all variables shall be named here, default - ``['u',]``, representing a single variable *u*. - eq_sparsity_interval : tuple, optional - The left and right boundaries of interval with sparse regression values. Undirectly influences the - number of active terms in the equation, the default is ``(1e-4, 2.5)``. - derivs : list or list of lists of np.ndarrays, optional - Pre-computed values of derivatives. If ``None`` is passed, the derivatives are calculated in the - method. Recommended to use, if the computations of derivatives take too long. For further information - about using data, prepared in advance, check ``epde.preprocessing.derivatives.preprocess_derivatives`` - function, default - None. - max_deriv_order : int | list | tuple, optional - Highest order of calculated derivatives, the default is 1. - additional_tokens : list of TokenFamily or Prepared_tokens, optional - Additional tokens, that would be used to construct the equations among the main variables and their - derivatives. Objects of this list must be of type ``epde.interface.token_family.TokenFamily`` or - of ``epde.interface.prepared_tokens.Prepared_tokens`` subclasses types. The default is None. - field_smooth : bool, optional - Parameter, if the input variable fields shall be smoothed to avoid the errors. If the data is - assumed to be noiseless, shall be set to False, otherwise - True, the default - False. - memory_for_cache : int | float, optional - Limit for the cache (in fraction of the memory) for precomputed tensor values to be stored: - if int, will be considered as the percentage of the entire memory, and if float, - then as a fraction of memory, the default is 5. - data_fun_pow : int, optional - Maximum power of token, the default is 1. - optimizer : SimpleOptimizer | MOEADDOptimizer, optional - Pre-defined optimizer, that will be used during evolution. Shall correspond with the mode - (single- and multiobjective). The default is None, matching no use of pre-defined optimizer. - pool : TFPool, optional - Pool of tokens, that can be explicitly passed. The default is None, matching no use of passed pool. - population : Population | ParetoLevels, optional - Population of candidate equatons, that can be optionally passed in explicit form. The type of objects - must match the optimization algorithm: epde.optimizers.single_criterion.optimizer.Population for - single-objective mode and epde.optimizers.moeadd.moeadd.ParetoLevels for multiobjective optimization. - The default is None, specifing no passed population. - - Returns - ------- - None. + This method orchestrates the equation discovery process by initializing the search space, + setting up the optimization algorithm, and running the evolutionary search. It leverages + the provided data samples and parameter settings to identify candidate equations that + effectively model the underlying dynamics. + + Parameters + ---------- + samples : List[Tuple] + List of data samples, where each sample is a tuple containing the values of the + modeled variables. + equation_terms_max_number : int, optional + The maximum number of terms allowed in the derived equations, the default is 6. + equation_factors_max_number : int, optional + The maximum number of factors (token functions; real-valued coefficients are not counted here) + allowed in each term of the equation, the default is 1. + variable_names : list | str, optional + Names of the independent variables, passed into search mechanism. Length of the list must correspond + to the number of np.ndarrays, sent with in ``data`` parameter. In case of system of differential equation discovery, + all variables shall be named here, default - ``['u',]``, representing a single variable *u*. + eq_sparsity_interval : tuple, optional + The left and right boundaries of interval with sparse regression values. Undirectly influences the + number of active terms in the equation, the default is ``(1e-4, 2.5)``. + derivs : list or list of lists of np.ndarrays, optional + Pre-computed values of derivatives. If ``None`` is passed, the derivatives are calculated in the + method. Recommended to use, if the computations of derivatives take too long. For further information + about using data, prepared in advance, check ``epde.preprocessing.derivatives.preprocess_derivatives`` + function, default - None. + max_deriv_order : int | list | tuple, optional + Highest order of calculated derivatives, the default is 1. + additional_tokens : list of TokenFamily or Prepared_tokens, optional + Additional tokens, that would be used to construct the equations among the main variables and their + derivatives. Objects of this list must be of type ``epde.interface.token_family.TokenFamily`` or + of ``epde.interface.prepared_tokens.Prepared_tokens`` subclasses types. The default is None. + data_fun_pow : int, optional + Maximum power of token, the default is 1. + deriv_fun_pow : int, optional + Maximum power of derivative token, the default is 1. + optimizer : SimpleOptimizer | MOEADDOptimizer, optional + Pre-defined optimizer, that will be used during evolution. Shall correspond with the mode + (single- and multiobjective). The default is None, matching no use of pre-defined optimizer. + pool : TFPool, optional + Pool of tokens, that can be explicitly passed. The default is None, matching no use of passed pool. + population : Population | ParetoLevels, optional + Population of candidate equatons, that can be optionally passed in explicit form. The type of objects + must match the optimization algorithm: epde.optimizers.single_criterion.optimizer.Population for + single-objective mode and epde.optimizers.moeadd.moeadd.ParetoLevels for multiobjective optimization. + The default is None, specifing no passed population. + + Returns + ------- + None. + The method does not return any value. The discovered equations and related information + are stored within the class instance. """ # TODO: ADD EXPLICITLY SENT POPULATION PROCESSING cur_params = {'variable_names' : variable_names, 'max_deriv_order' : max_deriv_order, diff --git a/epde/interface/logger.py b/epde/interface/logger.py index 6315bd4f..3c946cad 100644 --- a/epde/interface/logger.py +++ b/epde/interface/logger.py @@ -17,6 +17,29 @@ from epde.interface.equation_translator import translate_equation, parse_equation_str def equations_match(eq_checked: Equation, eq_ref: Equation): + """ + Checks the structural similarity between two equations by comparing their terms. + + This method quantifies how well the term structure of one equation matches + another. It identifies common terms, missing terms, and extra terms, providing + a measure of structural agreement. This comparison is crucial for assessing + the quality of discovered equations against a reference equation. + + Args: + eq_checked: The equation to be checked. + eq_ref: The reference equation. + + Returns: + tuple: A tuple containing three integers: + - matching: The number of terms in the reference equation that are also + present in the checked equation. + - missing: The number of terms in the reference equation that are not + present in the checked equation. + - extra: The number of terms in the checked equation that are not + present in the reference equation. + + WHY: This method is used to evaluate the discovered equations by comparing them to a reference equation. + """ def parse_equation(equation: Equation, eps = 1e-9): term_weights = [] for idx, term in enumerate(equation.structure): @@ -40,11 +63,72 @@ def parse_equation(equation: Equation, eps = 1e-9): return (matching, missing, extra) def systems_match(checked_system: SoEq, reference_system: SoEq): + """ + Compares the equations associated with each variable in two systems. + + This function is essential for evaluating the similarity between candidate equation systems + generated during the equation discovery process. By comparing equations variable-by-variable, + the evolutionary algorithm can effectively assess the fitness and diversity of the population. + + Args: + checked_system: The system to be checked (SoEq object). + reference_system: The reference system to compare against (SoEq object). + + Returns: + A list of booleans, where each boolean indicates whether the equations + for a corresponding variable match between the two systems. The order of booleans + corresponds to the order of variables in `checked_system.vars_to_describe`. + """ return [equations_match(checked_system.vals[var], reference_system.vals[var]) for var in checked_system.vars_to_describe] class Logger(): + """ + A simple logger class for recording and dumping log data. + + Class Methods: + - __init__ + - reset + - dump + - add_log + + Class Fields: + _log (dict): An empty dictionary to store log data. + _meta (dict): A dictionary to store metadata, initialized with an empty list for 'aggregation_key'. + name (str): The name of the logger, updated only if the 'name' argument is provided. + """ + def __init__(self, name, referential_equation = None, pool = None): + """ + Initializes a Logger instance with a name and an optional referential equation. + + This method sets up the logger with a given name and associates it with + a referential equation, which can be used to guide the equation discovery + process. If the equation is provided as a string or dictionary, it is + translated into a suitable format using a provided pool of tokens. This + translation step ensures that the equation is compatible with the + equation discovery algorithms. + + Args: + name (str): The name of the logger. + referential_equation (str, dict, or pre-translated equation, optional): + An optional equation that serves as a reference point for the + equation discovery process. Defaults to None. + pool (list of Token, optional): A pool of tokens used for translating the + equation if it's a string or dictionary. Required if + `referential_equation` is a string or dictionary. Defaults to None. + + Raises: + ValueError: If the referential equation is a string or dictionary, + but the pool of tokens is missing, as translation is impossible. + + Returns: + None + + Class Fields: + _name (str): The name of the object, initialized by `reset()`. + _referential_equation: The referential equation associated with the object. + """ self.reset(name = name) if isinstance(referential_equation, str) or isinstance(referential_equation, dict): if pool is None: @@ -53,6 +137,23 @@ def __init__(self, name, referential_equation = None, pool = None): self._referential_equation = referential_equation def reset(self, name = None): + """ + Resets the logger to a clean state, preparing it for a new equation discovery task. + + This method clears all stored log data and metadata, effectively starting a new search. It also allows for renaming the logger, which can be useful for tracking different equation discovery runs or experiments. + + Args: + name (str, optional): A new name for the logger. If None, the current name is retained. Defaults to None. + + Returns: + None + + This method ensures that previous equation search results do not interfere with new ones by clearing the internal state. + It initializes the following class fields: + _log (dict): An empty dictionary to store log data. + _meta (dict): A dictionary to store metadata, initialized with an empty list for 'aggregation_key'. + name (str): The name of the logger, updated only if the 'name' argument is provided. + """ try: self.log_out() except AttributeError: @@ -64,12 +165,44 @@ def reset(self, name = None): self.name = name def dump(self): + """ + Dumps the current log data to a JSON file and resets the log. + + This method serializes the internal log data, including metadata, + into a JSON file specified by the object's name attribute. After + dumping the data, it resets the internal log to prepare for new entries. + This is done to persist the log data for later analysis or use in equation discovery. + + Args: + self: The Logger instance. + + Returns: + None. + """ self._log['meta'] = self._meta with open(self.name, 'w') as outfile: json.dump(self._log, outfile) self.reset() def add_log(self, key, entry, aggregation_key = None, **kwargs): + """ + Adds a new equation to the internal log, tracking its characteristics and performance. + + This method stores the equation's textual form, term matching statistics, + mean absolute error (MAE) on the training data, and an optional aggregation key + for grouping similar equations. This information is crucial for analyzing + the population of discovered equations and selecting the most promising candidates + during the evolutionary search process. + + Args: + key: A unique identifier for the equation being logged. + entry: The equation object to log. + aggregation_key: An optional key for grouping similar equations. + **kwargs: Additional keyword arguments to store in the log entry. + + Returns: + None. + """ match = systems_match(entry, self._referential_equation) if self._referential_equation is not None else (0, 0, 0) try: mae = [np.mean(np.abs(eq.evaluate(False, True)[0])) for eq in entry] @@ -91,7 +224,20 @@ class LogLoader(object): ''' Object for the basic analytics of the equation discovery process ''' + def __init__(self, filename: Union[str, list]): + """ + Initializes the LogLoader, reading log data from specified file(s) to prepare for equation discovery. + + This method loads log data, which serves as the foundation for identifying underlying differential equations. + It handles both single and multiple log files, appending the data from each into a unified log structure. + + Args: + filename (Union[str, list]): The path to a single log file or a list of paths to multiple log files. + + Returns: + None: The method initializes the LogLoader instance and loads data, but does not return any value. + """ self.reset() if isinstance(filename, str): file = open(filename, 'r') @@ -104,11 +250,55 @@ def __init__(self, filename: Union[str, list]): file.close() def reset(self): + """ + Resets the internal state, preparing for a new data loading and equation discovery cycle. + + This method clears the current log and any stored variables, ensuring a clean slate + for subsequent data processing and equation search operations. This is crucial for + isolating the impact of each data loading attempt and avoiding contamination from + previous states. + + Args: + self: The object instance. + + Returns: + None. This method modifies the object's internal state. + + Initializes: + _log (list): An empty list to store log messages. + _variables (None): A variable to store variables, initialized to None. + """ self._log = [] self._variables = None @staticmethod def eq_analytics(equation_string: str): + """ + Analyzes an equation string to extract term statistics for equation discovery. + + This method parses an equation string, extracts the terms, and + calculates statistics based on the terms. It uses the + `parse_equation_str` function to split the equation into its + constituent terms. The statistics are stored in a dictionary where + the keys are frozensets of the terms (excluding the coefficient for + terms on the left-hand side) and the values are either the + coefficient (as a string) or 1.0 for the right-hand side term. + This information is crucial for identifying potential terms and their + contributions within the equation. + + Args: + equation_string: The equation string to analyze. The expected + format is a string where terms are separated by ' + ' on the + left-hand side and ' * ' within each term. The left and right + sides are separated by ' = '. + + Returns: + dict: A dictionary containing term statistics. The keys are + frozensets of strings representing the terms (excluding the + coefficient for terms on the left-hand side), and the values + are either the coefficient (as a string) or 1.0 for the + right-hand side term. + """ eq_terms = parse_equation_str(equation_string) term_stats = {frozenset(eq_terms[-1]) : 1.} for term in eq_terms[:-2]: @@ -116,12 +306,49 @@ def eq_analytics(equation_string: str): return term_stats def get_aggregation_keys(self): + """ + Retrieves the aggregation keys from the log entries. + + This method iterates through the internal log data and extracts the + 'aggregation_key' from the 'meta' dictionary of each log entry. + The aggregation keys are essential for grouping and processing log entries + that share common characteristics, enabling the framework to identify + patterns and relationships within the data relevant to discovering + governing differential equations. + + Args: + self: The object instance. + + Returns: + list: A list of aggregation keys extracted from the log entries. + """ keys = [] for log in self._log: keys.append(log['meta']['aggregation_key']) return keys def obtain_parsed_log(self, variables: list = ['u',], aggregation_key: tuple = None): + """ + Obtains and parses the system log to extract term statistics for equation discovery. + + This method processes log entries, extracts equation forms, and analyzes terms + based on specified variables and an aggregation key. The extracted term statistics + are essential for identifying candidate equation structures within the EPDE framework. + It filters log entries based on the aggregation key and parses the equation forms + associated with each entry. The term statistics are then aggregated to provide + a comprehensive view of term presence and their values across different equations. + This information is crucial for the evolutionary algorithm to effectively search + for the best-fitting differential equation. + + Args: + variables: A list of variable names to consider during parsing. Defaults to ['u']. + aggregation_key: A tuple representing the aggregation key to filter log entries. + + Returns: + dict: A dictionary containing term presence logs, where keys are variable indices + and values are dictionaries of term statistics (term -> list of values). + The term statistics are represented as lists of floating-point values. + """ if self._variables is None: self._variables = variables else: @@ -160,6 +387,27 @@ def strap_cases(eq_string: str): @staticmethod def get_stats(terms: Union[list, tuple], parsed_log: dict, metrics: list = [np.mean, np.var, np.size], metric_names: list = ['mean', 'var', 'disc_num'], variable_eq: str = 'u', variables = ['u',]): + """ + Calculates statistical features of equation terms based on their coefficients in the parsed log data. + + This method extracts coefficient values for each specified term from the parsed log, computes statistical + metrics on the absolute values of these coefficients (excluding zeros), and returns the calculated + statistics along with corresponding labels. This helps to quantify the importance and characteristics + of each term within the discovered equation. + + Args: + terms: A list or tuple of frozensets, where each frozenset represents a term in the equation. + parsed_log: A dictionary containing parsed log data, with keys corresponding to variable indices and terms. + metrics: A list of functions to apply as metrics (default: [np.mean, np.var, np.size]). + metric_names: A list of names corresponding to the metrics (default: ['mean', 'var', 'disc_num']). + variable_eq: The variable to extract from the parsed log (default: 'u'). + variables: List of variables to consider + + Returns: + tuple: A tuple containing two lists: + - labels: A list of strings representing the labels for each statistic, constructed from term names and metric names. + - stats: A list of calculated statistics, providing a numerical representation of term characteristics. + """ assert all([isinstance(term, frozenset) for term in terms]) stats = [] for term in terms: @@ -182,6 +430,21 @@ def get_stats(terms: Union[list, tuple], parsed_log: dict, metrics: list = [np.m def to_pandas(self, terms: Union[list, tuple], metrics: list = [np.mean, np.var, np.size], metric_names: list = ['mean', 'var', 'disc_num'], variable: str = 'u', variables: list = ['u',]): + """ + Converts the internal log data into a list of Pandas DataFrames, facilitating subsequent equation discovery. + + This method transforms the raw log data into a structured format suitable for analysis by EPDE's equation discovery algorithms. It aggregates data based on specified terms and metrics, constructing a Pandas DataFrame for each log entry. This aggregation is crucial for summarizing the data and extracting relevant features that can be used to identify underlying differential equations. + + Args: + terms: The terms to use for aggregation, guiding the feature extraction process. + metrics: A list of metric functions to apply (e.g., mean, variance). Defaults to [np.mean, np.var, np.size]. These metrics quantify different aspects of the data relevant to equation discovery. + metric_names: A list of names corresponding to the metrics. Defaults to ['mean', 'var', 'disc_num']. + variable: The primary variable to analyze. Defaults to 'u'. + variables: A list of variables to include in the analysis. Defaults to ['u']. + + Returns: + list: A list of Pandas DataFrames, where each DataFrame represents a log entry and contains aggregated statistics for the specified terms and metrics. These DataFrames serve as input for the equation discovery process. + """ metric_frames = [] for log_entry in self._log: aggregation_keys = log_entry['meta']['aggregation_key'] diff --git a/epde/interface/prepared_tokens.py b/epde/interface/prepared_tokens.py index a088806c..29abd302 100644 --- a/epde/interface/prepared_tokens.py +++ b/epde/interface/prepared_tokens.py @@ -25,21 +25,55 @@ class PreparedTokens(ABC): """ - Parent class for all prepared tokens, that have corresponding class, defined here - - Attributes: - _token_family (`TokenFamily`): the family of functions to which the token belongs + Base class for tokens that have a corresponding class definition within the system. + + + Attributes: + _token_family (`TokenFamily`): the family of functions to which the token belongs """ + def __init__(self, *args, **kwargs): + """ + Initializes the `PreparedTokens` object. + + The `PreparedTokens` class manages a specific family of tokens, in this case, 'Placeholder' tokens. This initialization prepares the token family for use in the equation discovery process. By creating a `TokenFamily` instance with the 'Placeholder' type, the system ensures that placeholder tokens are properly handled during the search for the best equation structure. + + Args: + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + None. + """ self._token_family = TokenFamily(token_type='Placeholder') @property def token_family(self): + """ + Returns the token family associated with the layer. + + This property provides access to the token family, which defines the building blocks for constructing equation candidates. + It ensures that the token family is fully configured before being accessed, guaranteeing that all necessary components for equation discovery are available. + + Args: + None + + Raises: + AttributeError: If the token family's evaluator or parameters have not been set, + indicating that the equation search space is not yet fully defined. + + Returns: + TokenFamily: The token family object, representing the set of allowed operations and variables in the equation search. + """ if not (self._token_family.evaluator_set and self._token_family.params_set): raise AttributeError(f'Some attributes of the token family have not been declared.') return self._token_family class ArbitraryDataFunction(PreparedTokens): + """ + Class for tokens, representing arbitrary functions of the modelled variable passed in `var_name` or its derivatives. + """ + def __init__(self, token_type: str, var_name: str, token_labels: list, evaluator: Union[CustomEvaluator, EvaluatorTemplate, Callable], params_ranges: dict, params_equality_ranges: dict = None, unique_specific_token=True, @@ -60,6 +94,12 @@ def __init__(self, token_type: str, var_name: str, token_labels: list, self._token_family.set_evaluator(evaluator) class DerivSignFunction(PreparedTokens): + """ + Class for representing the derivative sign function. + + This class represents the sign function of a derivative of a variable. + """ + def __init__(self, token_type: str, var_name: str, token_labels: list, unique_specific_token=True, # evaluator: Union[CustomEvaluator, EvaluatorTemplate, Callable], # params_ranges: dict, params_equality_ranges: dict = None, @@ -82,11 +122,28 @@ def __init__(self, token_type: str, var_name: str, token_labels: list, unique_sp self._token_family.set_evaluator(sign_evaluator) class DataPolynomials(PreparedTokens): + """ + Class for generating and managing polynomial features from input data. + + Attributes: + max_power: The maximum power to which the input data will be raised. + """ + def __init__(self, var_name: str, max_power: int = 1): """ - Class for tokens, representing power products of the modelled variable. - Argument `max_power` represents the maximum power, in which the tokens will exponentiated. - Should be included into the pool by default, replacing the default 1-st power of the data. + Initializes a family of tokens representing power products of a specified variable. + + This class creates tokens that represent the variable raised to different powers, + up to a specified maximum. These tokens are designed to be included in the pool + of potential terms when searching for differential equations. By using powers of + the variable, the search space is expanded to include polynomial relationships. + + Args: + var_name (str): The name of the variable to be exponentiated. + max_power (int, optional): The maximum power to which the variable will be raised. Defaults to 1. + + Returns: + None """ self._token_family = TokenFamily(token_type=f'poly of {var_name}', variable = var_name, family_of_derivs=True) @@ -124,11 +181,20 @@ def latex_form(label, **params): self._token_family.set_evaluator(simple_function_evaluator) class DataSign(PreparedTokens): + """ + Class for signing data using a specified method. + """ + def __init__(self, var_name: str, max_power: int = 1): """ - Class for tokens, representing power products of the modelled variable. - Argument `max_power` represents the maximum power, in which the tokens will exponentiated. - Should be included into the pool by default, replacing the default 1-st power of the data. + Represents a family of tokens that are polynomial powers of a single variable. This allows the search space to include polynomial terms of the modeled variable. + + Args: + var_name (str): The name of the independent variable to be exponentiated. + max_power (int): The maximum power to which the variable will be raised. This defines the range of polynomial terms to consider. + + Returns: + None """ raise NotImplementedError('TBD.') self._token_family = TokenFamily(token_type=f'poly of {var_name}', variable = var_name, @@ -167,9 +233,57 @@ def latex_form(label, **params): self._token_family.set_evaluator(simple_function_evaluator) class ControlVarTokens(PreparedTokens): + """ + Represents a family of control variable tokens for use in symbolic regression. + + This class manages the token type, variable names, derivative orders, and + evaluation functions associated with control variables in a symbolic + regression context. It supports both single and multiple control components + and prepares data for evaluation using neural networks or custom functions. + + Attributes: + _token_family (TokenFamily): An instance of the TokenFamily class, configured for control tokens. + It manages the token type, variable names, derivative orders, and evaluation functions. + _token_family.ftype (str): The type of the token family, set to 'ctrl'. + _token_family.variable (str): The variable associated with the token family. + _token_family.family_of_derivs (bool): A flag indicating whether the token family includes derivatives (True). + _token_family.evaluator_set (bool): A flag indicating whether the evaluator has been set (True after initialization). + _token_family.params_set (bool): A flag indicating whether the parameters have been set (True after initialization). + _token_family.cache_set (bool): A flag indicating whether the cache has been set (False by default). + _token_family.deriv_evaluator_set (bool): A flag indicating whether the derivative evaluator has been set (True by default). + """ + def __init__(self, sample: Union[np.ndarray, List[np.ndarray]], ann: torch.nn.Sequential = None, var_name: Union[str, List[str]] = 'ctrl', arg_var: List[Tuple[Union[int, List]]] = [(0, [None,]),], eval_torch: Union[Callable, dict] = None, eval_np: Union[Callable, dict] = None, device:str = 'cpu'): + """ + Initializes the object, configuring the token family for control variables and setting up the evaluation functions. + + This setup is crucial for representing and evaluating control variables within the equation discovery process. It prepares the data and neural network (if provided) for use in the evolutionary search for differential equations. The evaluation functions determine how well the control variable, potentially learned by a neural network, fits the data. + + Args: + sample: Sample data, can be a NumPy array or a list of NumPy arrays. + ann: A PyTorch Sequential model for the control neural network. + var_name: Name(s) of the control variable(s). Can be a string or a list of strings. + arg_var: List of tuples, where each tuple contains the variable index and derivative orders. + eval_torch: Custom evaluation function for PyTorch tensors. Can be a callable or a dictionary. + eval_np: Custom evaluation function for NumPy arrays. Can be a callable or a dictionary. + device: The device to run the neural network on ('cpu' or 'cuda'). + + Fields: + _token_family (TokenFamily): An instance of the TokenFamily class, configured for control tokens. + It manages the token type, variable names, derivative orders, and evaluation functions. + _token_family.ftype (str): The type of the token family, set to 'ctrl'. + _token_family.variable (str): The variable associated with the token family. + _token_family.family_of_derivs (bool): A flag indicating whether the token family includes derivatives (True). + _token_family.evaluator_set (bool): A flag indicating whether the evaluator has been set (True after initialization). + _token_family.params_set (bool): A flag indicating whether the parameters have been set (True after initialization). + _token_family.cache_set (bool): A flag indicating whether the cache has been set (False by default). + _token_family.deriv_evaluator_set (bool): A flag indicating whether the derivative evaluator has been set (True by default). + + Returns: + None + """ vars, der_ords = zip(*arg_var) if isinstance(sample, List): assert isinstance(var_name, List), 'Both samples and var names have to be set as Lists or single elements.' @@ -222,15 +336,28 @@ class TrigonometricTokens(PreparedTokens): """ Class for prepared tokens, that belongs to the trigonometric family """ + def __init__(self, freq: tuple = (np.pi/2., 2*np.pi), dimensionality=1): """ - Initialization of class - - Args: - freq (`tuple`): optional, default - (pi/2., 2*pi) - interval for parameter frequency in trigonometric token - dimensionality (`int`): optional, default - 1 - data dimension + Initializes a family of trigonometric tokens for equation discovery. + + This method sets up the building blocks for representing trigonometric functions + within the equation search space. It defines the token family, its parameters + (frequency and dimensionality), and the evaluation function. This setup allows + the evolutionary algorithm to explore trigonometric terms as potential components + of the discovered differential equations. + + Args: + freq (`tuple`): optional, default - (pi/2., 2*pi) + Interval for the frequency parameter within the trigonometric token. + This range constrains the frequencies that can be explored during the + equation discovery process. + dimensionality (`int`): optional, default - 1 + The dimensionality of the input data that the trigonometric token operates on. + This specifies the number of independent variables the token can accept. + + Returns: + None """ assert freq[1] > freq[0] and len(freq) == 2, 'The tuple, defining frequncy interval, shall contain 2 elements with first - the left boundary of interval and the second - the right one. ' @@ -270,7 +397,40 @@ def latex_form(label, **params): class PhasedSine1DTokens(PreparedTokens): + """ + Represents a family of phased sine wave tokens in 1D. + + This class sets up a token family for phased sine waves, defines their + parameters, and configures how they are evaluated and represented in + LaTeX format. + + Attributes: + _token_family (TokenFamily): An instance of the TokenFamily class, configured for 'phased_sine_1d' tokens. + _token_family.ftype (str): The type of token family, set to 'phased_sine_1d'. + _token_family.variable (None): The variable associated with the token family, initialized to None. + _token_family.family_of_derivs (bool): A flag indicating if the token family includes derivatives, initialized to False. + _token_family.evaluator_set (bool): A flag indicating if the evaluator is set, initialized to False. + _token_family.params_set (bool): A flag indicating if the parameters are set, initialized to False. + _token_family.cache_set (bool): A flag indicating if the cache is set, initialized to False. + _token_family.deriv_evaluator_set (bool): A flag indicating if the derivative evaluator is set, initialized to True. + _token_family.latex_form_constructor (Callable): A callable object used to construct LaTeX representations of tokens, initialized by `set_latex_form_constructor`. + """ + def __init__(self, freq: tuple = (np.pi/2., 2*np.pi)): + """ + Initializes the object, configuring a token family for phased sine waves. + + This setup defines the token's parameters (frequency, phase, power), their evaluation method, + and their representation in LaTeX format, which is essential for symbolic manipulation and display + of discovered equations. The frequency range is specified to constrain the search space + during the equation discovery process. + + Args: + freq (tuple): A tuple containing the minimum and maximum frequency values. + + Returns: + None + """ self._token_family = TokenFamily(token_type='phased_sine_1d') self._token_family.set_status(unique_specific_token=True, unique_token_type=True, meaningful=False) @@ -311,13 +471,22 @@ class GridTokens(PreparedTokens): """ Class for prepared tokens, that describe family of grids as values """ + def __init__(self, labels = ['t',], max_power: int = 1, dimensionality=1): """ - Initialization of class - - Args: - dimensionality (`int`): optional, default - 1 - data dimension + Initializes a `GridTokens` object, defining a family of tokens representing grid-based features. + + This class sets up the token family with specified labels, maximum power, and dimensionality. + It configures how these tokens are represented in LaTeX format and prepares them for evaluation within the EPDE framework. + The grid tokens are designed to capture spatial or temporal relationships within the data, enabling the discovery of differential equations that incorporate these relationships. + + Args: + labels (`list` of `str`): Labels for each dimension of the grid, plus a base label. The length of the list must be equal to dimensionality + 1. + max_power (`int`): The maximum power to which a grid token can be raised. Defaults to 1. + dimensionality (`int`): The number of dimensions of the grid tokens. Defaults to 1. + + Returns: + None """ assert len(labels) == dimensionality + 1, 'Incorrect labels for grids.' @@ -355,7 +524,38 @@ def latex_form(label, **params): class LogfunTokens(PreparedTokens): + """ + Represents a collection of tokens with associated log probabilities. + + This class is designed to store and manage a sequence of tokens, + along with their corresponding log probabilities. It provides + functionality for accessing and manipulating these tokens and + probabilities. + """ + def __init__(self, *args, **kwargs): + """ + Initializes the `LogfunTokens` class. + + This method is intentionally left unimplemented to enforce its abstract nature. + Subclasses must provide their own implementation. + + Args: + *args: Variable length argument list. These arguments would typically + define the specific tokens and their associated functionalities + for a particular equation discovery task. + **kwargs: Arbitrary keyword arguments. These keyword arguments might + configure the token behavior or influence the equation search process. + + Returns: + None. + + Raises: + NotImplementedError: Always raised to indicate that the base class + `LogfunTokens` should not be instantiated directly. This ensures + that concrete token sets are defined in subclasses, tailoring the + equation search to specific problem domains. + """ raise NotImplementedError @@ -363,27 +563,40 @@ class CustomTokens(PreparedTokens): """ Class for customer tokens """ + def __init__(self, token_type: str, token_labels: list, evaluator: Union[CustomEvaluator, EvaluatorTemplate, Callable], params_ranges: dict, params_equality_ranges: dict = None, dimensionality: int = 1, unique_specific_token=True, unique_token_type=True, meaningful=False, non_default_power = False): """ - Initialization of class - - Args: - token_type (`str`): type of token for class `TokenFamily` - token_labels (`list`): name of parameters of customer token - evaluator (`CustomEvaluator|EvaluatorTemplate|Callable`): method for evaluating customer token - params_range (`dict`): intervals for each parameter for customer token - params_equality_ranges (`dict`): optional, default - None - acceptable deviations of parameters for equivalence tokens - unique_specific_token (`boolean`): optional, default - True - if True, a specific token can be present only once per term - unique_token_type (`boolean`): optional, default - True - if True, only one token of the family can be present in the term - meaningful (`boolean`): optional, default - True - token significance flag + Initializes a custom token family for equation discovery. + + This method sets up a new token family with specific properties, + an evaluation function, and parameter ranges. This allows the system + to explore different equation structures by combining these custom tokens. + The parameters control the behavior and constraints of the token within + the evolutionary search process. + + Args: + token_type (`str`): The unique identifier for this token family. + token_labels (`list`): Names of the parameters associated with this token. + evaluator (`CustomEvaluator|EvaluatorTemplate|Callable`): + The function used to evaluate the token's contribution to the equation. + params_ranges (`dict`): Defines the valid ranges for each parameter of the token. + params_equality_ranges (`dict`, optional): Acceptable deviations for parameters + when considering tokens as equivalent. Defaults to None. + dimensionality (`int`): Number of dimensions for token parameters. Defaults to 1. + unique_specific_token (`bool`, optional): If True, only one instance of this + specific token (with the same parameters) can exist in a term. Defaults to True. + unique_token_type (`bool`, optional): If True, only one token from this family + can exist in a term. Defaults to True. + meaningful (`bool`, optional): A flag indicating whether this token is considered + significant in the equation. Defaults to False. + non_default_power (`bool`, optional): A flag indicating whether token power is non-default. Defaults to False. + + Returns: + None """ self._token_family = TokenFamily(token_type=token_type) self._token_family.set_status(unique_specific_token=unique_specific_token, @@ -411,10 +624,55 @@ def __init__(self, token_type: str, token_labels: list, class CacheStoredTokens(CustomTokens): + """ + A class to store and manage cached tokens. + + This class provides a mechanism to store and retrieve tokens, + potentially optimizing performance by avoiding redundant computations. + """ + def __init__(self, token_type: str, token_labels: list, token_tensors: dict, params_ranges: dict, params_equality_ranges: Union[None, dict], dimensionality: int = 1, unique_specific_token=True, unique_token_type=True, meaningful=False, non_default_power = True): + """ + Initializes a SimpleEquationToken object, preparing it for equation discovery. + + This constructor validates the provided token labels and tensors, then stores the tensors + in a global cache for efficient access during the equation search process. This ensures + that the building blocks for constructing equations are readily available. + + Args: + token_type: The type of the token. + token_labels: A list of labels for the tokens. + token_tensors: A dictionary mapping token labels to their corresponding tensors. + params_ranges: A dictionary defining the ranges for the token's parameters. + params_equality_ranges: A dictionary defining the equality ranges for the token's parameters, or None. + dimensionality: The dimensionality of the token (default: 1). + unique_specific_token: A boolean indicating whether the token should be unique (default: True). + unique_token_type: A boolean indicating whether the token type should be unique (default: True). + meaningful: A boolean indicating whether the token is meaningful (default: False). + non_default_power: A boolean indicating whether the token has a non-default power (default: True). + + Raises: + KeyError: If the labels of the tokens do not match the labels of the passed tensors, + ensuring data consistency for equation construction. + + Returns: + None + + Fields: + token_type (str): The type of the token. + token_labels (list): A list of labels for the tokens. + evaluator (function): The evaluator function for the token (set to simple_function_evaluator). + params_ranges (dict): A dictionary defining the ranges for the token's parameters. + params_equality_ranges (dict): A dictionary defining the equality ranges for the token's parameters, or None. + dimensionality (int): The dimensionality of the token. + unique_specific_token (bool): A boolean indicating whether the token should be unique. + unique_token_type (bool): A boolean indicating whether the token type should be unique. + meaningful (bool): A boolean indicating whether the token is meaningful. + non_default_power (bool): A boolean indicating whether the token has a non-default power. + """ if set(token_labels) != set(list(token_tensors.keys())): raise KeyError('The labels of tokens do not match the labels of passed tensors') upload_simple_tokens(list(token_tensors.keys()), global_var.tensor_cache, list(token_tensors.values())) @@ -425,10 +683,57 @@ def __init__(self, token_type: str, token_labels: list, token_tensors: dict, par class ExternalDerivativesTokens(CustomTokens): + """ + Represents tokens derived from external sources. + + This class handles tokens that are derivatives obtained from external data, + managing their names, orders, and tensor representations. + + Attributes: + token_type: The type of the token. + token_labels: The labels for the tokens (derivative names). + evaluator: The function used for evaluating the token. Set to `simple_function_evaluator`. + params_ranges: Ranges for the parameters. + params_equality_ranges: Ranges for parameter equality. + dimensionality: The dimensionality of the data. + unique_specific_token: Whether specific tokens should be unique. + unique_token_type: Whether token types should be unique. + meaningful: A flag indicating whether the token is meaningful. + """ + def __init__(self, token_type: str, time_axis: int, base_token_label: list, token_tensor: np.ndarray, max_orders: Union[int, tuple], deriv_method: str, deriv_method_kwargs: dict, params_ranges: dict, params_equality_ranges: Union[None, dict], dimensionality: int = 1, unique_specific_token=True, unique_token_type=True, meaningful=False): + """ + Initializes a new instance of the `ExternalDerivativesTokens` class. + + This method orchestrates the creation of tokens representing derivatives, + preparing them for use in the equation discovery process. It preprocesses + the input tensor to compute derivatives, defines the names and orders + of these derivatives, arranges the data into a suitable tensor format, + and uploads these derivative tokens into the system's cache. This ensures + that the derivative information is readily available for subsequent + equation learning steps. + + Args: + token_type (str): The type of the token. + time_axis (int): The axis representing time. + base_token_label (list): The base label for the token. + token_tensor (np.ndarray): The tensor representing the token. + max_orders (Union[int, tuple]): The maximum order of derivatives. Can be an integer or a tuple. + deriv_method (str): The method used for calculating derivatives. + deriv_method_kwargs (dict): Keyword arguments for the derivative method. + params_ranges (dict): Ranges for the parameters. + params_equality_ranges (Union[None, dict]): Ranges for parameter equality. + dimensionality (int): The dimensionality of the data. Defaults to 1. + unique_specific_token (bool): Whether specific tokens should be unique. Defaults to True. + unique_token_type (bool): Whether token types should be unique. Defaults to True. + meaningful (bool): A flag indicating whether the token is meaningful. Defaults to False. + + Returns: + None + """ deriv_method_kwargs['max_order'] = max_orders # TODO: rewrite preprocessing of external derivatives to match selected preprocessing pipelines data_tensor, derivs_tensor = preprocess_derivatives(token_tensor, method=deriv_method, @@ -450,16 +755,23 @@ class ConstantToken(PreparedTokens): """ Variety of tokens for keeping constanting variables """ + def __init__(self, values_range=(-np.inf, np.inf)): """ - Initialization of constant token - - Args: - values_range (`tuple`): optional, default - (-inf, +inf) - interval in which the value of token exist - - Returns: - None + Initializes a constant token, defining its properties within the equation discovery process. + + This method sets up the constant token with a specified range of possible values. + The range is used to constrain the search space during the equation discovery process, + ensuring that the constant values explored are within reasonable bounds. + It also configures the token family, sets parameters for evaluation and differentiation, + and associates the token with its corresponding evaluators. + + Args: + values_range (`tuple`): optional, default - (-inf, +inf) + Interval in which the value of the token can exist. + + Returns: + None """ assert len(values_range) == 2 and values_range[0] < values_range[1], 'Range of the values has not been stated correctly.' self._token_family = TokenFamily(token_type='constants') @@ -485,7 +797,33 @@ class VelocityHEQTokens(PreparedTokens): """ Сustom type of tokens for the equation of thermal conductivity """ + def __init__(self, param_ranges): + """ + Initializes the VelocityAssumptionTokenFamilyGenerator. + + This constructor configures the token family for velocity assumption tokens. + It defines the token's parameters, how it's evaluated, and how its gradient is calculated, + which is essential for optimizing the equation discovery process. The configuration ensures + that the generated tokens are suitable for representing velocity-related terms within + the differential equations being discovered. + + Args: + param_ranges: A list of 15 tuples, where each tuple represents the + range (min, max) for a specific parameter. These ranges constrain + the parameter space during the equation search. + + Returns: + None + + Class Fields: + _token_family (TokenFamily): A TokenFamily object configured for + 'velocity_assuption' tokens. It manages the token type, + uniqueness constraints, meaningfulness, parameters, evaluators, + and derivative evaluators. This family is used to generate + and manage tokens representing velocity-related terms in the + discovered equations. + """ assert len(param_ranges) == 15 self._token_family = TokenFamily(token_type='velocity_assuption') self._token_family.set_status(unique_specific_token=True, unique_token_type=True, diff --git a/epde/interface/token_family.py b/epde/interface/token_family.py index 68dcfaa1..4504bf4b 100644 --- a/epde/interface/token_family.py +++ b/epde/interface/token_family.py @@ -19,62 +19,88 @@ def constancy_hard_equality(tensor, epsilon=1e-7): + """ + Checks if a tensor can be considered constant based on a hard equality threshold. + + This method verifies if the difference between the maximum and minimum values + in the tensor is less than a predefined epsilon. It helps to identify + tensors that exhibit near-constant behavior, which is useful for + simplifying equation discovery by identifying terms that might be + effectively constant within the given data. + + Args: + tensor (np.ndarray): The input tensor to be checked. + epsilon (float): The tolerance value for determining near-equality. + + Returns: + bool: True if the tensor is approximately constant, False otherwise. + """ return np.abs(np.max(tensor) - np.min(tensor)) < epsilon class TokenFamily(object): """ - Class for the type (family) of tokens, from which the tokens are taken as factors in the terms of the equation - - Attributes: - _deriv_evaluators (`dict`): dict containing the derivatives by each of the token parameter, where elements are the functions, used in the evaluator, or the evaluators. - ftype (`string`): the symbolic name of the token family (e.g. 'logarithmic', 'trigonometric', etc.) - status (`dict`): dictionary, containing markers, describing the token properties. Key - property, value - bool variable: - 'mandatory' - if True, a token from the family must be present in every term; - 'unique_token_type' - if True, only one token of the family can be present in the term; - 'unique_specific_token' - if True, a specific token can be present only once per term; - family_of_derivs (`bool`): flag about the presence of derivatives in the token family - evaluator_set (`bool`): flag about the existence of a method for evaluation - params_set (`bool`): flag, that exist params in that token fami;y - cache_set (`bool`): flag, that exist cache for the token family - deriv_exaluator_set (`bool`): flag about the existing a evaluator for derivatives - _evaluator (`EvaluatorContained object`): Evaluator, which is used to get values of the tokens from that family; - tokens (`list of strings`): List of function names, describing all of the functions, belonging to the family. E.g. for 'trigonometric' token type, this list will be ['sin', 'cos'] - token_params (`OrderedDict`): Available range for token parameters. Ordered dictionary with key - token parameter name, and value - tuple with 2 elements: - (lower boundary, higher boundary), while type of boundaries describes the avalable token params: - if int - the parameters will be integer, if float - float. - equality_ranges (`dict`): error for equality of token parameters, key is name of parameter - derivs_ords (`dict`): keys for derivatides for `solver` for each token in family - opt_param_labels (`list`): name of parameters of tokens, that used in elements functions - test_token (): - test_evaluation (): - - Methods: - set_status(demands_equation = False, meaningful = False, - s_and_d_merged = True, unique_specific_token = False, - unique_token_type = False, requires_grid = False)) - Method to set the markers of the token status; - - set_params(tokens, token_params) - Method to set the list of tokens, present in the family, and their parameters; - - set_evaluator(eval_function) - Method to set the evaluator for the token family & its parameters; - - test_evaluator() - Method to test, if the evaluator and tokens are set properly - - evaluate(token, token_params) - Method, which uses the specific token evaluator to evaluate the passed token with its parameters + Represents a category of tokens used as building blocks for constructing equation terms. + + + Attributes: + _deriv_evaluators (`dict`): dict containing the derivatives by each of the token parameter, where elements are the functions, used in the evaluator, or the evaluators. + ftype (`string`): the symbolic name of the token family (e.g. 'logarithmic', 'trigonometric', etc.) + status (`dict`): dictionary, containing markers, describing the token properties. Key - property, value - bool variable: + 'mandatory' - if True, a token from the family must be present in every term; + 'unique_token_type' - if True, only one token of the family can be present in the term; + 'unique_specific_token' - if True, a specific token can be present only once per term; + family_of_derivs (`bool`): flag about the presence of derivatives in the token family + evaluator_set (`bool`): flag about the existence of a method for evaluation + params_set (`bool`): flag, that exist params in that token fami;y + cache_set (`bool`): flag, that exist cache for the token family + deriv_exaluator_set (`bool`): flag about the existing a evaluator for derivatives + _evaluator (`EvaluatorContained object`): Evaluator, which is used to get values of the tokens from that family; + tokens (`list of strings`): List of function names, describing all of the functions, belonging to the family. E.g. for 'trigonometric' token type, this list will be ['sin', 'cos'] + token_params (`OrderedDict`): Available range for token parameters. Ordered dictionary with key - token parameter name, and value - tuple with 2 elements: + (lower boundary, higher boundary), while type of boundaries describes the avalable token params: + if int - the parameters will be integer, if float - float. + equality_ranges (`dict`): error for equality of token parameters, key is name of parameter + derivs_ords (`dict`): keys for derivatides for `solver` for each token in family + opt_param_labels (`list`): name of parameters of tokens, that used in elements functions + test_token (): + test_evaluation (): + + Methods: + set_status(demands_equation = False, meaningful = False, + s_and_d_merged = True, unique_specific_token = False, + unique_token_type = False, requires_grid = False)) + Method to set the markers of the token status; + + set_params(tokens, token_params) + Method to set the list of tokens, present in the family, and their parameters; + + set_evaluator(eval_function) + Method to set the evaluator for the token family & its parameters; + + test_evaluator() + Method to test, if the evaluator and tokens are set properly + + evaluate(token, token_params) + Method, which uses the specific token evaluator to evaluate the passed token with its parameters """ + def __init__(self, token_type: str, variable:str = None, family_of_derivs: bool = False): """ - Initialize the token family; - + Initialize the token family. + + This initialization is crucial for defining the basic characteristics of a token family, + such as its type and whether it includes derivatives. This information is used later + to construct valid equation structures and evaluate their fitness against the data. + Args: - token_type (`string`): The name of the token family; must be unique among other families. - family_of_derivs (`bool`): tha flag about the existence of a derivative in the fanily of token + token_type (`str`): The name of the token family; must be unique among other families. + variable (`str`, optional): variable name for token. Defaults to None. + family_of_derivs (`bool`, optional): A flag indicating the existence of derivatives within this token family. Defaults to False. + + Returns: + None """ self.ftype = token_type @@ -88,6 +114,18 @@ def __init__(self, token_type: str, variable:str = None, family_of_derivs: bool self.set_latex_form_constructor() def __len__(self): + """ + Returns the number of tokens forming the equation family. + + This is essential for assessing the complexity and size of the equation family + during the evolutionary search for the best equation. + + Args: + self: The Family instance. + + Returns: + int: The number of tokens in the family. + """ assert self.params_set, 'Familiy is not fully initialized.' return len(self.tokens) @@ -95,20 +133,16 @@ def set_status(self, demands_equation=False, meaningful=False, s_and_d_merged=True, unique_specific_token=False, unique_token_type=False, requires_grid=False, non_default_power=False): """ - Set the status of the elements of the token family; - - Args: - demands_equation (`boolean`): default - False - flag about the existence of restrictions for equation - meaningful (`boolean`):if True, a token from the family must be present in every term - unique_token_type (`boolean`): if True, only one token of the family can be present in the term - unique_specific_token (`boolean`): if True, a specific token can be present only once per term - s_and_d_merged (`boolean`): default - True, - flag, that the base values of the token are used as the structural values (generally, in other cases the normalized values are used as structural) - requires_grid (`boolean`): default - False, - flag, that a grid is required to evaluate the token - non_default_power (`boolean`): default - False, - flag, that the behavior of power parameter is different. If `True`, values, other than 1 can be created during init as separate tokens. + Sets the constraints and requirements for tokens within this family, influencing how they are used in equation discovery. + + Args: + demands_equation (`bool`, optional): Indicates if the presence of this token family imposes constraints on the equation structure. Defaults to `False`. + meaningful (`bool`, optional): If `True`, ensures that at least one token from this family is present in each term of the equation. Defaults to `False`. + unique_token_type (`bool`, optional): If `True`, allows only one token of this family to be present in each term. Defaults to `False`. + unique_specific_token (`bool`, optional): If `True`, restricts a specific token from this family to appear only once per term. Defaults to `False`. + s_and_d_merged (`bool`, optional): Determines whether the default values of the token are used directly as structural values. Defaults to `True`. + requires_grid (`bool`, optional): Specifies if a spatial grid is necessary for evaluating tokens from this family. Defaults to `False`. + non_default_power (`bool`, optional): Indicates whether power values other than 1 are treated as distinct tokens during initialization. Defaults to `False`. """ self.status = {} self.status['demands_equation'] = demands_equation @@ -121,22 +155,25 @@ def set_status(self, demands_equation=False, meaningful=False, def set_params(self, tokens, token_params, equality_ranges, derivs_solver_orders=None): """ - Define the token family with list of tokens and their parameters - - Args: - tokens (`list of strings`): List of function names, describing all of the functions, belonging to the family. E.g. for 'trigonometric' token type, - this list will be ['sin', 'cos'] - token_params (`OrderedDict`): Available range for token parameters. Ordered dictionary with key - token parameter name, and value - tuple with 2 elements: - (lower boundary, higher boundary), while type of boundaries describes the avalable token params: - if int - the parameters will be integer, if float - float. - equality_ranges (`dict`): error for equality of token parameters, key is name of parameter - derivs_solver_orders (`list`): keys for derivatides on `int` format for `solver` - - Example: - ---------- - >>> token_names_trig = ['sin', 'cos'] - >>> trig_token_params = OrderedDict([('power', (1, 1)), ('freq', (0.9, 1.1)), ('dim', (0, u_initial.ndim))]) - >>> trigonometric_tokens.set_params(token_names_trig, trig_token_params) + Define the token family, its parameters, and derivative orders if applicable. + + This method configures the token family with essential information for equation discovery. + It specifies the tokens (functions) belonging to the family, their parameter ranges, and, + if the family represents derivatives, the corresponding solver orders. This setup is crucial + for generating and evaluating candidate equations within the search space. + + Args: + tokens (`list of strings`): List of function names, describing all of the functions, belonging to the family. + E.g., for a 'trigonometric' token type, this list will be ['sin', 'cos']. + token_params (`OrderedDict`): Available range for token parameters. Ordered dictionary with key - token parameter name, + and value - tuple with 2 elements: (lower boundary, higher boundary). The type of boundaries (int or float) + determines the parameter type. + equality_ranges (`dict`): Error tolerance for equality of token parameters; key is the name of the parameter. + derivs_solver_orders (`list`): Keys for derivatives in `int` format for the solver. Required only if the token family + represents derivatives. + + Returns: + None """ assert bool(derivs_solver_orders is not None) == bool( @@ -157,50 +194,13 @@ def set_params(self, tokens, token_params, equality_ranges, derivs_solver_orders def set_evaluator(self, eval_function, suppress_eval_test=True): """ - Define the evaluator for the token family and its parameters - - Args: - eval_function (`function or EvaluatorContained object`): Function, used in the evaluator, or the evaluator - eval_params (`keyword arguments`): The parameters for evaluator; must contain params_names (names of the token parameters) & - param_equality (for each of the token parameters, range in which it considered as the same), - suppress_eval_test (`boolean`): if True, run `test_evaluator` for testing of method for evaluating token - - Example: - >>> def trigonometric_evaluator(token, token_params, eval_params): - >>> - >>> ''' - >>> - >>> Example of the evaluator of token values, appropriate for case of trigonometric functions to be calculated on grid, with results in forms of tensors - >>> - >>> Parameters - >>> ---------- - >>> token: {'sin', 'cos'} - >>> symbolic form of the function to be evaluated: - >>> token_params: dictionary: key - symbolic form of the parameter, value - parameter value - >>> names and values of the parameters for trigonometric functions: amplitude, frequency & dimension - >>> eval_params : dict - >>> Dictionary, containing parameters of the evaluator: in this example, it contains coordinates np.meshgrid with coordinates for points, - >>> names of the token parameters (frequency, axis and power). Additionally, the names of the token parameters must be included with specific key 'params_names', - >>> and parameters range, for which each of the tokens if consedered as "equal" to another, like sin(1.0001 x) can be assumed as equal to (0.9999 x) - >>> - >>> Returns - >>> ---------- - >>> value : numpy.ndarray - >>> Vector of the evaluation of the token values, that shall be used as target, or feature during the LASSO regression. - >>> - >>> ''' - >>> - >>> assert 'grid' in eval_params - >>> trig_functions = {'sin' : np.sin, 'cos' : np.cos} - >>> function = trig_functions[token] - >>> grid_function = np.vectorize(lambda *args: function(token_params['freq']*args[token_params['dim']])**token_params['power']) - >>> value = grid_function(*eval_params['grid']) - >>> return value - >>> - >>> der_eval_params = {'token_matrices':simple_functions, 'params_names':['power'], 'params_equality':{'power' : 0}} - # >>> trig_eval_params = {'grid':grid, 'params_names':['power', 'freq', 'dim'], 'params_equality':{'power': 0, 'freq':0.05, 'dim':0}} - >>> trigonometric_tokens.set_evaluator(trigonometric_evaluator,) - + Define how tokens within this family are evaluated. + + This method configures the evaluation process for tokens belonging to this family. It's essential for translating symbolic tokens into numerical values, enabling the equation discovery process. By setting the evaluator, the framework knows how to interpret and quantify the contribution of each token within a candidate equation. + + Args: + eval_function (`function or EvaluatorContained object`): Function, used in the evaluator, or the evaluator + suppress_eval_test (`boolean`): if True, run `test_evaluator` for testing of method for evaluating token """ if isinstance(eval_function, EvaluatorContained): self._evaluator = eval_function @@ -215,6 +215,18 @@ def set_deriv_evaluator(self, eval_functions, suppress_eval_test=True): # eval_k Define the evaluator for the derivatives of the token family and its parameters Args: + """ + Define the evaluator for the derivatives of the token family with respect to its parameters. + + This method configures how the derivatives of the token family are calculated, which is crucial for refining the equation discovery process by understanding the sensitivity of the equation to changes in its parameters. + + Args: + eval_functions (`dict|EvaluatorContained`): A dictionary where keys are parameter names and values are either functions or `EvaluatorContained` instances that compute the derivatives with respect to those parameters. + suppress_eval_test (`boolean`): If `True`, the `test_evaluator` method is skipped after setting the evaluator. This is useful to suppress tests during initial setup or when testing is not immediately required. + + Returns: + None + """ eval_functions (`dict|EvaluatorContained`): Dict containing the derivatives by each of the token parameter, where elements are the functions, used in the evaluator, or the evaluators. Keys represent the parameter name, and values are the corresponding functions. eval_kwargs_keys (`list`): The parameters for evaluator; must contain params_names (names of the token parameters) & @@ -235,13 +247,37 @@ def set_deriv_evaluator(self, eval_functions, suppress_eval_test=True): # eval_k self.test_evaluator(deriv=True) def set_latex_form_constructor(self, latex_constructor: Callable = None): + """ + Sets the method to be used for constructing the LaTeX representation of the token family. + + This allows customization of how token families are displayed in a human-readable format, + which is particularly useful when exploring and presenting discovered equation structures. + + Args: + latex_constructor (Callable, optional): A callable that, when called, returns the LaTeX representation of the object. Defaults to None. + + Returns: + None. + """ self.latex_constructor = latex_constructor def test_evaluator(self, deriv=False): """ - Method to test, if the evaluator and tokens are set properly - - Raises Exception, if the evaluator does not work properly. + Tests the evaluator's functionality with a newly created token. + + This method creates a test token, disables scaling, and evaluates it using the configured evaluator. + It's crucial for verifying that the token set and evaluator are correctly set up to produce valid results + before running the equation discovery process. The test ensures that the evaluator can process tokens + generated by the family without errors. + + Args: + deriv (bool, optional): If True, tests the derivative evaluators instead of the main evaluator. Defaults to False. + + Returns: + None + + Raises: + Exception: If the evaluator fails to process the test token, indicating a problem with the evaluator or token configuration. """ _, self.test_token = self.create() self.test_token.use_cache() @@ -258,15 +294,20 @@ def test_evaluator(self, deriv=False): print('Test evaluation performed correctly') def chech_constancy(self, **tfkwargs): - ''' - Method to check, if any single simple token in the studied domain is constant, or close to it. The constant token is to be displayed and deleted from tokens and cache. - - Args: - test_function (`callable`): the method used to evaluate - - Returns: - None - ''' + """ + Method to identify and remove constant tokens within the defined domain. + + This method iterates through the tokens, evaluates their constancy based on cached data, + and removes those identified as constant from both the token list and the cache. + This ensures that the equation search focuses on non-constant tokens, improving efficiency + and accuracy in discovering the underlying differential equation. + + Args: + tfkwargs (`dict`): Additional keyword arguments for the test function (currently unused). + + Returns: + None + """ assert self.params_set constant_tokens_labels = [] for label in self.tokens: @@ -289,7 +330,21 @@ def chech_constancy(self, **tfkwargs): def evaluate(self, token): """ - Applying evaluator in token + Evaluate the token using a predefined evaluator. + + This method attempts to apply a pre-configured evaluator to the given token. + The evaluation process is crucial for assessing the token's contribution to the overall equation discovery process. + It ensures that each token is properly assessed for its relevance and impact on the equation's ability to fit the observed data. + + Args: + token: The token to be evaluated. + + Returns: + The result of the evaluator applied to the token. + + Raises: + NotImplementedError: If the method is called directly from the TokenFamily class. + TypeError: If the evaluator function or its parameters are not set before application. """ raise NotImplementedError('Method has been moved to the Factor class.') if self.evaluator_set: @@ -301,18 +356,35 @@ def evaluate(self, token): def create(self, label=None, token_status: dict = None, all_vars: List[str] = None, create_derivs: bool = False, **factor_params): """ - Method for creating element of the token family - - Args: - label (`str`): one name of them, if label is None - random selection occurs from possible tokns for that family - token_status (`dict`): information about usage of all tokens that belong to this family, - if `label` is not None, this argument will not be considered. Example: (number of used, max number for using, flag about permission to use) - create_derivs (`boolean`): default - False - flag about the presence of derivatives in the token structure - - Returns: - occupied_by_factor (`dict`): information about blocked elements after cteated the factor - new_factor (`Factor`): resulting factor for that token family + Creates a factor representing a specific element (token) within this token family. + + This method is responsible for generating a new factor, which is a building block + in the equation discovery process. The created factor is based on the specified + label (token name) and incorporates derivative information if needed. The method + also handles the allocation of resources (tokens) based on the family's constraints + and sets the factor's parameters, either randomly or based on provided values. + This ensures that the generated factors adhere to the defined structure and + constraints of the token family, contributing to the construction of valid + equation candidates. + + Args: + label (`str`): The name of the token to create. If `None`, a token is randomly selected + from the available tokens in this family, subject to usage constraints. + token_status (`dict`): A dictionary containing information about the usage of each token + in this family. This is used to track how many times each token has been used, + its maximum allowed usage, and whether it is allowed to be used. This argument + is ignored if `label` is not `None`. Example: `{'token1': (2, 5, True), 'token2': (0, 3, False)}`. + all_vars (`List[str]`): List of all variables. + create_derivs (`bool`): A flag indicating whether to create a derivative factor. If `True`, + the selected token must have derivative information associated with it. Defaults to `False`. + **factor_params: Keyword arguments that will be passed as parameters to created Factor. + + Returns: + `tuple`: A tuple containing: + - `dict`: A dictionary representing the tokens blocked after creating the factor. + This indicates which tokens are now unavailable for further use due to constraints. + - `Factor`: The newly created factor object, representing the selected token and its + associated parameters and derivative information. """ if token_status is None or token_status == {}: token_status = {label: (0, self.token_params['power'][1], False) @@ -358,14 +430,23 @@ def create(self, label=None, token_status: dict = None, all_vars: List[str] = No def cardinality(self, token_status: Union[dict, None] = None): """ - Method for getting number of free place for creating new factors for that token family - + Calculates the number of tokens in the family that can still accommodate new factors, + considering their current usage. + Args: - token_status (`dict`): information about usage of all tokens that belong to this family, - Example: (number of used, max number for using, flag about permission to use) - + token_status (`dict`, optional): A dictionary containing usage information for each token in the family. + The dictionary should map token labels to a tuple containing: + - The number of factors currently using the token. + - The maximum number of factors allowed to use the token. + - A flag indicating whether the token is permitted for use. + If `None` or empty, it's assumed that no tokens are currently in use. Defaults to `None`. + Returns: - number of place (`int`) + `int`: The number of tokens in the family that have available capacity for new factors. + + Why: + This method determines the remaining capacity within the token family, guiding the evolutionary process + to efficiently explore equation structures by considering token availability. """ if token_status is None or token_status == {}: token_status = {label: (0, self.token_params['power'][1], False) @@ -374,7 +455,13 @@ def cardinality(self, token_status: Union[dict, None] = None): def evaluate_all(self, all_vars: List[str]): """ - Apply method of evaluation for all tokens in token family + Applies the evaluation method to all tokens within this family, exploring different parameter combinations to identify optimal configurations. This ensures comprehensive coverage of the token family's parameter space, facilitating the discovery of equation structures that accurately represent the underlying dynamics. + + Args: + all_vars (List[str]): A list of all variable names used in the equation. + + Returns: + None """ for token_label in self.tokens: params_vals = [] @@ -402,15 +489,55 @@ def evaluate_all(self, all_vars: List[str]): class TFPool(object): """ - Class stored pool for token families - - Args: - families (`list`): toen families that using in that run - """ + Manages a collection of token families. This class provides functionality for storing, accessing, and manipulating these families, facilitating the construction and optimization of equation structures. + + + Args: + families (`list`): toen families that using in that run + """ + def __init__(self, families: list): + """ + Initializes a TFPool instance with a list of symbolic equation families. + + This initialization prepares the pool to manage and evolve families of equations, + facilitating the search for the best equation structures that fit the data. + The pool serves as a central point for multi-objective optimization. + + Args: + families: A list of family objects, each representing a set of candidate equations. + + Returns: + None + + Why: + The TFPool needs to be initialized with equation families to start the evolutionary process + of discovering differential equations from data. These families represent the initial + population of candidate solutions that will be evolved and optimized. + """ self.families = families def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Reconstructs a specific object attribute manually. + + This method allows for the targeted modification of an object's internal state, + bypassing the typical reconstruction process. It is used to fine-tune the object's + configuration, especially when dealing with attributes that are not automatically + handled during standard loading or initialization procedures. This is particularly + useful for attributes that influence the equation discovery process. + + Args: + attribute (str): The name of the attribute to reconstruct. + value: The new value to assign to the attribute. + except_attrs (dict): A dictionary of attributes to exclude during the reconstruction process. + + Raises: + ValueError: If the specified attribute is not supported. + + Returns: + None. + """ from epde.loader import obj_to_pickle, attrs_from_dict supported_attrs = [] if attribute not in supported_attrs: @@ -419,35 +546,69 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): @property def families_meaningful(self): """ - Getting token families, that are meaningful + Filters and retrieves token families marked as meaningful. + + This method iterates through the available token families and returns only those that have been + designated as 'meaningful' based on their status. This selection is crucial for focusing on the + most relevant equation structures during the equation discovery process, improving efficiency and accuracy. + + Args: + None + + Returns: + list: A list of token families where each family's status indicates it is meaningful. """ return [family for family in self.families if family.status['meaningful']] @property def families_demand_equation(self): """ - Getting token families, that must have an individual equation + Return: + list: A list of token families that require individual equation discovery. + + This property identifies token families within the pool that are marked as requiring a dedicated equation. + These families are processed separately during the equation discovery phase to ensure accurate and specific modeling. + This is needed to allow the framework to discover more complex relationships between different token families. + Args: + None """ return [family for family in self.families if family.status['demands_equation']] @property def families_supplementary(self): """ - Getting token families, that are not meaningful + Returns a list of token families that have been identified as not contributing significantly to the equation discovery process. These families are excluded from consideration during the search for governing differential equations to improve efficiency and focus on more relevant terms. + + Args: + None + + Returns: + list: A list of `TokenFamily` objects that are considered supplementary. """ return [family for family in self.families if not family.status['meaningful']] @property def families_equationless(self): """ - Getting token families, whose presence in the equation is optional + Gets token families that are not required to be present in the discovered equations. + + Args: + None + + Returns: + list: A list of token families. These families represent optional components in the equation discovery process, allowing the algorithm to explore simpler models where these terms might not be necessary to accurately represent the data. This helps to avoid overfitting and identify the most parsimonious equation structure. """ return [family for family in self.families if not family.status['demands_equation']] @property def labels_overview(self): """ - Getting pairs from each token familly by next form: (name of token for this family, max number of token for using) + Provides an overview of the token families, indicating the maximum allowed usage for each token within its family. + + This information is crucial for configuring the search space of the evolutionary algorithm, ensuring that the generated equation structures adhere to predefined constraints on token usage. + + Returns: + list: A list of tuples, where each tuple contains the token family name and the maximum allowed power for tokens in that family. """ overview = [] for family in self.families: @@ -457,14 +618,21 @@ def labels_overview(self): def families_cardinality(self, meaningful_only: bool = False, token_status: Union[dict, None] = None): """ - Getting number of free place for creating new factors for each token family - - Args: - meaningful_only (`boolean`): using only meaningful families - token_status (`dict`): information about usage of all tokens that belong to all families of the class - - Returns: - numpy.array with integer values (number of free place in each `self.families`) + Calculates the available capacity for adding new factors within each token family. + + This method determines how many more factors can be accommodated in each family, + considering either all families or only the meaningful ones. This information + is crucial for the evolutionary process, guiding the creation of new equation candidates + by ensuring that the token families are not over-saturated. + + Args: + meaningful_only (`bool`): If True, considers only the meaningful families. + token_status (`dict`, optional): A dictionary containing usage information for all tokens + belonging to the families. Defaults to None. + + Returns: + `numpy.ndarray`: An array of integers, where each element represents the number of + available slots for new factors in the corresponding family. """ if meaningful_only: return np.array([family.cardinality(token_status) for family in self.families_meaningful]) @@ -474,16 +642,31 @@ def families_cardinality(self, meaningful_only: bool = False, def create(self, label=None, create_meaningful: bool = False, token_status=None, create_derivs: bool = False, **kwargs) -> Union[str, Factor]: """ - Create token from family for current running - + Creates a token (either a variable or a constant) by sampling from available token families. + + This method facilitates the generation of equation components during the evolutionary search process. + It selects a token family based on predefined probabilities and then delegates the actual token creation + to the chosen family. This ensures diversity in the generated equations while adhering to the constraints + defined by the token families. + Args: - label (`str`): if noe None, that create token with name == label - create_meaningful (`boolean`): the choice of the token to create is selected from meaningful family - token_status (`dict`): information about status of all families - create_derivs (`boolean`): flag about the presence of derivatives in the token structure + label (str, optional): If provided, creates a token with the specified name. Defaults to None. + create_meaningful (bool, optional): If True, selects a token from the 'meaningful' families. + Defaults to False. Meaningful families typically represent variables that directly contribute + to the equation's structure. + token_status (dict, optional): A dictionary containing information about the status of each token family. + This can be used to influence the selection probabilities. Defaults to None. + create_derivs (bool, optional): A flag indicating whether derivative tokens should be created. + Defaults to False. + **kwargs: Additional keyword arguments that are passed to the `create` method of the selected token family. Returns: - created `Factor` + Union[str, Factor]: The created token, which can be either a string (if a simple token is created) + or a Factor object (representing a more complex expression). + + Raises: + ValueError: If `create_meaningful` is True and there are no meaningful families available. + Exception: If the specified `label` is found in more than one token family or in none. """ if label is None: if create_meaningful: @@ -526,14 +709,18 @@ def create(self, label=None, create_meaningful: bool = False, token_status=None, def create_from_family(self, family_label: str, token_status=None, **kwargs): """ - Create token from choosing family - + Create a new factor (token) based on a specified family. + + This method selects a family of factors based on the provided label and uses it to create a new factor. + This is useful for constructing more complex equation structures by combining factors from different families. + Args: - family_label (`str`): the name of the family from which the token will be created - token_status (`dict`): information about status of all families + family_label (str): The label of the family to create the factor from. + token_status (dict, optional): Information about the status of all families. Defaults to None. + **kwargs: Additional keyword arguments passed to the family's create method. Returns: - created `Factor` + Factor: The newly created factor. """ # print([f.ftype for f in self.families], family_label) family = [f for f in self.families if family_label == f.ftype][0] @@ -543,14 +730,19 @@ def create_from_family(self, family_label: str, token_status=None, **kwargs): def create_with_var(self, variable: str, token_status=None, **kwargs): """ - Create token from choosing family - - Args: - family_label (`str`): the name of the family from which the token will be created - token_status (`dict`): information about status of all families + Creates a token (Factor) associated with a specific variable, selecting from available families based on token availability. - Returns: - created `Factor` + This method is used to generate new equation terms by probabilistically choosing a family (representing a type of term) + that matches the given variable. The selection is weighted by the number of existing tokens within each family, + favoring families with more tokens. This helps to diversify the generated equations and explore different term combinations. + + Args: + variable (str): The variable that the created token should be associated with. + token_status (dict, optional): Information about the status of all token families. Defaults to None. + **kwargs: Additional keyword arguments to be passed to the family's `create` method. + + Returns: + Factor: A new token (Factor) created from the selected family. """ # print([f.ftype for f in self.families], family_label) assert variable is not None, 'Can not create token with a specific variable for ' @@ -567,20 +759,51 @@ def create_with_var(self, variable: str, token_status=None, **kwargs): families.remove(family) def __add__(self, other): + """ + Combines two TFPool objects to create a new pool containing all families. + + This operation is essential for expanding the search space of possible equation structures by merging different sets of equation components. + + Args: + other (TFPool): The TFPool object to merge with. + + Returns: + TFPool: A new TFPool object containing the combined families from both input pools. + """ return TFPool(families=self.families + other.families) def __len__(self): + """ + Returns the number of equation families stored in the pool. + + This reflects the diversity of equation structures being explored. + + Args: + self: The TFPool instance. + + Returns: + int: The number of equation families. + """ return len(self.families) def get_families_by_label(self, label): """ - Getting family by input name of token - + Retrieves the `TokenFamily` that contains a specific token label. + + This method is used to locate the family to which a token belongs, + ensuring that each token is uniquely associated with a single family + within the pool. This is crucial for maintaining the structural + integrity of the equation discovery process. + Args: - label (`str`): the name of the token that will be used to search for his family - + label (`str`): The name of the token to search for within the families. + Returns: - `TokenFamily` for token by input name + `TokenFamily`: The family containing the specified token label. + + Raises: + ValueError: If more than one family contains the same token label, indicating a potential ambiguity in the token assignments. + IndexError: If no family contains the specified token label, suggesting that the token is not properly associated with any family. """ containing_families = [family for family in self.families if label in family.tokens] diff --git a/epde/interface/type_checks.py b/epde/interface/type_checks.py index 9f926c52..f54846d3 100644 --- a/epde/interface/type_checks.py +++ b/epde/interface/type_checks.py @@ -11,12 +11,43 @@ def check_nparray(obj): + """ + Checks if an object is a NumPy array. + + This check is crucial to ensure that the data is in the correct format + before proceeding with equation discovery, as the evolutionary algorithm + relies on efficient numerical operations provided by NumPy. + + Args: + obj: The object to check. + + Returns: + None. + + Raises: + ValueError: If the object is not a NumPy array. + """ if type(obj) != np.ndarray: raise ValueError( f'mismatching types of object: passed {type(obj)} instead of numpy.ndarray') def check_nparray_iterable(objs): + """ + Verifies that all elements within the provided iterable are NumPy arrays. + + This ensures data consistency before proceeding with equation discovery. + + Args: + objs: An iterable containing objects to be checked. + + Returns: + None. The function raises an exception if the check fails. + + Raises: + ValueError: If any object in the iterable is not a NumPy array, + indicating a type mismatch that could disrupt the equation discovery process. + """ if any([type(obj) != np.ndarray for obj in objs]): raise ValueError( f'mismatching types of object: passed {[type(obj) for obj in objs]} instead of numpy.ndarray') diff --git a/epde/loader.py b/epde/loader.py index 5c701a67..305e527a 100644 --- a/epde/loader.py +++ b/epde/loader.py @@ -41,6 +41,18 @@ # LOADING_PRESETS = {'SoEq' : {'SoEq' : []}} def get_size(obj, seen=None): + """ + Get a recursive size of an object in bytes. + + This function recursively calculates the size of an object to estimate the memory footprint of data structures used within the equation discovery process. By traversing attributes, items, and slots, it provides a comprehensive size calculation, which is crucial for memory management and performance optimization when dealing with complex equation structures and large datasets. It handles dictionaries, objects with `__dict__` attributes, iterable objects, and objects with `__slots__`. It avoids infinite recursion by keeping track of already visited objects. + + Args: + obj: The object to get the size of. + seen: A set to keep track of already visited objects (default: None). + + Returns: + The size of the object in bytes. + """ size = sys.getsizeof(obj) if seen is None: seen = set() @@ -64,36 +76,52 @@ def get_size(obj, seen=None): return size def parse_obj_type(obj): + """ + Parses an object to extract and return its type as a string. + + This function is used to determine the type of a given object, which is then utilized to represent it as a symbolic token within the equation discovery process. + + Args: + obj: The object whose type needs to be determined. + + Returns: + str: The type of the object as a string, extracted from the object's type information. + """ return str(type(obj)).split('.')[-1][:-2] def get_typespec_attrs(obj): + """ + Retrieves the type specification attributes for a given object. + + This function is used to access pre-defined attributes associated with the type of the input object. + These attributes are essential for defining search space of equation structures. + + Args: + obj: The object to retrieve the type specification attributes from. + + Returns: + The type specification attributes associated with the object's type. + These attributes are stored in a dictionary `TYPESPEC_ATTRS` under the key, + which is a string representation of the object's type. + """ key = parse_obj_type(obj) # Get key of the attribute, strapping extra symbols from type() return TYPESPEC_ATTRS[key] def obj_to_pickle(obj, not_to_pickle: list = [], manual_pickle: list = []): - ''' - - Template method for adapting pickling of an object. Shall be copied to objects, that are - to be pickable with local rules. - - Parameters - ---------- + """ + Template method for creating a dictionary representation of an object for persistence. This method should be incorporated into classes that require custom serialization logic. It allows selective and recursive conversion of object attributes. - obj : - Object, which is translated into a dictionary for pickling. + Args: + obj: The object to be serialized. + not_to_pickle (list of str, optional): List of attribute names to exclude from serialization. Defaults to []. + manual_pickle (list of str, optional): List of attribute names to serialize using a custom `obj_to_pickle` call on their values. Defaults to []. - not_to_pickle : list of strings - ``obj`` object attributes to keep from saving to the resulting dict. - - manual_pickle : list of strings - ``obj`` object attributes that will be pickled with their own ``obj_to_pickle()`` calls. - - Returns - ------- - dict_to_pickle : dict - Dictionary representation of the object attributes. - - ''' + Returns: + dict: A dictionary containing the serialized representation of the object. + + Why: + This method facilitates the conversion of complex objects into a dictionary format suitable for saving or transmitting. It provides control over which attributes are included and how they are serialized, especially useful for handling nested objects or those requiring special serialization procedures. + """ ntp_add, mp_add = get_typespec_attrs(obj) not_to_pickle = ntp_add manual_pickle = mp_add @@ -149,6 +177,22 @@ def obj_to_pickle(obj, not_to_pickle: list = [], manual_pickle: list = []): return dict_to_pickle def attrs_from_dict(obj, attributes, except_attrs: dict = {}): + """ + Populates an object's attributes from a dictionary, with special handling for reconstruction and exceptions. + + This method sets an object's attributes based on a provided dictionary, + accommodating specific reconstruction procedures and attribute exclusions. + It ensures that the object is correctly initialized with the data necessary + for equation discovery. + + Args: + obj: The object to populate with attributes. + attributes: A dictionary containing the attributes to set on the object. + except_attrs: A dictionary specifying attributes to exclude or handle differently. + + Returns: + None. This method modifies the object in place. + """ except_dict = except_attrs[parse_obj_type(obj)] if 'obj_type' not in except_dict.keys(): except_dict['obj_type'] = None @@ -177,6 +221,22 @@ def attrs_from_dict(obj, attributes, except_attrs: dict = {}): def temp_pickle_save(obj : Union[SoEq, Cache, TFPool, ParetoLevels, Population], not_to_pickle = [], manual_pickle = []): + """ + Saves a complex object to a temporary pickle file for efficient storage and retrieval during the equation discovery process. + + This method serializes the given object using the EPDELoader and saves it + to a temporary file. This is useful for caching intermediate results or + transferring complex data structures between different stages of the + equation discovery workflow. + + Args: + obj (Union[SoEq, Cache, TFPool, ParetoLevels, Population]): The object to be pickled and saved. + not_to_pickle (list, optional): A list of attributes to exclude from pickling. Defaults to []. + manual_pickle (list, optional): A list of attributes to pickle manually. Defaults to []. + + Returns: + tempfile._TemporaryFileWrapper: The temporary file object containing the pickled data. + """ loader = EPDELoader() pickled_obj = loader.saves(obj, not_to_pickle = [], manual_pickle = []) @@ -186,7 +246,37 @@ def temp_pickle_save(obj : Union[SoEq, Cache, TFPool, ParetoLevels, Population], class LoaderAssistant(object): + """ + Provides preset configurations for various data structures and components. + + This class offers a collection of methods that return preset dictionaries + tailored for initializing different aspects of a system, such as TFPools, + caches, population data structures, and Pareto levels. These presets + provide default configurations and initial states for these components. + + Class Methods: + - system_preset + - pool_preset + - cache_preset + - population_preset + - pareto_levels_preset + """ + def __init__(self): + """ + Initializes the LoaderAssistant. + + This class manages the loading and preprocessing of data required for the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. + + Why: + The LoaderAssistant is initialized to prepare the data pipeline, ensuring that the data is in the correct format and structure for subsequent equation discovery steps. + """ pass @staticmethod @@ -204,14 +294,78 @@ def system_preset(pool: TFPool): # Validate correctness of attribute definitions @staticmethod def pool_preset(): + """ + Returns a preset dictionary for a TFPool. + + This provides a standardized, empty TFPool configuration, + ensuring consistency when initializing or resetting equation search spaces. + + Returns: + dict: A dictionary containing an empty TFPool preset. + """ return {'TFPool' : {}} @staticmethod def cache_preset(): + """ + Caches a preset, ensuring that default configurations are readily available for equation discovery. This improves efficiency by avoiding redundant computations when exploring the solution space. + + Args: + None + + Returns: + dict: An empty dictionary representing the cached preset. This can be extended in the future to hold actual preset configurations. + """ return {} @staticmethod def population_preset(pool: TFPool): + """ + Generates a preset configuration for initializing the population of symbolic expressions. + + This method creates a dictionary containing configurations for + various components of the population, such as 'Population', 'SoEq', + 'Equation', 'Term', and 'Factor'. These configurations define the + initial state and available operations for the evolutionary search + of differential equations. It sets up the initial pools of tokens + and other necessary parameters for the search process. + + Args: + pool: A TFPool object containing the initial set of tokens + (functions and variables) to be used in constructing equations. + + Returns: + dict: A dictionary containing preset configurations for the + population. The dictionary has the following structure: + { + 'Population': {}, + 'SoEq': { + 'tokens_for_eq': TFPool, initialized with pool.families_demand_equation, + 'tokens_supp': TFPool, initialized with pool.families_equationless, + 'latex_form': None + }, + 'Equation': { + 'pool': TFPool, the input pool, + 'latex_form': None, + '_history': None + }, + 'Term': { + 'pool': TFPool, the input pool, + 'latex_form': None + }, + 'Factor': { + '_latex_constructor': None, + '_evaluator': None + } + } + + Why: + This preset configuration is crucial for setting up the initial + conditions and available building blocks for the evolutionary + search of differential equations. It defines the search space + and ensures that the algorithm starts with a valid and + well-defined population. + """ return {'Population' : {}, 'SoEq' : {'tokens_for_eq' : TFPool(pool.families_demand_equation), 'tokens_supp' : TFPool(pool.families_equationless), @@ -226,6 +380,27 @@ def population_preset(pool: TFPool): @staticmethod def pareto_levels_preset(pool: TFPool): + """ + Returns a preset configuration for equation discovery components. + + This method configures the search space for the evolutionary algorithm + by defining settings for Pareto levels, symbolic equations (SoEq), + equations, terms, and factors. It initializes these components with + specific settings and associations, using a TFPool object to manage + the available equation and term building blocks. This ensures that the + search process starts with a well-defined and consistent set of + components, facilitating the discovery of meaningful equations. + + Args: + pool: A TFPool object containing the token families used for + constructing equations and terms. + + Returns: + dict: A dictionary containing preset configurations for + 'ParetoLevels', 'SoEq', 'Equation', 'Term', and 'Factor'. Each + key maps to a dictionary of settings specific to that component, + defining the initial state of the equation discovery process. + """ return {'ParetoLevels' : {}, 'SoEq' : {'tokens_for_eq' : TFPool(pool.families_demand_equation), 'tokens_supp' : TFPool(pool.families_equationless), @@ -242,10 +417,28 @@ def pareto_levels_preset(pool: TFPool): class EPDELoader(object): ''' Universal loader for EPDE objects, applicable to system of equations as - ``SoEq``, token families pool as ``TFPool``, populations as + ``SoEq``, token families pool as ``TFPool``, populations as ''' + def __init__(self, directory = None): + """ + Initializes the EPDELoader, setting up the designated directory for storing intermediate results and models. + + The directory is crucial for preserving progress and enabling efficient re-evaluation of solutions during the equation discovery process. If a custom directory is not provided, a default directory named 'epde_cache' is created two levels above the current working directory to keep the project workspace organized. + + Args: + directory (str, optional): The path to the directory for storing cached data. Defaults to None, which triggers the use of the default directory. + + Returns: + None + + Raises: + TypeError: If the provided directory is not a string or if the path is invalid and the directory cannot be created. + + Fields: + _directory (str): The directory used for caching objects. + """ if directory is not None: if not isinstance(directory, str): raise TypeError(f'Incorrect format of repo to save objects, expected str, got {type(directory)}.') @@ -262,23 +455,101 @@ def __init__(self, directory = None): '..','epde_cache'))) def save(self, obj, filename:str = None, not_to_pickle:list = [], manual_pickle:list = []): + """ + Saves a discovered equation or model to a file, preserving its structure and parameters for later use. This allows for persistence of found solutions and their subsequent loading and application. + + Args: + obj: The equation or model object to be saved. + filename: The name of the file to save the object to. + not_to_pickle: A list of attribute names to exclude from pickling. + manual_pickle: A list of attribute names to manually handle during pickling. + + Returns: + None. + """ with open(filename, mode = 'wb') as file: pickle.dump(obj_to_pickle(obj, not_to_pickle, manual_pickle), file) def saves(self, obj, not_to_pickle:list = [], manual_pickle:list = []): + """ + Serializes a given object into a pickle string, preparing it for storage or transfer within the EPDE framework. + + This method converts an object into a byte stream suitable for persisting its state. + It provides mechanisms to exclude certain attributes from serialization and handle others with custom logic, + ensuring compatibility and efficiency when reconstructing the object later. + This is important because some objects might contain unserializable attributes or require special handling to be correctly restored. + + Args: + obj: The object to be serialized. + not_to_pickle: A list of attribute names that should not be included in the serialized data. + manual_pickle: A list of attribute names that require custom serialization logic. + + Returns: + bytes: A pickle string representing the serialized object. + """ pickling_form = obj_to_pickle(obj, not_to_pickle, manual_pickle) return pickle.dumps(pickling_form) def use_pickles(self, obj_pickled, **kwargs): + """ + Uses a previously serialized representation to reconstruct an object. + + This method leverages stored object data to instantiate and initialize a new object instance, + bypassing the standard initialization process. This is useful for efficiently restoring + complex objects with pre-computed states, ensuring consistency and speed in object creation. + + Args: + obj_pickled (dict): A dictionary containing the serialized object's data, including its type and attributes. + **kwargs: Keyword arguments specifying attributes to exclude from being loaded from the serialized data. + + Returns: + object: The newly created and initialized object. + """ obj = TYPES[obj_pickled['obj_type']].__new__(TYPES[obj_pickled['obj_type']]) attrs_from_dict(obj, obj_pickled, except_attrs = kwargs) return obj def load(self, filename:str, **kwargs): + """ + Loads a previously serialized equation candidate from a file. + + This method retrieves a pickled equation candidate from the specified file + and prepares it for further evaluation within the EPDE framework. The loaded + object is then passed to the `use_pickles` method for processing and + integration into the equation discovery workflow. + + Args: + filename: The name of the file containing the pickled equation candidate. + **kwargs: Keyword arguments to pass to the `use_pickles` method, allowing + for customization of the candidate's evaluation. + + Returns: + The result of calling `use_pickles` with the unpickled equation candidate + and any provided keyword arguments. This typically involves further + processing or evaluation of the candidate within the EPDE framework. + """ with open(filename, mode = 'rb') as file: obj_pickled = pickle.load(file) return self.use_pickles(obj_pickled, **kwargs) def loads(self, byteobj, **kwargs): + """ + Deserialize a pickled object from a byte string and prepare it for equation discovery. + + This method deserializes a Python object (typically representing data or + initial conditions) from a byte string using the `pickle` module. The + deserialized object is then passed to the `use_pickles` method for + further processing, ensuring it's in a suitable format for the + equation discovery process. This step is crucial for loading pre-processed + data or configurations into the EPDE framework. + + Args: + byteobj: The byte string representing the pickled object. + **kwargs: Additional keyword arguments to pass to `use_pickles`. + + Returns: + The deserialized and processed Python object, ready for use in + equation discovery. + """ obj_pickled = pickle.loads(byteobj) return self.use_pickles(obj_pickled, **kwargs) \ No newline at end of file diff --git a/epde/operators/common/coeff_calculation.py b/epde/operators/common/coeff_calculation.py index f183dbe5..5cb54168 100644 --- a/epde/operators/common/coeff_calculation.py +++ b/epde/operators/common/coeff_calculation.py @@ -14,34 +14,40 @@ from epde.structure.main_structures import Equation class LinRegBasedCoeffsEquation(CompoundOperator): - ''' + """ + The operator dedicated to calculating the coefficients of a linear regression-based equation. It determines the optimal weights for each term in the equation, excluding the target term, and includes a free coefficient. - The operatror, dedicated to the calculation of the weights of the equation (for the free coefficient and - each of its terms except the target one). - - Attributes: - _tags (`set`): - g_fun_vals (`numpy.ndarray`): - Methods: - apply(equation) - Calculate the coefficients of the equation, using the linear regression. The result is stored in the - equation.weights_final attribute - ''' + Attributes: + _tags (`set`): + g_fun_vals (`numpy.ndarray`): + + Methods: + apply(equation) + Calculate the coefficients of the equation, using the linear regression. The result is stored in the + equation.weights_final attribute + ''' + """ + key = 'LinRegCoeffCalc' def apply(self, objective : Equation, arguments : dict = None): """ - Calculate the coefficients of the equation, using the linear regression.The result is stored in the - objective.weights_final attribute - - Args: - objective (`Equation`): the equation object, to that the fitness function is obtained. - arguments (`dict`): - - Returns: - None - """ + Calculates the final equation coefficients using linear regression, leveraging the intermediate weights to refine the equation's structure. + + This method refines the equation by determining the optimal coefficients for each term, + considering the previously established intermediate weights. This step is crucial for + achieving a balance between model complexity and accuracy, ensuring that the final equation + accurately represents the underlying dynamics of the system. The calculated coefficients + are stored in the `objective.weights_final` attribute. + + Args: + objective (`Equation`): The equation object containing the structure and intermediate weights. + arguments (`dict`, optional): Additional arguments (not used in the current implementation). Defaults to None. + + Returns: + None: The result is stored directly within the `objective` object. + """ # self_args, subop_args = self.parse_suboperator_args(arguments = arguments) assert objective.weights_internal_evald, 'Trying to calculate final weights before evaluating intermeidate ones (no sparsity).' @@ -89,4 +95,16 @@ def apply(self, objective : Equation, arguments : dict = None): objective.weights_final = weights def use_default_tags(self): + """ + Sets the operator's tags to a predefined default. This configuration ensures that the operator is correctly identified and handled within the equation discovery process, particularly with respect to coefficient calculation at the gene level, its standalone nature, and its in-place operation. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields Initialized: + _tags (set): A set containing default tags: 'coefficient calculation', 'gene level', 'no suboperators', and 'inplace'. + """ self._tags = {'coefficient calculation', 'gene level', 'no suboperators', 'inplace'} diff --git a/epde/operators/common/fitness.py b/epde/operators/common/fitness.py index 76be597a..729a11f4 100644 --- a/epde/operators/common/fitness.py +++ b/epde/operators/common/fitness.py @@ -25,44 +25,45 @@ class L2Fitness(CompoundOperator): """ The operator, which calculates fitness function to the individual (equation) as the L2 norm - of the vector of disrepancy between left part of the equation and the right part, evaluated - on the grid nodes. + of the vector of disrepancy between left part of the equation and the right part, evaluated + on the grid nodes. - Notable attributes: - ------------------- - - params : dict - Inhereted from the ``CompoundOperator`` class. - Parameters of the operator; main parameters: - - penalty_coeff - penalty coefficient, to that the fitness function value of equation with no non-zero coefficients, is multiplied; - - suboperators : dict - - - Methods: - ----------- - apply(equation) - calculate the fitness function of the equation, that will be stored in the equation.fitness_value. - + Notable attributes: + ------------------- + + params : dict + Inhereted from the ``CompoundOperator`` class. + Parameters of the operator; main parameters: + + penalty_coeff - penalty coefficient, to that the fitness function value of equation with no non-zero coefficients, is multiplied; + + suboperators : dict + + + Methods: + ----------- + apply(equation) + calculate the fitness function of the equation, that will be stored in the equation.fitness_value. """ + key = 'DiscrepancyBasedFitness' def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = False): """ - Calculate the fitness function values. The result is not returned, but stored in the equation.fitness_value attribute. + Calculates the fitness of an equation by evaluating its ability to fit the target data, considering sparsity and coefficient values. The fitness value, reflecting the equation's accuracy, is then stored within the equation object. - Parameters: - ------------ - equation : Equation object - the equation object, to that the fitness function is obtained. - - Returns: - ------------ + Args: + objective (Equation): The equation object to evaluate. Its `fitness_value` attribute will be updated with the calculated fitness. + arguments (dict): A dictionary containing arguments needed for sub-operators like sparsity and coefficient calculation. + force_out_of_place (bool, optional): If True, the fitness value is returned directly instead of being stored in the equation object. Defaults to False. + + Returns: + float, None: If `force_out_of_place` is True, returns the calculated fitness value. Otherwise, returns None. The fitness value is also stored in `objective.fitness_value`. - None - """ + Why: + This method quantifies how well a given equation represents the underlying relationships in the data. By calculating a fitness score, the evolutionary algorithm can effectively search for equations that accurately capture the system's dynamics. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) self.suboperators['sparsity'].apply(objective, subop_args['sparsity']) @@ -96,25 +97,52 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = objective.fitness_value = fitness_value def use_default_tags(self): + """ + Sets the operator's tags to a predefined default. This ensures a consistent and informative categorization of the operator, facilitating its proper use and interpretation within the evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields Initialized: + _tags (set): A set containing default tags: 'fitness evaluation', 'gene level', 'contains suboperators', and 'inplace'. These tags provide essential metadata about the operator's functionality and behavior. + """ self._tags = {'fitness evaluation', 'gene level', 'contains suboperators', 'inplace'} class L2LRFitness(CompoundOperator): + """ + Represents the L2-LR fitness evaluation method. + + Calculates fitness based on the L2 loss with L2 regularization. + + Class Attributes: + - lambd + - use_g_fun + - g_fun_key + + Class Methods: + - apply: + """ + key = 'DiscrepancyBasedFitnessWithCV' def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = False): """ - Calculate the fitness function values. The result is not returned, but stored in the equation.fitness_value attribute. - - Parameters: - ------------ - equation : Equation object - the equation object, to that the fitness function is obtained. - - Returns: - ------------ - - None + Calculates the fitness of an equation by evaluating its ability to capture the underlying dynamics of the data. The fitness value, representing the equation's accuracy and stability, is stored directly within the equation object. + + Args: + objective (Equation): The equation to evaluate. Its `fitness_value` attribute will be updated with the calculated fitness. + arguments (dict): A dictionary containing arguments needed for sub-operators. + force_out_of_place (bool, optional): If True, the fitness value is returned instead of being stored in the equation object. Defaults to False. + + Returns: + float, optional: The fitness value, only returned if `force_out_of_place` is True. + + Why: + This method quantifies how well a candidate equation represents the relationships present in the data. By calculating a fitness score, the evolutionary algorithm can effectively search for equations that accurately model the observed system behavior. """ self_args, subop_args = self.parse_suboperator_args(arguments=arguments) @@ -324,6 +352,23 @@ def apply(self, objective: Equation, arguments: dict, force_out_of_place: bool = objective.coefficients_stability = lr def feature_reshape(self, features_vals): + """ + Reshapes and prepares feature values for equation discovery. + + This method consolidates feature value arrays, incorporates a constant + feature, and transposes the result to create a standardized input + format suitable for the equation learning process. The constant feature + allows the model to account for bias or offset terms in the discovered equations. + + Args: + features_vals: A list of NumPy arrays, where each array + represents a set of feature values. + + Returns: + A NumPy array containing the reshaped feature values. The array + is transposed, has a constant feature added, and is guaranteed + to be a 2D array. + """ features = features_vals[0] if len(features_vals) > 1: for i in range(1, len(features_vals)): @@ -335,24 +380,109 @@ def feature_reshape(self, features_vals): return features def get_g_fun_vals(self): + """ + Retrieves and prepares the g-function values for fitness evaluation. + + The g-function, representing a component of the discovered differential equation, + is retrieved from the global grid cache. If found, it's reshaped into a 1D array + and stored in `self.g_fun_vals` for efficient fitness calculation. This is done to + speed up fitness evaluation by pre-calculating and storing this component. If the + g-function is not available, `self.g_fun_vals` is set to None, indicating its absence + in the current equation candidate. + + Args: + self: The instance of the L2LRFitness class. + + Returns: + np.ndarray: The reshaped g-function values if available in the grid cache; otherwise, None. + + Attributes: + g_fun_vals (np.ndarray): Stores the reshaped g-function values, or None if the g-function is not available. + """ try: self.g_fun_vals = global_var.grid_cache.g_func.reshape(-1) except AttributeError: self.g_fun_vals = None def use_default_tags(self): + """ + Applies a predefined set of tags to categorize the fitness evaluation operator. + + This tagging helps in organizing and filtering operators based on their characteristics, + allowing for more efficient management and selection of appropriate operators + during the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. + + Initializes: + _tags (set): A set containing default tags: 'fitness evaluation', 'gene level', 'contains suboperators', and 'inplace'. + """ self._tags = {'fitness evaluation', 'gene level', 'contains suboperators', 'inplace'} class SolverBasedFitness(CompoundOperator): + """ + A base class for fitness evaluation based on solving a system of equations. + + This class provides a framework for evaluating the fitness of a solution by + solving a system of equations and comparing the solution to reference data. + + Class Methods: + - __init__ + - set_adapter + - apply + - use_default_tags + """ + # To be modified to include physics-informed information criterion (PIC) key = 'SolverBasedFitness' def __init__(self, param_keys: list): + """ + Initializes the object with parameter keys and sets the adapter to None. + + This initialization prepares the fitness evaluation component by storing the parameter keys + associated with the equation's structure. The adapter, responsible for interfacing with a + numerical solver, is initially set to None, as it will be configured later based on the + specific equation and solver chosen during the evolutionary process. + + Args: + param_keys: A list of parameter keys, defining the variables and constants in the equation. + + Returns: + None. + + Class Fields: + adapter (Any): An adapter object for numerical solvers, initialized to None. + """ super().__init__(param_keys) self.adapter = None def set_adapter(self, net = None): + """ + Sets up the solver adapter, which is crucial for training the neural network to approximate the solution of the discovered differential equation. + + This method initializes or updates the `SolverAdapter` with a given neural network. If no adapter exists or a new network is provided, a new `SolverAdapter` is created and configured with default parameters for compilation, optimization, training, and early stopping. This ensures that the neural network is properly set up to learn the solution of the differential equation. + + Args: + net: The neural network to be used by the adapter. If None, the + existing network is used, or a new one is created if no adapter + exists. + + Returns: + None + + Class Fields Initialized: + adapter (SolverAdapter): An instance of the SolverAdapter class, + responsible for managing the optimization and training of the + neural network. It is initialized with the provided network (or a + new one if none is provided), caching disabled, and the specified + device (CUDA if available, otherwise CPU). + """ if self.adapter is None or net is not None: compiling_params = {'mode': 'autograd', 'tol':0.01, 'lambda_bound': 100} # 'h': 1e-1 @@ -371,6 +501,22 @@ def set_adapter(self, net = None): self.adapter.set_training_params(**training_params) def apply(self, objective : SoEq, arguments : dict, force_out_of_place: bool = False): + """ + Applies the operator to the given objective function. + + This method orchestrates the application of sub-operators to refine equation structure and coefficient values, + solves the EPDE system using a neural network-based solver, and calculates the fitness value based on the solution's accuracy + compared to referential data. This process evaluates how well a candidate equation represents the underlying dynamics. + + Args: + objective: The objective function (SoEq) representing the EPDE system to be solved. + arguments: A dictionary containing arguments for the sub-operators, such as sparsity and coefficient calculation. + force_out_of_place: A boolean indicating whether to force out-of-place updates to the fitness values. + + Returns: + None. The method updates the fitness values of the equations within the objective, + reflecting the equation's ability to accurately model the system. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) try: @@ -421,18 +567,92 @@ def apply(self, objective : SoEq, arguments : dict, force_out_of_place: bool = F eq.fitness_value = fitness_value def use_default_tags(self): + """ + Sets the operator's tags to a predefined default. + + This ensures the operator is configured with a standard set of properties + related to its behavior within the evolutionary process. This includes + characteristics like its role in fitness evaluation, the level of genetic + material it operates on, its composition of sub-operators, and whether + it modifies data directly. + + Args: + self: The operator instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing default tags for the operator, initialized to {'fitness evaluation', 'chromosome level', 'contains suboperators', 'inplace'}. + """ self._tags = {'fitness evaluation', 'chromosome level', 'contains suboperators', 'inplace'} class PIC(CompoundOperator): + """ + Represents a Physics-Informed Controller (PIC) for solving EPDE systems. + + The PIC class orchestrates the application of suboperators, solves the equation + using a neural network adapter, and calculates fitness and stability metrics. + + Class Methods: + - __init__ + - set_adapter + - apply + - feature_reshape + - get_g_fun_vals + - use_default_tags + """ + key = 'PIC' def __init__(self, param_keys: list): + """ + Initializes the PIC object with parameter keys and sets the adapter to None. + + This initialization prepares the object to work with specific parameters + required for equation discovery. The adapter, initially set to None, + will later be used to facilitate the interaction with different data + sources or numerical solvers during the equation search process. + + Args: + param_keys: A list of parameter keys that define the variables + and constants to be used in the equation discovery process. + + Returns: + None. + + Class Fields: + adapter (Any): An adapter object, initialized to None. It will be + used for data handling and interaction with numerical solvers. + """ super().__init__(param_keys) self.adapter = None def set_adapter(self, net=None): + """ + Sets up the solver adapter with default configurations for equation discovery. + + This method initializes or resets the solver adapter with default parameters + for compiling, optimization, early stopping, and training. It configures + the device (CUDA if available, otherwise CPU) to ensure efficient computation + during the equation discovery process. This setup is crucial for exploring + the search space of possible differential equations and identifying the best + candidates that fit the observed data. + + Args: + net: An optional neural network to use with the adapter. If None, the + existing network in the adapter is used (if one exists). + + Returns: + None + + Class Fields Initialized: + adapter (SolverAdapter): An instance of the SolverAdapter class, configured + with the specified network and default parameters. This adapter is used + for training the neural network. + """ if self.adapter is None or net is not None: compiling_params = {'mode': 'autograd', 'tol': 0.01, 'lambda_bound': 100} # 'h': 1e-1 @@ -451,6 +671,20 @@ def set_adapter(self, net=None): self.adapter.set_training_params(**training_params) def apply(self, objective: SoEq, arguments: dict, force_out_of_place: bool = False): + """ + Applies suboperators to refine equation structure and calculate coefficients, then solves the EPDE system. + + This method orchestrates the application of suboperators to identify the most relevant terms and their coefficients within the equation. It then leverages a neural network adapter to solve the equation and evaluates the fitness and stability of the resulting solution. This process aims to find a balance between model complexity and accuracy in representing the underlying dynamics of the system. + + Args: + objective: The objective function (SoEq) representing the EPDE system to be solved. + arguments: A dictionary containing arguments for the suboperators. + force_out_of_place: A boolean indicating whether to force out-of-place + operations. + + Returns: + float: The sum of errors if `force_out_of_place` is True, otherwise None. + """ self_args, subop_args = self.parse_suboperator_args(arguments=arguments) try: @@ -638,6 +872,24 @@ def apply(self, objective: SoEq, arguments: dict, force_out_of_place: bool = Fal print('Lr: ', lr, '\t Lp: ', lp) def feature_reshape(self, features_vals): + """ + Reshapes and prepares feature values for equation discovery. + + This method consolidates feature value arrays, introduces a constant + feature to account for potential bias or offset in the equation, + transposes the data for compatibility with the equation fitting process, + and ensures the output is a 2D array suitable for subsequent + mathematical operations. + + Args: + features_vals: A list of NumPy arrays, each representing a set of + feature values. + + Returns: + A NumPy array containing the reshaped feature values. The array + is transposed, has a constant feature added, and is guaranteed + to be 2D. + """ features = features_vals[0] if len(features_vals) > 1: for i in range(1, len(features_vals)): @@ -649,16 +901,66 @@ def feature_reshape(self, features_vals): return features def get_g_fun_vals(self): + """ + Retrieves the g-function values from the global grid cache to enable efficient computation of particle interactions. + + The method attempts to retrieve the g-function from the global grid cache. If found, it reshapes the data for use in subsequent calculations and stores it in the `g_fun_vals` attribute. If the g-function is not available, `g_fun_vals` is set to None, indicating that this component of the calculation cannot proceed. This caching mechanism avoids redundant computations and speeds up the overall process of modeling the system. + + Args: + self: The object instance. + + Returns: + None. The method updates the `g_fun_vals` attribute of the object. + + Class Fields Initialized: + g_fun_vals (numpy.ndarray or None): The reshaped g-function values + from the global grid cache, or None if the g-function is not + available. + """ try: self.g_fun_vals = global_var.grid_cache.g_func.reshape(-1) except AttributeError: self.g_fun_vals = None def use_default_tags(self): + """ + Resets the operator's tags to the default set. + + This ensures the operator is correctly categorized for equation discovery, + allowing it to be properly utilized within the evolutionary process. + The default tags reflect common characteristics of operators used in this framework. + + Args: + self: The operator instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing the tags associated with the operator. + Initialized to {'fitness evaluation', 'chromosome level', 'contains suboperators', 'inplace'}. + """ self._tags = {'fitness evaluation', 'chromosome level', 'contains suboperators', 'inplace'} def plot_data_vs_solution(grid, data, solution): + """ + Plots the data and solution against the grid to validate discovered equations. + + This method visualizes the data alongside the solution obtained from the + identified differential equation on the provided grid. It supports 1D and 2D grids, + using scatter plots for 1D and surface plots for 2D, allowing for a visual + comparison between the data and the solution. This comparison helps assess + how well the discovered equation fits the observed data. + + Args: + grid (numpy.ndarray): The grid points where the data and solution are defined. + data (numpy.ndarray): The data values at the grid points. + solution (numpy.ndarray): The solution values at the grid points. + + Returns: + None: Displays the plot. + """ if grid.shape[1]==2: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') diff --git a/epde/operators/common/right_part_selection.py b/epde/operators/common/right_part_selection.py index 14787554..7ec82f12 100644 --- a/epde/operators/common/right_part_selection.py +++ b/epde/operators/common/right_part_selection.py @@ -17,33 +17,50 @@ from epde.structure.main_structures import Term, Equation class EqRightPartSelector(CompoundOperator): - ''' + """ + Operator for selecting the right-hand side of an equation to approximate a function. - Operator for selection of the right part of the equation to emulate approximation of non-trivial function. - Works in the following manner: in a loop each term is considered as the right part, for this division the - fitness function value is calculated. The term, corresponding to the separation with the highest FF value is - saved as the correct right part. + This operator iterates through each term in an equation, treating it as the right-hand side. For each such division, a fitness function is evaluated. The term that yields the highest fitness function value when isolated on the right-hand side is then selected as the appropriate right-hand side. - Noteable attributes: + Key Attributes: ----------- suboperators : dict - Inhereted from the CompoundOperator class - key - str, value - instance of a class, inhereted from the CompoundOperator. - Suboperators, performing tasks of equation processing. In this case, only one suboperator is present: - fitness_calculation, dedicated to calculation of fitness function value. - - Methods: - ----------- - apply(equation) - return None - Inplace detection of index of the best separation into right part, saved into ``equation.target_idx`` - + Inherited from the CompoundOperator class. + key - str, value - instance of a class, inherited from the CompoundOperator. + Suboperators responsible for equation processing tasks. In this case, it typically includes a 'fitness_calculation' suboperator, which calculates the fitness function value. + + + Methods: + ----------- + apply(equation) + return None + Inplace detection of index of the best separation into right part, saved into ``equation.target_idx`` + + + ''' + """ - ''' key = 'FitnessCheckingRightPartSelector' @HistoryExtender('\n -> The equation structure was detected: ', 'a') def apply(self, objective : Equation, arguments : dict): + """ + Applies a series of sub-operators to refine the equation's right-hand side. + + This method iteratively selects and simplifies terms on the right-hand side + of the equation. It focuses on terms containing both the main variable and its + derivative, aiming to isolate the most relevant components for explanation. + The process continues until a suitable term is identified and the equation is simplified. + This iterative refinement helps in discovering the underlying structure of the equation + by strategically dissecting its components. + + Args: + objective (Equation): The equation to be solved, containing the structure to be refined. + arguments (dict): A dictionary of arguments for the sub-operators, guiding the simplification process. + + Returns: + None: The method modifies the `objective` in place, refining its structure. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) objective.reset_state(True) @@ -79,6 +96,21 @@ def apply(self, objective : Equation, arguments : dict): objective.reset_explaining_term(objective.target_idx) def simplify_equation(self, objective: Equation): + """ + Simplifies the given equation to enhance the search for the optimal equation structure. + + It identifies non-zero terms in the equation, finds common factors among them, + and attempts to reduce the order of these factors if they share the same dimension. + If a term's order becomes zero after simplification, it is replaced with a new random term to maintain diversity in the equation's structure. + This simplification process helps to refine the equation and potentially reduce its complexity, + making it easier to find a solution that accurately represents the underlying dynamics of the system. + + Args: + objective: The equation to simplify. + + Returns: + None. The method modifies the equation object in place. + """ # Get nonzero terms nonzero_terms_mask = np.array([False if weight == 0 else True for weight in objective.weights_internal], dtype=np.integer) nonrs_terms = [term for i, term in enumerate(objective.structure) if i != objective.target_idx] @@ -128,37 +160,57 @@ def simplify_equation(self, objective: Equation): objective.right_part_selected = True def use_default_tags(self): + """ + Uses a predefined set of tags to categorize and manage the selection process. + + This method overwrites any existing tags with a predefined set, ensuring consistency in how equation right-part selections are handled within the evolutionary process. This standardization aids in the effective discovery of differential equations by maintaining a clear and consistent categorization of operations. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing the default tags: 'equation right part selection', 'gene level', 'contains suboperators', and 'inplace'. + """ self._tags = {'equation right part selection', 'gene level', 'contains suboperators', 'inplace'} class RandomRHPSelector(CompoundOperator): - ''' + """ + Operator for selecting the optimal right-hand side of an equation by iteratively evaluating each term as a potential candidate. For each term, a fitness function is computed, and the term yielding the highest fitness value is chosen as the right-hand side. - Operator for selection of the right part of the equation to emulate approximation of non-trivial function. - Works in the following manner: in a loop each term is considered as the right part, for this division the - fitness function value is calculated. The term, corresponding to the separation with the highest FF value is - saved as the correct right part. + Key attributes: + + * `suboperators`: A dictionary containing sub-operators for equation processing. In this case, it includes a `fitness_calculation` sub-operator responsible for computing the fitness function value. + + Methods: + ----------- + apply(equation) + return None + Inplace detection of index of the best separation into right part, saved into ``equation.target_idx`` - Noteable attributes: - ----------- - suboperators : dict - Inhereted from the CompoundOperator class - key - str, value - instance of a class, inhereted from the CompoundOperator. - Suboperators, performing tasks of equation processing. In this case, only one suboperator is present: - fitness_calculation, dedicated to calculation of fitness function value. - - Methods: - ----------- - apply(equation) - return None - Inplace detection of index of the best separation into right part, saved into ``equation.target_idx`` - - ''' + ''' + """ + key = 'RandomRightPartSelector' @HistoryExtender('\n -> The equation structure was detected: ', 'a') def apply(self, objective : Equation, arguments : dict): + """ + Applies a selection strategy to identify a suitable term on the right-hand side of the equation for further refinement. + + This method focuses on selecting a term within the equation's structure that can be modified or expanded to better represent the underlying dynamics. It prioritizes terms that already contain the derivative of the main variable, as these are more likely to contribute meaningfully to the equation's explanatory power. If no such term exists, the method introduces a new term containing the derivative to guide the search process. This ensures that the equation evolves towards a form that accurately captures the relationships between variables and their rates of change. + + Args: + objective: The equation to which the selection strategy is applied. + arguments: A dictionary of arguments (currently unused in the selection logic). + + Returns: + None. This method modifies the `objective` in place. + """ # print(f'CALLING RIGHT PART SELECTOR FOR {objective.text_form}') self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -187,4 +239,19 @@ def apply(self, objective : Equation, arguments : dict): def use_default_tags(self): + """ + Sets the default operational tags. + + This method initializes the internal tag set with a predefined collection of tags. + These tags indicate specific characteristics and constraints relevant to the equation discovery process, + such as operations on the equation's right-hand side, gene-level considerations, the inclusion of sub-operators, + and whether operations are performed in place. This configuration ensures that the equation search and simplification + process starts with a consistent and relevant set of constraints. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'equation right part selection', 'gene level', 'contains suboperators', 'inplace'} diff --git a/epde/operators/common/sparsity.py b/epde/operators/common/sparsity.py index e4ebe93b..888dd634 100644 --- a/epde/operators/common/sparsity.py +++ b/epde/operators/common/sparsity.py @@ -18,44 +18,43 @@ class LASSOSparsity(CompoundOperator): """ The operator, which applies LASSO regression to the equation object to detect the - valuable term coefficients. + valuable term coefficients. - Notable attributes: - ------------------- - - params : dict - Inhereted from the ``CompoundOperator`` class. - Parameters of the operator; main parameters: - - sparsity - value of the sparsity constant in the LASSO operator; - - g_fun : np.ndarray or None: - values of the function, used during the weak derivatives estimations. - - Methods: - ----------- - apply(equation) - calculate the coefficients of the equation, that will be stored in the equation.weights np.ndarray. - + Notable attributes: + ------------------- + + params : dict + Inhereted from the ``CompoundOperator`` class. + Parameters of the operator; main parameters: + + sparsity - value of the sparsity constant in the LASSO operator; + + g_fun : np.ndarray or None: + values of the function, used during the weak derivatives estimations. + + Methods: + ----------- + apply(equation) + calculate the coefficients of the equation, that will be stored in the equation.weights np.ndarray. """ + key = 'LASSOBasedSparsity' def apply(self, objective : Equation, arguments : dict): """ - Apply the operator, to fit the LASSO regression to the equation object to detect the - valueable terms. In the Equation class, a term is selected to represent the right part of - the equation, and its values are used here as the target, and the values of the other - terms are utilizd as the features. The method does not return the vector of coefficients, - but rather assigns the result to the equation attribute ``equation.weights_internal`` + Applies LASSO regression to identify significant terms within an equation. - Parameters: - ------------ - equation : Equation object - the equation object, to that the coefficients are obtained. - - Returns: - ------------ - None + This method leverages LASSO regression to automatically determine the importance of each term in the equation. + By treating one term as the target variable and the remaining terms as features, LASSO regression identifies the coefficients that best reconstruct the target term. + The L1 regularization of LASSO encourages sparsity, effectively selecting the most relevant terms and simplifying the equation. + The resulting coefficients are then stored within the equation object, providing a compact representation of the equation's structure. + + Args: + objective (Equation): The equation object to which the coefficients are applied. + arguments (dict): A dictionary containing arguments for the operator. + + Returns: + None: The method modifies the equation object in place, assigning the LASSO coefficients to the `weights_internal` attribute. """ # print(f'Metaparameter: {objective.metaparameters}, objective.metaparameters[("sparsity", objective.main_var_to_explain)]') self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -71,6 +70,23 @@ def apply(self, objective : Equation, arguments : dict): objective.weights_internal = estimator.coef_ def use_default_tags(self): + """ + Sets the operator's tags to a predefined default set. + + This ensures that the operator is correctly categorized with general characteristics + related to sparsity, gene-level operations, lack of sub-operators, and in-place computation. + This is important for the framework to properly identify and utilize this operator within + the equation discovery process. + + Args: + self: The operator instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing default tags: 'sparsity', 'gene level', 'no suboperators', and 'inplace'. These tags describe general characteristics of the operator. + """ self._tags = {'sparsity', 'gene level', 'no suboperators', 'inplace'} diff --git a/epde/operators/multiobjective/moeadd_specific.py b/epde/operators/multiobjective/moeadd_specific.py index 0635e4b3..26fb60d5 100644 --- a/epde/operators/multiobjective/moeadd_specific.py +++ b/epde/operators/multiobjective/moeadd_specific.py @@ -17,97 +17,99 @@ def penalty_based_intersection(sol_obj, weight, ideal_obj, penalty_factor = 1.) -> float: - ''' - Calculation of the penalty pased intersection, that is minimized for the solutions inside the - domain, specified by **weight** vector. The calculations are held, according to the following formulas: - - .. math:: g^{pbi}(\mathbf{x}|\mathbf{w}, \mathbf{z^{*}}) = d_1 + \Theta d_2 \longrightarrow min - - subject to :math:`\mathbf{x} \in \Omega` - - where: - - .. math:: - d_1 = ||(\mathbf{f}(\mathbf{x}) - \mathbf{z^{*}})^{t}\mathbf{w}|| (||\mathbf{w}||)^{-1} - - d_2 = || \mathbf{f}(\mathbf{x}) - (\mathbf(z^{*}) + d_1 \mathbf{w} (||\mathbf{w}||)^{-1})|| - - Arguments: - ---------- + """ + Calculates the Penalty-based Intersection (PBI) value for a given solution, considering its objective function values, a weight vector, and an ideal point. - sol_obj : object of subclass of ``src.moeadd.moeadd_solution_template.MOEADDSolution`` - The solution, for which the penalty based intersection is calculated. In the equations above, - it denotes :math:`\mathbf{x}`, with the :math:`\mathbf{F}(\mathbf{x})` representing the - objective function values. + This function evaluates how well a solution aligns with a specific subregion of the objective space, + guided by the weight vector. The PBI value combines the distance along the weight vector (d1) and + the distance to the weight vector (d2), penalizing solutions that are far from the ideal point + and the direction indicated by the weight vector. This metric is used to promote solutions that + are both close to the ideal point and well-distributed across the objective space. - weight : np.array - Values of the weight vector, specific to the domain, in which the solution is located. - Represents the :math:`\mathbf{w}` in the equations above. + The calculations are held, according to the following formulas: + + .. math:: g^{pbi}(\mathbf{x}|\mathbf{w}, \mathbf{z^{*}}) = d_1 + \Theta d_2 \longrightarrow min + + subject to :math:`\mathbf{x} \in \Omega` - ideal_obj : `np.array` - The value of best achievable objective functions values; denoted as - :math:`\mathbf{z^{*}} = (z^{*}_1, z^{*}_2, \; ... \;, z^{*}_m)`. + where: + + .. math:: + d_1 = ||(\mathbf{f}(\mathbf{x}) - \mathbf{z^{*}})^{t}\mathbf{w}|| (||\mathbf{w}||)^{-1} - penalty_factor : float, optional, default 1. - The penalty parameter, represents :math:`\Theta` in the equations. + d_2 = || \mathbf{f}(\mathbf{x}) - (\mathbf(z^{*}) + d_1 \mathbf{w} (||\mathbf{w}||)^{-1})|| - ''' + Args: + ---------- + + sol_obj : object of subclass of ``src.moeadd.moeadd_solution_template.MOEADDSolution`` + The solution, for which the penalty based intersection is calculated. In the equations above, + it denotes :math:`\mathbf{x}`, with the :math:`\mathbf{F}(\mathbf{x})` representing the + objective function values. + + weight : np.array + Values of the weight vector, specific to the domain, in which the solution is located. + Represents the :math:`\mathbf{w}` in the equations above. + + ideal_obj : `np.array` + The value of best achievable objective functions values; denoted as + :math:`\mathbf{z^{*}} = (z^{*}_1, z^{*}_2, \; ... \;, z^{*}_m)`. + + penalty_factor : float, optional, default 1. + The penalty parameter, represents :math:`\Theta` in the equations. + + Returns: + ------- + float + The calculated penalty-based intersection value. + """ d_1 = np.dot((sol_obj.obj_fun - ideal_obj), weight) / np.linalg.norm(weight) d_2 = np.linalg.norm(sol_obj.obj_fun - (ideal_obj + d_1 * weight/np.linalg.norm(weight))) return d_1 + penalty_factor * d_2 def population_to_sectors(population, weights): - ''' - - The distribution of the solutions into the domains, defined by weights vectors. + """ + Distributes the population of solutions across different subdomains, each associated with a specific weight vector. - Parameters: - ----------- + This division is crucial for localized evolutionary search within each subdomain, + allowing the algorithm to focus on improving solutions that are most relevant to a particular + region of the objective space, guided by the weight vectors. - population : list - List, containing the candidate solutions for the evolutionary algorithm. Elements shall - belong to the case-specific subclass of ``src.moeadd.moeadd_solution_template.MOEADDSolution``. - - weights : np.ndarray - Numpy ndarray of weight vectors; first dimension - weight index, second dimension - - weight value in the objective function space. - - Returns: - --------- + Args: + population (list): A list of candidate solutions. Each solution should be an instance of a subclass + of `src.moeadd.moeadd_solution_template.MOEADDSolution` and must implement a `get_domain` method + that determines its associated weight vector index. + weights (np.ndarray): A NumPy array containing the weight vectors. The first dimension represents the + weight vector index, and the second dimension represents the weight values in the objective function space. - population_divided : list - List of candidate solutions, belonging to the weight domain. The outer index of the list - - the weight vector index, inner - the index of a particular candidate solution inside the domain. - - - ''' + Returns: + list: A list of lists, where each inner list contains the solutions belonging to a specific weight vector's + domain. The outer index corresponds to the weight vector index, and the inner index corresponds to the + index of a particular solution within that domain. + """ solution_selection = lambda weight_idx: [solution for solution in population if solution.get_domain(weights) == weight_idx] return list(map(solution_selection, np.arange(len(weights)))) def locate_pareto_worst(levels, weights, best_obj, penalty_factor = 1.): - ''' - - Function, dedicated to the selection of the worst solution on the Pareto levels. + """ + Locates the worst-performing solution within the Pareto levels to maintain diversity and convergence in the evolutionary process. - Arguments: - ---------- - - levels : pareto_levels obj - The levels, on which the worst candidate solution is detected. - - weights : np.ndarray - The weight vectors of the moeadd optimizer. - - best_obj : np.array - Best achievable values of the objective functions. + This function identifies the solution that contributes least to the overall Pareto front, + focusing on densely populated regions and higher non-dominated levels. By removing such solutions, + the algorithm encourages exploration of less crowded areas and promotes better convergence towards the true Pareto front. - penalty_factor : float, optional, default 1. - The penalty parameter, used during penalty based intersection value calculation. + Args: + levels (pareto_levels obj): The Pareto levels containing the population's non-dominated sorting. + weights (np.ndarray): The weight vectors used in the MOEA/D algorithm to decompose the multi-objective problem. + best_obj (np.array): The best-known objective function values for normalization purposes. + penalty_factor (float, optional): The penalty parameter used in the penalty-based intersection calculation. Defaults to 1.0. - ''' + Returns: + Individual: The individual identified as the worst-performing solution on the Pareto levels. + """ domain_solutions = population_to_sectors(levels.population, weights) most_crowded_count = max([len(domain) for domain in domain_solutions]); crowded_domains = [domain_idx for domain_idx in np.arange(len(weights)) if len(domain_solutions[domain_idx]) == most_crowded_count] @@ -132,13 +134,29 @@ def locate_pareto_worst(levels, weights, best_obj, penalty_factor = 1.): class PopulationUpdater(CompoundOperator): + """ + Updates a population by removing the worst element based on a penalty-based boundary intersection (PBI) value and updating Pareto levels. + + Attributes: + problem (Problem): The optimization problem being solved. + ref_points (np.ndarray): The reference points used for PBI calculation. + ideal_point (np.ndarray): The ideal point for normalization. + utopian_point (np.ndarray): The utopian point for normalization. + """ + key = 'PopulationUpdater' def apply(self, objective : ParetoLevels, arguments : dict): - ''' - Update population to get the pareto-nondomiated levels with the worst element removed. - Here, "worst" means the solution with highest PBI value (penalty-based boundary intersection) - ''' + """ + Updates the Pareto levels by removing the solution with the highest PBI value from the worst level. This focuses the search on promising areas of the Pareto front, maintaining diversity while prioritizing solutions that effectively balance multiple objectives. + + Args: + objective (ParetoLevels): The current Pareto levels, containing both the population and its non-dominated sorting. + arguments (dict): A dictionary containing necessary parameters, including weights for PBI calculation and the best objective values. + + Returns: + None: The method modifies the `objective` in place, updating the Pareto levels. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) # print(f'PopulationUpdater.params is {self.params}') @@ -187,28 +205,86 @@ def apply(self, objective : ParetoLevels, arguments : dict): @property def arguments(self): + """ + Returns the set of arguments used by the object. These arguments are necessary for updating the population based on the optimization process. Specifically, 'weights' are used to control the contribution of each objective, and 'best_obj' represents the best objective value achieved so far, guiding the search towards better solutions. + + Returns: + set: A set containing the strings 'weights' and 'best_obj', + representing the arguments used. + """ return set(['weights', 'best_obj']) def use_default_tags(self): + """ + Sets the tags to the default set. + + This method overwrites any existing tags with a predefined set of default tags. This ensures that the PopulationUpdater operates with a consistent and predefined set of characteristics, which is useful for maintaining a standardized approach to equation discovery. By using default tags, the system can reliably apply specific update strategies and constraints during the evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'pareto level update', 'custom level', 'no suboperators', 'inplace'} class PopulationUpdaterConstrained(object): + """ + Updates a population by removing the worst element based on PBI and constraint violations to maintain Pareto-nondominated levels. + + Class Methods: + - apply: Update population to get the pareto-nondomiated levels with the worst element removed. + - arguments: Returns the set of arguments used by the object. + - use_default_tags: Uses default tags for the object. + """ + key = 'PopulationUpdaterConstrined' def __init__(self, param_keys : list = [], constraints : Union[list, tuple, set] = []): + """ + Initializes a ConstrainedOptimizer object. + + This class is intended to manage parameter updates within specified constraints, ensuring that the evolved solutions adhere to predefined boundaries. + Currently, constrained optimization is not implemented. + + Args: + param_keys (list): List of parameter keys to optimize. + constraints (Union[list, tuple, set]): Constraints to apply during optimization. + + Raises: + NotImplementedError: Always raised, as constrained optimization is not yet implemented. + + Returns: + None. + + Fields: + constraints (Union[list, tuple, set]): Constraints to apply during optimization. Currently, constrained optimization is not implemented, so this field is not used. + + Why: + This initializer is designed to eventually handle constraints during the evolutionary process, ensuring that the discovered equations and their parameters remain within physically or empirically relevant ranges. + """ super().__init__(param_keys = param_keys) raise NotImplementedError('Constrained optimization has not been implemented yet.') self.constraints = constraints # TODO: add constraint setting for the constructor def apply(self, objective : ParetoLevels, arguments : dict): - ''' - Update population to get the pareto-nondomiated levels with the worst element removed. - Here, "worst" means the solution with highest PBI value (penalty-based boundary intersection). - Additionally, the constraint violations are considered in the selection of the - "worst" individual. - ''' + """ + Update the Pareto-nondominated levels by removing the "worst" solution from the population. + + The "worst" solution is determined based on its PBI (Penalty-based Boundary Intersection) value, + prioritizing solutions with constraint violations. This process refines the Pareto front approximation + by iteratively removing less desirable solutions, guiding the search towards better equation discovery. + + Args: + objective (ParetoLevels): The current Pareto-nondominated levels. The first element is assumed to be the initial population, the second - population with levels + arguments (dict): A dictionary containing necessary parameters, including weights, best objective values, + and PBI penalty. + + Returns: + None: The method modifies the `objective` in place by removing the worst solution. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) objective[1].update(objective[0]) @@ -276,19 +352,75 @@ def apply(self, objective : ParetoLevels, arguments : dict): @property def arguments(self): + """ + Returns the set of arguments used by the object. + + This method specifies the arguments required for updating the population, + ensuring that the evolutionary process has the necessary information + (weights and best objective value) to guide the search for optimal solutions. + + Returns: + set: A set containing the strings 'weights' and 'best_obj', + representing the arguments used. + """ return set(['weights', 'best_obj']) def use_default_tags(self): + """ + Uses a predefined set of tags to characterize the update operation. + + This method assigns a default set of tags to the updater, providing a standardized way to identify its key characteristics and ensure consistent behavior within the evolutionary process. These tags help in categorizing and managing different update strategies. + + Args: + self: The PopulationUpdaterConstrained instance. + + Returns: + None. + + This method initializes the following object properties: + - _tags: A set containing the default tags: 'pareto level update', 'custom level', 'no suboperators', and 'inplace'. + """ self._tags = {'pareto level update', 'custom level', 'no suboperators', 'inplace'} def use_item_if_no_default(key, arg : dict, replacement_arg : dict): + """ + Adds a key-value pair from a replacement dictionary to the original dictionary if the key is missing. + + This ensures that essential parameters, potentially discovered during the equation search process, + are incorporated into the configuration, even if they weren't initially specified. This helps to refine + the equation discovery process by ensuring that important terms are not overlooked. + + Args: + key: The key to check for in both dictionaries. + arg (dict): The original dictionary to be updated. + replacement_arg (dict): The dictionary providing replacement values. + + Returns: + dict: The updated dictionary. + """ if key in replacement_arg.keys(): arg[key] = replacement_arg[key] return arg def get_basic_populator_updater(params : dict = {}): + """ + Creates a `PopulationUpdater` instance and configures it with essential parameters. + + This function initializes a `PopulationUpdater` and enriches it with + fundamental parameters, preparing it for evolutionary processes. This + ensures that the population update mechanism is properly set up with + necessary configurations before the evolutionary search begins. + + Args: + params (dict, optional): A dictionary holding parameters to be incorporated + into the `PopulationUpdater`. Defaults to an empty dictionary. + + Returns: + PopulationUpdater: A configured `PopulationUpdater` object, ready for use + in the evolutionary process. + """ add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = params) pop_updater = PopulationUpdater() @@ -298,6 +430,22 @@ def get_basic_populator_updater(params : dict = {}): def get_constrained_populator_updater(params : dict = {}, constraints : list = []): + """ + Creates and returns a population updater with specified constraints. + + This method initializes a `PopulationUpdaterConstrained` object, incorporating + user-defined constraints to guide the evolutionary process. It also integrates + essential parameters, ensuring the population update adheres to the problem's + specific requirements. This ensures that the search for differential equations + respects predefined boundaries and conditions. + + Args: + params (dict, optional): A dictionary of parameters to be added to the operator. Defaults to {}. + constraints (list, optional): A list of constraints to be applied to the population updater. Defaults to []. + + Returns: + PopulationUpdaterConstrained: An instance of the population updater, configured with the provided constraints and parameters. + """ add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = params) pop_updater = PopulationUpdaterConstrained(constraints = constraints) @@ -307,44 +455,111 @@ def get_constrained_populator_updater(params : dict = {}, constraints : list = [ class SimpleNeighborSelector(CompoundOperator): + """ + A simple selector of neighboring weight vectors. + + This class selects the n-closest neighboring weight vectors to a processed one, + where n is the number_of_neighbors. It is designed for use within the MOEA/D algorithm. + + Methods: + - apply + """ + key = 'SortingBasedNeighborSelector' def apply(self, objective : list, arguments : dict): - ''' - Simple selector of neighboring weight vectors: takes n-closest (*n = number_of_neighbors*)ones to the - processed one. Defined to be used inside the moeadd algorithm. + """ + Selects a subset of the most relevant objective vectors based on proximity. - Arguments: - ---------- - - sorted_neighbors : list - proximity list of neighboring vectors, ranged in the ascending order of the angles between vectors. - - number_of_neighbors : int - numbers of vectors to be considered as the adjacent ones - - Returns: - --------- - - sorted_neighbors[:number_of_neighbors] : list - self evident slice of proximity list - ''' + This method is used to focus the evolutionary search on the most promising regions + of the objective space, thereby improving the efficiency of the equation discovery + process. By selecting only a few neighboring objectives, the algorithm can + concentrate its computational resources on refining the equation models that + best represent those objectives. + + Args: + objective (list): A list of objective vectors, typically sorted by a proximity metric. + arguments (dict): A dictionary containing additional arguments (not directly used in this method). + + Returns: + list: A slice of the input `objective` list, containing the top `number_of_neighbors` vectors. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) return objective[:self.params['number_of_neighbors']] def use_default_tags(self): + """ + Resets the selector's tags to the default set. + + This ensures the selector operates with a standard configuration, + suitable for typical equation discovery tasks. The default tags + define common characteristics and constraints for neighbor selection. + + Args: + self: The SimpleNeighborSelector instance. + + Returns: + None. + """ self._tags = {'neighbor selector', 'custom level', 'no suboperators', 'inplace'} def best_obj_values(levels : ParetoLevels): + """ + Returns the extreme objective function values from a set of Pareto-optimal solutions. + + This function identifies the minimum and maximum values for each objective + across all solutions within the provided Pareto levels. This helps to + understand the range of possible objective values achievable within the + Pareto-optimal set, providing insights into trade-offs between objectives. + + Args: + levels: A ParetoLevels object containing a set of Pareto-optimal solutions. + + Returns: + np.ndarray: A NumPy array containing the minimum and maximum objective + function values across all solutions in the Pareto levels. The array + is sorted along axis 0, and the first and last elements are returned, + representing the minimum and maximum values respectively. + """ vals = np.array([solution.obj_fun for solution in levels]) return np.sort(vals, axis = 0)[(0, -1), ...] class OffspringUpdater(CompoundOperator): + """ + Updates the offspring population by applying a series of suboperators. + + This class orchestrates the process of improving a Pareto front by + iteratively applying mutation, selection, and fitness evaluation + suboperators to unplaced candidate solutions. It manages attempts to + avoid duplicates and enforces attempt limits. + """ + key = 'ParetoLevelUpdater' def apply(self, objective: ParetoLevels, arguments: dict): + """ + Applies a series of suboperators to integrate unplaced candidates into the Pareto front. + + This method iteratively processes unplaced candidates, applying mutation, selection, + and fitness evaluation suboperators to find suitable solutions. It manages attempts + to avoid duplicates and enforces attempt limits to ensure diversity and prevent + premature convergence during the equation discovery process. The goal is to refine + the Pareto front by incorporating new candidate solutions that effectively model + the underlying dynamics. + + Args: + objective: The ParetoLevels object to be improved, representing the current + set of candidate equations and their performance metrics. + arguments: A dictionary containing arguments for the suboperators, configuring + their behavior during the optimization process. + + Returns: + ParetoLevels: The modified ParetoLevels object after applying the suboperators, + containing the updated Pareto front with potentially new and improved + candidate equations. + """ self_args, subop_args = self.parse_suboperator_args(arguments=arguments) while objective.unplaced_candidates: @@ -386,6 +601,28 @@ def get_pareto_levels_updater(right_part_selector : CompoundOperator, chromosome mutation : CompoundOperator = None, constrained : bool = False, mutation_params : dict = {}, pl_updater_params : dict = {}, combiner_params : dict = {}): + """ + Creates and configures an offspring updater for Pareto levels. + + This method constructs an `OffspringUpdater` and configures it with + sub-operators responsible for mutation, Pareto level updating, right part selection, + and chromosome fitness evaluation. This ensures that the evolutionary process + effectively explores the search space and identifies optimal equation structures + by iteratively refining the population of candidate solutions. It also handles the case where a mutation + operator is not provided, creating a default one. + + Args: + right_part_selector: Operator for selecting the right part of the offspring. + chromosome_fitness: Operator for evaluating chromosome fitness. + mutation: Operator for chromosome mutation (optional). If None, a basic mutation is used. + constrained: A boolean indicating whether the problem is constrained (not used in the provided code). + mutation_params: Parameters for the mutation operator. + pl_updater_params: Parameters for the Pareto level updater. + combiner_params: Parameters for combining the offspring. + + Returns: + OffspringUpdater: The configured offspring updater. + """ add_kwarg_to_updater = partial(add_base_param_to_operator, target_dict = combiner_params) updater = OffspringUpdater() add_kwarg_to_updater(operator = updater) @@ -400,24 +637,48 @@ def get_pareto_levels_updater(right_part_selector : CompoundOperator, chromosome return updater class InitialParetoLevelSorting(CompoundOperator): + """ + Initial sorting of the candidates in pareto levels. + + Class Attributes: + key + + Class Methods: + - apply: ''' + Initial sorting of the candidates in pareto levels. + + Parameters + ---------- + objective : ParetoLevels + DESCRIPTION. + arguments : dict + DESCRIPTION. + + Returns + ------- + None. + + ''' + """ + key = 'InitialParetoLevelSorting' def apply(self, objective : ParetoLevels, arguments : dict): - ''' - Initial sorting of the candidates in pareto levels. - - Parameters - ---------- - objective : ParetoLevels - DESCRIPTION. - arguments : dict - DESCRIPTION. - - Returns - ------- - None. - - ''' + """ + Sorts the initial population into Pareto levels based on their objective values. + + This method is crucial for initiating the evolutionary process by + organizing the candidate solutions based on dominance relationships. + It ensures that the selection process favors non-dominated solutions, + driving the evolution towards better-performing equations. + + Args: + objective (ParetoLevels): The ParetoLevels object containing the population and related information. + arguments (dict): A dictionary containing arguments for the sub-operators. + + Returns: + ParetoLevels: The updated ParetoLevels object with the population sorted into initial Pareto levels. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) if len(objective.population) == 0: @@ -441,6 +702,22 @@ def apply(self, objective : ParetoLevels, arguments : dict): def get_initial_sorter(right_part_selector : CompoundOperator, chromosome_fitness : CompoundOperator, sorter_params : dict = {}): + """ + Creates and configures the initial Pareto level sorting operator for equation discovery. + + This method instantiates an `InitialParetoLevelSorting` operator, configures it with provided parameters, + and sets its sub-operators for right part selection and chromosome fitness evaluation. + This sorting is crucial in the evolutionary process to identify promising equation structures + by evaluating their fitness and complexity. + + Args: + right_part_selector: Operator for selecting the right part of the chromosome (equation). + chromosome_fitness: Operator for evaluating chromosome fitness (equation fitness). + sorter_params: Optional dictionary of parameters to be added to the sorter. + + Returns: + InitialParetoLevelSorting: The configured initial Pareto level sorting operator. + """ add_kwarg_to_updater = partial(add_base_param_to_operator, target_dict = sorter_params) sorter = InitialParetoLevelSorting() add_kwarg_to_updater(operator = sorter) diff --git a/epde/operators/multiobjective/mutations.py b/epde/operators/multiobjective/mutations.py index a0b9e3ba..fe5f200c 100644 --- a/epde/operators/multiobjective/mutations.py +++ b/epde/operators/multiobjective/mutations.py @@ -23,6 +23,13 @@ class SystemMutation(CompoundOperator): + """ + Applies mutation suboperators to an objective function. + + This class orchestrates the application of mutation suboperators to a + given objective function, facilitating the evolution of the system. + """ + key = 'SystemMutation' def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best_individuals & worst individuals self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -33,6 +40,22 @@ def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best affected_by_mutation = np.random.random() < self.params['indiv_mutation_prob'] if affected_by_mutation: + """ + Applies equation and parameter mutation suboperators to an objective function. + + This method orchestrates the mutation of an objective function by applying + equation and parameter mutation suboperators. The mutation is applied probabilistically, + allowing for exploration of the search space by introducing variations into the + objective function's equations and parameters. This is a crucial step in the evolutionary + process, enabling the algorithm to discover new and potentially better-fitting equation structures. + + Args: + objective (SoEq): The objective function to be mutated. + arguments (dict): A dictionary of arguments for the suboperators. + + Returns: + SoEq: The mutated objective function. + """ for eq_key in eqs_keys: altered_eq = self.suboperators['equation_mutation'].apply(altered_objective.vals[eq_key], subop_args['equation_mutation']) @@ -49,13 +72,51 @@ def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best return altered_objective def use_default_tags(self): + """ + Uses a predefined set of tags to categorize the mutation operator. + + This method overwrites any existing tags, ensuring the operator is correctly identified for downstream processing and analysis within the EPDE framework. This ensures consistent categorization and facilitates proper handling during the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing the tags 'mutation', 'chromosome level', and 'contains suboperators'. + """ self._tags = {'mutation', 'chromosome level', 'contains suboperators'} class EquationMutation(CompoundOperator): + """ + Applies mutation to the equation's structure based on a mutation rate. + + Class Attributes: + - mutation_rate: The probability of applying a mutation sub-operator to a term. + - sub_operators: A list of mutation sub-operators to apply. + """ + key = 'EquationMutation' @HistoryExtender(f'\n -> mutating equation', 'ba') def apply(self, objective : Equation, arguments : dict): + """ + Applies mutation to the equation's structure based on a mutation rate. + + It iterates through the mutable terms of the equation's structure, + and for each term, it applies a mutation sub-operator with a certain probability + defined by the mutation rate. This process refines the equation's structure + to better fit the observed data by exploring alternative mathematical + relationships. + + Args: + objective (Equation): The equation to be mutated. + arguments (dict): A dictionary of arguments for the sub-operators. + + Returns: + Equation: The mutated equation. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) for term_idx in range(objective.n_immutable, len(objective.structure)): @@ -65,13 +126,49 @@ def apply(self, objective : Equation, arguments : dict): return objective def use_default_tags(self): + """ + Applies a predefined set of tags to categorize and characterize the equation mutation operation. + + This method assigns default tags to the mutation, providing a standardized way to identify and group mutations based on their characteristics. This facilitates analysis and comparison of different mutation strategies within the evolutionary process. + + Args: + self: The EquationMutation object instance. + + Returns: + None. + + This method initializes the following object properties: + - _tags (set): A set containing the default tags: 'mutation', 'gene level', and 'contains suboperators'. + """ self._tags = {'mutation', 'gene level', 'contains suboperators'} class MetaparameterMutation(CompoundOperator): + """ + Represents a mutation operation that modifies metaparameters. + + This class provides a base for implementing mutation strategies that + alter the values of metaparameters within a search space. + + Attributes: + params: A dictionary containing parameters for the mutation. + """ + key = 'MetaparameterMutation' def apply(self, objective : Union[int, float], arguments : dict): + """ + Applies a random perturbation to the objective value. + + This method introduces slight variations to the objective value, simulating the inherent noise and uncertainty present in real-world data. By adding a normally distributed random value (defined by the `mean` and `std` parameters), the method explores the solution space more broadly, potentially escaping local optima and promoting the discovery of more robust and generalizable equation structures. The non-negativity constraint ensures that the objective value remains physically meaningful within the context of equation discovery. + + Args: + objective (Union[int, float]): The original objective value. + arguments (dict): A dictionary of arguments that can be used to configure the perturbation. + + Returns: + float: The perturbed objective value, guaranteed to be non-negative. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) altered_objective = objective + np.random.normal(loc = self.params['mean'], scale = self.params['std']) @@ -81,6 +178,15 @@ def apply(self, objective : Union[int, float], arguments : dict): return altered_objective def use_default_tags(self): + """ + Sets the object's tags to a predefined set, ensuring consistency in identifying the type and characteristics of the mutation being applied. This standardization is crucial for the evolutionary process, allowing the system to effectively track and manage different mutation strategies. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'mutation', 'gene level', 'no suboperators'} @@ -88,26 +194,22 @@ class TermMutation(CompoundOperator): """ Specific operator of the term mutation, where the term is replaced with a randomly created new one. """ + key = 'TermMutation' def apply(self, objective : tuple, arguments : dict): #term_idx, equation): """ - Return a new term, randomly created to be unique from other terms of this particular equation. + Return a new, randomly generated term to replace an existing one within an equation. - Parameters: - ----------- - term_idx : integer - The index of the mutating term in the equation. - - equation : Equation object - The equation object, in which the term is present. + This ensures diversity in the equation population during the evolutionary search process. The new term is created based on the equation's pool of available operators and variables, and it is checked for uniqueness to maintain the integrity of the equation structure. - Returns: - ---------- - new_term : Term object - A new, randomly created, term. - - """ + Args: + objective (tuple): A tuple containing the index of the term to be mutated and the equation object. + arguments (dict): A dictionary containing additional arguments (not directly used in this method, but potentially used in sub-methods). + + Returns: + Term: A new, randomly created term that is unique within the equation. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) create_derivs = bool(objective[1].structure[objective[0]].descr_variable_marker) @@ -124,6 +226,24 @@ def apply(self, objective : tuple, arguments : dict): #term_idx, equation): return new_term def use_default_tags(self): + """ + Sets the default tags for this term mutation operator. These tags categorize the operator's behavior and capabilities within the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields Initialized: + _tags (set): A set containing the default tags: 'mutation', 'term level', 'exploration', and 'no suboperators'. + + Why: + These tags help the system understand the nature of this mutation operator, + allowing for informed decisions during the equation search process, such as + whether it introduces new terms ('exploration'), modifies existing ones ('term level'), + or avoids creating sub-operators ('no suboperators'). + """ self._tags = {'mutation', 'term level', 'exploration', 'no suboperators'} @@ -131,6 +251,7 @@ class TermParameterMutation(CompoundOperator): """ Specific operator of the term mutation, where the term parameters are changed with a random increment. """ + key = 'TermParameterMutation' def apply(self, objective : tuple, arguments : dict): # term_idx, objective @@ -138,6 +259,18 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective Specific operator of the term mutation, where the term parameters are changed with a random increment. Parameters: + """ + Specific operator for refining equation terms by randomly adjusting their parameters. + + This method fine-tunes the parameters of a selected term within an equation to improve the overall fit and accuracy of the model. By introducing small, random increments to the parameters, the algorithm explores the solution space and seeks to minimize the error between the equation's predictions and the observed data. This process helps to discover the optimal parameter values that best describe the underlying dynamics of the system. + + Args: + objective (tuple): A tuple containing the index of the term to be mutated and the equation object. + arguments (dict): A dictionary containing additional arguments required for the mutation process. + + Returns: + Term: The modified term with updated parameter values. + """ ----------- term_idx : integer The index of the mutating term in the equation. @@ -199,10 +332,37 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective return term def use_default_tags(self): + """ + Sets the operator's tags to a predefined default. This ensures that the operator is correctly categorized with respect to its function within the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. The operator's tags are updated in place. + """ self._tags = {'mutation', 'term level', 'exploitation', 'no suboperators'} def get_basic_mutation(mutation_params): + """ + Generates a basic mutation operator for evolving equation systems. + + This method creates and configures a system mutation operator composed + of equation and metaparameter mutation sub-operators. Term mutation is + nested under equation mutation to modify individual terms within equations. + This hierarchical structure allows for fine-grained control over the + evolutionary process, enabling the discovery of increasingly accurate + and parsimonious equation models. + + Args: + mutation_params: A dictionary containing mutation parameters that + control the behavior of the mutation operators. + + Returns: + SystemMutation: A configured system mutation operator ready to be + used in the evolutionary process. + """ add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = mutation_params) term_mutation = TermMutation([]) diff --git a/epde/operators/multiobjective/selections.py b/epde/operators/multiobjective/selections.py index 207f54e0..0227b60e 100644 --- a/epde/operators/multiobjective/selections.py +++ b/epde/operators/multiobjective/selections.py @@ -16,6 +16,15 @@ class MOEADDSelection(CompoundOperator): + ''' + This class implements the selection mechanism used in the MOEA/DD algorithm. + + Class Methods: + - apply: Selects parents for crossover based on weight vectors and neighborhood. + - arguments: Returns the set of arguments used by the layer. + - use_default_tags: Uses the default set of tags for the object. + ''' + key = 'MOEADDSelection' def apply(self, objective : ParetoLevels, arguments : dict): # pareto_levels @@ -26,6 +35,22 @@ def apply(self, objective : ParetoLevels, arguments : dict): # pareto_levels connected weight vector. Parameters: + """ + Selects parents for crossover, prioritizing solutions within the neighborhood of a weight vector to encourage localized search and maintain diversity in the population. This approach balances exploration and exploitation by focusing on promising regions of the search space while still allowing for broader exploration. + + Args: + objective (ParetoLevels): The current Pareto front, containing the population and their objective values. + arguments (dict): A dictionary containing the necessary parameters for the mating selection process, including: + - weight_idx (int): Index of the processed weight vector. + - weights (np.ndarray): Numpy array, containing weight vectors. + - neighborhood_vectors (list): List of lists, containing indexes: i-th element is the list of k - closest to the i-the weight vector weight vectors. + - population (list): List of candidate solutions. + - neighborhood_selector (function): Method of finding "close neighbors" of the vector with proximity list. The baseline example of the selector, presented in ``moeadd.moeadd_stc.simple_selector``, selects n-adjacent ones. + - delta (float): The probability of mating selection to be limited only to the selected subregions (adjacent to the weight vector domain). :math:`\delta \in [0., 1.)` + + Returns: + ParetoLevels: The updated Pareto front with incremented selection counters for the chosen parents. + """ ------------ weight_idx : int, @@ -98,54 +123,68 @@ def apply(self, objective : ParetoLevels, arguments : dict): # pareto_levels @property def arguments(self): + """ + Returns the set of arguments required to evaluate the contribution of each candidate solution. + + These arguments define the information needed to assess the quality and diversity of solutions within the population. + + Returns: + set: A set containing the strings 'neighborhood_vectors', + 'weight_idx', and 'weights', representing the arguments + used by the layer. + """ return set(['neighborhood_vectors', 'weight_idx', 'weights']) def use_default_tags(self): + """ + Uses a predefined set of tags to categorize and manage the selection process. + + This method overwrites any existing tags with a predefined set of default tags, ensuring consistency in how selection mechanisms are classified within the evolutionary process. This standardization aids in the organization and analysis of different selection strategies. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing the tags associated with the object. Initialized to a set containing 'selection', 'population level', 'auxilary', 'suboperators', and 'standard'. + """ self._tags = {'selection', 'population level', 'auxilary', 'suboperators', 'standard'} class MOEADDSelectionConstrained(CompoundOperator): + """ + MOEADDSelectionConstrained performs the selection process in MOEA/D with constraint handling. + + This class implements a selection mechanism that considers both objective function values and constraint violations + to guide the search towards feasible and optimal solutions. + """ + key = 'MOEADDSelectionConstrained' def apply(self, objective : ParetoLevels, arguments : dict): - ''' + """ + Selects parents for crossover, prioritizing solutions within specific regions of the objective space. - The mating operator, designed to select parents for the crossover with respect - to the location of the point in the objective functions values space and the - connected weight vector. + This method strategically chooses parent solutions for the crossover operation. + It focuses on selecting parents that reside in proximity to particular weight vectors, + thereby encouraging the exploration of promising areas within the objective space. + This targeted selection aims to enhance the discovery of equation structures that + effectively model the underlying dynamics of the system. - Parameters: - ------------ - - weight_idx : int, - Index of the processed weight vector. - - weights : np.ndarray, - Numpy array, containing weight vectors. - - neighborhood_vectors : list, - List of lists, containing indexes: i-th element is the list of - k - closest to the i-the weight vector weight vectors. - - population : list, - List of candidate solutions. - - neighborhood_selector : function, - Method of finding "close neighbors" of the vector with proximity list. - The baseline example of the selector, presented in - ``moeadd.moeadd_stc.simple_selector``, selects n-adjacent ones. - - delta : float - The probability of mating selection to be limited only to the selected - subregions (adjacent to the weight vector domain). :math:`\delta \in [0., 1.)` - - Returns: - --------- - - parent_idxs : list - List of the selected parents in the population pool. - - ''' + Args: + objective (ParetoLevels): The current Pareto front of solutions. + arguments (dict): A dictionary containing necessary parameters, including: + - weight_idx (int): Index of the processed weight vector. + - weights (np.ndarray): Numpy array of weight vectors. + - neighborhood_vectors (list): List of neighboring weight vector indices for each weight vector. + - population (list): List of candidate solutions. + - neighborhood_selector (function): Method for selecting neighboring weight vectors. + - delta (float): Probability of limiting mating selection to adjacent subregions. + Returns: + objective (ParetoLevels): Updated Pareto front with incremented counters for selected parents. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) if 'constraint_processer' in list(self.suboperators.keys()): @@ -184,21 +223,91 @@ def apply(self, objective : ParetoLevels, arguments : dict): @property def arguments(self): + """ + Returns the set of arguments required for the selection process. + + These arguments define the information needed to evaluate and compare + candidate solutions within the evolutionary algorithm. Specifically, + 'neighborhood_vectors' are used to consider solutions within a defined + vicinity, 'weight_idx' helps in navigating the Pareto front, and + 'weights' are the weights used for multi-objective optimization. + + Args: + None + + Returns: + set: A set containing the strings 'neighborhood_vectors', + 'weight_idx', and 'weights', representing the arguments + used by the selection mechanism. + """ return set(['neighborhood_vectors', 'weight_idx', 'weights']) def use_default_tags(self): + """ + Uses a predefined set of tags to categorize the selection operator. + + This method overwrites any existing tags with a default set, ensuring consistent categorization. This helps in organizing and identifying the selection operator within the broader evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'selection', 'population level', 'auxilary', 'suboperators', 'standard'} class SelectionConstraintProcesser(object): + """ + Processes selection constraints to filter individuals based on constraint violations. + + This class is designed to apply constraints during a selection process, + typically within an optimization or evolutionary algorithm. It identifies + and handles groups of individuals with identical constraint violations, + ensuring diversity in the selection. + """ + key = 'SelectionConstraintProcesser' def __init__(self, constraints = None, param_keys = []): + """ + Initializes the ConstraintSet object. + + This constructor initializes the constraint set with a list of constraints + and a list of parameter keys. These constraints will be used to guide the search + for suitable equation structures by imposing restrictions on the possible solutions. + It also calls the superclass's init method to initialize parameter keys. + + Args: + constraints (list): A list of Constraint objects that define the restrictions on the equation search space. + param_keys (list): A list of parameter keys associated with the constraints. + + Returns: + None + + Class Fields: + _constraints (list): A list of Constraint objects associated with the set. + """ assert all(isinstance(constraints, Constraint) for constr in constraints) self._constraints = constraints super().init(param_keys) def apply(self, indexes, pareto_levels): + """ + Applies a selection process to enhance population diversity based on constraint violations within groups. + + This method selects individuals from a given set of indexes, prioritizing diversity by examining constraint violation scores within groups. + It groups individuals and selects a random representative from groups where all members exhibit identical constraint violations. + This approach helps maintain a diverse population by ensuring that similar solutions within a group do not dominate the selection process. + Finally, the selected individuals are shuffled to further promote randomness in the evolutionary process. + + Args: + indexes (list): The indexes of the individuals to consider. + pareto_levels (ParetoLevels): The Pareto levels object containing the population data. + + Returns: + list: A list of selected individual indexes. + """ def violation(individual): return reduce(lambda y, z: y + z(individual.vals()), self._constraints, initial = 0) @@ -214,4 +323,19 @@ def violation(individual): return selected_idxs def use_default_tags(self): + """ + Resets the tag set to a predefined default. + + This method is used to ensure a consistent and well-defined set of tags + for processing selection constraints, providing a known starting point. + + Args: + self: The object instance. + + Returns: + None. + + Attributes: + _tags (set): A set containing the default tags: 'constraints', 'selection', 'custom level', and 'auxilary'. + """ self._tags = {'constraints', 'selection', 'custom level', 'auxilary'} \ No newline at end of file diff --git a/epde/operators/multiobjective/variation.py b/epde/operators/multiobjective/variation.py index ea7120b5..43c0de80 100644 --- a/epde/operators/multiobjective/variation.py +++ b/epde/operators/multiobjective/variation.py @@ -26,40 +26,28 @@ class ParetoLevelsCrossover(CompoundOperator): """ - The crossover operator, combining parameter crossover for terms with same - factors but different parameters & full exchange of terms between the - completely different ones. + This crossover operator combines parameter crossover for terms sharing the same factors but differing in parameters, and exchanges entire terms between dissimilar equations. It leverages sub-operators for parent selection, parameter crossover, term crossover, coefficient calculation, and fitness evaluation. - Noteable attributes: - ----------- - suboperators : dict - Inhereted from the Specific_Operator class. - Suboperators, performing tasks of parent selection, parameter crossover, full terms crossover, calculation of weights for each terms & - fitness function calculation. Dictionary: keys - strings from 'Selection', 'Param_crossover', 'Term_crossover', 'Coeff_calc', 'Fitness_eval'. - values - corresponding operators (objects of Specific_Operator class). - - Methods: - ----------- - apply(population) - return the new population, created with the noted operators and containing both parent individuals and their offsprings. - copy_properties_to + + Methods: + ----------- + apply(population) + return the new population, created with the noted operators and containing both parent individuals and their offsprings. + copy_properties_to """ + key = 'ParetoLevelsCrossover' def apply(self, objective : ParetoLevels, arguments : dict): """ - Method to obtain a new population by selection of parent individuals (equations) and performing a crossover between them to get the offsprings. + Method to generate new candidate solutions (offsprings) by combining existing solutions from the population using crossover. This process aims to explore the solution space and discover novel equation structures that better fit the data. - Attributes: - ----------- - population : list of Equation objects - the population, to that the operator is applied; - - Returns: - ----------- - population : list of Equation objects - the new population, containing both parents and offsprings; + Args: + objective (ParetoLevels): An object containing the population of equations, organized by Pareto levels, and unplaced candidates. + arguments (dict): A dictionary containing arguments for the crossover operator and its sub-operators. + Returns: + ParetoLevels: The updated ParetoLevels object, now containing the generated offsprings in the `unplaced_candidates` attribute. The offsprings are equations created by crossing over parent equations selected based on their Pareto level. """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -91,13 +79,53 @@ def apply(self, objective : ParetoLevels, arguments : dict): return objective def use_default_tags(self): + """ + Applies a predefined set of tags to categorize the crossover operator. These tags provide a concise description of the operator's characteristics, such as its type, level of operation, and structural properties. + + Args: + self: The object instance. + + Returns: + None. The method modifies the object's tags in place to reflect its default categorization. + + Why: + Tagging operators facilitates their organization and selection within the evolutionary process, enabling the framework to effectively manage and explore diverse search strategies for identifying differential equations. + """ self._tags = {'crossover', 'population level', 'contains suboperators', 'standard'} class ChromosomeCrossover(CompoundOperator): + """ + Applies crossover at the chromosome level using sub-operators. + + This class facilitates crossover between two chromosomes (offspring) by + applying specialized sub-operators to their constituent genes. It manages + the application of these sub-operators for both equation and parameter + genes, effectively recombining genetic material to create new offspring. + + Class Methods: + - apply: Applies crossover to the equation and parameter genes of two offspring. + - use_default_tags: Uses default tags for the object. + """ + key = 'ChromosomeCrossover' def apply(self, objective : tuple, arguments : dict): + """ + Applies crossover to the equation and parameter genes of two offspring. + + This method exchanges genetic material between two offspring to create new candidate solutions. + It iterates through the equation and parameter keys, applying specialized sub-operators for each. + The resulting genes are then used to replace the original genes in the offspring. + + Args: + objective (tuple): A tuple containing two offspring objects to be crossed over. + arguments (dict): A dictionary containing arguments for the sub-operators. + + Returns: + tuple: A tuple containing the two modified offspring after crossover. This ensures diversity + in the population, which is crucial for effective equation discovery. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) assert objective[0].vals.same_encoding(objective[1].vals) @@ -124,13 +152,51 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Applies a predefined set of tags to categorize the crossover operation. + + This method assigns default tags to the crossover object, providing a standardized way to identify and classify its characteristics. This tagging facilitates filtering and selection of appropriate crossover operators during the evolutionary search process. + + Args: + self: The ChromosomeCrossover instance. + + Returns: + None. The method modifies the object's internal `_tags` attribute. + """ self._tags = {'crossover', 'chromosome level', 'contains suboperators', 'standard'} class MetaparamerCrossover(CompoundOperator): + """ + Applies a metaparameter-controlled crossover operation on two parent solutions. + + This class implements a crossover operator that uses a metaparameter + to blend the genetic material of two parent solutions, creating two offspring. + + Class Attributes: + - metaparameter: The metaparameter that controls the blend of the parents' genetic material. + """ + key = 'MetaparamerCrossover' def apply(self, objective : tuple, arguments : dict): + """ + Applies the blend crossover operator to generate two offspring. + + This method refines the search for differential equation structures by + creating new candidate solutions (offspring) through a linear combination + of existing solutions (parents). The `metaparam_proportion` controls the + balance between the parent solutions, influencing the exploration of the + solution space. This exploration is crucial for discovering equations that + accurately represent the underlying dynamics of the data. + + Args: + objective: A tuple containing the objective values of two parent solutions. + arguments: A dictionary containing additional arguments for the operator. + + Returns: + tuple: A tuple containing the objective values of the two offspring. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) offspring_1 = objective[0] + self.params['metaparam_proportion'] * (objective[1] - objective[0]) @@ -138,14 +204,67 @@ def apply(self, objective : tuple, arguments : dict): return offspring_1, offspring_2 def use_default_tags(self): + """ + Sets a predefined set of tags for the crossover operator. + + This method assigns a default set of tags to the crossover operator, + characterizing its behavior and properties within the evolutionary process. + These tags help in categorizing and selecting appropriate operators + during the equation discovery process. + + Args: + self: The MetaparamerCrossover instance. + + Returns: + None. + + Initializes: + _tags (set): A set containing the default tags: 'crossover', 'gene level', and 'no suboperators'. + """ self._tags = {'crossover', 'gene level', 'no suboperators'} class EquationCrossover(CompoundOperator): + """ + Applies equation crossover to a pair of equations within an objective. + + This class performs equation crossover between two equations within a + given objective. It identifies similar terms in both equations and applies + different sub-operators ('term_param_crossover' and 'term_crossover') to + these terms to generate new equation structures. The method also ensures + the uniqueness of the generated terms within their respective equations. + + Class Methods: + - apply: Applies equation crossover to the given objective using sub-operators. + - use_default_tags: Uses the default set of tags for this object. + """ + key = 'EquationCrossover' @HistoryExtender(f'\n -> performing equation crossover', 'ba') def apply(self, objective : tuple, arguments : dict): + """ + Applies equation crossover to a pair of equations, exploring the space of possible equation structures. + + This method aims to evolve more accurate and generalizable equation models + by recombining building blocks from existing equations. It identifies + and exchanges both similar and dissimilar terms between two equations, + leveraging sub-operators to modify and refine the equation structures. + The method prioritizes the creation of unique and valid equation terms + to maintain diversity and prevent redundancy in the search process. + This crossover operation facilitates the discovery of novel equation forms + that better capture the underlying dynamics of the system. + + Args: + objective (tuple): A tuple containing two equation objects to be crossed over. + arguments (dict): A dictionary containing arguments for the sub-operators, + organized by sub-operator name (e.g., 'term_param_crossover', 'term_crossover'). + + Returns: + tuple: A tuple containing the two modified equation objects after crossover. + These equations now have potentially different structures resulting + from the exchange and modification of terms. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) equation1_terms, equation2_terms = detect_similar_terms(objective[0], objective[1]) @@ -170,53 +289,96 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Applies a pre-defined set of tags to the operator. + + This method resets the operator's tags to a default configuration, ensuring consistency in identifying its characteristics. This is useful for standardizing the representation of operators within the evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'gene level', 'contains suboperators', 'standard'} class EquationExchangeCrossover(CompoundOperator): + """ + Applies equation exchange crossover to a pair of objectives. + + Class Methods: + - apply: Applies equation exchange crossover to a pair of objectives. + - use_default_tags: Uses the default set of tags for this object. + """ + key = 'EquationExchangeCrossover' @HistoryExtender(f'\n -> performing equation exchange crossover', 'ba') def apply(self, objective : tuple, arguments : dict): + """ + Applies equation exchange crossover to a pair of objectives. + + This method facilitates the exploration of the search space by recombining equation structures. It swaps the 'structure' attributes of the two objective functions, effectively exchanging building blocks between them. This allows the evolutionary algorithm to explore new combinations of equation terms and potentially discover better-fitting models. + + Args: + objective (tuple): A tuple containing two objective objects, whose equation structures will be exchanged. + arguments (dict): A dictionary containing arguments for the operator and its sub-operators. + + Returns: + tuple: A tuple containing the two objective objects with their structures swapped, enabling the exploration of new equation combinations. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) objective[0].structure, objective[1].structure = objective[1].structure, objective[0].structure return objective[0], objective[1] def use_default_tags(self): + """ + Resets the operator's tags to the default set. + + This ensures the operator is correctly categorized with its fundamental properties. This is important for proper identification and selection of operators within the evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'gene level', 'contains suboperators', 'standard'} class TermParamCrossover(CompoundOperator): """ - The crossover exchange between parent terms with the same factor functions, that differ only in the factor parameters. - - Noteable attributes: - ----------- - params : dict - Inhereted from the Specific_Operator class. - Main key - 'proportion', value - proportion, in which the offsprings' parameter values are chosen. - - Methods: - ----------- - apply(population) - return the offspring terms, constructed as the parents' factors with parameter values, selected between the parents' ones. + Performs crossover by exchanging parameters between parent terms that share the same factor functions but differ in their parameter values. + + + Noteable attributes: + ----------- + params : dict + Inhereted from the Specific_Operator class. + Main key - 'proportion', value - proportion, in which the offsprings' parameter values are chosen. + + Methods: + ----------- + apply(population) + return the offspring terms, constructed as the parents' factors with parameter values, selected between the parents' ones. """ + key = 'TermParamCrossover' def apply(self, objective : tuple, arguments : dict): """ - Get the offspring terms, constructed as the parents' factors with parameter values, selected between the parents' ones. + Applies crossover to the parameters of two parent terms, creating two offspring terms with a blend of their parents' characteristics. - Attributes: - ------------ - term_1, term_2 : Term objects - The parent terms. + This method iterates through the tokens of the parent terms, adjusting the numerical parameter values of each token based on a defined proportion. + The goal is to explore the space of possible parameter combinations, potentially leading to improved equation discovery. - Returns: - ------------ - offspring_1, offspring_2 : Term objects - The offspring terms. + Args: + objective (tuple): A tuple containing two `Term` objects, representing the parent terms to be crossed over. + arguments (dict): A dictionary containing arguments required for the sub-operators. + Returns: + tuple: A tuple containing two `Term` objects, representing the offspring terms resulting from the crossover operation. """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -255,40 +417,60 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Uses a predefined set of tags to categorize the crossover operator. + + This method overwrites any existing tags with a predefined set of default tags. This ensures consistency and allows the system to quickly identify the operator's characteristics during the equation discovery process. By using a standard set of tags, the framework can efficiently filter and select appropriate operators based on the desired search criteria. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'term level', 'exploitation', 'no suboperators', 'standard'} class TermCrossover(CompoundOperator): """ - The crossover exchange between parent terms, done by complete exchange of terms. - - Noteable attributes: - ----------- - params : dict - Inhereted from the Specific_Operator class. - Main key - 'crossover_probability', value - probabilty of the term exchange. - - Methods: - ----------- - apply(population) - return the offspring terms, which are the same parents' ones, but in different order, if the crossover occured. - . - """ + Performs crossover by exchanging complete terms between parent equations. + + + Noteable attributes: + ----------- + params : dict + Inhereted from the Specific_Operator class. + Main key - 'crossover_probability', value - probabilty of the term exchange. + + Methods: + ----------- + apply(population) + return the offspring terms, which are the same parents' ones, but in different order, if the crossover occured. + . + """ + key = 'TermCrossover' def apply(self, objective : tuple, arguments : dict): """ - Get the offspring terms, which are the same parents' ones, but in different order, if the crossover occured. + Performs crossover between two terms based on a defined probability. - Attributes: - ------------ - term_1, term_2 : Term objects - The parent terms. - - Returns: - ------------ - offspring_1, offspring_2 : Term objects - The offspring terms. + This method determines whether to swap the order of two parent terms + to produce offspring. The decision is based on a crossover probability + and whether the parent terms share a common variable marker. + + Args: + objective (tuple): A tuple containing two Term objects (the parent terms). + arguments (dict): A dictionary containing arguments needed for the sub-operators. + Returns: + tuple: A tuple containing two Term objects (the offspring terms). The order + of the terms may be swapped depending on the crossover probability + and variable marker condition. + + Why: + This crossover operation helps explore different combinations of terms + within the equation search space, potentially leading to the discovery + of more accurate or parsimonious models. """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -299,10 +481,37 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Resets the operator's tags to the default set. This ensures that the operator is correctly categorized with its fundamental properties, such as being a crossover operator that operates at the term level, encouraging exploration, not using sub-operators and being a standard operator. This is important for maintaining consistency in the search process and ensuring that the evolutionary algorithm explores the space of possible operators effectively. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'term level', 'exploration', 'no suboperators', 'standard'} def get_basic_variation(variation_params : dict = {}): + """ + Creates a basic variation operator setup for equation discovery. + + This method initializes a Pareto Levels Crossover operator, configuring it with + sub-operators like term parameter crossover, term crossover, equation crossover, + metaparameter crossover, equation exchange crossover, and chromosome crossover. + These sub-operators are structured to explore diverse equation structures during + the evolutionary search process. The relationships and probabilities between these + sub-operators are carefully set to guide the variation process towards potentially + better-fitting equations. This setup facilitates the exploration of the equation + search space by combining different genetic operators at various levels of granularity. + + Args: + variation_params: A dictionary to store base parameters for operators. + + Returns: + ParetoLevelsCrossover: A ParetoLevelsCrossover operator configured with the basic variation setup. + """ # TODO: generalize initiation with test runs and simultaneous parameter and object initiation. add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = variation_params) diff --git a/epde/operators/singleobjective/ea_stop_criteria.py b/epde/operators/singleobjective/ea_stop_criteria.py index 55753469..26e866f6 100644 --- a/epde/operators/singleobjective/ea_stop_criteria.py +++ b/epde/operators/singleobjective/ea_stop_criteria.py @@ -10,43 +10,157 @@ class StopCondition(ABC): - ''' + """ + The `StopCondition` class provides a flexible mechanism for defining criteria to halt the equation discovery process. It allows users to specify conditions based on various metrics, such as the number of generations, the fitness score of the best equation, or a custom function. This enables fine-grained control over the search process and ensures that the algorithm terminates when satisfactory results are achieved or when further exploration is unlikely to yield significant improvements. + + + Abstract class for generalized stop condition for the evolutionary algorithm. + + Methods: + ---------- + + reset(): + Reset the object. Mainly useful, if some sort of counter is implemented inside the instance. Returns None. + + check() + + ''' + """ - Abstract class for generalized stop condition for the evolutionary algorithm. - - Methods: - ---------- - - reset(): - Reset the object. Mainly useful, if some sort of counter is implemented inside the instance. Returns None. - - check() - - ''' def __init__(self, estrategy): + """ + Initializes the StopCondition object with an evaluation strategy. + + The evaluation strategy determines when the search for differential equations should terminate. + This allows for flexible control over the trade-off between exploration and convergence + during the equation discovery process. + + Args: + estrategy: An object that implements a stopping condition strategy. + + Returns: + None + """ pass def reset(self): + """ + Resets the internal state of the stop condition. + + This ensures that the search for differential equations starts fresh, + allowing the evolutionary algorithm to explore the solution space + without being biased by previous iterations. + + Args: + self: The StopCondition instance. + + Returns: + None. + """ pass def check(self): + """ + Checks if the stopping criteria for the evolutionary algorithm have been met. + + This method determines whether the search for differential equations should be terminated based on predefined conditions. + + Args: + self: The object instance. + + Returns: + bool: True if the stopping criteria are met, False otherwise. + + Why: + This check is crucial for controlling the duration and efficiency of the equation discovery process. It prevents the algorithm from running indefinitely or prematurely terminating before finding a satisfactory solution. + """ pass class IterationLimit(StopCondition): + """ + A class to limit the number of iterations in a process. + + Class Methods: + - __init__ + - reset + - check + """ + def __init__(self, limit=100): + """ + Initializes the iteration limit for the evolutionary process. + + This method sets the maximum number of iterations allowed during the equation discovery process and initializes a counter to track the progress. Limiting iterations prevents the algorithm from running indefinitely and ensures timely convergence. + + Args: + limit (int, optional): The maximum number of iterations. Defaults to 100. + + Returns: + None + """ self.limit = limit + 1 self.checks = 0 def reset(self): + """ + Resets the check counter. + + This method resets the internal counter (`checks`) that tracks the number of checks + performed during the equation discovery process. This is useful for restarting or + managing the search within a defined computational budget. + + Args: + self: The object instance. + + Returns: + None. + """ self.checks = 0 def check(self): + """ + Checks if the iteration limit has been reached. + + This method is crucial for controlling the evolutionary search process, ensuring it terminates after a specified number of iterations to prevent excessive computation. It increments the internal counter and determines if the search has exceeded its allocated budget. + + Args: + self: The object instance. + + Returns: + bool: True if the iteration limit has been reached, False otherwise. + """ self.checks += 1 return self.checks > self.limit class FitnessStabilized(StopCondition): + """ + A fitness-stabilized evolutionary strategy. + + This class adapts the fitness function during evolution to stabilize + selection pressure and improve search efficiency. + + Attributes: + estrategy: The underlying evolutionary strategy. + """ + def __init__(self, estrategy): + """ + Initializes the FitnessStabilized object. + + This method must be overridden by subclasses to provide a concrete implementation for fitness evaluation within a stabilized evolutionary process. The initialization should set up the fitness evaluation strategy. + + Args: + estrategy: The fitness evaluation strategy to be used. + + Raises: + NotImplementedError: Always raised, indicating that the method must be implemented in a subclass. + + Returns: + None. + + Why: This abstract initialization ensures that each specific fitness stabilization technique defines its own setup, tailoring the fitness evaluation process to its particular needs within the broader equation discovery framework. + """ raise NotImplementedError diff --git a/epde/operators/singleobjective/mutations.py b/epde/operators/singleobjective/mutations.py index e8ac9ac1..4d6a1cae 100644 --- a/epde/operators/singleobjective/mutations.py +++ b/epde/operators/singleobjective/mutations.py @@ -23,6 +23,10 @@ class SystemMutation(CompoundOperator): + """ + Represents a mutation operator that applies sub-operators to an objective's equations and parameters. + """ + key = 'SystemMutation' def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best_individuals & worst individuals @@ -30,6 +34,23 @@ def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best altered_objective = deepcopy(objective) if objective.elite == 'immutable': + """ + Applies mutation to the objective's equations and parameters. + + This method takes an objective function representing a system of equations (SoEq) and a set of arguments for mutation sub-operators. + It selectively mutates the equations and parameters within the objective, creating diversity in the population of candidate solutions. + The mutation process involves iterating through the equation and parameter keys, applying the corresponding mutation sub-operator to each. + The mutated equations and parameters are then updated within a copy of the objective. + This ensures exploration of the search space by introducing variations in the equation structures and parameter values. + Finally, the objective's state is reset to ensure consistency. + + Args: + objective (SoEq): The objective function (system of equations) to be mutated. + arguments (dict): A dictionary containing arguments for the sub-operators, specifying mutation parameters. + + Returns: + SoEq: A mutated copy of the objective function. + """ return altered_objective eqs_keys = altered_objective.vals.equation_keys; params_keys = altered_objective.vals.params_keys @@ -52,14 +73,45 @@ def apply(self, objective : SoEq, arguments : dict): # TODO: add setter for best return altered_objective def use_default_tags(self): + """ + Applies a predefined set of tags to the object. + + This method resets the object's tags to a default configuration, ensuring consistency in identifying the type of operation performed. This is useful for categorizing and managing different operations within the system. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'mutation', 'chromosome level', 'contains suboperators', 'standard'} class EquationMutation(CompoundOperator): + """ + Applies mutation to the objective equation's structure. + + Attributes: + mutation_rate: The probability of applying a mutation to each term. + sub_operators: A list of mutation sub-operators to apply. + """ + key = 'EquationMutation' @HistoryExtender(f'\n -> mutating equation', 'ba') def apply(self, objective : Equation, arguments : dict): + """ + Applies mutation to the structure of the equation. + + Iterates through the mutable terms of the equation's structure, applying a mutation sub-operator to each term with a probability determined by the mutation rate. This process aims to explore the space of possible equation structures to find one that better fits the observed data. + + Args: + objective (Equation): The equation to be mutated. + arguments (dict): A dictionary of arguments for the operators. + + Returns: + Equation: The mutated equation. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) for term_idx in range(objective.n_immutable, len(objective.structure)): if np.random.uniform(0, 1) <= self.params['r_mutation']: @@ -68,13 +120,50 @@ def apply(self, objective : Equation, arguments : dict): return objective def use_default_tags(self): + """ + Applies a predefined set of tags to the equation mutation object. + + This method resets the object's tags to a default set, ensuring consistency + in identifying and categorizing equation mutations. This is useful for + standardizing the representation of mutations within the evolutionary + process, facilitating filtering and analysis based on predefined criteria. + + Args: + self: The EquationMutation object instance. + + Returns: + None. The method modifies the object's internal state by updating its tags. + """ self._tags = {'mutation', 'gene level', 'contains suboperators', 'standard'} class MetaparameterMutation(CompoundOperator): + """ + Represents a mutation operation that modifies a metaparameter. + + This class encapsulates the logic for altering a metaparameter's value + during an optimization or evolutionary process. + + Attributes: + parameter_name: The name of the metaparameter to be mutated. + mutation_range: The range within which the metaparameter can be mutated. + """ + key = 'MetaparameterMutation' def apply(self, objective : Union[int, float], arguments : dict): + """ + Applies a random normal perturbation to the objective value. + + This method introduces controlled noise to the objective value, simulating the inherent uncertainty and variability often encountered in real-world data and model evaluations. By adding a random value drawn from a normal distribution, parameterized by a mean and standard deviation, the method explores the solution space more robustly. This helps the evolutionary algorithm escape local optima and discover more generalizable equation structures. If the altered objective becomes negative, its absolute value is returned to maintain a valid objective range. + + Args: + objective (Union[int, float]): The objective value to be altered. + arguments (dict): A dictionary of arguments, including those for sub-operators. + + Returns: + float: The altered objective value after applying the random perturbation and ensuring it is non-negative. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) # print('objective', objective) @@ -86,6 +175,17 @@ def apply(self, objective : Union[int, float], arguments : dict): return altered_objective def use_default_tags(self): + """ + Applies a pre-defined set of tags to the mutation, ensuring consistency and adherence to established categories. + + This method overwrites any existing tags with a default set, providing a standardized categorization. This is useful for maintaining a consistent vocabulary across different mutation types, facilitating filtering and analysis within the evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'mutation', 'gene level', 'no suboperators', 'standard'} @@ -93,26 +193,22 @@ class TermMutation(CompoundOperator): """ Specific operator of the term mutation, where the term is replaced with a randomly created new one. """ + key = 'TermMutation' def apply(self, objective : tuple, arguments : dict): #term_idx, equation): """ - Return a new term, randomly created to be unique from other terms of this particular equation. + Return a new, randomly generated term to replace an existing one within an equation. - Parameters: - ----------- - term_idx : integer - The index of the mutating term in the equation. - - equation : Equation object - The equation object, in which the term is present. + This ensures diversity in the equation population during the evolutionary search process. The new term is created based on the equation's pool of available operators and variables, and it is checked for uniqueness to maintain the integrity of the equation structure. - Returns: - ---------- - new_term : Term object - A new, randomly created, term. - - """ + Args: + objective (tuple): A tuple containing the index of the term to be mutated and the equation object. + arguments (dict): A dictionary containing additional arguments (not directly used in this method, but potentially used in sub-methods). + + Returns: + Term: A new, randomly created term that is unique within the equation. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) create_derivs = bool(objective[1].structure[objective[0]].descr_variable_marker) @@ -129,6 +225,19 @@ def apply(self, objective : tuple, arguments : dict): #term_idx, equation): return new_term def use_default_tags(self): + """ + Resets the operator's tags to the default set. + + This ensures the operator is configured with a standard set of characteristics, + allowing it to be used in a general-purpose equation discovery process. + This is useful for ensuring a consistent starting point for different search strategies. + + Args: + self: The object instance. + + Returns: + None. This method modifies the object's tags in place. + """ self._tags = {'mutation', 'term level', 'exploration', 'no suboperators', 'standard'} @@ -136,6 +245,7 @@ class TermParameterMutation(CompoundOperator): """ Specific operator of the term mutation, where the term parameters are changed with a random increment. """ + key = 'TermParameterMutation' def apply(self, objective : tuple, arguments : dict): # term_idx, objective @@ -143,6 +253,18 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective Specific operator of the term mutation, where the term parameters are changed with a random increment. Parameters: + """ + Specific operator for refining equation terms by randomly adjusting their parameters. + + This method fine-tunes the parameters of a selected term within an equation to improve the overall fit and accuracy of the model. By introducing small, random increments to the parameters, the algorithm explores the solution space and seeks to minimize the error between the equation's predictions and the observed data. This process helps to discover the optimal parameter values that best describe the underlying dynamics of the system. + + Args: + objective (tuple): A tuple containing the index of the term to be mutated and the equation object. + arguments (dict): A dictionary containing additional arguments required for the mutation process. + + Returns: + Term: The modified term with updated parameter values. + """ ----------- term_idx : integer The index of the mutating term in the equation. @@ -204,6 +326,17 @@ def apply(self, objective : tuple, arguments : dict): # term_idx, objective return term def use_default_tags(self): + """ + Sets the tags to the default set. + + This method resets the current set of tags, ensuring the mutation operation is characterized by a standard set of properties. This is useful for reverting to a known state or ensuring consistency in the mutation process. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'mutation', 'term level', 'exploitation', 'no suboperators', 'standard'} # TODO: reorganize mutation and similar operators into the blocks of "common" operators. @@ -231,6 +364,32 @@ def get_multiobjective_mutation(mutation_params): # TODO: rename function calls def get_singleobjective_mutation(mutation_params): + """ + Creates and configures a mutation operator tailored for single-objective equation discovery. + + This method constructs a hierarchical mutation strategy, comprising chromosome, + equation, and term mutation components. This layered approach allows for + fine-grained control over the evolutionary process, enabling efficient + exploration of the equation space. The relationships between these components + are established to ensure coordinated mutation. + + Args: + mutation_params: A dictionary containing parameters for the mutation operators, + such as probabilities and mutation types. These parameters guide the + mutation process at each level of the hierarchy. + + Returns: + SystemMutation: A configured chromosome mutation operator, ready to be + integrated into the evolutionary algorithm. This operator orchestrates + the mutation of entire equation systems. + + Why: + This method is crucial for evolving populations of equation candidates. By + providing a structured mutation strategy, it facilitates the discovery of + equations that accurately describe the underlying dynamics of the data. + The hierarchical design allows for targeted exploration of different aspects + of the equation structure, improving the efficiency of the search process. + """ # TODO: generalize initiation with test runs and simultaneous parameter and object initiation. add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = mutation_params) diff --git a/epde/operators/singleobjective/selections.py b/epde/operators/singleobjective/selections.py index 39380b67..c804d7eb 100644 --- a/epde/operators/singleobjective/selections.py +++ b/epde/operators/singleobjective/selections.py @@ -16,8 +16,36 @@ class RouletteWheelSelection(CompoundOperator): + """ + Implements roulette wheel selection, a fitness-proportionate selection method. + + This class selects individuals from a population with probabilities proportional + to their fitness, simulating a roulette wheel where each individual occupies + a slot sized according to its fitness. + + Methods: + - apply + - use_default_tags + """ + key = 'RouletteWheelSelection' def apply(self, objective : Population, arguments: dict): + """ + Applies a selection operator to a population based on fitness. + + This method selects parent candidates from a population based on their fitness + scores, using a roulette wheel selection approach. It increments a counter for + each selected candidate. This selection mechanism favors individuals with better + fitness, increasing their chance to be chosen as parents for the next generation, + driving the evolutionary process towards identifying better equation structures. + + Args: + objective (Population): The population to select from. + arguments (dict): A dictionary of arguments for the selection process. + + Returns: + Population: The updated population with incremented counters for selected candidates. + """ # TODO: add docstring if isinstance(objective, ParetoLevels): raise TypeError('Tring to call method, implemented for Population class objects, to a ParetoLevels object. Must be a wrong type of evolution.') @@ -40,4 +68,15 @@ def apply(self, objective : Population, arguments: dict): return objective def use_default_tags(self): + """ + Applies a predefined set of tags to the object. + + This method resets the object's tags to a default configuration, ensuring consistency in how the object is categorized within the evolutionary process. This is useful for standardizing the object's role and properties, especially during initialization or after modifications that might have altered its tagging. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'selection', 'population level', 'auxilary', 'suboperators', 'standard'} \ No newline at end of file diff --git a/epde/operators/singleobjective/so_specific.py b/epde/operators/singleobjective/so_specific.py index b2dc54fd..54b293f2 100644 --- a/epde/operators/singleobjective/so_specific.py +++ b/epde/operators/singleobjective/so_specific.py @@ -5,21 +5,80 @@ from epde.optimizers.single_criterion.optimizer import Population class SizeRestriction(CompoundOperator): + """ + Restricts the size of an objective to a specified maximum. + """ + key = 'SizeRestriction' def apply(self, objective: Population, arguments: dict): + """ + Applies a selection operator to the population. + + Sorts the population based on fitness and truncates it to the desired length, ensuring the population size remains consistent. + Also, it records the fitness of the best individual in the history to track the evolutionary progress. This selection process refines the population, favoring individuals that better represent the underlying dynamics of the system being modeled. + + Args: + objective: The population to apply the selection to. + arguments: A dictionary of arguments for the operator. + + Returns: + The modified objective (population). + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) objective.population = objective.sort()[:objective.length] global_var.history.add([eq.fitness_value for eq in objective.population[0]][0]) return objective def use_default_tags(self): + """ + Applies a predefined set of tags to the object, ensuring consistency in categorization. + + This method overwrites any existing tags with a default set, providing a standardized + classification for objects within the system. This is important for maintaining a + uniform structure for objects that are subject to size restrictions, population levels, + lack suboperators, and are considered standard. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + _tags (set): A set containing the default tags: 'size restriction', 'population level', 'no suboperators', and 'standard'. + """ self._tags = {'size restriction', 'population level', 'no suboperators', 'standard'} class FractionElitism(CompoundOperator): + """ + Implements a fraction-based elitism strategy for evolutionary algorithms. + + This class ensures that a specified fraction of the best individuals + in a population are preserved in the next generation. + + Attributes: + fraction (float): The fraction of the population to be considered elite. + """ + key = 'FractionElitism' def apply(self, objective: Population, arguments: dict): + """ + Applies the elite strategy to the population, preserving the best solution found so far. + + This method sorts the population based on fitness and designates the + best individual as the elite, making it immutable to ensure that the best-performing equation + is retained throughout the evolutionary process. All other individuals + are marked as non-elite. This ensures that the evolutionary process does not lose the best equation found. + + Args: + objective: The Population object to which the elite strategy is applied. + arguments: A dictionary containing arguments for the sub-operators. + + Returns: + Population: The modified Population object with the elite strategy applied. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) objective.population = objective.sort() @@ -33,4 +92,18 @@ def apply(self, objective: Population, arguments: dict): @property def operator_tags(self): + """ + Returns a set of operator tags. + + This method returns a set of predefined tags associated with the operator. + These tags provide metadata about the operator's functionality and characteristics, + allowing the evolutionary algorithm to effectively explore the search space of possible equations + by categorizing and selecting operators based on their properties. + + Args: + self: The instance of the class. + + Returns: + set: A set containing string literals representing operator tags. + """ return {'elitism', 'population level', 'auxilary', 'no suboperators', 'standard'} \ No newline at end of file diff --git a/epde/operators/singleobjective/variation.py b/epde/operators/singleobjective/variation.py index 1ee5b61f..93f59c28 100644 --- a/epde/operators/singleobjective/variation.py +++ b/epde/operators/singleobjective/variation.py @@ -28,40 +28,34 @@ class PopulationLevelCrossover(CompoundOperator): """ - The crossover operator, combining parameter crossover for terms with same - factors but different parameters & full exchange of terms between the - completely different ones. + The crossover operator combines parameter crossover for terms with identical symbolic structures but differing parameters and exchanges entire terms between dissimilar equations. - Noteable attributes: - ----------- - suboperators : dict - Inhereted from the Specific_Operator class. - Suboperators, performing tasks of parent selection, parameter crossover, full terms crossover, calculation of weights for each terms & - fitness function calculation. Dictionary: keys - strings from 'Selection', 'Param_crossover', 'Term_crossover', 'Coeff_calc', 'Fitness_eval'. - values - corresponding operators (objects of Specific_Operator class). - - Methods: - ----------- - apply(population) - return the new population, created with the noted operators and containing both parent individuals and their offsprings. - copy_properties_to + Key attributes: + + * `suboperators`: A dictionary containing sub-operators for parent selection, parameter crossover, term crossover, coefficient calculation, and fitness evaluation. The keys are strings: 'Selection', 'Param\_crossover', 'Term\_crossover', 'Coeff\_calc', and 'Fitness\_eval'. The values are the corresponding operator objects. + + + Methods: + ----------- + apply(population) + return the new population, created with the noted operators and containing both parent individuals and their offsprings. + copy_properties_to """ + key = 'PopulationLevelCrossover' def apply(self, objective : Union[ParetoLevels, Population], arguments : dict): """ - Method to obtain a new population by selection of parent individuals (equations) and performing a crossover between them to get the offsprings. + Generates new candidate solutions by combining genetic material from selected parent equations through crossover. - Attributes: - ----------- - population : list of Equation objects - the population, to that the operator is applied; - - Returns: - ----------- - population : list of Equation objects - the new population, containing both parents and offsprings; + This method selects parent equations based on their `crossover_times` attribute, performs crossover using a sub-operator, and integrates the resulting offspring into the population. This process aims to explore the solution space more effectively by creating new equation structures that inherit characteristics from successful parents. + Args: + objective (Union[ParetoLevels, Population]): The population or Pareto level to which the crossover operator is applied. + arguments (dict): A dictionary containing arguments for the crossover operator and its sub-operators. + + Returns: + Union[ParetoLevels, Population]: The updated population or Pareto level, now including the offspring generated by crossover. """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -97,13 +91,59 @@ def apply(self, objective : Union[ParetoLevels, Population], arguments : dict): return objective def use_default_tags(self): + """ + Applies a predefined set of tags to categorize this crossover operator. + + This tagging helps in organizing and filtering operators based on their characteristics within the evolutionary process. + + Args: + self: The object instance. + + Returns: + None. + + Sets: + _tags (set): A set containing the default tags: 'crossover', 'population level', 'contains suboperators', and 'standard'. + """ self._tags = {'crossover', 'population level', 'contains suboperators', 'standard'} class ChromosomeCrossover(CompoundOperator): + """ + Applies crossover at the chromosome level using sub-operators. + + This class facilitates the crossover of genetic material between two parent + chromosomes to produce two offspring chromosomes. It utilizes sub-operators + to perform crossover at a finer level of granularity. + + Class Attributes: + - equation_crossover + - parameter_crossover + + Class Methods: + - apply: + """ + key = 'ChromosomeCrossover' def apply(self, objective : tuple, arguments : dict): + """ + Applies crossover between two objectives (parents) to create two offspring. + + This method facilitates the evolutionary search for optimal equation structures by + combining genetic material from two parent objectives. It performs crossover at both + the equation and parameter levels, leveraging sub-operators to generate new candidate + solutions. This process aims to explore the search space efficiently and discover + equation forms that better fit the observed data. + + Args: + objective (tuple): A tuple containing two objectives (parents) to be crossed over. + arguments (dict): A dictionary containing arguments for the crossover operation, + including arguments for sub-operators. + + Returns: + tuple: A tuple containing two objectives (offspring) resulting from the crossover. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) assert objective[0].vals.same_encoding(objective[1].vals) @@ -136,13 +176,45 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Applies a predefined set of tags to this operator. + + These tags categorize the operator based on its function and characteristics within the evolutionary process. This helps in managing and filtering operators based on their roles. + + Args: + self: The object instance. + + Returns: + None. + + Initializes: + _tags (set): A set containing default tags: 'crossover', 'chromosome level', 'contains suboperators', and 'standard'. + """ self._tags = {'crossover', 'chromosome level', 'contains suboperators', 'standard'} class MetaparamerCrossover(CompoundOperator): + """ + Applies crossover at the level of metaparameters. + + This class is designed to perform crossover operations on metaparameters, + facilitating the exploration of different configurations in optimization + processes. + """ + key = 'MetaparamerCrossover' def apply(self, objective : tuple, arguments : dict): + """ + Applies a blending crossover to the objective values, creating new solutions that inherit characteristics from both parents. This operation refines the search for optimal equation structures by exploring combinations of existing high-performing solutions. + + Args: + objective (tuple): A tuple containing two objective values representing the fitness of parent equations. + arguments (dict): A dictionary of arguments for the crossover operator, including parameters like `metaparam_proportion`. + + Returns: + tuple: A tuple containing two offspring values, representing the fitness of the newly generated equation structures. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) offspring_1 = objective[0] + self.params['metaparam_proportion'] * (objective[1] - objective[0]) @@ -150,14 +222,53 @@ def apply(self, objective : tuple, arguments : dict): return offspring_1, offspring_2 def use_default_tags(self): + """ + Uses a predefined set of tags to categorize the crossover operator. + + This method overwrites any existing tags, ensuring the operator is correctly identified + within the evolutionary process. This is important for filtering and selecting appropriate + operators during the search for differential equations. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'gene level', 'no suboperators'} class EquationCrossover(CompoundOperator): + """ + Applies equation crossover to a pair of equations. + + This crossover method combines aspects of both sub-operator crossover and + term crossover. It identifies similar terms in the equations and applies + sub-operators to crossover these terms. It also applies term crossover to + the remaining different terms, ensuring uniqueness of terms after crossover. + """ + key = 'EquationCrossover' @HistoryExtender(f'\n -> performing equation crossover', 'ba') def apply(self, objective : tuple, arguments : dict): + """ + Applies equation crossover to a pair of equation objects. + + This method aims to evolve better equation structures by exchanging genetic material + between two parent equations. It identifies and recombines similar and dissimilar + terms using specialized sub-operators, ensuring the resulting equations remain + syntactically valid and diverse. This process facilitates the exploration of the + equation search space, potentially leading to the discovery of more accurate or + parsimonious models. + + Args: + objective (tuple): A tuple containing two equation objects to be crossed over. + arguments (dict): A dictionary containing arguments for the sub-operators. + + Returns: + tuple: A tuple containing the two modified equation objects after crossover. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) equation1_terms, equation2_terms = detect_similar_terms(objective[0], objective[1]) @@ -182,53 +293,95 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Uses a predefined set of tags to categorize the equation crossover operator. + + This method resets the operator's tags to a default set, ensuring consistent categorization and facilitating its identification within the EPDE framework. This is useful for maintaining a standard set of characteristics for the operator. + + Args: + self: The EquationCrossover instance. + + Returns: + None. The method modifies the `_tags` attribute of the object in place. + """ self._tags = {'crossover', 'gene level', 'contains suboperators', 'standard'} class EquationExchangeCrossover(CompoundOperator): + """ + Applies equation exchange crossover to a pair of objectives. + + Class Methods: + - apply: Applies equation exchange crossover to a pair of objectives. + - use_default_tags: Uses the default set of tags for this object. + """ + key = 'EquationExchangeCrossover' @HistoryExtender(f'\n -> performing equation exchange crossover', 'ba') def apply(self, objective : tuple, arguments : dict): + """ + Applies equation exchange crossover to a pair of objectives. + + This method facilitates the exploration of the search space by recombining equation structures. It swaps the 'structure' attributes of the two objective functions, effectively exchanging building blocks between them. This allows the evolutionary algorithm to explore new combinations of equation terms and potentially discover better-fitting models. + + Args: + objective (tuple): A tuple containing two objective objects, whose equation structures will be exchanged. + arguments (dict): A dictionary containing arguments for the operator and its sub-operators. + + Returns: + tuple: A tuple containing the two objective objects with their structures swapped, enabling the exploration of new equation combinations. + """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) objective[0].structure, objective[1].structure = objective[1].structure, objective[0].structure return objective[0], objective[1] def use_default_tags(self): + """ + Uses a predefined set of tags to categorize the crossover operator. + + This method overwrites any existing tags, ensuring the operator is correctly identified within the equation discovery process. This is important for filtering and selecting appropriate operators during the evolutionary search. + + Args: + self: The EquationExchangeCrossover instance. + + Returns: + None. + """ self._tags = {'crossover', 'gene level', 'contains suboperators', 'standard'} class TermParamCrossover(CompoundOperator): """ - The crossover exchange between parent terms with the same factor functions, that differ only in the factor parameters. - - Noteable attributes: - ----------- - params : dict - Inhereted from the Specific_Operator class. - Main key - 'proportion', value - proportion, in which the offsprings' parameter values are chosen. - - Methods: - ----------- - apply(population) - return the offspring terms, constructed as the parents' factors with parameter values, selected between the parents' ones. + Performs crossover between parent terms that share the same factor functions but differ in their factor parameters. + + + Noteable attributes: + ----------- + params : dict + Inhereted from the Specific_Operator class. + Main key - 'proportion', value - proportion, in which the offsprings' parameter values are chosen. + + Methods: + ----------- + apply(population) + return the offspring terms, constructed as the parents' factors with parameter values, selected between the parents' ones. """ + key = 'TermParamCrossover' def apply(self, objective : tuple, arguments : dict): """ - Get the offspring terms, constructed as the parents' factors with parameter values, selected between the parents' ones. + Modifies the parameters of the parent terms' factors to create offspring terms. - Attributes: - ------------ - term_1, term_2 : Term objects - The parent terms. + This method iterates through the factors (tokens) of the parent terms, and for each parameter within those factors (excluding 'power' and 'dim'), it performs a weighted average of the parameter values from both parents. This process effectively blends the characteristics of the parent terms, generating new terms with potentially improved fitness. The proportion of each parent's parameter value that contributes to the offspring is determined by the `term_param_proportion` parameter. This parameter controls the balance between exploration and exploitation in the search for optimal equation structures. - Returns: - ------------ - offspring_1, offspring_2 : Term objects - The offspring terms. + Args: + objective (tuple): A tuple containing two `Term` objects, representing the parent terms to be crossed over. + arguments (dict): A dictionary containing additional arguments required for the crossover operation. + Returns: + tuple: A tuple containing two `Term` objects, representing the offspring terms generated by the crossover operation. """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -263,40 +416,57 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Applies a pre-defined set of tags to this operator. + + This method resets the operator's tags to a default configuration, ensuring consistency in its identification and categorization within the evolutionary process. This is useful for standardizing the operator's behavior and characteristics. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'term level', 'exploitation', 'no suboperators', 'standard'} class TermCrossover(CompoundOperator): """ - The crossover exchange between parent terms, done by complete exchange of terms. - - Noteable attributes: - ----------- - params : dict - Inhereted from the Specific_Operator class. - Main key - 'crossover_probability', value - probabilty of the term exchange. - - Methods: - ----------- - apply(population) - return the offspring terms, which are the same parents' ones, but in different order, if the crossover occured. - . + Performs crossover by exchanging complete terms between parent equations. + + + Noteable attributes: + ----------- + params : dict + Inhereted from the Specific_Operator class. + Main key - 'crossover_probability', value - probabilty of the term exchange. + + Methods: + ----------- + apply(population) + return the offspring terms, which are the same parents' ones, but in different order, if the crossover occured. + . """ + key = 'TermCrossover' def apply(self, objective : tuple, arguments : dict): """ - Get the offspring terms, which are the same parents' ones, but in different order, if the crossover occured. + Performs crossover between two terms based on a defined probability. - Attributes: - ------------ - term_1, term_2 : Term objects - The parent terms. - - Returns: - ------------ - offspring_1, offspring_2 : Term objects - The offspring terms. + This method determines whether to swap the order of two parent terms + to create offspring. The decision is based on a crossover probability + and a check to ensure that the descriptive variable markers of the + objectives are the same. This ensures that crossover only occurs + between compatible terms, maintaining the integrity of the equation + structure being evolved. + Args: + objective (tuple): A tuple containing two Term objects (the parents). + arguments (dict): A dictionary containing arguments needed for the sub-operators. + + Returns: + tuple: A tuple containing two Term objects (the offspring). The order + may be swapped depending on the crossover probability. """ self_args, subop_args = self.parse_suboperator_args(arguments = arguments) @@ -307,10 +477,39 @@ def apply(self, objective : tuple, arguments : dict): return objective[0], objective[1] def use_default_tags(self): + """ + Sets the tags to a predefined default set. + + This method resets the internal tag set to a predefined default, ensuring + consistency and adherence to standard configurations within the evolutionary + process. This is useful for initializing or resetting the search strategy + to a known state, promoting exploration of common equation structures. + + Args: + self: The object instance. + + Returns: + None. + """ self._tags = {'crossover', 'term level', 'exploration', 'no suboperators', 'standard'} def get_singleobjective_variation(variation_params : dict = {}): + """ + Creates and configures a population-level crossover operator for single-objective optimization. + + This method initializes and configures a set of crossover operators, including term parameter crossover, + term crossover, equation crossover, and chromosome crossover, before combining them into a population-level + crossover operator. It uses partial function application to inject base parameters into the operators. + This setup is crucial for effectively exploring the space of possible equation structures by recombining + different components of equations during the evolutionary search. + + Args: + variation_params: A dictionary to store variation parameters. + + Returns: + The configured PopulationLevelCrossover operator. + """ # TODO: generalize initiation with test runs and simultaneous parameter and object initiation. add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = variation_params) diff --git a/epde/operators/utils/default_parameter_loader.py b/epde/operators/utils/default_parameter_loader.py index 3e39f1d7..87706c2d 100644 --- a/epde/operators/utils/default_parameter_loader.py +++ b/epde/operators/utils/default_parameter_loader.py @@ -11,9 +11,37 @@ import warnings class ParamContainerMeta(type): + """ + Metaclass for managing instances of ParamContainer classes. + + This metaclass implements a singleton pattern for ParamContainer classes, + ensuring that only one instance of each ParamContainer subclass exists. + + Class Attributes: + - _container_instances + + Class Methods: + - __call__: + """ + _container_instances = {} def __call__(cls, *args, **kwargs): + """ + Ensures that only one instance of a parameter container class exists, centralizing parameter management for equation discovery. + + This method implements a singleton pattern, so that only one instance of each parameter container class is created. + If the container was already created - the method returns a link to the existing container. + Otherwise, it creates a new instance, stores it, and returns it. + + Args: + cls: The class to instantiate. + *args: Variable length argument list passed to the class constructor. + **kwargs: Arbitrary keyword arguments passed to the class constructor. + + Returns: + The singleton instance of the class. + """ if cls not in cls._container_instances: instance = super().__call__(*args, **kwargs) cls._container_instances[cls] = instance @@ -21,15 +49,46 @@ def __call__(cls, *args, **kwargs): return cls._container_instances[cls] def reset(self): + """ + Resets the container instances. + + This method clears the internal dictionary that stores container instances. + This ensures that subsequent calls to retrieve container instances will create new instances if necessary, + allowing for a fresh configuration or re-initialization of parameters. + + Args: + self: The object instance. + + Returns: + None. + """ self._container_instances = {} class EvolutionaryParams(metaclass = ParamContainerMeta): ''' Loading of default parameters. - Inspired by https://github.com/aimclub/FEDOT/blob/master/fedot/core/repository/default_params_repository.py + Inspired by https://github.com/aimclub/FEDOT/blob/master/fedot/core/repository/default_params_repository.py ''' + def __init__(self, parameter_file : str = None, mode : str = 'multi objective') -> None: + """ + Initializes the EvolutionaryParams class, loading parameter settings essential for configuring the evolutionary search process. + + This ensures that the evolutionary algorithm has the necessary information to explore the search space effectively. If no parameter file is provided, default configurations are loaded based on the specified optimization mode (single or multi-objective). + + Args: + parameter_file (str, optional): The path to a JSON file containing custom parameter settings. Defaults to None, in which case a default parameter file is loaded. + mode (str, optional): Specifies the optimization mode, either 'single objective' or 'multi objective'. Defaults to 'multi objective'. This influences which default parameter file is loaded. + + Returns: + None + + Class Fields Initialized: + mode (str): The optimization mode, either 'single objective' or 'multi objective'. + _repo_path (str): The full path to the parameter file within the repository. + _repo (dict): The initialized repository (loaded from the parameter file), containing the parameter settings for the evolutionary algorithm. + """ if parameter_file is None: if mode == 'single objective': parameter_file = 'default_parameters_single_objective.json' @@ -43,25 +102,91 @@ def __init__(self, parameter_file : str = None, mode : str = 'multi objective') self._repo = self._initialise_repo() def __enter__(self): + """ + Enters the context, making the `EvolutionaryParams` instance available for use within a `with` statement. This ensures that the parameter settings for the evolutionary equation discovery process are properly managed and scoped. + + Args: + None + + Returns: + self: The instance of the `EvolutionaryParams` class, allowing for chained operations within the context. + """ return self def __exit__(self, type, value, traceback): + """ + Exits the context manager. + + Releases the repository path to ensure resources are properly managed after equation discovery. This is crucial for maintaining a clean state and preventing potential conflicts in subsequent equation searches. + + Args: + type: The exception type, if any occurred within the context. + value: The exception value, if any. + traceback: The traceback, if any. + + Returns: + None. + """ self._repo_path = None # self._repo = None def _initialise_repo(self) -> dict: + """ + Initializes the parameter repository by loading its JSON file. + + This method reads the JSON file located at the repository path + and loads its content into a Python dictionary, making the + parameter settings accessible for configuring the evolutionary + search process. This ensures that the evolutionary algorithm + starts with a defined set of parameters. + + Args: + self: The object instance. + + Returns: + dict: A dictionary containing the data loaded from the + repository's JSON file, representing the initial + parameter configuration. + """ with open(self._repo_path) as repository_json_file: repository_json = json.load(repository_json_file) return repository_json def get_default_params_for_operator(self, operator_name : str) -> dict: + """ + Retrieves the default parameter set associated with a specified operator. + + This function is essential for configuring the evolutionary search process, ensuring that each operator + is initialized with a valid and potentially effective set of parameters. These parameters guide the + operator's behavior during the equation discovery process. + + Args: + operator_name (str): The unique identifier of the operator. + + Returns: + dict: A dictionary containing the default parameter values for the specified operator. + + Raises: + Exception: If no operator with the given name is found within the internal operator repository. + """ if operator_name in self._repo: return self._repo[operator_name] else: raise Exception(f'Operator with key {operator_name} is missing from the repo with params') def change_operator_param(self, operator_name : str, parameter_name : str, new_value): + """ + Changes the value of a parameter for a given operator in the repository. This allows fine-tuning of the evolutionary search process by modifying operator-specific settings. A warning is issued if the new value's type differs from the original parameter's type. + + Args: + operator_name (str): The name of the operator whose parameter needs to be changed. + parameter_name (str): The name of the parameter to be changed. + new_value: The new value for the parameter. + + Returns: + None. + """ if type(new_value) != type(self._repo[operator_name][parameter_name]): old_type = type(self._repo[operator_name][parameter_name]) new_type = type(new_value) diff --git a/epde/operators/utils/operator_mappers.py b/epde/operators/utils/operator_mappers.py index b0bf60f2..2b17e25e 100644 --- a/epde/operators/utils/operator_mappers.py +++ b/epde/operators/utils/operator_mappers.py @@ -14,7 +14,26 @@ from epde.operators.utils.template import OPERATOR_LEVELS, CompoundOperator class OperatorCondition(CompoundOperator): + """ + Wraps a compound operator and applies it only when a condition is met. + + Class Methods: + - __init__: + """ + def __init__(self, operator: CompoundOperator, condition: Callable = None): + """ + Initializes a ConditionalOperator. + + Wraps a compound operator and applies it only when a condition is met. This allows to construct more complex equation structures by enabling or disabling parts of the equation based on specific criteria, thus improving the equation discovery process. + + Args: + operator (CompoundOperator): The compound operator to be conditionally applied. + condition (Callable): A callable that determines whether the operator should be applied. + + Returns: + None + """ super().__init__() self._conditioned_operator = operator @@ -22,6 +41,21 @@ def __init__(self, operator: CompoundOperator, condition: Callable = None): self._tags = operator.operator_tags def apply(self, objective, arguments: dict): + """ + Applies a specific transformation to an objective based on a predefined condition and the nature of the transformation itself. + + This method orchestrates the application of a transformation to a given objective, but only if a specified condition is met. + The type of transformation (e.g., in-place modification or creation of a new object) is determined by the operator tags. + This ensures that the appropriate transformation is applied to the objective, facilitating the evolutionary search for the best equation structure. + If the '_tags' contain 'population level', the objective is returned without transformation. + + Args: + objective: The objective to be transformed. This could represent a candidate equation or a population of equations. + arguments: A dictionary containing arguments required by the transformation operator. + + Returns: + The transformed objective if the operator creates a new object or if the '_tags' contain 'population level'. Returns None if the operator modifies the objective in-place. + """ if self.condition(objective): if 'inplace' in self.operator_tags: self._conditioned_operator.apply(objective, arguments) @@ -35,8 +69,54 @@ def apply(self, objective, arguments: dict): class OperatorMapper(CompoundOperator): + """ + Maps operators based on specified conditions. + + This class facilitates the mapping of one operator onto elements of another, + allowing for conditional application based on objective and element criteria. + + Class Methods: + - __init__ + - apply + + Attributes: + _suboperators (dict): A dictionary storing sub-operators, with 'to_map' key holding the operator_to_map. + objective_condition (Callable): A callable that determines whether the operator should be applied to a specific objective. + element_condition (Callable): A callable that determines whether the operator should be applied to a specific element. + _tags (set): A set of tags associated with the operator, derived from the operator_to_map's tags, with the source_tag removed and the objective_tag added. + """ + def __init__(self, operator_to_map: CompoundOperator, objective_tag: str, source_tag: str, objective_condition: Callable = None, element_condition: Callable = None): + """ + Initializes the MapToElementsOperator. + + This operator prepares a given operator for application within a broader equation discovery process. It configures how the operator will be applied to specific elements of a larger expression, based on user-defined conditions. This setup is crucial for exploring the solution space of possible differential equations. + + Args: + operator_to_map: The operator to be mapped to the elements. + objective_tag: The tag to be assigned to the resulting operator. + source_tag: The tag identifying the elements to which the operator is mapped. + objective_condition: An optional callable that determines whether the operator + should be applied to a specific objective. Defaults to None. + element_condition: An optional callable that determines whether the operator + should be applied to a specific element. Defaults to None. + + Raises: + ValueError: If the source_tag is not present in the operator_to_map's tags. + + Returns: + None + + Class Fields: + _suboperators (dict): A dictionary storing sub-operators, with 'to_map' key holding the operator_to_map. + objective_condition (Callable): A callable that determines whether the operator should be applied to a specific objective. + element_condition (Callable): A callable that determines whether the operator should be applied to a specific element. + _tags (set): A set of tags associated with the operator, derived from the operator_to_map's tags, with the source_tag removed and the objective_tag added. + + Why: + This method sets up the operator for selective application to different parts of a potential equation, enabling the evolutionary search to explore diverse equation structures effectively. + """ super().__init__() self.set_suboperators({'to_map' : operator_to_map}) @@ -49,6 +129,29 @@ def __init__(self, operator_to_map: CompoundOperator, objective_tag: str, source self._tags.add(objective_tag) def apply(self, objective, arguments: dict): + """ + Applies a sub-operator to elements of the objective based on specified conditions and operator tags. + + This method facilitates the application of a sub-operator to each element of the objective + that satisfies a given element condition. The behavior adapts based on whether the operator + is marked as 'inplace' or 'standard'. For 'inplace' operators, the sub-operator directly + modifies the elements. For 'standard' operators, the sub-operator returns a new value that + replaces the original element. This mapping is essential for evolving and refining the + objective (e.g., a population of equations) by applying transformations defined by the + sub-operators. + + Args: + objective: The objective to be mapped over (e.g., a population of equations). + arguments: A dictionary of arguments to be passed to the sub-operator. + + Returns: + The modified objective if the operator tag contains 'population level'. Otherwise, returns None. + If the operator tag contains 'inplace', the original objective is modified directly. + If the operator tag contains 'standard', a new objective with modified elements is returned. + + Raises: + TypeError: If the operator tags do not include 'inplace' or 'standard'. + """ if self.objective_condition is None or self.objective_condition(objective): if 'inplace' in self.operator_tags: for elem in objective: @@ -67,6 +170,24 @@ def apply(self, objective, arguments: dict): def map_operator_between_levels(operator, original_level: Union[str, int], target_level: Union[str, int], objective_condition: Callable = None, element_condition: Callable = None) -> CompoundOperator: + """ + Maps an operator between two levels by sequentially refining it through a series of transformations. + + This method takes an initial operator and progressively adapts it to a target level of complexity. + It achieves this by applying a chain of `OperatorMapper` instances, each responsible for a single level transition. + This iterative refinement allows the operator to evolve, incorporating more detailed features and dependencies + as it moves closer to the target level. This is useful for gradually building up complex operators from simpler ones. + + Args: + operator: The initial operator to be mapped between levels. + original_level: The starting level for the operator mapping (int or str). + target_level: The desired level for the operator after mapping (int or str). + objective_condition: An optional callable specifying a condition for the objective. + element_condition: An optional callable specifying a condition for individual elements. + + Returns: + CompoundOperator: The resulting operator after mapping through the specified levels. + """ if isinstance(original_level, str): original_level = OPERATOR_LEVELS.index(original_level) if isinstance(target_level, str): target_level = OPERATOR_LEVELS.index(target_level) diff --git a/epde/operators/utils/template.py b/epde/operators/utils/template.py index 850b723c..64d095d9 100644 --- a/epde/operators/utils/template.py +++ b/epde/operators/utils/template.py @@ -18,24 +18,56 @@ 'chromosome level': Chromosome, 'population level': ParetoLevels} def add_base_param_to_operator(operator, target_dict): + """ + Adds necessary parameters to the operator to ensure its proper functioning within the evolutionary process. + + It iterates through the default parameters defined for the given operator type and + populates the operator's parameter dictionary. If a parameter key + is present in the provided `target_dict`, its value is used, allowing customization. + Otherwise, the predefined default value is applied, ensuring a complete parameter set. + This ensures that each operator has all the necessary parameters, using defaults where + custom values are not provided, which is crucial for the evolutionary algorithm to + explore the search space effectively. + + Args: + operator: The operator to which parameters are being added. + target_dict: A dictionary containing parameter values to override the defaults. + + Returns: + None. The operator's parameter dictionary is modified in place. + """ params_container = EvolutionaryParams() for param_key, param_value in params_container.get_default_params_for_operator(operator.key).items(): operator.params[param_key] = target_dict[param_key] if param_key in target_dict.keys( ) else param_value class CompoundOperator(): - ''' - Universal class for operator of an arbitrary purpose + """ + Represents a general operator with customizable functionality. + + + Attributes: + suboperators (`dict`): dictionary with name of suboperators and its argumetns + params (`dict`): dictionary with names and values of parameters for the operator + param_keys (`list`): names of parameters of the operator + level_index (`tuple`): abstraction level, to indicate which classes of objects the operator is applied to + operator_tags (`set`): log about operator + ''' + """ - Attributes: - suboperators (`dict`): dictionary with name of suboperators and its argumetns - params (`dict`): dictionary with names and values of parameters for the operator - param_keys (`list`): names of parameters of the operator - level_index (`tuple`): abstraction level, to indicate which classes of objects the operator is applied to - operator_tags (`set`): log about operator - ''' def __init__(self, param_keys: list = []): + """ + Initializes a compound operator, preparing it to manage parameters and sub-operators for equation discovery. + + This setup is crucial for organizing the components of a complex equation and managing their individual parameters during the evolutionary search process. By initializing the parameter keys and internal dictionaries, the compound operator ensures a structured environment for defining and manipulating equation structures. + + Args: + param_keys (list, optional): A list of parameter keys that this operator will manage. Defaults to an empty list. + + Returns: + None + """ self.param_keys = param_keys self._params = {} self._suboperators = {} @@ -44,10 +76,34 @@ def __init__(self, param_keys: list = []): @property def params(self): + """ + Returns the parameters of the compound operator. + + These parameters define the specific configuration of the operator, + allowing it to be adapted and optimized during the equation discovery process. + + Returns: + dict: The parameters of the compound operator. + """ return self._params @params.setter def params(self, param_dict: dict): + """ + Sets the operator's parameters, ensuring they align with the expected keys. This is crucial for configuring the operator's behavior within the equation discovery process. + + Args: + param_dict (dict): A dictionary containing the parameters to be set. The keys of this dictionary must match the expected parameter keys defined for the operator. + + Raises: + KeyError: If the keys in the input dictionary do not match the expected parameter keys. This ensures that the operator is configured correctly with the necessary parameters. + + Returns: + None: This method modifies the operator's internal state by updating its parameters. + + Class Fields: + _params (dict): A dictionary storing the parameters of the object. Initialized with the values from the input `param_dict`. + """ if set(self.param_keys) != set(param_dict.keys()): print('self.param_keys:', set(self.param_keys), ' param_dict.keys():', set(param_dict.keys())) @@ -56,16 +112,32 @@ def params(self, param_dict: dict): @property def suboperators(self): + """ + Returns the suboperators that constitute this compound operator. + + This property provides access to the individual operators that are combined to form the larger, more complex operator. + Accessing suboperators allows for inspection, manipulation, or analysis of the operator's structure. + + Returns: + list: The list of suboperators. + """ return self._suboperators def set_suboperators(self, operators: dict, probas: dict = {}): """ - Setting suboperators - + Sets the sub-operators that define the composite evolutionary operation. + + This method configures the building blocks of a more complex evolutionary step. + By combining simpler operators, EPDE can explore a wider range of equation structures. + Args: - operators (`dict`): dictionary with names and methods for evoluation of operators - probas (`dict`): dictionary with names of operators and them probability of execution - + operators (dict): A dictionary where keys are names (strings) and values are the corresponding sub-operators. + Sub-operators can be other CompoundOperators, lists, tuples, or dictionaries of operators, + allowing for hierarchical construction of evolutionary steps. + probas (dict, optional): A dictionary specifying the probabilities of execution for each sub-operator. + Keys are the names of the operators, and values are their corresponding probabilities. + Defaults to an empty dictionary, implying uniform probabilities. + Returns: None """ @@ -81,13 +153,15 @@ def set_suboperators(self, operators: dict, probas: dict = {}): def get_suboperator_args(self, personal=False): """ - Get arguments of the operator and its suboperators - + Collect the arguments required by this operator and its constituent sub-operators. + + This method aggregates the arguments needed for the current operator and recursively gathers arguments from all sub-operators within the compound structure. This is essential for understanding the complete set of inputs required to execute the entire computational graph represented by this operator. + Args: - personal (`boolean`): if True - gives unique arguments, default - True - + personal (`boolean`): If True, return only the arguments directly associated with this operator, without including those of its sub-operators. Defaults to False, which means arguments from sub-operators are included. + Returns: - args (`list`): arguments of the operator and its suboperators. + `set`: A set containing the names of all arguments required by this operator and, if `personal` is False, its sub-operators. """ args = self.arguments if not personal: @@ -102,6 +176,27 @@ def get_suboperator_args(self, personal=False): return args def _check_objective_type(method): + """ + Wraps a method to validate the type of the objective being processed by the operator. + + This decorator ensures that the objective passed to the decorated method + conforms to the expected type based on the operator's defined level. + This type checking is bypassed for 'custom level' operators, allowing + greater flexibility in objective types when needed. + + Args: + method: The method to be wrapped. + + Returns: + The wrapped method. + + Why: + This type checking ensures that operators at different levels + receive objectives of the correct type, maintaining consistency + and preventing unexpected errors during the equation discovery + process. It is important because different equation levels may + require different data types for processing. + """ @wraps def wrapper(self, *args, **kwargs): objective = args[0] @@ -125,14 +220,18 @@ def wrapper(self, *args, **kwargs): def parse_suboperator_args(self, arguments: dict): """ - Getting args of suboperators in the from of a dictionary - + Parses and distributes arguments to sub-operators. + + This method takes a dictionary of arguments and distributes them to the appropriate sub-operators based on their declared arguments. + It ensures that each sub-operator receives only the arguments it needs, facilitating modular execution and reducing potential conflicts. + Args: - arguments (`dict`): dictionary with names of operator's arguments and its values - + arguments (dict): A dictionary containing argument names and their corresponding values. + Returns: - `dict` with all parameters in one pile - `dict` with parameters separated by each operator + tuple[dict, dict]: A tuple containing two dictionaries: + - The first dictionary contains all arguments relevant to the compound operator and its sub-operators. + - The second dictionary is structured with sub-operator names as keys, and each key maps to a dictionary of arguments specific to that sub-operator. """ def parse_args(keys, args): return {key: args[key] for key in keys} @@ -146,12 +245,47 @@ def parse_args(keys, args): @_check_objective_type def apply(self, objective, arguments: dict): + """ + Applies the compound operator to the given objective by delegating to its sub-operators. + + This abstract method serves as a template for applying a sequence of operators. + It parses the arguments, separating those relevant to the compound operator itself + from those intended for its constituent sub-operators. This ensures that each + operator in the sequence receives the correct inputs for its specific transformation. + The base class implementation raises a NotImplementedError to enforce that concrete + subclasses define the specific application logic. + + Args: + objective: The objective to which the compound operator is applied. This represents + the current state of the equation being built or modified. + arguments (dict): A dictionary containing arguments for the compound operator and + its sub-operators. These arguments guide the behavior of the operators during + the equation discovery process. + + Returns: + NotImplementedError: Always raises a NotImplementedError, as the base class + implementation is abstract. Subclasses must implement the actual application logic. + """ self_args, subop_args = self.parse_suboperator_args( arguments=arguments) raise NotImplementedError('Trying to apply abstract superclass of the operator.') @property def level_index(self): + """ + Determines the precedence level of the operator within the equation discovery process. + + This method helps to establish the order in which operators are applied when constructing and evaluating equation candidates. + Operators with higher precedence levels are applied before those with lower levels, influencing the structure of the discovered equations. + + Args: + None + + Returns: + tuple: A tuple containing the index (representing precedence) and the level (a string describing the level) of the operator. + Returns (0, 'custom level') if the operator's level is not found in the OPERATOR_LEVELS list, + indicating a user-defined or less common operator precedence. + """ try: return [(idx, level) for idx, level in enumerate(OPERATOR_LEVELS) if level in self.operator_tags][0] @@ -159,23 +293,84 @@ def level_index(self): return (0, 'custom level') def use_default_tags(self): + """ + Resets the operator's tags to an empty set. + + This ensures that the operator relies on the default tagging behavior + defined within the system. This is useful when specific tags are + no longer relevant or when reverting to a standard configuration + for equation discovery. + + Args: + self: The CompoundOperator instance. + + Returns: + None. The method modifies the operator's tags in place. + """ self._tags = set() @property def operator_tags(self): + """ + Returns the tags associated with the operator. These tags provide metadata + about the operator, which can be used to filter and organize operators + during the equation discovery process. For example, tags might indicate + the type of operation (e.g., 'derivative', 'nonlinear') or the variables + involved. + + This information is valuable for guiding the search and selection of + equation structures within the EPDE framework. + + Returns: + list: A list of tags associated with the operator. + """ return self._tags @property def arguments(self): + """ + Returns the set of arguments used by the operator. + + This method is designed to provide a way to inspect the dependencies + or inputs required by this operator, which is essential for + understanding its role within a larger equation or model. + Since this is a base class, it returns an empty set. + + Args: + None + + Returns: + set: An empty set, as a `CompoundOperator` has no arguments by default. + """ return set() class SuboperatorContainer(): - def __init__(self, suboperators: dict = {}, probas: dict = {}): - ''' - Object, implemented to contain the suboperators of a CompoundOperator object. + """ + Object, implemented to contain the suboperators of a CompoundOperator object. Main purpose: support of uneven probabilities of similar suboperator application. - ''' + + Class Methods: + - __init__ + - __getitem__ + - keys + - __iter__ + """ + + def __init__(self, suboperators: dict = {}, probas: dict = {}): + """ + Object that manages a collection of sub-operators, assigning probabilities to each for selection during the compound operator's execution. This allows for a flexible and weighted application of different sub-operators within a larger operation. + + Args: + suboperators (dict, optional): A dictionary mapping labels to sub-operators (or lists/tuples/arrays of sub-operators). Defaults to {}. + probas (dict, optional): A dictionary mapping labels to probabilities for sub-operators. Defaults to {}. + + Raises: + ValueError: If the number of sub-operators for a given label does not match the number of defined probabilities for that label. + + Returns: + None + """ self.suboperators = suboperators self.probas = {} @@ -193,20 +388,86 @@ def __init__(self, suboperators: dict = {}, probas: dict = {}): self.probas[label] = operator_probability def __getitem__(self, item): + """ + Retrieves a sub-operator based on the given item, enabling the evolutionary search for optimal equation structures. + + If the sub-operator at the given index is a CompoundOperator, it is returned directly, preserving the hierarchical structure of the equation. + Otherwise, if it's a list, tuple, or NumPy array, a random element is chosen from it + based on the corresponding probability distribution. This introduces stochasticity, allowing the evolutionary algorithm to explore a wider range of equation candidates. + + Args: + item: The index or key of the sub-operator to retrieve. + + Returns: + The sub-operator at the given index, or a randomly chosen element from it. This element will be used in constructing and evaluating candidate equations. + """ if isinstance(self.suboperators[item], CompoundOperator): return self.suboperators[item] elif isinstance(self.suboperators[item], (list, tuple, np.ndarray)): return np.random.choice(a=self.suboperators[item], p=self.probas[item]) def keys(self): + """ + Returns a view of the names (keys) of the suboperators stored within this container. + + This allows iteration and inspection of available suboperators, which represent + individual components or terms within a larger equation or model being constructed. + Accessing keys is essential for managing and manipulating these suboperators during + the equation discovery process. + + Args: + self: The SuboperatorContainer instance. + + Returns: + dict_keys: A view object containing the names (keys) of the suboperators. + """ return self.suboperators.keys() def __iter__(self): + """ + Returns an iterator for the suboperators within this container. This allows to seamlessly traverse and process individual suboperators, which is essential for applying evolutionary algorithms and multi-objective optimization techniques to discover the best equation structures. + + Args: + None + + Returns: + SuboperatorContainerIterator: An iterator object for traversing the suboperators within this container. + """ return SuboperatorContainerIterator(self) class SuboperatorContainerIterator(object): + """ + An iterator for traversing suboperators within a container. + + This iterator flattens and iterates through a container's suboperators, + handling nested CompoundOperator instances and other iterable values. + + Attributes: + _suboperators: A list containing all suboperators extracted from the + container. CompoundOperator instances are appended directly, while + other values are extended (assumed to be iterable). + _idx: An integer representing the current index, initialized to 0. + """ + def __init__(self, container): + """ + Initializes the iterator with a container of suboperators. + + The constructor flattens the nested structure of suboperators within the + provided container, preparing them for sequential access. This flattening + ensures that all suboperators, regardless of their nesting level, can be + iterated over in a consistent manner, which is essential for the equation + discovery process where each operator needs to be evaluated and combined + efficiently. + + Args: + container: An object holding suboperators, expected to have a + 'suboperators' attribute which is a dictionary. + + Returns: + None + """ self._suboperators = [] for val in container.suboperators.values(): self._suboperators.append(val) if isinstance( @@ -214,6 +475,21 @@ def __init__(self, container): self._idx = 0 def __next__(self): + """ + Return the next suboperator. + + This iterator provides sequential access to the suboperators within the container. + It's used to traverse the equation structure, enabling the evolutionary algorithm to explore and modify different parts of the equation during the search process. + + Args: + self: The iterator instance. + + Returns: + The next suboperator in the sequence. + + Raises: + StopIteration: If all suboperators have been visited. This signals the end of the current equation's structure traversal. + """ if self._idx < len(self._suboperators): res = self._suboperators[self._idx] self._idx += 1 diff --git a/epde/optimizers/blocks.py b/epde/optimizers/blocks.py index 438f901d..b6500af4 100644 --- a/epde/optimizers/blocks.py +++ b/epde/optimizers/blocks.py @@ -13,31 +13,92 @@ class Block(ABC): """ - Base class, that is used to build other base classes - - Attributes: - _incoming (`list`): - _outgoing (`list`): - id_set (`boolean`): - initial (`boolean`): - applied (`boolean`): flag, that the block has been applied - terminal (`boolean`): The terminality marker of the evolutionary block: if ``True``, than the execution of - the LinkedBlocks will terminate after appying the block's operator. - combinator (`Callable`): Method to define, how the block performs the combination of inputs, passed from "upper" blocks + Base class for constructing equation components. + + + Attributes: + _incoming (`list`): + _outgoing (`list`): + id_set (`boolean`): + initial (`boolean`): + applied (`boolean`): flag, that the block has been applied + terminal (`boolean`): The terminality marker of the evolutionary block: if ``True``, than the execution of + the LinkedBlocks will terminate after appying the block's operator. + combinator (`Callable`): Method to define, how the block performs the combination of inputs, passed from "upper" blocks """ + def __init__(self, initial = False, terminal = False): + """ + Initializes a new block (state) within the equation discovery process. + + A block represents a potential component or term within a differential equation. + This constructor sets up the block's initial state, including its connections + to other blocks (incoming and outgoing transitions) and flags indicating its role + in the equation (initial, terminal). These flags are important for the evolutionary + algorithm to correctly construct and evaluate candidate equations. + + Args: + initial (bool): True if this block can be the starting point of an equation. + terminal (bool): True if this block can be the ending point of an equation. + + Returns: + None + + Fields: + _incoming (list): List of incoming transitions to this state. + _outgoing (list): List of outgoing transitions from this state. + id_set (bool): Flag indicating if the state ID has been assigned. + initial (bool): Flag indicating if the state is an initial state. + applied (bool): Flag indicating if the state has been applied. + terminal (bool): Flag indicating if the state is a terminal state. + """ self._incoming = []; self._outgoing = [] self.id_set = False; self.initial = initial self.applied = False; self.terminal = terminal def add_incoming(self, incoming): + """ + Adds an incoming connection to the block's list of incoming connections. + + This is crucial for tracking data dependencies and ensuring that the block receives the necessary inputs for its operations within the computational graph. + + Args: + incoming: The incoming connection to add. + + Returns: + None + """ self._incoming.append(incoming) def add_outgoing(self, outgoing): + """ + Adds a new outgoing connection to this block. + + This is crucial for establishing the flow of data and dependencies between blocks, + ensuring that the computational graph accurately reflects the relationships + defined by the discovered equations. + + Args: + outgoing: The outgoing connection to add. + + Returns: + None. + """ self._outgoing.append(outgoing) @property def op_id(self): + """ + Return the unique identifier for this processing block. + + The ID is lazily generated upon first access to ensure each block has a distinct identity for tracking and management within the equation discovery process. This ID is used to differentiate blocks during evolutionary operations and analysis. + + Args: + self: The Block instance. + + Returns: + int: The operation ID, a randomly generated integer if not previously set. + """ if not self.id_set: self._id = np.random.randint(0, 1e6) self.id_set = True @@ -45,17 +106,45 @@ def op_id(self): @property def available(self): + """ + Check if all incoming blocks have been processed. + + This check ensures that the current block is ready for further computations + by verifying that all its dependencies (incoming blocks) have been successfully applied. + + Returns: + bool: True if all incoming blocks have been applied, False otherwise. + """ return all([block.applied for block in self._incoming]) def apply(self, *args): + """ + Applies the block's operation. + + This method marks the block as processed within the equation discovery workflow. By setting the `applied` flag, it ensures that this block is not re-evaluated, contributing to the efficiency of the search process. + + Args: + *args: Variable length argument list (not directly used but kept for potential future extensions). + + Returns: + None. Sets the `applied` attribute to True, signaling that this block has been processed. + """ self.applied = True def set_input_combinator(self, combinator : Callable): """ - Method to define, how the block performs the combination of inputs, passed from "upper" - blocks. In most scenarios, this input will be a list of one element (from a single - upper block), thus the combinator shall be a (lambda) function to select the first - element of the list. + Sets the function to combine inputs from upstream blocks. + + This combinator is used to merge the outputs of the blocks connected to the current block. + It is essential for handling scenarios where a block receives inputs from multiple sources, + allowing the block to process the combined input effectively. + + Args: + combinator (Callable): A callable (e.g., a function or lambda expression) that + takes a list of inputs and returns a single combined output. + + Returns: + None """ self.combinator = combinator @@ -63,18 +152,36 @@ def set_input_combinator(self, combinator : Callable): class EvolutionaryBlock(Block): """ Class, that represents an evolutionary operator, placed into the analogue of the - "computational graph" of the iteration of evolutionary algorithm. + "computational graph" of the iteration of evolutionary algorithm. - Attributes: - _operator (`epde.operators.utils.template.Compound_Operator`): The evolutionary operator, contained by the block. - terminal (`boolean`): The terminality marker of the evolutionary block: if ``True``, than the execution of - the LinkedBlocks will terminate after appying the block's operator. - args_keys (`list`): names of arguments of `_opeartor` - op_id (`integer`): individual operator's id + Attributes: + _operator (`epde.operators.utils.template.Compound_Operator`): The evolutionary operator, contained by the block. + terminal (`boolean`): The terminality marker of the evolutionary block: if ``True``, than the execution of + the LinkedBlocks will terminate after appying the block's operator. + args_keys (`list`): names of arguments of `_opeartor` + op_id (`integer`): individual operator's id """ + def __init__(self, operator, parse_operator_args = None, terminal = False): #, initial = False """ Args: + """ + Initializes an evolutionary block with an operator and configures how the operator's arguments are parsed. + + This setup determines how the block interacts with the evolutionary process, defining the operation it performs and how it obtains necessary parameters. The configuration of argument parsing is crucial for adapting the operator to different stages of the evolutionary search. + + Args: + operator (`epde.operators.utils.template.Compound_Operator`): The evolutionary operator that this block will apply. + parse_operator_args (`str|tuple|list`): Specifies the method for obtaining arguments for the operator's `apply` method. + - If `None`, no arguments are passed. + - If a `tuple` or `list`, its elements (strings) are used as keys to retrieve arguments. + - If `'use inspect'`, the `inspect` library is used to determine the required arguments. + - If `'use operator attribute'`, arguments are taken from the operator's `arg_keys` attribute. + terminal (`bool`): Indicates whether this block is the final step in a sequence of operations. If `True`, the evolutionary process terminates after this block. + + Returns: + None + """ operator (`epde.operators.utils.template.Compound_Operator`): The evolutionary operator, contained by the block. parse_operator_args (`str|tuple|list`): Approach to getting additional arguments for operator.apply method. If ``None``, no arguments will be obtained, if tuple, then the elements of the tuple (of type ``str``) will be considered as the keys. Other options are 'use inspect' or 'use operator attribute'. @@ -105,9 +212,15 @@ class will try to take the arguments from the operator object. def check_integrity(self): """ - Checking block: - - that block-terminal does not have any output (attribute `_outgoing`) - - that block (not terminal) have any output (attribute `_outgoing`) + Verifies the block's output connections based on whether it's designated as a terminal block. This ensures that terminal blocks do not produce further outputs, while non-terminal blocks must have at least one output to propagate the computation forward. + + Args: + self: The instance of the EvolutionaryBlock. + + Returns: + None. Raises a ValueError if the block's output connections are inconsistent with its terminal status. + + Why: This check ensures that the computational graph defined by the blocks is valid, preventing terminal blocks from producing further outputs and ensuring that non-terminal blocks contribute to the overall computation. This is crucial for the correct evolution and evaluation of equation structures. """ if self.terminal and len(self._outgoing) > 0: raise ValueError('The block is set as the terminal, while it has some output') @@ -116,13 +229,18 @@ def check_integrity(self): def apply(self, EA_kwargs): """ - Applying `operator` with arguments in the evolution algorithm by now - + Applies the operator within the evolutionary algorithm using provided arguments. + + This step propagates information through the computational graph, combining the outputs + of incoming blocks according to the specified operator and arguments. This allows the algorithm + to explore different equation structures and parameter values. + Args: - EA_kwargs (`dict`): dictionary with names and values for parameters in the evolution algorithm - + EA_kwargs (dict): A dictionary containing the keyword arguments required by the operator. + These arguments define the specific parameters and settings used in the operator's application. + Returns: - None + None: The method updates the `output` attribute of the block with the result of the operator application. """ self.check_integrity() kwargs = {kwarg_key : EA_kwargs[kwarg_key] for kwarg_key in self.arg_keys} @@ -132,6 +250,20 @@ def apply(self, EA_kwargs): @property def op_id(self): + """ + Return the unique identifier of the evolutionary operator. + + This ID is crucial for tracking the operator's performance and lineage + throughout the evolutionary process. If an ID hasn't been assigned yet, + a new random integer ID is generated to ensure each operator can be + uniquely identified. + + Args: + self: The EvolutionaryBlock instance. + + Returns: + int: The operator ID. + """ if not self.id_set: self._id = np.random.randint(0, 1e6) #self._operator.op_id self.id_set = True @@ -142,34 +274,113 @@ class InputBlock(Block): """ Blocks with fucntionality running before each evolutionary step """ + def __init__(self, to_pass): + """ + Initializes the InputBlock with a specified initial value. + + This block serves as a starting point in the computational graph, + providing an initial value that can be passed to subsequent operations. + The `applied` flag is set to True, indicating that this block is always + considered to have been "applied" since it represents the initial input. + + Args: + to_pass (any): The initial value to be held by this block. This value + will be the output of this block. + + Returns: + None. + + Initializes: + output (any): The output value, initialized using the provided `to_pass` argument. + applied (bool): A boolean flag indicating that the block has been applied, initialized to True. + """ self.set_output(to_pass) self.applied = True super().__init__(initial = True) def add_incoming(self, incoming): + """ + Adds an incoming link to the operator. + + This method prevents establishing connections from preceding operations. + This is because this type of block is designed to be a starting point, + receiving external data rather than results from other operators. + + Args: + incoming: The incoming link to be added. + + Raises: + TypeError: Always raised, indicating that incoming links are not allowed. + + Returns: + None + """ raise TypeError('Objects of this type shall not have incoming links from other operators') def set_output(self, to_pass): + """ + Sets the output of this processing block. This value will be available to subsequent blocks in the processing chain. + + Args: + to_pass: The value to set as the output. This will typically be data that needs to be processed by subsequent blocks. + + Returns: + None. + + Why: This method allows the block to pass its processed data to the next block in a sequential manner, enabling a chain of operations to be performed. + """ self.output = to_pass @property def available(self): + """ + Indicates if the input data stream is ready for processing. + + This property always returns True, signifying that the input block is continuously ready to supply data. + This ensures that the equation discovery process can proceed without interruption, as the input data is always considered available. + + Returns: + bool: Always True, indicating continuous data availability. + """ return True class LinkedBlocks(object): """ - The sequence (not necessarily chain: divergencies can be present) of blocks with evolutionary - operators; it represents the modular and customizable structure of the evolutionary operator. - - Attributes: - blocks_labeled (`dict`): dictionary with names and arguments of operators - supress_structure_check (`boolean`): checking of structure - initial (`list`): keeping initialized evolution operators - output (`ndarray`): result from applying operator with evoluyion algorithm's arguments + A sequence of blocks with evolutionary operators, allowing for a modular and customizable evolutionary process. The structure supports complex relationships between blocks, enabling diverse evolutionary strategies. + + + Attributes: + blocks_labeled (`dict`): dictionary with names and arguments of operators + supress_structure_check (`boolean`): checking of structure + initial (`list`): keeping initialized evolution operators + output (`ndarray`): result from applying operator with evoluyion algorithm's arguments """ + def __init__(self, blocks_labeled : dict, suppress_structure_check : bool = False): + """ + Initializes the BlockProgram object, preparing it for execution by associating blocks and managing program state. + + The initialization process involves storing the blocks, setting the structure check flag, identifying initial blocks, and setting the terminated flag to False. + This setup is crucial for the subsequent execution and analysis of the program's structure. + + Args: + blocks_labeled (dict): A dictionary mapping block IDs to Block objects, representing the program's blocks. + suppress_structure_check (bool): A boolean indicating whether to suppress structure checks during execution. + + Returns: + None + + Class Fields Initialized: + blocks_labeled (dict): A dictionary mapping block IDs to Block objects, representing the program's blocks. + suppress_structure_check (bool): A boolean indicating whether to suppress structure checks during execution. + initial (list): A list of tuples, where each tuple contains the index and the Block object that is marked as the initial block. + terminated (bool): A boolean indicating whether the program has terminated, initialized to False. + + Raises: + NotImplementedError: If there are multiple initial blocks, as the current implementation only supports a single entry point. + """ self.blocks_labeled = blocks_labeled self.suppress_structure_check = suppress_structure_check self.initial = [(idx, block) for idx, block in enumerate(self.blocks_labeled.values()) if block.initial] @@ -178,17 +389,38 @@ def __init__(self, blocks_labeled : dict, suppress_structure_check : bool = Fals raise NotImplementedError('The ability to process the multiple initial blocks is not implemented') def reset_traversal_cond(self): + """ + Resets the 'applied' flag for all blocks, except InputBlocks, to prepare for a new equation discovery iteration. + + This method ensures that only InputBlocks retain their 'applied' status, allowing the algorithm to re-evaluate the contribution of other blocks in constructing potential equation candidates. By resetting the 'applied' flag, the search process can explore alternative equation structures and avoid being biased by previously evaluated combinations. + + Args: + self: The instance of the LinkedBlocks class. + + Returns: + None. + """ for block in self.blocks_labeled.values(): if not isinstance(block, InputBlock): block.applied = False def traversal(self, input_obj, EA_kwargs): - ''' - Sequential execution of the evolutionary algorithm's blocks. - - Args: - input_obj (`ndarray`): source data - EA_kwargs (`dict`): dictionary with names and values for parameters in the evolution algorithm - ''' + """ + Sequentially executes the linked blocks of the equation discovery pipeline. + + This method orchestrates the application of evolutionary algorithm blocks, + propagating data and control flow through the defined computational graph. + It continues until a terminal condition is met, effectively driving the + equation discovery process towards a potential solution. + + Args: + input_obj (`ndarray`): The initial data for equation discovery. + EA_kwargs (`dict`): Parameters for the evolutionary algorithm within the blocks. + + Returns: + None: The method updates the internal state of the LinkedBlocks instance, + specifically the `final_vertex` attribute, which stores the terminal + block containing the discovered equation. + """ self.reset_traversal_cond() self.initial[0][1].set_output(input_obj) @@ -211,4 +443,15 @@ def traversal(self, input_obj, EA_kwargs): @property def output(self): + """ + Returns the output of the final processing step in the linked sequence. + + This provides the final result after all transformations and operations have been applied. + + Args: + self: The object instance. + + Returns: + The output of the final vertex, representing the overall result of the processing pipeline. + """ return self.final_vertex.output \ No newline at end of file diff --git a/epde/optimizers/builder.py b/epde/optimizers/builder.py index 983a60df..906de1f6 100644 --- a/epde/optimizers/builder.py +++ b/epde/optimizers/builder.py @@ -13,9 +13,17 @@ def link(op1, op2): ''' - - Set the connection of operators in format op1 -> op2 - + Set the connection between two computational blocks, defining the data flow direction. + + This method establishes a directed connection from `op1` to `op2`, indicating that the output of `op1` serves as an input to `op2`. + This ensures proper data flow and dependency management within the computational graph. + + Args: + op1 (EvolutionaryBlock or InputBlock): The source computational block. + op2 (EvolutionaryBlock or InputBlock): The destination computational block. + + Returns: + None ''' assert isinstance(op1, (EvolutionaryBlock, InputBlock)) and isinstance(op2, (EvolutionaryBlock, InputBlock)), 'An operator is not defined as the Placed operator object' op1.add_outgoing(op2) @@ -24,26 +32,53 @@ def link(op1, op2): class StrategyBuilder(): """ - Class instance for building a strategy - - Attributes: - processer (`Strategy`): class strategy, that is used in this algorithm - blocks_labeled (`dict`): dictionary with operator blocks in evolutionary algorithm - blocks_connected (`dict`): dictionary with names of operators and list with boolean flags about "neighbours" (some operator before and some operator after) - reachable (`dict`): dictionary with operators, that definitely not a single node: key - operator's name, item - block's id - initial_label (`str`): name of operator, that is executed first in the queue of operators + Class for constructing an equation discovery strategy. + + + Attributes: + processer (`Strategy`): class strategy, that is used in this algorithm + blocks_labeled (`dict`): dictionary with operator blocks in evolutionary algorithm + blocks_connected (`dict`): dictionary with names of operators and list with boolean flags about "neighbours" (some operator before and some operator after) + reachable (`dict`): dictionary with operators, that definitely not a single node: key - operator's name, item - block's id + initial_label (`str`): name of operator, that is executed first in the queue of operators """ + def __init__(self, strategy_class = Strategy): + """ + Initializes the StrategyBuilder with a specific strategy. + + This constructor prepares the builder to create and manage solving strategies. + It sets the strategy class to be used and immediately resets the builder + to ensure a clean initial state with the provided strategy. + + Args: + strategy_class (class): The strategy class to use for solving. Defaults to Strategy. + + Returns: + None + + Class Fields: + strategy_class (class): The class used to instantiate the solving strategy. + strategy (object): An instance of the `strategy_class`, used for solving. + + The StrategyBuilder needs to know which strategy to use, so it can later produce specific solver instances. + The reset is called to ensure that the builder is in a consistent state before any solving begins. + """ self.strategy_class = strategy_class self.reset(strategy_class) def reset(self, strategy_class): """ - Setting initial parameters for evolutionary strategy - + Resets the internal state of the strategy builder with a new optimization strategy. + + This prepares the builder for constructing a new equation by clearing any previously + stored information about the equation's structure, such as labeled blocks, + connectivity, and reachability. This ensures a clean slate for the next equation + discovery process. + Args: - strategy_class (`Strategy`): class with choosing optimization strategy - + strategy_class (`Strategy`): The class defining the optimization strategy to be used. + Returns: None """ @@ -57,17 +92,33 @@ def reset(self, strategy_class): @property def processer(self): + """ + Provides access to the data processing component. + + This property allows retrieval of the configured data processor, + which is responsible for preparing the input data for the equation discovery process. + It is essential for ensuring that the data is in the correct format and scale + before being used to train and validate candidate equation models. + + Args: + None + + Returns: + The configured data processing component. + """ return self._processer def add_init_operator(self, operator_label : str = 'initial'): """ - Setting operator, that is started optimization - - Args: - operator_label (`str`): name of operator, that is started operator's quene + Adds an initial operator to the strategy, marking the starting point for the evolutionary search. - Returns: - None + This method designates a specific operator as the starting point for the optimization process. It initializes the necessary data structures to track reachable blocks and their connections, ensuring the evolutionary algorithm begins exploring the search space from the defined origin. + + Args: + operator_label (str): A unique identifier for the initial operator. Defaults to 'initial'. + + Returns: + None """ self.initial_label = operator_label new_block = InputBlock(to_pass = None) @@ -77,12 +128,19 @@ def add_init_operator(self, operator_label : str = 'initial'): def set_input_combinator(self, non_default : dict = dict()): """ - Setting callable method for each operator, that performs a combination operation with input data - + Sets the method for combining inputs for each processing block. + + This method configures how each block within the strategy combines its inputs. + If a custom combination method is not provided for a specific block, a default method + that selects the first input is used. This ensures that each block has a defined + input combination strategy, which is crucial for the overall data processing pipeline + within the equation discovery process. + Args: - non_default (`dict`): dictionary with callable methods. - key - name of operator, value - callable method for combination input data by the operator - + non_default (dict): A dictionary where keys are block names and values are callable + methods that define how to combine inputs for the corresponding block. If a block's + name is not present in this dictionary, a default input combination method is used. + Returns: None """ @@ -97,17 +155,18 @@ def set_input_combinator(self, non_default : dict = dict()): def add_operator(self, operator_label, operator, parse_operator_args = 'inspect', terminal_operator : bool = False): """ - Adding new evolutionary operator - - Args: - operator_label (`str`): name of new operator - operator (`Block`): directly the operator himself - parse_operator_args (`str`): - terminal_operator (`boolean`): The terminality marker of the evolutionary block: if ``True``, than the execution of - the LinkedBlocks will terminate after appying the block's operator. + Adds a new evolutionary operator to the strategy. - Return: - None + This method registers a new operator within the strategy builder, associating it with a label and defining its behavior within the evolutionary process. The operator is encapsulated within an `EvolutionaryBlock`, which manages its execution and argument parsing. The `terminal_operator` flag determines whether applying this operator concludes a sequence of operations. This registration allows the strategy to utilize the operator during the evolutionary search for optimal equation structures. + + Args: + operator_label (`str`): A unique name for the operator. + operator (`Block`): The operator itself, an instance of `epde.operators.utils.template.Compound_Operator`. + parse_operator_args (`str`): Specifies how arguments for the operator's `apply` method should be parsed. Options include 'inspect' (using the `inspect` library), 'use operator attribute' (retrieving arguments from the operator's `arg_keys` attribute), or a list/tuple of argument names. Defaults to 'inspect'. + terminal_operator (`bool`): Indicates whether applying this operator should terminate the current sequence of operations. Defaults to `False`. + + Returns: + None """ new_block = EvolutionaryBlock(operator, parse_operator_args = parse_operator_args, terminal=terminal_operator) @@ -117,12 +176,18 @@ def add_operator(self, operator_label, operator, parse_operator_args = 'inspect' def link(self, label_out, label_in): """ - Create connect between operators (the result of one goes to the input of the other) - + Connects two operators, directing the output of one to the input of the other. + + This establishes a data flow dependency between the specified operators, + allowing the system to chain operations and build more complex processing pipelines. + This is a crucial step in constructing a computational graph where the output of one + operator serves as the input for another, enabling the system to perform sequential + data transformations and analysis. + Args: - label_out (`str`): name of operator, which goes to the entrance of the second - label_in (`str`): name of operator, which takes the result of the first - + label_out (str): The label of the operator whose output will be used as input. + label_in (str): The label of the operator that will receive the output as input. + Returns: None """ @@ -132,24 +197,46 @@ def link(self, label_out, label_in): self.blocks_connected[label_in][0] = True def check_connectedness(self): + """ + Checks if all labeled blocks are reachable from the initial block. + + This ensures that the discovered equation structure is based on a cohesive and interconnected set of relationships within the data. + + Args: + self: The StrategyBuilder instance. + + Returns: + bool: True if the number of labeled blocks equals the number of blocks reachable from the initial block, False otherwise. + """ return len(self.blocks_labeled) == len(self.reachable[self.initial_label]) def assemble(self, suppress_structure_check = False): + """ + Assembles the linked blocks into a cohesive structure using the configured processor. + + This process prepares the discovered equation components for further analysis and refinement. + + Args: + suppress_structure_check: Whether to bypass structural validation during linked block creation. + + Returns: + None. + """ self._processer.create_linked_blocks(blocks = self.blocks_labeled, suppress_structure_check = suppress_structure_check) def add_sequential_operators(builder : StrategyBuilder, operators : list): """ - Adding to builder operators and create link between them - - Args: - builder (`SectorProcesserBuilder`): MOEADD evolutionary strategy builder (sector processer), which will contain added operators. - operators (`list`): Operators, which will be added into the processer. The elements of the list shall be tuple in - format of (label, operator), where the label is str (e.g. 'selection'), while the operator is - an object of subclass of CompoundOperator. - - Returns: - builder (`OperatorBuilder`): Modified builder. + Adds a sequence of operators to the strategy builder, linking them together to form a processing pipeline. + + This method streamlines the construction of an evolutionary strategy by sequentially adding and connecting operators. It initializes the pipeline with a starting operator, then iterates through the provided operators, adding each one and linking it to the previous one. Finally, it assembles the complete pipeline. + + Args: + builder (`StrategyBuilder`): The strategy builder to which the operators will be added. + operators (`list`): A list of operators to be added sequentially. Each element should be a tuple containing a label (str) and an operator (a subclass of `CompoundOperator`). + + Returns: + `StrategyBuilder`: The modified strategy builder with the added and linked operators. """ builder.add_init_operator('initial') for idx, operator in enumerate(operators): @@ -166,19 +253,68 @@ def add_sequential_operators(builder : StrategyBuilder, operators : list): class OptimizationPatternDirector(object): """ - Base class of director for optimization's strategy - - Attributes: - builder (`StrategyBuilder`) + Base class for directing the optimization process, defining the overall strategy and workflow. + + + Attributes: + builder (`StrategyBuilder`) """ + def __init__(self): + """ + Initializes the OptimizationPatternDirector. + + This director orchestrates the construction of optimization patterns + by utilizing a builder object. The initialization process sets up + the director with a clean slate, ready to receive a builder and + begin the pattern construction. + + Args: + self: The OptimizationPatternDirector instance. + + Returns: + None. + + Why: + This initialization ensures that the director is ready to + construct a new optimization pattern. The director needs to be + initialized before it can work with a builder to create a + specific optimization strategy. + """ self._builder = None @property def builder(self): + """ + Returns the builder associated with this object. + The builder is responsible for constructing the optimization pattern. + + Returns: + The builder object. + + Why: + The builder encapsulates the logic for creating complex optimization patterns, + allowing for a modular and configurable approach to pattern construction. + """ return self._builder @builder.setter def builder(self, sector_processer_builder : StrategyBuilder): + """ + Sets the strategy builder for constructing sector processors. + + This method configures the internal builder responsible for creating sector processors, + allowing the director to utilize specific construction strategies. + + Args: + sector_processer_builder (StrategyBuilder): The builder to use for creating sector processors. + + Returns: + None. + + Why: + This allows customization of how sector processors are created, enabling the director + to adapt to different equation discovery approaches and optimization techniques. + """ print(f'setting builder with {sector_processer_builder}') self._builder = sector_processer_builder \ No newline at end of file diff --git a/epde/optimizers/moeadd/moeadd.py b/epde/optimizers/moeadd/moeadd.py index 25d0c73b..04d5662d 100644 --- a/epde/optimizers/moeadd/moeadd.py +++ b/epde/optimizers/moeadd/moeadd.py @@ -11,6 +11,20 @@ from typing import Union, List def flatten_chain(matrix): + """ + Flattens a 2D matrix (list of lists) into a 1D list. + + This operation is crucial for preparing data structures + used in the equation discovery process, where a flattened + representation is often required for efficient processing + by evolutionary algorithms and other optimization techniques. + + Args: + matrix (list[list]): A 2D list (list of lists) that needs to be flattened. + + Returns: + list: A 1D list containing all elements from the input matrix. + """ return list(chain.from_iterable(matrix)) # from copy import deepcopy @@ -26,42 +40,56 @@ def flatten_chain(matrix): from scipy.spatial import ConvexHull def clear_list_of_lists(inp_list) -> list: - ''' - Delete elements-lists with len(0) from the list - ''' + """ + Removes empty lists from a list of lists. + + This function is used to clean up the equation structures during the evolutionary process, + ensuring that only valid and meaningful equations are considered for further evaluation and optimization. + Empty lists can arise from various genetic operations or simplification steps, and removing them + helps to maintain the integrity and efficiency of the search. + + Args: + inp_list (list): A list of lists. + + Returns: + list: A new list containing only the lists from the input list that have a length greater than zero. + """ return [elem for elem in inp_list if len(elem) > 0] class ParetoLevels(object): ''' - The representation of Pareto levels, comprised of a finite number of objects in the - objective function space. Introduced to be used in methods of the moeadd.optimizer class + objective function space. Introduced to be used in methods of the moeadd.optimizer class - Attributes: - population (`list`): List with the elements - canidate solutions of the case-specific subclass of - ``src.moeadd.moeadd_solution_template.MOEADDSolution``. - levels (`list`): List with the elements - lists of solutions, representing non-dominated levels. - The 0-th element - the current Pareto frontier. - unplaced_candidates (`list`): candidates, that dont using in structure - _sorting_method (`callable`): The method of population separation into non-dominated levels. - _update_method (`callable`): The method of point addition into the population and onto the non-dominated levels. - - Notes: - The initialization of objects of this class is held automatically in the __init__ of - moeadd optimizer, thus no extra interactions of a user with this class are necessary. + Attributes: + population (`list`): List with the elements - canidate solutions of the case-specific subclass of + ``src.moeadd.moeadd_solution_template.MOEADDSolution``. + levels (`list`): List with the elements - lists of solutions, representing non-dominated levels. + The 0-th element - the current Pareto frontier. + unplaced_candidates (`list`): candidates, that dont using in structure + _sorting_method (`callable`): The method of population separation into non-dominated levels. + _update_method (`callable`): The method of point addition into the population and onto the non-dominated levels. + Notes: + The initialization of objects of this class is held automatically in the __init__ of + moeadd optimizer, thus no extra interactions of a user with this class are necessary. ''' + def __init__(self, population, sorting_method = fast_non_dominated_sorting, update_method = ndl_update, initial_sort = False): """ Args: - population (`list`): List with the elements - canidate solutions of the case-specific subclass of - ``src.moeadd.moeadd_solution_template.MOEADDSolution``. - sorting_method (`callable`): optional, default - ``src.moeadd.moeadd_supplementary.fast_non_dominated_sorting`` - The method of population separation into non-dominated levels - update_method (`callable`): optional, defalut - ``src.moeadd.moeadd_supplementary.ndl_update`` - The method of point addition into the population and onto the non-dominated levels. + population (`list`): List of candidate solutions, each representing a potential differential equation model. + sorting_method (`callable`): Method used to rank solutions based on their non-domination level. Defaults to fast non-dominated sorting. + update_method (`callable`): Method for adding new solutions to the Pareto levels. Defaults to NDL update. + initial_sort (`bool`): Flag indicating whether to perform an initial sorting of the population. Defaults to False. + + Returns: + None + + Why: + This class organizes a population of candidate differential equation models into Pareto levels based on their performance metrics. The sorting and update methods allow for efficient management and evolution of these levels during the equation discovery process. """ self._sorting_method = sorting_method self.population = [] @@ -70,6 +98,23 @@ def __init__(self, population, sorting_method = fast_non_dominated_sorting, self.history = set() def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Manually reconstructs a specific attribute of the object. + + This method facilitates the restoration of object attributes, primarily the 'population', from a serialized state. It ensures that the population is correctly initialized with `SoEq` objects, populated with their respective data, to maintain the state of discovered equations. This is crucial when loading previously discovered equation sets. After reconstruction, the levels are sorted to reflect the updated population. + + Args: + attribute (str): The attribute to reconstruct. Currently, only 'population' is supported. + value (list): The data used to reconstruct the attribute. For 'population', it's a list of dictionaries, each representing an equation. + except_attrs (dict): A dictionary of attributes to exclude during the reconstruction. + + Returns: + None. The method modifies the object's state in place. + + Class Fields (initialized or modified): + population (list): A list of `SoEq` objects representing the population. Initialized or overwritten during 'population' attribute reconstruction. + levels (list): A sorted list of levels, updated by the `sort` method. + """ from epde.loader import attrs_from_dict supported_attrs = ['population'] if attribute not in supported_attrs: @@ -84,26 +129,104 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): self.levels = self.sort() def attrs_from_dict(self, attributes, except_keys = ['obj_type']): + """ + Populates the object's attributes from a dictionary, excluding specified keys. + + This method streamlines the assignment of attributes to the object + by directly mapping dictionary key-value pairs to the object's + internal dictionary, excluding any keys specified in `except_keys`. + This ensures that the object's state is efficiently updated based on + the provided data, which is essential for representing different + Pareto levels and their corresponding equation structures. + + Args: + attributes (dict): A dictionary containing attribute names and their values. + except_keys (list, optional): A list of keys to exclude from being set as attributes. + Defaults to ['obj_type']. + + Returns: + None: This method modifies the object's attributes in place. + """ self.__dict__ = {key : item for key, item in attributes.items() if key not in except_keys} @property def levels(self): + """ + Returns the Pareto levels of the solutions. + + These levels represent the non-dominated sets of solutions at each stage of the Pareto optimization process. + They are used to assess the trade-offs between different objectives and to guide the search towards the Pareto front. + + Returns: + list: The Pareto levels, where each level contains a set of non-dominated solutions. + """ return self._levels @levels.setter def levels(self, value : list): + """ + Sets the levels to define the Pareto front. These levels determine the different sets of non-dominated solutions. + + Args: + value (list): A list representing the levels. + + Returns: + None + + Initializes: + _levels (list): The levels of the object. + + Why: + This method is essential for defining the Pareto front, which is a crucial step in multi-objective optimization. By setting the levels, we can identify different sets of non-dominated solutions, allowing for a more comprehensive analysis of the trade-offs between objectives. + """ self._levels = value def __len__(self): + """ + Returns the number of individuals currently present in the population. + + This reflects the size of the population being evolved and optimized + to discover governing differential equations. + + Args: + self: The ParetoLevels instance. + + Returns: + int: The number of individuals in the population. + """ return len(self.population) def __iter__(self): + """ + Returns an iterator for traversing the Pareto levels. + + This allows iterating through the discovered equation structures, + organized by their Pareto dominance, to explore the trade-offs + between different model complexities and accuracies. + + Args: + None + + Returns: + ParetoLevelsIterator: An iterator that yields Pareto levels, + each containing a set of non-dominated equations. + """ return ParetoLevelsIterator(self) def initial_placing(self): """ - Method for adding candidates into the structure who were not previously in it + Places initial candidates into the population for the Pareto optimization process. + + This method moves all candidates from the unplaced pool to the main population, + preparing them for the initial sorting into Pareto levels. This ensures that + all provided initial guesses are considered during the equation discovery process. + + Args: + None + + Returns: + None """ while self._unplaced_candidates: self.population.append(self._unplaced_candidates.pop()) @@ -115,28 +238,63 @@ def initial_placing(self): def sort(self): """ - Sorting of the population into Pareto non-dominated levels. + Sorts the population into Pareto non-dominated levels to identify a diverse set of equation candidates that represent different trade-offs between accuracy and complexity. This ensures that the evolutionary process explores a wide range of potential solutions. + + Args: + self: The ParetoLevels object containing the population to be sorted. + + Returns: + A list of Pareto levels, where each level contains a set of non-dominated individuals (equations). """ # self.levels = self._sorting_method(self.population) return self._sorting_method(self.population) @property def unplaced_candidates(self): + """ + Returns the set of unplaced candidates. + + This property provides access to the candidates that haven't been assigned yet, + allowing the algorithm to efficiently explore the search space of possible equation structures. + These candidates represent potential terms or components that can be added to the equation. + + Args: + self: The object instance. + + Returns: + set: The set of unplaced candidates. + """ return self._unplaced_candidates @unplaced_candidates.setter def unplaced_candidates(self, candidates : Union[list, set, tuple]): + """ + Initializes the set of candidates that have not yet been assigned to a Pareto level. + + This initialization is crucial for the algorithm to keep track of which candidates still need to be evaluated and assigned during the Pareto front construction process. + + Args: + candidates (Union[list, set, tuple]): A collection of candidates that have not yet been placed on any Pareto level. + + Returns: + None + """ # ADD EXTRA CHECKS IF NECESSARY self._unplaced_candidates = candidates def update(self, point): """ - Addition of a candidate solution point into the pareto levels and the population list. - + Adds a candidate solution to the Pareto levels and population. + + This method integrates a new solution into the existing set of non-dominated solutions, + refining the approximation of the Pareto front. By maintaining a diverse and well-distributed + set of solutions, it contributes to the exploration of the solution space and the + identification of optimal trade-offs between conflicting objectives. + Args: - point (`MOEADDSolution`): The point, added into the candidate solutions pool. - + point (`MOEADDSolution`): The candidate solution to be added. + Returns: None """ @@ -145,13 +303,15 @@ def update(self, point): def delete_point(self, point): """ - Deletion of a candidate solution point from the pareto levels and the population list. + Deletes a candidate solution from the Pareto levels and population. - Args: - point (`MOEADDSolution`): The point, removed from the candidate solutions pool. - - Returns: - None + This ensures that the Pareto levels and population accurately reflect the current set of non-dominated solutions after a candidate has been processed or determined to be unsuitable. This is important for maintaining the integrity of the Pareto front approximation during the evolutionary search process. + + Args: + point (`MOEADDSolution`): The solution to be removed. + + Returns: + None """ new_levels = [] history = [] @@ -181,12 +341,32 @@ def delete_point(self, point): self.population = population_cleared def get_stats(self): + """ + Collect objective function values from all discovered equations. + This is done to evaluate and compare the performance + of different equation candidates found at various levels of the search process. + + Returns: + np.ndarray: A NumPy array containing the objective function values + from all equation candidates across all levels. + """ return np.array(flatten_chain([[element.obj_fun for element in level] for level in self.levels])) def fit_convex_hull(self): """ - + Fits a convex hull to the Pareto front of the population. + + This method calculates the convex hull of the objective function values of the solutions in the population. + It is used to approximate the Pareto front, which represents the set of non-dominated solutions. + The convex hull provides a computationally efficient way to estimate the extent and shape of the Pareto front, + facilitating analysis and visualization of the trade-offs between different objectives. + + Args: + None + + Returns: + None """ if len(self.levels) > 1: warnings.warn('Algorithm has not converged to a single Pareto level yet!') @@ -198,13 +378,16 @@ def fit_convex_hull(self): def get_by_complexity(self, complexity): """ - Method for getting solutions with choosing complexity - + Retrieves solutions that satisfy a specific complexity requirement. + + This method filters the solutions within the first Pareto level to identify those that align with the given complexity. + This is useful for exploring the trade-offs between model complexity and accuracy, allowing users to select solutions that are both parsimonious and well-fitting. + Args: - complexity (`int`): number indicating the complexity of the solution - + complexity (int): The desired complexity level of the solutions. + Returns: - matching_solutions (`list`): solutions with input complexity + list: A list of solutions from the first Pareto level that match the specified complexity. """ matching_solutions = [solution for solution in self.levels[0] if solution.matches_complexitiy(complexity)] @@ -214,11 +397,45 @@ class ParetoLevelsIterator(object): """ Class for iteration by object of Pareto Levels """ + def __init__(self, pareto_levels): + """ + Initializes the iterator with a sequence of Pareto levels. + + This iterator facilitates traversal through a set of Pareto levels, + enabling access to increasingly refined approximations of the Pareto front. + The Pareto front represents the set of optimal trade-offs between + competing objectives in the equation discovery process. + + Args: + pareto_levels: A sequence of Pareto levels, each representing a + successive refinement of the Pareto front. + + Returns: + None + + Class Fields: + _levels: The sequence of Pareto levels to iterate over. + _idx: The index of the current Pareto level in the sequence. + """ self._levels = pareto_levels self._idx = 0 def __next__(self): + """ + Returns the next individual from the population. + + Iterates through the population of the current level. This is done to explore the solution space level by level, ensuring that diverse and non-dominated solutions are considered during the evolutionary process. + + Args: + self: The object instance. + + Returns: + Individual: The next individual in the population. + + Raises: + StopIteration: If the end of the population is reached. + """ if self._idx < len(self._levels.population): res = self._levels.population[self._idx] self._idx += 1 @@ -229,77 +446,63 @@ def __next__(self): class MOEADDOptimizer(object): """ - Solving multiobjective optimization problem (minimizing set of functions) with an - evolutionary approach. In this class, the unconstrained variation of the problem is - considered. - - Attributes: - abbreviated_search_executed (`boolean`): flag about executing abbreviated search - weights (`np.ndarray`): Weight vectors, introduced to decompose the optimization problem into - several subproblems by dividing Pareto frontier into a numeber of sectors. - pareto_levels (`ParetoLevels`): Pareto levels object, containing the population of candidate solution as a list of - points and as a list of levels. - neighborhood_lists (`list`): keeping neighbours for each solution. stored as lists with all solutions, which are sorted by the owner of the `list` (first element) - best_obj (`np.array`): The best achievable values of objective functions. Should be introduced with - ``self.pass_best_objectives`` method. - sector_processer (`MOEADDSectorProcesser`): keeping evolutionary process + Solves multi-objective optimization problems using an evolutionary algorithm. This class focuses on unconstrained problem variations. It aims to find a set of solutions that represent the best trade-offs between multiple objective functions. + - Example: - -------- + Attributes: + abbreviated_search_executed (`boolean`): flag about executing abbreviated search + weights (`np.ndarray`): Weight vectors, introduced to decompose the optimization problem into + several subproblems by dividing Pareto frontier into a numeber of sectors. + pareto_levels (`ParetoLevels`): Pareto levels object, containing the population of candidate solution as a list of + points and as a list of levels. + neighborhood_lists (`list`): keeping neighbours for each solution. stored as lists with all solutions, which are sorted by the owner of the `list` (first element) + best_obj (`np.array`): The best achievable values of objective functions. Should be introduced with + ``self.pass_best_objectives`` method. + sector_processer (`MOEADDSectorProcesser`): keeping evolutionary process - >>> pop_constr = test_population_constructor() - >>> optimizer = moeadd_optimizer(pop_constr, 40, 40, - >>> None, delta = 1/50., - >>> neighbors_number = 5) - >>> operator = test_evolutionary_operator(mixing_xover, - >>> gaussian_mutation) - >>> optimizer.set_evolutionary(operator=operator) - >>> optimizer.pass_best_objectives(0, 0) - >>> optimizer.optimize(simple_selector, 0.95, (4,), 100, 0.75) + Example: + -------- - In that case, we solve the optimization problem with two objective functions. The population - constructor is defined with the imported dummy class ``test_population_constructor``, - and evolutionary operator contains mutation and crossover suboperators. + >>> pop_constr = test_population_constructor() + >>> optimizer = moeadd_optimizer(pop_constr, 40, 40, + >>> None, delta = 1/50., + >>> neighbors_number = 5) + >>> operator = test_evolutionary_operator(mixing_xover, + >>> gaussian_mutation) + >>> optimizer.set_evolutionary(operator=operator) + >>> optimizer.pass_best_objectives(0, 0) + >>> optimizer.optimize(simple_selector, 0.95, (4,), 100, 0.75) + In that case, we solve the optimization problem with two objective functions. The population + constructor is defined with the imported dummy class ``test_population_constructor``, + and evolutionary operator contains mutation and crossover suboperators. """ + def __init__(self, population_instruct, weights_num, pop_size, solution_params, delta: float, neighbors_number: int, nds_method = fast_non_dominated_sorting, ndl_update = ndl_update, passed_population: Union[List, ParetoLevels] = None): """ - Initialization of the evolutionary optimizer is done with the introduction of - initial population of candidate solutions, divided into Pareto non-dominated - levels (sets of solutions, such, as none of the solution of a level dominates - another on the same level), and creation of set of weights with a proximity list - defined for each of them. - - Parameters - ---------- - population_instruct : dict - Parameters of the individual creation. - weights_num : int - Number of the weight vectors, dividing the objective function values space. Often, shall be same, as the population size. - best_obj : List[int] - List of best obtaiable values for each criteria in the optimization problem. - pop_size : int - The size of the candidate solution population. - solution_params : dict - The dicitionary with the solution parameters, passed into each new created solution during the initialization. - delta : float - The parameter of uniform spacing between the weight vectors; *H = 1 / delta* should be integer - a number of divisions along an objective coordinate axis. - neighbors_number : int - The number of neighboring weight vectors to be considered during the operation of evolutionary operators as the "neighbors" of the processed sectors. - nds_method : callable, optional - Method of non-dominated sorting of the candidate solutions. The default method is implemented according to the article - *K. Deb, A. Pratap, S. Agarwal, and T. Meyarivan, “A fast and elitist multiobjective genetic algorithm: NSGA-II,” IEEE Trans. Evol. Comput., - vol. 6, no. 2, pp. 182–197, Apr. 2002.* The default is ``moeadd.moeadd_supplementary.fast_non_dominated_sorting`` - ndl_update : callable, optional - Method of adding a new solution point into the objective functions space, introduced - to minimize the recalculation of the non-dominated levels for the entire population. - The default method was taken from the *K. Li, K. Deb, Q. Zhang, and S. Kwong, “Efficient non-domination level - update approach for steady-state evolutionary multiobjective optimization,” - Dept. Electr. Comput. Eng., Michigan State Univ., East Lansing, - MI, USA, Tech. Rep. COIN No. 2014014, 2014.* The default - ``moeadd.moeadd_supplementary.ndl_update`` + Initializes the MOEADDOptimizer, creating an initial population of candidate solutions and a set of weight vectors. + + The population is divided into Pareto non-dominated levels, ensuring diversity and exploration of the objective space. + Weight vectors guide the search process by decomposing the multi-objective problem into a set of single-objective subproblems, + each associated with a specific region of the objective space. This initialization is crucial for the optimizer to effectively + explore the solution space and identify a well-distributed Pareto front. + + Args: + population_instruct (dict): Parameters for individual creation within the population. + weights_num (int): The number of weight vectors used to divide the objective function space. Often matches the population size. + pop_size (int): The size of the candidate solution population. + solution_params (dict): Parameters passed to each newly created solution during initialization. + delta (float): Parameter controlling the uniform spacing between weight vectors; *H = 1 / delta* should be an integer. + neighbors_number (int): The number of neighboring weight vectors considered during evolutionary operations. + nds_method (callable, optional): Method for non-dominated sorting of candidate solutions. Defaults to ``moeadd.moeadd_supplementary.fast_non_dominated_sorting``. + ndl_update (callable, optional): Method for efficiently updating non-dominated levels when adding new solutions. Defaults to ``moeadd.moeadd_supplementary.ndl_update``. + passed_population (Union[List, ParetoLevels], optional): Initial population to use. If None, a new population is created. Defaults to None. + + Returns: + None """ assert weights_num == pop_size, 'Each individual in population has to correspond to a sector' self.abbreviated_search_executed = False @@ -365,14 +568,18 @@ def __init__(self, population_instruct, weights_num, pop_size, solution_params, def abbreviated_search(self, population, sorting_method, update_method): """ - Searching data by pareto levels with enterned sorting and updating methods. - + Initializes the Pareto levels based on the provided population and methods, then warns if population is too small. + + This method constructs the Pareto levels structure, which is essential for multi-objective optimization. + It uses the provided sorting and update methods to organize the population into non-dominated fronts. + If the population size is small, a warning is issued, indicating that the search may be limited due to insufficient diversity. + Args: population (`list`): List with the elements - canidate solutions of the case-specific subclass of ``src.moeadd.moeadd_solution_template.MOEADDSolution``. sorting_method (`callable`): The method of population separation into non-dominated levels update_method (`callable`): The method of point addition into the population and onto the non-dominated levels. - + Returns: None """ @@ -387,18 +594,14 @@ def abbreviated_search(self, population, sorting_method, update_method): @staticmethod def weights_generation(weights_num, delta) -> list: """ - Method to calculate the set of vectors to divide the problem of Pareto frontier - discovery into several subproblems of Pareto frontier sector discovery, where - each sector is defined by a weight vector. + Method to generate a set of weight vectors used to guide the search towards different regions of the Pareto front. These vectors define subproblems, each focusing on a specific trade-off between objectives, thereby promoting diversity in the solutions found. - Args: - weights_num (`int`): Number of the weight vectors, dividing the objective function values space. - delta (`float`): Parameter of uniform spacing between the weight vectors; *H = 1 / delta* - should be integer - a number of divisions along an objective coordinate axis. + Args: + weights_num (`int`): The number of weight vectors to generate, determining the granularity of the Pareto front exploration. + delta (`float`): A parameter controlling the spacing between weight vectors, influencing the uniformity of the search. It is required that *H = 1 / delta* is an integer. - Returns: - weights (`list`): weight vectors (`np.ndarrays`), introduced to decompose the optimization problem into - several subproblems by dividing Pareto frontier into a number of sectors. + Returns: + `list`: A list of weight vectors (`np.ndarrays`), each representing a different preference for the objectives and guiding the evolutionary search towards diverse solutions on the Pareto front. """ weights = np.empty(weights_num) assert 1./delta == round(1./delta) # check, if 1/delta is integer number @@ -414,13 +617,15 @@ def weights_generation(weights_num, delta) -> list: def pass_best_objectives(self, *args) -> None: """ - Setter of the `moeadd_optimizer.best_obj` attribute. + Updates the current best objective values observed during the optimization process. - Args: - args (`np.ndarray|list`): The values of the objective functions for the many-objective optimization problem. + This method is crucial for tracking the progress of the MOEA/D algorithm, ensuring that the best-performing objective values are recorded for comparison and selection during the evolutionary search. It iterates through the provided objective values and updates the internal `best_obj` attribute with the improved values. This ensures that the algorithm maintains a record of the best solutions encountered so far, guiding the search towards the Pareto front. - Returns: - None + Args: + *args (`np.ndarray|list`): The values of the objective functions for a candidate solution in the many-objective optimization problem. The number of arguments must match the number of objective functions. + + Returns: + None """ if len(self.pareto_levels.population) != 0: print('comparing lengths', len(args), len(self.pareto_levels.population[0].obj_funs)) @@ -436,15 +641,32 @@ def pass_best_objectives(self, *args) -> None: def set_sector_processer(self, processer: MOEADDSectorProcesser) -> None: """ - Setter of the `moeadd_optimizer.sector_processer` attribute. + Sets the sector processor for the MOEADDOptimizer. + + This operator is responsible for driving the evolutionary search within each sector, + guiding the optimization process towards identifying suitable equation structures. Args: - processer (`MOEADDSectorProcesser`): The operator, which defines the evolutionary process + processer (`MOEADDSectorProcesser`): The sector processor to be used. + + Returns: + `None` """ self.sector_processer = processer def set_strategy(self, strategy_director): + """ + Sets the sector processing strategy. + + This method configures the sector processor using a strategy director, enabling dynamic adaptation of the optimization process to different problem characteristics. By employing the director's builder to assemble a specific processing strategy, the optimizer can tailor its search behavior to enhance performance and convergence. + + Args: + strategy_director: The director responsible for building the strategy. + + Returns: + None. + """ builder = strategy_director.builder builder.assemble(True) self.set_sector_processer(builder.processer) @@ -452,17 +674,19 @@ def set_strategy(self, strategy_director): def optimize(self, epochs): """ - Method for the main unconstrained evolutionary optimization. Can be applied repeatedly to - the population, if the previous results are insufficient. The output of the - optimization shall be accessed with the ``optimizer.pareto_level`` object and - its attributes ``.levels`` or ``.population``. + Method for performing evolutionary optimization to refine the Pareto levels. This process iteratively improves the set of non-dominated solutions by exploring different equation structures and parameter values. The optimization output, representing the discovered equations and their corresponding fitness values, can be accessed through the ``optimizer.pareto_level`` object and its attributes ``.levels`` or ``.population``. - Args: - epochs (`int`): Maximum number of iterations, during that the optimization will be held. + Args: + epochs (`int`): The maximum number of evolutionary optimization iterations. - Note: - that if the algorithm converges to a single Pareto frontier, the optimization is stopped. + Returns: + None. The optimization results are stored within the `pareto_levels` attribute of the optimizer. + Note: + The optimization process terminates early if the algorithm converges to a single Pareto frontier, indicating that further iterations are unlikely to yield significant improvements. + + Why: + This optimization refines the Pareto levels by iteratively processing subsets of the population, guided by different weight vectors. This approach allows the algorithm to explore the search space more effectively and identify a diverse set of non-dominated solutions, each representing a potential equation structure that fits the data well. """ if not self.abbreviated_search_executed: self.hist = [] @@ -491,12 +715,40 @@ def form_processer_args(self, cur_weight : int): # TODO: inspect the most conven def get_hist(self, best_only: bool = True): + """ + Returns the search history, allowing inspection of the optimization process. + + This function provides a way to track the progress of the equation discovery process. By examining the history, users can understand how the search evolved and identify potential areas for improvement in the search strategy. + + Args: + best_only (bool, optional): If True, returns only the best equation scores found so far. If False, returns the complete history, including equation scores and corresponding parameters at each step. Defaults to True. + + Returns: + list: The history of the search. If `best_only` is True, returns a list of the best equation scores. Otherwise, returns the complete history as a list of tuples, where each tuple contains the best equation score and the corresponding parameters. + """ if best_only: return [elem[0] for elem in self._hist] else: return self._hist def plot_pareto(self, dimensions:list, **visualizer_kwargs): + """ + Plots the Pareto front for two objectives, visualizing the trade-offs achieved by the multi-objective evolutionary algorithm. + + This method leverages the ParetoVisualizer to display the Pareto front, + representing the non-dominated solutions found during the optimization + process. By plotting the front, users can analyze the trade-offs between + different objectives and select solutions that best meet their specific + requirements. It currently supports only two-dimensional plots. + + Args: + dimensions: A list containing the two objective dimensions to be plotted. + visualizer_kwargs: Keyword arguments to be passed to the + ParetoVisualizer's plotting function, allowing customization of the plot's appearance. + + Returns: + None. The method displays a plot of the Pareto front. + """ assert len(dimensions) == 2, 'Current approach supports only two dimensional plots' visualizer = ParetoVisualizer(self.pareto_levels) # visualizer.plot_pareto(dimensions = dimensions, **visualizer_kwargs) diff --git a/epde/optimizers/moeadd/population_constr.py b/epde/optimizers/moeadd/population_constr.py index 67089c3c..9c1a665f 100644 --- a/epde/optimizers/moeadd/population_constr.py +++ b/epde/optimizers/moeadd/population_constr.py @@ -13,17 +13,35 @@ class SystemsPopulationConstructor(object): """ - Class for creating population according to the current optimization method/ - - Attributes: - pool (``): - terms_number (`int`): - max_factors_in_term (`int`): - vars_demand_equation (``): - sparsity_internal (`tuple`): + Class for constructing an initial population of symbolic expressions to represent potential solutions. The construction process is tailored to the chosen optimization method and the defined search space. + + + Attributes: + pool (``): + terms_number (`int`): + max_factors_in_term (`int`): + vars_demand_equation (``): + sparsity_internal (`tuple`): """ + def __init__(self, pool, use_pic: bool = True, terms_number : int = 8, max_factors_in_term : int = 2, obj_functions : Callable = None, sparsity_interval : tuple = (0, 1)): + """ + Initializes the SystemsPopulationConstructor. + + This constructor sets up the necessary parameters for generating a population of symbolic expressions representing potential equation structures. It configures the search space by defining the pool of families (building blocks for equations), the complexity of terms, and sparsity constraints. Extracting variables from the demand equation ensures that the generated expressions are relevant to the specific problem being addressed. + + Args: + pool: A pool of families, defining the basic elements for constructing equations. + use_pic: A boolean indicating whether to use PIC (Physics-Informed Collocation) loss function. Defaults to True. + terms_number: The number of terms in the symbolic expression. Defaults to 8. + max_factors_in_term: The maximum number of factors allowed within a single term. Defaults to 2. + obj_functions: Objective functions used for evaluating the fitness of candidate equations. Defaults to None. + sparsity_interval: A tuple representing the desired sparsity range for the generated equations. Defaults to (0, 1). + + Returns: + None. + """ self.pool = pool self.use_pic = use_pic self.terms_number = terms_number @@ -33,12 +51,46 @@ def __init__(self, pool, use_pic: bool = True, terms_number : int = 8, max_facto print('self.vars_demand_equation', self.vars_demand_equation) def applyToPassed(self, passed_solution: SoEq, **kwargs): + """ + Applies objective functions to a passed solution. + + This method is crucial for evaluating the fitness of a candidate solution (equation) within the evolutionary process. It determines how well the equation describes the observed data by applying specified objective functions. + + Args: + passed_solution: The solution (equation) to which the objective functions will be applied. This solution's performance will be assessed based on these functions. + **kwargs: Keyword arguments. If 'obj_funs' is provided, these objective functions will be set for the solution. + + Returns: + None. The method modifies the passed_solution in place by setting or using objective functions. + """ try: passed_solution.set_objective_functions(kwargs['obj_funs']) except KeyError: passed_solution.use_default_multiobjective_function(self.use_pic) def create(self, **kwargs): + """ + Creates a new equation (SoEq) tailored for the equation discovery process. + + This method constructs a `SoEq` object, configuring its objective functions + and defining its structural components based on provided parameters. The + equation's structure is crucial for the evolutionary search of governing + equations. + + Args: + **kwargs: Keyword arguments containing parameters for equation creation. + sparsity: Sparsity value for the equation. If not provided, it's + randomly generated within the `sparsity_interval`. + terms_number: The number of terms in the equation. Defaults to + `self.terms_number`. + max_factors_in_term: The maximum number of factors in a term. + Defaults to `self.max_factors_in_term`. + obj_funs: Objective functions for the equation. If not provided, + default multiobjective function is used. + + Returns: + SoEq: The created `SoEq` object representing the new equation. + """ sparsity = kwargs.get('sparsity', 10 ** (np.random.uniform(low = np.log10(self.sparsity_interval[0]), high = np.log10(self.sparsity_interval[1]), size = len(self.vars_demand_equation)))) diff --git a/epde/optimizers/moeadd/solution_template.py b/epde/optimizers/moeadd/solution_template.py index 11dfaa2d..3e45760a 100644 --- a/epde/optimizers/moeadd/solution_template.py +++ b/epde/optimizers/moeadd/solution_template.py @@ -14,17 +14,16 @@ def get_domain_idx(solution, weights) -> int: """ - Function, devoted to finding the domain, defined by **weights**, to which the - **solutions** belongs. The belonging is determined by the acute angle between solution and - the weight vector, defining the domain. - + Finds the domain index for a given solution based on its objective function values and a set of weight vectors. + + The domain is determined by identifying the weight vector that forms the smallest acute angle with the solution's objective function vector. This assignment is crucial for effectively distributing solutions across different subproblems during the evolutionary optimization process, ensuring diversity and convergence towards the Pareto front. + Args: - solution (`np.ndarray|src.moeadd.moeadd_solution_template.MOEADDSolution`): The candidate solution, for which we are determining the domain, or its objective - function values, stored in np.ndarray. - weights (`np.ndarray`): Numpy ndarray, containing weights from the moeadd optimizer. - + solution (`np.ndarray|src.moeadd.moeadd_solution_template.MOEADDSolution`): The candidate solution, either as a NumPy array of objective function values or a `MOEADDSolution` object containing the objective function values. + weights (`np.ndarray`): A NumPy array containing the weight vectors that define the different domains. + Returns: - idx (`int`): Index of the domain (i.e. index of corresponing weight vector), to which the solution belongs. + `int`: The index of the weight vector (domain) to which the solution belongs, corresponding to the smallest acute angle. """ if type(solution) == np.ndarray: @@ -40,47 +39,99 @@ class CrossoverSelectionCounter(object): """ Counter of individs participating in the crossover """ + def __init__(self): + """ + Initializes a new instance of the CrossoverSelectionCounter class. + + This class is designed to track and manage crossover selections during the evolutionary process of equation discovery. + It initializes the counter by calling the reset method to ensure a clean state for each run or evaluation. + + Args: + None + + Returns: + None + """ self.reset() def reset(self): + """ + Resets the selection counter. + + This method resets the internal counter `_counter` to 0. It is used to ensure that each generation starts with a fresh count of crossover selections, preventing bias towards earlier individuals in the population. + + Args: + self: The instance of the class. + + Returns: + None. + """ self._counter = 0 def incr(self): + """ + Increments the internal counter, tracking the number of crossover selection operations performed. This is crucial for monitoring the evolutionary algorithm's progress and ensuring sufficient exploration of the search space. + + Args: + self: The instance of the CrossoverSelectionCounter class. + + Returns: + None. This method modifies the internal state of the object by incrementing the counter. + """ self._counter += 1 def __call__(self): + """ + Return the number of crossover selections performed. + + This counter tracks the number of times crossover is applied during the evolutionary process, providing insights into the exploration of the search space. By monitoring this value, we can analyze the effectiveness of crossover in generating new candidate solutions. + + Args: + self: The object instance. + + Returns: + int: The current value of the crossover selection counter. + """ return self._counter class MOEADDSolution(ABC): """ - Abstract superclass of the moeadd solution. *__hash__* method must be declared in the subclasses. - Overloaded *__eq__* method of MOEADDSolution uses strict equatlity between self.vals attributes, - therefore, can not be used with the real-valued strings. - - Attributes: - vals : arbitrary object - An arbitrary object, representing the solution gene. - obj_funs (`list of functions`): Objective functions, that would be optimized by the - evolutionary algorithm. - precomputed_value (`bool`): Indicator, if the value of the objective functions is already calculated. - Implemented to avoid redundant computations. - precomputed_domain (`bool`): Indicator, if the solution has been already placed in a domain in objective function - space. Implemented to avoid redundant computations during the point placement. - obj_fun (`np.array`): Property, that calculates/contains calculated value of objective functions. - _domain (`int`): Index of the domain, to that the solution belongs. - + Abstract base class for solutions within the multi-objective evolutionary algorithm. Subclasses must implement the *__hash__* method. The overloaded *__eq__* method relies on strict equality of the `self.vals` attribute, which may not be suitable for real-valued strings. + + + Attributes: + vals : arbitrary object + An arbitrary object, representing the solution gene. + obj_funs (`list of functions`): Objective functions, that would be optimized by the + evolutionary algorithm. + precomputed_value (`bool`): Indicator, if the value of the objective functions is already calculated. + Implemented to avoid redundant computations. + precomputed_domain (`bool`): Indicator, if the solution has been already placed in a domain in objective function + space. Implemented to avoid redundant computations during the point placement. + obj_fun (`np.array`): Property, that calculates/contains calculated value of objective functions. + _domain (`int`): Index of the domain, to that the solution belongs. """ + def __init__(self, x, obj_funs): """ + Initializes a solution with its gene and objective functions. + + This method prepares a solution by storing its gene representation and the objective functions + that will be used to evaluate its fitness. It also initializes flags to track precomputed values + and crossover selection counts. + Args: x : arbitrary object, An arbitrary object, representing the solution gene. For example, - it can be a string of floating-point values, implemented as np.ndarray + it can be a string of floating-point values, implemented as np.ndarray. This gene encodes a candidate equation. obj_funs (`list of functions`): Objective functions, that would be optimized by the - evolutionary algorithm. + evolutionary algorithm. These functions quantify how well the candidate equation fits the data and satisfies other criteria. + + Returns: + None """ self.vals = x self.obj_funs = obj_funs @@ -90,6 +141,24 @@ def __init__(self, x, obj_funs): @property def obj_fun(self): + """ + Calculates and returns the objective function value for this solution. + + This property provides access to the solution's objective function values. + It efficiently computes these values only once and stores them for subsequent access. + This ensures that the objective functions, which represent the solution's performance + according to different criteria, are evaluated consistently and without redundant computation. + + Args: + self: The object instance. + + Returns: + np.ndarray: The calculated objective function value. + + Class Fields Initialized: + _obj_fun (np.ndarray): Stores the calculated objective function value. Initialized only if `precomputed_value` is False. + precomputed_value (bool): A flag indicating whether the objective function value has been precomputed. Set to True after the objective function value is computed for the first time. + """ if self.precomputed_value: return self._obj_fun else: @@ -99,13 +168,15 @@ def obj_fun(self): def get_domain(self, weights): """ - Method that regulates the execution of the function finding the definition area once - + Determines the domain to which the solution belongs based on provided weights. + + This method optimizes performance by caching the domain index after its initial computation. Subsequent calls will return the cached value, avoiding redundant calculations. This is crucial for efficient multi-objective optimization, where domain assignments are frequently needed. + Args: - weights (`np.ndarray`): Numpy ndarray, containing weights from the moeadd optimizer - + weights (`np.ndarray`): A NumPy array containing weights from the MOEA/D optimizer, used to determine the solution's domain. + Returns: - domains (`int`): Index of the domain, to that the solution belongs. + `int`: The index of the domain to which the solution belongs. """ if self.precomputed_domain: return self._domain @@ -115,38 +186,120 @@ def get_domain(self, weights): return self._domain def set_domain(self, idx): + """ + Sets the domain for the solution. + + This method marks the domain as precomputed and stores its index. This is done to efficiently manage and access different problem domains during the evolutionary search process, where each solution needs to be evaluated within a specific domain. By precomputing and indexing domains, the search algorithm can quickly switch between them without redundant calculations. + + Args: + idx (int): The index representing the precomputed domain. + + Returns: + None + """ self.precomputed_domain = True self._domain = idx def __eq__(self, other): + """ + Compares this solution to another solution for equality based on their variable values. + + This is important for comparing solutions within the evolutionary algorithm, + allowing the algorithm to identify and eliminate duplicate solutions, + maintaining diversity in the population. + + Args: + other: The object to compare with. + + Returns: + bool: True if the objects are equal, NotImplemented if the other object is not of the same type. + """ if isinstance(other, type(self)): return self.vals == other.vals else: return NotImplemented def __call__(self): + """ + Evaluates the solution by returning its objective function value. + + This allows the solution to be easily used in optimization algorithms + where the objective function value is required for comparison and selection. + + Args: + self: The MOEADDSolution instance. + + Returns: + The objective function value associated with this solution. + """ return self.obj_fun @abstractmethod def __hash__(self): + """ + Raises a NotImplementedError, enforcing implementation in subclasses. + + This abstract method ensures that concrete solution classes define their + own hashing mechanism. This is crucial for utilizing solution objects + in hash-based data structures, which is essential for efficient + multi-objective optimization algorithms that rely on comparing and + grouping solutions based on their properties. + + Args: + self: The instance of the class. + + Returns: + None. + + Raises: + NotImplementedError: Always raised, prompting implementation in subclasses. + """ raise NotImplementedError( 'The hash needs to be defined in the subclass') def incr_counter(self, incr_val: int = 1): """ - Increase counter of allowed participations in the crossover process for and individual. - + Increase the counter of crossover participations. + + This counter tracks how often an individual has been selected for crossover, + influencing its likelihood of being chosen again and promoting diversity + in the evolutionary process. + Args: - incr_val (`int`): Value, for which the counter is increased. - Optional, the default is 1. - + incr_val (int): The amount to increment the counter by. Defaults to 1. + + Returns: + None """ for i in range(incr_val): self.crossover_selected_times.incr() def crossover_times(self): + """ + Returns the times when crossover operations were applied during the evolutionary process. These times indicate when genetic material was exchanged between solutions to explore the search space. + + Args: + None + + Returns: + list: A list of crossover times, representing the iterations at which crossover was performed. These times help track the evolution of solutions and the impact of crossover on the search process. + """ return self.crossover_selected_times() def reset_counter(self): + """ + Resets the crossover selection counter. + + This method resets the internal counter that tracks how many times this + crossover operator has been selected during the evolutionary process. + This is important for maintaining a fair comparison of different crossover + operators and ensuring diversity in the search for optimal equation structures. + + Args: + self: The instance of the class. + + Returns: + None. + """ self.crossover_selected_times.reset() diff --git a/epde/optimizers/moeadd/strategy.py b/epde/optimizers/moeadd/strategy.py index b10ff88a..6158e113 100644 --- a/epde/optimizers/moeadd/strategy.py +++ b/epde/optimizers/moeadd/strategy.py @@ -28,9 +28,32 @@ class MOEADDDirector(OptimizationPatternDirector): """ Class for creating strategy builder of multicriterian optimization """ + def use_baseline(self, use_solver: bool = False, use_pic: bool = True, variation_params : dict = {}, mutation_params : dict = {}, sorter_params : dict = {}, pareto_combiner_params : dict = {}, pareto_updater_params : dict = {}, **kwargs): + """ + Configures the evolutionary strategy builder with a pre-defined, general-purpose setup. + + This method establishes a foundational evolutionary strategy by integrating selection, + variation, and population updating mechanisms. It offers flexibility through adjustable + parameters and the choice between solver-based and L2-based fitness evaluations. + This baseline configuration provides a starting point for discovering differential equations + from data by evolving equation structures that effectively model the underlying dynamics. + + Args: + use_solver: Flag indicating whether to use a solver-based fitness evaluation. + use_pic: Flag indicating whether to use the PIC solver-based fitness evaluation. + variation_params: Parameters for the variation operator. + mutation_params: Parameters for the mutation operator. + sorter_params: Parameters for the initial sorter. + pareto_combiner_params: Parameters for the Pareto levels combiner. + pareto_updater_params: Parameters for the Pareto levels updater. + **kwargs: Additional keyword arguments to be passed to operators. + + Returns: + None + """ add_kwarg_to_operator = partial(add_base_param_to_operator, target_dict = kwargs) neighborhood_selector = SimpleNeighborSelector(['number_of_neighbors']) @@ -96,4 +119,20 @@ def use_baseline(self, use_solver: bool = False, use_pic: bool = True, variation ('pareto_updater_compl', population_updater)]) def use_constrained_eq_search(self): + """ + Raises a NotImplementedError, indicating that constrained equation search is not yet implemented. + + This functionality is intended to allow the search for differential equations to be guided by constraints, + improving the efficiency and accuracy of the equation discovery process. Currently, this feature is not + available. + + Args: + self: The instance of the class. + + Raises: + NotImplementedError: Always raised, as the functionality is not implemented. + + Returns: + None. + """ raise NotImplementedError('No constraints have been implemented yest') diff --git a/epde/optimizers/moeadd/strategy_elems.py b/epde/optimizers/moeadd/strategy_elems.py index 797fd509..b267c713 100644 --- a/epde/optimizers/moeadd/strategy_elems.py +++ b/epde/optimizers/moeadd/strategy_elems.py @@ -13,8 +13,21 @@ class MOEADDSectorProcesser(Strategy): ''' Specific implemtation of evolutionary strategy of MOEADD algorithm. Defines - processing of a population in respect to a weight vector. + processing of a population in respect to a weight vector. ''' + def run(self, population_subset, EA_kwargs : dict): + """ + Runs the linked blocks traversal on a population subset. + + This method orchestrates the search for governing equations by traversing linked blocks of equation components within a subset of the population. It leverages evolutionary algorithm parameters to guide the search process. + + Args: + population_subset: The subset of the population to process, representing candidate equation structures. + EA_kwargs (dict): Keyword arguments for the evolutionary algorithm, controlling parameters like mutation rates and selection pressures. + + Returns: + The output from the linked blocks traversal, representing the discovered equation structures and their associated fitness values. This output is crucial for identifying the best candidate equations that fit the observed data. + """ self.linked_blocks.traversal(population_subset, EA_kwargs) return self.linked_blocks.output diff --git a/epde/optimizers/moeadd/supplementary.py b/epde/optimizers/moeadd/supplementary.py index 8e4fe3f4..8b28bfbb 100644 --- a/epde/optimizers/moeadd/supplementary.py +++ b/epde/optimizers/moeadd/supplementary.py @@ -34,20 +34,18 @@ def check_dominance(target, compared_with) -> bool: """ - - Function to check, if one solution is dominated by another. - + Determines if one solution dominates another based on their objective function values. + + This function is crucial for identifying Pareto-optimal solutions within the evolutionary process. + By comparing objective values, the algorithm can effectively navigate the search space towards + solutions that represent the best trade-offs between different objectives. + Args: - target (`src.moeadd.moeadd_solution_template.MOEADDSolution`): case-specific subclass object - The individual solution on the pareto levels, compared with the other element. - compared_with (`src.moeadd.moeadd_solution_template.MOEADDSolution`): case-specific subclass object - The individual solution on the pareto levels, with which the target is compared. - + target (`src.moeadd.moeadd_solution_template.MOEADDSolution`): A solution to be evaluated for dominance. + compared_with (`src.moeadd.moeadd_solution_template.MOEADDSolution`): The solution against which the target is compared. + Returns: - domiated (`bool`): Function returns True, if the **target** dominates (has at least one objective - functions with less values, while the others are the same) the **compared_with**; - False in all other cases. - + `bool`: True if the `target` solution dominates the `compared_with` solution (i.e., it is at least as good in all objectives and strictly better in at least one), False otherwise. """ flag = False @@ -66,6 +64,28 @@ def ndl_update(new_solution, levels) -> list: # efficient_ndl_update Computationally-cheap method of adding new solution into the existing Pareto levels. Args: + """ + Efficiently integrates a new solution into existing Pareto levels by considering dominance relationships. + + This method strategically places a new solution within the non-dominated levels, + ensuring the Pareto optimality is maintained. It identifies where the new solution + fits best based on dominance checks with existing solutions in each level. + + Args: + new_solution (`src.moeadd.moeadd_solution_template.MOEADDSolution`): The solution to be added. + levels (`list`): A list of lists, where each sublist represents a non-dominated level containing + `src.moeadd.moeadd_solution_template.MOEADDSolution` objects. + + Returns: + `list`: A new list of lists representing the updated non-dominated levels, with the `new_solution` + integrated appropriately. The structure mirrors the input `levels` argument. + + Notes: + The idea for this method was introduced in *K. Li, K. Deb, Q. Zhang, and S. Kwong, + “Efficient non-domination level update approach for steady-state evolutionary + multiobjective optimization,” Dept. Electr. Comput. Eng., Michigan State Univ., + East Lansing, MI, USA, Tech. Rep. COIN No. 2014014, 2014.* + """ new_solution (`src.moeadd.moeadd_solution_template.MOEADDSolution`): case-specific subclass object The solution, that is to be added onto the non-dominated levels. levels (`list`): List of lists of ``src.moeadd.moeadd_solution_template.MOEADDSolution`` case-specific subclass @@ -120,23 +140,24 @@ def ndl_update(new_solution, levels) -> list: # efficient_ndl_update def fast_non_dominated_sorting(population) -> list: """ - Procedure of separating points from the general population into non-dominated levels. - This function is a faster alternative to the ``slow_non_dominated_sorting``, but requires - a little more memory to store indexes of elements, dominated by every solution. This - method was introduced in *K. Deb, A. Pratap, S. Agarwal, and T. Meyarivan, “A fast - and elitist multiobjective genetic algorithm: NSGA-II,” IEEE Trans. Evol. Comput., - vol. 6, no. 2, pp. 182–197, Apr. 2002.* The computational complexity of the method is - :math:`O(MN^2)`, where *N* is the population size, and *M* is the number of objective - functions in comparisson with :math:`O(MN^3)` of the straightforward way. - - - Args: - population (`list`): The input population, represented as a list of individuals. - - Returns: - levels (`list`): List of lists of population elements. The outer index is the number of a layer - (e.g. 0-th is the current Pareto frontier), while the inner is the index of an element on a level. - + Procedure to classify solutions into distinct non-dominated sets based on their objective values. + + This method efficiently identifies Pareto-optimal solutions by iteratively assigning individuals to different fronts. + It maintains a count of dominating solutions for each individual and a list of solutions dominated by each individual. + This approach reduces computational complexity compared to naive methods, making it suitable for larger populations. + This function is a faster alternative to the ``slow_non_dominated_sorting``, but requires + a little more memory to store indexes of elements, dominated by every solution. This + method was introduced in *K. Deb, A. Pratap, S. Agarwal, and T. Meyarivan, “A fast + and elitist multiobjective genetic algorithm: NSGA-II,” IEEE Trans. Evol. Comput., + vol. 6, no. 2, pp. 182–197, Apr. 2002.* The computational complexity of the method is + :math:`O(MN^2)`, where *N* is the population size, and *M* is the number of objective + functions in comparisson with :math:`O(MN^3)` of the straightforward way. + + Args: + population (list): A list of individuals, where each individual is represented by its objective values. + + Returns: + list: A list of lists, where each inner list represents a non-dominated front. The first front (index 0) contains the Pareto-optimal solutions. """ levels = [] @@ -179,19 +200,25 @@ def fast_non_dominated_sorting(population) -> list: def slow_non_dominated_sorting(population) -> list: """ - Procedure of separating points from the general population into non-dominated levels. - Operates in the straightforward way: each layer is comprised of elements, that are - not dominated by any other element in the population, except the ones, already put into - the output levels. Computational complexity of this variant of sorting in worst scenario is - :math:`O(MN^3)`, where *N* is the population size, and *M* is the number of objective functions. - - Args: - population (`list`): The input population, represented as a list of individuals. - - Returns: - levels (`list`): List of lists of population elements. The outer index is the number of a layer - (e.g. 0-th is the current Pareto frontier), while the inner is the index of an element on a level. - + Procedure to classify a population into non-dominated levels, identifying Pareto fronts. + + This function iteratively identifies and separates individuals into distinct non-dominated levels. + Each level represents a Pareto front, containing solutions that are not dominated by any other + solution in the population (excluding those already assigned to previous levels). + This process continues until all individuals are assigned to a level. + The computational complexity of this sorting algorithm is :math:`O(MN^3)` in the worst case, + where *N* is the population size and *M* is the number of objective functions. + This classification helps to identify the best trade-offs within the population, + which is crucial for constructing equation structures that accurately represent the underlying dynamics + of the system being modeled. + + Args: + population (`list`): The input population, represented as a list of individuals. + + Returns: + levels (`list`): List of lists of population elements. The outer index represents the Pareto front + number (e.g., 0-th is the current Pareto frontier), while the inner index indicates the + position of an element within that level. """ locked_idxs = [] levels = [] @@ -215,6 +242,18 @@ def slow_non_dominated_sorting(population) -> list: def acute_angle(vector_a, vector_b) -> float: + """ + Calculates the acute angle between two vectors. + + This function is crucial for determining the relationships between different terms and vectors within the equation discovery process. By calculating the angles, the algorithm can assess the similarity and independence of various components, aiding in the selection of the most relevant terms for the final equation. + + Args: + vector_a (np.ndarray): The first vector, representing a term or component in the equation. + vector_b (np.ndarray): The second vector, representing another term or component in the equation. + + Returns: + float: The acute angle in radians between the two vectors. This value is used to evaluate the correlation and redundancy between terms. + """ cos_val = np.dot(vector_a, vector_b)/(np.sqrt(np.dot(vector_a, vector_a))*np.sqrt(np.dot(vector_b, vector_b))) if np.abs(cos_val) > 1.: cos_val = np.sign(cos_val) @@ -223,80 +262,157 @@ def acute_angle(vector_a, vector_b) -> float: class Constraint(ABC): """ - - The abstract class for the constraint. Noteable subclasses: Inequality & Equality. - + Represents a constraint on the search space for equation discovery. It defines conditions that candidate equations must satisfy to be considered valid. This class serves as a base for implementing various types of constraints, such as limiting the number of terms, restricting the types of operations, or enforcing specific structural properties. + + + The abstract class for the constraint. Noteable subclasses: Inequality & Equality. """ + @abstractmethod def __init__(self, *args): + """ + Initializes the constraint object. + + This abstract method serves as a blueprint for initializing specific constraint types + within the equation discovery process. Subclasses should override this method to + define the constraint's parameters and behavior. + + Args: + *args: Variable length argument list. Arguments passed to the constraint. + + Returns: + None. + + Why: + This initialization is a part of defining constraints on the discovered equations. + Constraints are used to guide the search process towards more physically meaningful + or mathematically stable solutions. + """ pass @abstractmethod def __call__(self, *args): + """ + Applies the constraint to the provided arguments. + + This abstract method serves as the core logic for evaluating the constraint + defined by a subclass against a given set of arguments. Subclasses must + implement this method to define the specific constraint behavior. + + Args: + *args: Variable-length argument list representing the values to which + the constraint is applied. + + Returns: + None. This method's behavior is defined by its side effects within + subclasses, such as modifying internal state or raising exceptions + if the constraint is violated. + + Why: + This method enables the framework to evaluate candidate equation structures + by checking if they satisfy specific conditions or limitations imposed + during the equation discovery process. + """ pass class Inequality(Constraint): """ - The class of the constrain (subclass of Constraint), representing the inequality. - The format of inequality is assumed in format :math:`g(x) >= 0`. - + Represents an inequality constraint. Assumes the format :math:`g(x) >= 0`. Subclass of Constraint. + + Args: g (`function (lambda function)`): The constraint function, which shall penalize the candidate solution, if the value of :math:`g(x) >= 0` is not fulfilled (is less, than 0). The penalty is equal to the absolute value of constraint violation. - + Methods: __call__(self, x) : returns float Overloaded call operator returns the value of constaint violation. - """ + def __init__(self, g): + """ + Initializes the Inequality object with a graph representing inequality relationships. + + This graph is used to efficiently manage and traverse inequality constraints + during the equation discovery process, ensuring that discovered equations + adhere to the specified relationships. + + Args: + g: A graph object representing inequality relationships between terms. + + Returns: + None. + + Class Fields: + _g (object): The graph object associated with this instance, + representing inequality constraints. + """ self._g = g def __call__(self, x) -> float: """ - Method to evaluate the constraint violation of the candidate solution. - + Evaluates the constraint violation for a given candidate solution. + + This method determines the degree to which a candidate solution violates the defined inequality constraint. It returns a non-zero value only when the constraint is not satisfied, quantifying the extent of the violation. This is crucial for the evolutionary algorithm to penalize solutions that do not adhere to the problem's constraints, guiding the search towards feasible regions. + Args: - x (`np.ndarray`): Values (.vals attribute) of the candidate solution, that represent its gene. - + x (`np.ndarray`): The candidate solution's values (gene representation). + Returns: - cv (`float`): Constraint violation value. If the value of :math:`g(x) >= 0` is not - fulfilled (is less, than 0), than returns :math:`|g(x)|`, else 0. + `float`: The constraint violation value. Returns the absolute value of g(x) if g(x) < 0, otherwise 0. """ return - self._g(x) if self._g(x) < 0 else 0 class Equality(Constraint): """ - The class of the constrain (subclass of Constraint), representing the inequality. - The format of inequality is assumed in format :math:`h(x) = 0`. - + Represents an equality constraint. Assumes the format :math:`h(x) = 0`. Subclass of Constraint. + + Args: h (`function (lambda function)`): The constraint function, which shall be penalized, if the value does not match with the const - + Methods: __call__(self, x) : returns float Overloaded call operator returns the value of constaint violation. - """ + def __init__(self, h): + """ + Initializes the object's height, a crucial parameter for comparing instances within the equation discovery process. + + The height attribute influences how the evolutionary algorithm evaluates and selects equation candidates. + + Args: + h (float): The height value to be stored. + + Returns: + None. + + Class Fields: + _h (float): The height of the object. + """ self._h = h def __call__(self, x) -> float: """ - Method to evaluate the constraint violation of the candidate solution. - + Evaluates the constraint violation of a candidate solution for equality constraints. + + This method quantifies how well a candidate solution satisfies the equality constraint + defined by :math:`h(x) = 0`. It returns the absolute value of :math:`h(x)`, + representing the magnitude of the violation. This value is used by the evolutionary + algorithm to guide the search towards solutions that better satisfy the constraint. + Args: - x (`np.ndarray`): Values (.vals attribute) of the candidate solution, that represent its gene. - + x (`np.ndarray`): Values of the candidate solution, representing its gene. + Returns: - cv (`float`): Constraint violation value. If the value of :math:`h(x) = 0` is not - fulfilled, than returns :math:`|g(x)|`, else 0. - + `float`: The constraint violation value, which is the absolute value of :math:`h(x)`. + A value of 0 indicates that the constraint is perfectly satisfied. """ return np.abs(self._h(x)) diff --git a/epde/optimizers/moeadd/vis.py b/epde/optimizers/moeadd/vis.py index e5319c42..dcf3be32 100644 --- a/epde/optimizers/moeadd/vis.py +++ b/epde/optimizers/moeadd/vis.py @@ -7,14 +7,48 @@ class ParetoVisualizer(object): + """ + Visualizes Pareto frontiers obtained from multi-objective optimization. + + This class provides methods to plot Pareto levels in 2D, allowing for + visualization and analysis of trade-offs between different objectives. + + Methods: + - plot_pareto + - plot_pareto_mt + """ + def __init__(self, eq_pareto_levels): """ - Проще всего получить pareto_levels из атрибута optimizer.pareto_levels + Initializes the ParetoVisualizer with a set of Pareto levels. These levels represent the trade-offs between different objectives in the equation discovery process, allowing for the selection of models that balance accuracy and complexity. + + Args: + eq_pareto_levels: A list of Pareto levels obtained from the optimizer, representing the non-dominated solutions in the search space. + + Returns: + None """ self.pareto_frontier = eq_pareto_levels def plot_pareto(self, dimensions: tuple = (0, 1), annotate_best=True, plot_level=1, filename=None, save_format='eps'): + """ + Plots the Pareto frontier in a 2D plane to visualize trade-offs between objectives. + + This method projects the Pareto levels onto a 2D plane, enabling the user to analyze the solutions and understand the relationships between different objective function values. + It uses matplotlib to create a scatter plot of the Pareto frontier, with options to annotate the best solutions for better interpretability. + The plot helps in understanding the distribution and density of solutions along the Pareto frontier. + + Args: + dimensions: A tuple indicating the indices of the two objective functions to be plotted. + annotate_best: A boolean indicating whether to annotate the best solutions on the plot. + plot_level: An integer indicating the number of Pareto levels to plot. + filename: The name of the file to save the plot to. If None, the plot is not saved. + save_format: The format to save the plot in (e.g., 'eps', 'png', 'pdf'). + + Returns: + None. The method displays the plot and optionally saves it to a file. + """ #TODO replace latex with Mathtext mpl.rcParams.update(mpl.rcParamsDefault) @@ -93,6 +127,27 @@ def plot_pareto(self, dimensions: tuple = (0, 1), annotate_best=True, plot_level def plot_pareto_mt(self, dimensions: tuple = (0, 1), annotate_best=True, plot_level=1, filename=None, save_format='eps'): + """ + Plots the Pareto frontier for multi-objective optimization results. + + This method visualizes the Pareto frontier levels on a 2D plane, + allowing for analysis of trade-offs between two objectives. It supports + annotating the best solutions and saving the plot to a file. This helps to understand the trade-offs between different objectives when searching for the best equation structure. + + Args: + dimensions: A tuple specifying the indices of the two objective + functions to plot. + annotate_best: A boolean indicating whether to annotate the best + solutions on the plot. + plot_level: An integer specifying the number of Pareto frontier levels + to plot. + filename: A string specifying the filename to save the plot to. If + None, the plot is not saved. + save_format: A string specifying the format to save the plot in. + + Returns: + None. Displays the plot. + """ # Reset to default matplotlib settings mpl.rcParams.update(mpl.rcParamsDefault) plt.rcParams['text.usetex'] = False # Disable LaTeX rendering diff --git a/epde/optimizers/single_criterion/ea_stop_conds.py b/epde/optimizers/single_criterion/ea_stop_conds.py index 63f6f09c..21ca0d02 100644 --- a/epde/optimizers/single_criterion/ea_stop_conds.py +++ b/epde/optimizers/single_criterion/ea_stop_conds.py @@ -12,35 +12,142 @@ class StopCondition(ABC): ''' Abstract class for generalized stop condition for the evolutionary algorithm. - Methods: - reset(): - Reset the object. Mainly useful, if some sort of counter is implemented inside the instance. Returns None. - check() - + Methods: + reset(): + Reset the object. Mainly useful, if some sort of counter is implemented inside the instance. Returns None. + check() ''' + def __init__(self, estrategy): + """ + Initializes the stop condition with a specified evaluation strategy. + + The evaluation strategy determines how the evolutionary process is terminated, + ensuring the search for differential equations concludes based on predefined criteria. + + Args: + estrategy: An object defining the strategy for evaluating when to stop the evolutionary process. + + Returns: + None + """ pass def reset(self): + """ + Resets the internal state of the stop condition. + + This ensures that the search for differential equations starts fresh, + allowing the evolutionary algorithm to explore the solution space + without being biased by previous iterations. + + Args: + self: The StopCondition instance. + + Returns: + None. + """ pass def check(self): + """ + Checks if the stopping criteria for the evolutionary algorithm have been met. + + This method determines whether the search for differential equations should be terminated based on predefined conditions. + + Args: + self: The object instance. + + Returns: + bool: True if the stopping criteria are met, False otherwise. + + Why: + This check is crucial for controlling the duration and efficiency of the equation discovery process. It prevents the algorithm from running indefinitely or prematurely terminating before finding a satisfactory solution. + """ pass class IterationLimit(StopCondition): + """ + A class to limit the number of iterations in a process. + + Class Methods: + - __init__: + """ + def __init__(self, limit = 100): + """ + Initializes the iteration limit with a specified upper bound and a check counter. + + This setup is crucial for controlling the evolutionary search process, preventing it from running indefinitely and ensuring efficient exploration of the solution space. By limiting the number of iterations or checks, the algorithm can focus on promising equation structures and avoid unnecessary computations. + + Args: + limit (int, optional): The maximum number of iterations allowed. Defaults to 100. + + Returns: + None + """ self.limit = limit + 1 self.checks = 0 # shortcut^ checking a number of calls instead of the estrategy attr of some sort def reset(self, **kwargs): + """ + Resets the iteration counter and optionally adjusts the maximum iteration limit. + + This method prepares the iteration limit for a new search by resetting the internal counter + that tracks the number of iterations performed. It also allows for updating the maximum + number of iterations permitted, which is useful for controlling the computational cost + of the search process. This ensures that the equation discovery process can be restarted + or reconfigured with potentially different constraints on the search effort. + + Args: + **kwargs: Keyword arguments. If 'limit' is provided, the maximum iteration limit is updated. + + Returns: + None + """ self.checks = 0 self.limit = kwargs.get('limit', self.limit) def check(self): + """ + Checks if the iteration limit has been reached. + + This method is called to ensure that the search process does not run indefinitely. + It increments the internal counter and returns `True` if the maximum number of iterations + has been exceeded, signaling that the search should be terminated or adjusted. + + Args: + self: The object instance. + + Returns: + bool: True if the iteration limit has been reached, False otherwise. + """ self.checks += 1 return self.checks > self.limit class FitnessStabilized(StopCondition): + """ + A fitness-stabilized evolutionary strategy. + + This class adapts the fitness function during evolution to maintain a stable selection pressure. + """ + def __init__(self, estrategy): + """ + Initializes the FitnessStabilized object. + + This method must be overridden by subclasses to provide a concrete implementation for fitness evaluation within a stabilized evolutionary process. The initialization should set up the fitness evaluation strategy. + + Args: + estrategy: The fitness evaluation strategy to be used. + + Raises: + NotImplementedError: Always raised, indicating that the method must be implemented in a subclass. + + Returns: + None. + + Why: This abstract initialization ensures that each specific fitness stabilization technique defines its own setup, tailoring the fitness evaluation process to its particular needs within the broader equation discovery framework. + """ raise NotImplementedError diff --git a/epde/optimizers/single_criterion/optimizer.py b/epde/optimizers/single_criterion/optimizer.py index 2417c3ba..c44a113e 100644 --- a/epde/optimizers/single_criterion/optimizer.py +++ b/epde/optimizers/single_criterion/optimizer.py @@ -20,30 +20,62 @@ class EvolutionaryStrategy(Strategy): """ - Evolutionary strategy for the single criterion optimization. Includes method ``iteration`` for a single - iteration of the algotirhm & ``run`` for executing a full optimization. - - Attributes: - _stop_criterion (`IteratorLimit`): object for stored the condition for stopping the evolutionary algorithm - run_performed (`bool`): flag about ranning evolutionary strategy - linked_blocks (`LinkedBlocks`): the sequence (not necessarily chain: divergencies can be present) of blocks with evolutionary operators + Evolutionary strategy for single-objective optimization. Includes the `iteration` method for performing a single step of the algorithm and the `run` method for executing a complete optimization process. + + + Attributes: + _stop_criterion (`IteratorLimit`): object for stored the condition for stopping the evolutionary algorithm + run_performed (`bool`): flag about ranning evolutionary strategy + linked_blocks (`LinkedBlocks`): the sequence (not necessarily chain: divergencies can be present) of blocks with evolutionary operators """ + def __init__(self, stop_criterion = IterationLimit, sc_init_kwargs: dict = {'limit' : 50}): + """ + Initializes the EvolutionaryStrategy with a specified stopping criterion. + + This constructor configures the condition that will halt the evolutionary search + for differential equations. The stopping criterion is instantiated based on the + provided class and keyword arguments. A flag is also set to track whether the + evolutionary process has been executed. + + Args: + stop_criterion: The class defining the stopping criterion. Defaults to IterationLimit, + which stops the search after a fixed number of iterations. + sc_init_kwargs: Keyword arguments used to initialize the stop_criterion class. + Defaults to {'limit' : 50}, setting the iteration limit to 50. + + Returns: + None + + Why: + The stopping criterion is essential for controlling the duration of the evolutionary + search. It prevents the algorithm from running indefinitely and allows for balancing + exploration of the search space with computational cost. The default `IterationLimit` + ensures that the search terminates after a predefined number of generations, providing + a basic mechanism for managing the search process. + """ super().__init__() self._stop_criterion = stop_criterion(**sc_init_kwargs) self.run_performed = False def run(self, initial_population: Iterable, EA_kwargs: dict, stop_criterion_params: dict = {}): """ - Running evolutionary strategy for input population - - Args: - intial_population (`Iterable`): population only after initialized - EA_kwargs (`dict`): arguments for evolutionary algoritm - stop_criterion_params (`dict`): parameters for stoping evolutionary - - Returns: - None + Evolves the initial population to discover a differential equation. + + This method iteratively refines a population of candidate equations + using an evolutionary algorithm until a stopping criterion is met. + The evolutionary process involves traversing a sequence of linked + blocks that perform operations such as selection, mutation, and + crossover on the population. The goal is to find an equation that + accurately describes the underlying dynamics of the data. + + Args: + initial_population (Iterable): The initial set of candidate equations. + EA_kwargs (dict): Keyword arguments to be passed to the evolutionary algorithm. + stop_criterion_params (dict): Parameters for the stopping criterion. + + Returns: + None: The method modifies the internal state of the `EvolutionaryStrategy` object. """ self._stop_criterion.reset(**stop_criterion_params) population = initial_population @@ -54,72 +86,208 @@ def run(self, initial_population: Iterable, EA_kwargs: dict, stop_criterion_para class Population(object): """ - Class for keeping population - - Attributes: - population (`list`): list of individs - length (`int`): number of individs in population + Represents a collection of candidate solutions. Facilitates operations such as selection, reproduction, and evaluation, driving the evolutionary search process. Manages the population's state and provides methods for accessing and manipulating its members. + + + Attributes: + population (`list`): list of individs + length (`int`): number of individs in population """ + def __init__(self, elements: list, sorting_method: Callable = simple_sorting): """ + Initializes the population with a set of individuals and a sorting method. + + The population is initialized with a list of individuals, and its length is recorded. + A sorting method is assigned to maintain order based on individual fitness, + facilitating the evolutionary process of selecting better-performing individuals. + Args: - elements (`list`): list of individs - sorting_method (`Callable`): method for sortiing of individs in population + elements (`list`): A list of individuals to form the initial population. + sorting_method (`Callable`): A method used to sort the population based on fitness. Defaults to `simple_sorting`. + + Returns: + None """ self.population = elements self.length = len(elements) self._sorting_method = sorting_method def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Reconstructs a specific attribute of the population's search space manually. + + This method facilitates targeted modifications to the population, allowing for fine-grained control over the evolutionary process. By directly setting the value of an attribute, such as 'vals', the search can be guided towards promising regions of the solution space or to correct deviations introduced by evolutionary operators. This is particularly useful for injecting domain knowledge or recovering from unfavorable population states. Currently, only the 'vals' attribute is supported. + + Args: + attribute: The attribute to reconstruct. Currently, only 'vals' is supported. + value: The new value for the specified attribute. + except_attrs: A dictionary of attributes to exclude during reconstruction. + + Returns: + None. + + Raises: + ValueError: If the specified attribute is not supported. + """ from epde.loader import attrs_from_dict, get_typespec_attrs supported_attrs = ['vals'] if attribute not in supported_attrs: raise ValueError(f'Attribute {attribute} is not supported by manual_reconst method.') def sort(self): - ''' - Method, that returns sorted population of the candidates. - Does not change anything inside the population. - ''' + """ + Sorts the population based on the defined sorting method to prioritize candidates that better represent the underlying dynamics of the system. This is crucial for the evolutionary process, as it allows the algorithm to focus on more promising equation structures. + + Args: + self: The Population object. + + Returns: + A new list containing the sorted population of candidates. The original population remains unchanged. + """ return self._sorting_method(self.population) def sorted(self): - ''' - Method to sort the population. Operates in "in-place" mode. - ''' + """ + Sorts the population based on the fitness of individuals, arranging them in ascending order of error. Operates in "in-place" mode, updating the population directly. This ensures that the evolutionary process prioritizes the selection of better-performing equation candidates for subsequent generations. + + Args: + self: The Population instance. + + Returns: + None. The population is sorted in place. + """ self.population = self.sort() def update(self, point): """ - Adding new candidate to population + Adds a new candidate solution to the population. + + This expands the search space by incorporating a new potential solution + into the existing set of candidates, allowing the evolutionary algorithm + to explore a wider range of possibilities when searching for the + optimal equation. + + Args: + point: The candidate solution (equation representation) to add to the population. + + Returns: + None """ self.population.append(point) def delete_point(self, point): """ - Deleting specified candidate solution + Deletes a specified candidate solution from the population. This is necessary to maintain diversity and improve the overall quality of the population during the evolutionary process of discovering differential equations. By removing less promising solutions, the algorithm can focus on exploring more promising areas of the search space. + + Args: + point: The candidate solution to be removed from the population. + + Returns: + None """ self.population = [solution for solution in self.population if solution != point] def get_stats(self): + """ + Calculates the objective function values for each individual in the population. + + This is a crucial step in evaluating the fitness of candidate equation structures + within the evolutionary process. By calculating these values, the algorithm + can assess how well each equation represents the underlying dynamics of the + system being modeled. + + Args: + self: The Population instance. + + Returns: + np.ndarray: A NumPy array containing the objective function values for each + individual in the population. + """ return np.array([element.obj_fun for element in self.population]) def __setitem__(self, key, value): + """ + Sets an individual within the population. + + This method allows you to directly modify or add an individual (equation) to the population. + This is useful for injecting specific equations or modifying existing ones during the evolutionary process. + + Args: + key: The index or identifier of the individual in the population. + value: The individual (equation) to be stored in the population. + + Returns: + None + """ self.population[key] = value def __getitem__(self, key): + """ + Retrieves an individual from the population. + + This allows accessing and utilizing specific candidate solutions + within the evolutionary process. + + Args: + key: The index of the individual to retrieve. + + Returns: + The individual at the given index in the population. + """ return self.population[key] def __iter__(self): + """ + Returns an iterator for the population. + + This allows to traverse individuals in the population, enabling the evolutionary process to evaluate and evolve the population towards better solutions. + + Returns: + PopulationIterator: An iterator object for traversing the individuals in the population. + """ return PopulationIterator(self) class PopulationIterator(object): + """ + An iterator for traversing a population. + + Class Methods: + - __init__: + """ + def __init__(self, population): + """ + Initializes the iterator with a population. + + This iterator is designed to traverse a population, providing access to individual members during the evolutionary process. The index is initialized to 0 to start at the beginning of the population. + + Args: + population: The initial population to iterate over. + + Returns: + None + """ self._population = population self._idx = 0 def __next__(self): + """ + Retrieves the next individual from the population to be evaluated. + + This allows iterating through the population, providing individuals + for fitness evaluation and subsequent evolutionary operations. + + Args: + self: The PopulationIterator instance. + + Returns: + The next individual from the population. + + Raises: + StopIteration: If all individuals in the population have been processed. + """ if self._idx < len(self._population.population): res = self._population.population[self._idx] self._idx += 1 @@ -132,8 +300,56 @@ class SimpleOptimizer(object): """ """ + def __init__(self, population_instruct, pop_size, solution_params, sorting_method = simple_sorting, passed_population: Union[Population, List] = None): + """ + Initializes a new Population object for equation discovery. + + This constructor creates a population of candidate equation solutions. It either + generates a new population from scratch using a `SystemsPopulationConstructor` + or initializes it with a pre-existing `Population` object or a list of solutions. + When creating a new population, the constructor ensures that each generated + solution is unique within the population to promote diversity in the search space. + This diversity is crucial for the evolutionary process to effectively explore + potential equation structures. + + Args: + population_instruct: Instructions for the `SystemsPopulationConstructor`. + This argument is passed directly to the `SystemsPopulationConstructor` + to configure how individual equation structures are generated. For + example, it specifies the pool of potential equation terms and other + parameters influencing the structure of the generated equations. + pop_size: The desired size of the population, determining the number of + candidate equation solutions to be maintained. + solution_params: A dictionary of parameters passed to the + `SystemsPopulationConstructor.create()` method when generating new + solutions. These parameters can influence the specific form of the + generated equations. If None, an empty dictionary is used. + sorting_method: The method used to sort the population based on the + performance of the candidate equations. Defaults to `simple_sorting`. + passed_population: An optional `Population` object or a list of solutions + to initialize the population with. If None, a new population is + created. If a list, it's used as the initial population. If a + `Population` object, it's used directly as the population. + + Raises: + TypeError: If `passed_population` is not None, a list, or a `Population` + object. + RuntimeError: If the constructor fails to create a unique solution + after many attempts, indicating potential issues with the solution + generation process or parameter settings. + AssertionError: If `solution_params` is not None or a dictionary. + + Returns: + None + + Fields: + population (Population): The `Population` object representing the + collection of candidate equation solutions. It is initialized either + with newly generated solutions or with the `passed_population` if + provided. + """ soluton_creation_attempts_softmax = 10 soluton_creation_attempts_hardmax = 100 @@ -168,11 +384,43 @@ def __init__(self, population_instruct, pop_size, solution_params, sorting_metho self.population = passed_population def set_strategy(self, strategy_director): + """ + Sets the optimization strategy. + + This method configures the optimization strategy based on the provided + strategy director. It utilizes the director's builder to assemble + the strategy and then assigns the resulting processor to the object's + strategy attribute. This allows the optimizer to adapt its behavior + based on the specific problem and data characteristics, enabling + the discovery of governing equations. + + Args: + strategy_director: The director responsible for building the strategy. + + Returns: + None. This method does not return any value. + """ builder = strategy_director.builder builder.assemble(True) self.strategy = builder.processer def optimize(self, EA_kwargs: dict = {}, epochs: int = None): + """ + Optimizes the population by iteratively refining equation candidates using an evolutionary strategy. + + This method drives the search for the best differential equation model by + evolving a population of candidate equations. It leverages the configured + evolutionary strategy to explore the search space and improve the + population's fitness based on how well the equations fit the data. The global history is reset to ensure a fresh optimization run. + + Args: + EA_kwargs: Keyword arguments to be passed to the evolutionary algorithm's run method, + allowing for customization of the evolutionary process. + epochs: The maximum number of epochs to run the optimization for. If None, a default limit of 50 is used. + + Returns: + None + """ scp = {'limit' : epochs} if epochs is not None else {'limit' : 50} global_var.reset_hist() diff --git a/epde/optimizers/single_criterion/population_constr.py b/epde/optimizers/single_criterion/population_constr.py index b1d75bca..2a107d08 100644 --- a/epde/optimizers/single_criterion/population_constr.py +++ b/epde/optimizers/single_criterion/population_constr.py @@ -12,8 +12,58 @@ from epde.structure.main_structures import SoEq class SystemsPopulationConstructor(object): + """ + Constructs populations of systems of equations. + + This class is responsible for creating and managing populations of systems of equations, + allowing for the configuration of equation sparsity, number of terms, and other parameters. + + Methods: + - __init__ + - create + + Attributes: + pool: The pool of families. + terms_number: The number of terms. + max_factors_in_term: The maximum number of factors in a term. + vars_demand_equation: A set of variables from the families' demand equations. + sparsity_interval: The sparsity interval. + + Class Methods: + - __init__: + """ + def __init__(self, pool, terms_number : int = 8, max_factors_in_term : int = 2, obj_functions : Callable = None, sparsity_interval : tuple = (1., 1.)): + """ + Initializes the SystemsPopulationConstructor. + + This constructor sets up the necessary components for generating a population of equation system candidates. + It prepares the search space by extracting variables from the provided pool of equation families and + configuring the complexity constraints for the equation terms. The goal is to create a diverse initial + population that can be evolved to discover the underlying system of equations. + + Args: + pool: A pool of families, each representing a potential building block for the equation system. + terms_number: The number of terms in each equation candidate. Defaults to 8. + max_factors_in_term: The maximum number of factors allowed in a single term. Defaults to 2. + obj_functions: Objective functions (not used). Defaults to None. + sparsity_interval: The sparsity interval. Defaults to (1., 1.). + + Raises: + ValueError: If the pool contains demand equations with more than one variable, indicating an attempt + to discover a system of equations using a single-criterion optimization setup, which is not supported. + + Returns: + None. + + Fields: + pool: The pool of families. + terms_number: The number of terms. + max_factors_in_term: The maximum number of factors in a term. + vars_demand_equation: A set of variables from the families' demand equations. + sparsity_interval: The sparsity interval. + """ if sparsity_interval[0] != sparsity_interval[1]: warn(message = 'Single criterion optimization requires use of fixed sparsity constant. \ The right boundary of iterval will be used the value') @@ -29,6 +79,35 @@ def __init__(self, pool, terms_number : int = 8, max_factors_in_term : int = 2, self.sparsity_interval = sparsity_interval def create(self, **kwargs): + """ + Creates a new equation (SoEq) tailored for the evolutionary search process. + + This method instantiates a `SoEq` object, configuring its search space + based on provided parameters like sparsity and complexity (number of terms, + factors per term). These parameters define the landscape within which the + evolutionary algorithm will explore potential equation solutions. The method + also sets the objective functions that guide the search towards optimal + equation discovery. + + Args: + **kwargs: Keyword arguments to customize equation parameters. + Possible keys include: + - sparsity (float): Sparsity value influencing equation complexity. + - terms_number (int): Number of terms in the equation. + - max_factors_in_term (int): Maximum number of factors per term. + - obj_funs (list): Objective functions to evaluate equation fitness. + + Returns: + SoEq: The newly created `SoEq` object, ready for evolutionary search. + + Class Fields (initialized in `SoEq.__init__`): + - obj_funs (list): Objective functions for the equation. Initialized to None. + - metaparameters (dict): Metaparameters dictionary for the search. Key - label of the parameter (e.g. 'sparsity'), value - tuple, containing flag for metaoptimization and initial value. + - tokens_for_eq (TFPool): Pool, containing token families for the equation search algorithm. + - tokens_supp (TFPool): Pool, containing token families for the equationless part. + - moeadd_set (bool): Flag indicating if the MOEADD solution is set. Initialized to False. + - vars_to_describe (list): List of variables to describe in the equation. + """ sparsity = kwargs.get('sparsity', self.sparsity_interval[1]) terms_number = kwargs.get('terms_number', self.terms_number) max_factors_in_term = kwargs.get('max_factors_in_term', self.max_factors_in_term) diff --git a/epde/optimizers/single_criterion/strategy.py b/epde/optimizers/single_criterion/strategy.py index 58bc578e..c0de932e 100644 --- a/epde/optimizers/single_criterion/strategy.py +++ b/epde/optimizers/single_criterion/strategy.py @@ -15,11 +15,56 @@ from epde.operators.singleobjective.so_specific import SizeRestriction, FractionElitism class BaselineDirector(OptimizationPatternDirector): + """ + Directs the construction of a baseline evolutionary strategy. + + This class uses a StrategyBuilder to assemble a basic evolutionary algorithm + pipeline with selection, variation, mutation, fitness evaluation, and elitism. + + Methods: + - use_baseline + """ + def __init__(self) -> None: + """ + Initializes the BaselineDirector, preparing it to construct evolutionary strategies. + + This method sets up the director by initializing a `StrategyBuilder` + with the `EvolutionaryStrategy` class. This configuration ensures that + the director is ready to guide the construction of specific evolutionary + strategies tailored for equation discovery. + + Args: + self: The object instance. + + Returns: + None. + """ super().__init__() self.builder = StrategyBuilder(EvolutionaryStrategy) def use_baseline(self, params: dict, **kwargs): + """ + Configures the evolutionary strategy builder with a set of core operators for equation discovery. + + This method establishes a foundational sequence of operators essential for evolving equation structures. + It includes selection, variation, mutation, fitness evaluation, and elitism, configuring the `self.builder` + to form an evolutionary pipeline. Parameters for these operators are drawn from the input `params` dictionary. + This baseline configuration provides a solid starting point for the evolutionary search process, ensuring + that key steps for generating and evaluating candidate equations are in place. + + Args: + params: A dictionary containing parameters for the operators. It expects + 'variation_params' and 'mutation_params' keys, which are dictionaries + themselves, holding parameters for the variation and mutation operators, + respectively. + **kwargs: Arbitrary keyword arguments. These arguments are passed to the + `add_base_param_to_operator` function via `add_kwarg_to_operator` partial + function, which adds them as default parameters to the operators. + + Returns: + None. The method modifies the `self.builder` attribute in place. + """ variation_params = params.get('variation_params', {}) mutation_params = params.get('mutation_params', {}) diff --git a/epde/optimizers/strategy.py b/epde/optimizers/strategy.py index 3b9ea0a8..011d7950 100644 --- a/epde/optimizers/strategy.py +++ b/epde/optimizers/strategy.py @@ -14,18 +14,44 @@ class Strategy(): """ - Class instance of base for create evolution strategy - - Attributes: - suppress_structure_check (`boolean`): flag about checking of structure - blocks (`dict`): dictionary with operators for the strategy - linked_blocks (`LinkedBlocks`): the sequence (not necessarily chain: divergencies can be present) - of blocks with evolutionary operators - run_performed (`boolean`): flag, that strategy was running (exclude the possibility of taking a - result from a strategy that has not been applied) - + Base class for defining evolutionary strategies. It provides a foundation for implementing custom search algorithms for optimization problems. Subclasses should implement specific evolutionary operators and selection mechanisms to guide the search process. + + + Attributes: + suppress_structure_check (`boolean`): flag about checking of structure + blocks (`dict`): dictionary with operators for the strategy + linked_blocks (`LinkedBlocks`): the sequence (not necessarily chain: divergencies can be present) + of blocks with evolutionary operators + run_performed (`boolean`): flag, that strategy was running (exclude the possibility of taking a + result from a strategy that has not been applied) """ + def __init__(self): + """ + Initializes a new instance of the Strategy class. + + This method sets up the internal data structures required for managing + the search process. It initializes the block storage for equation components, + flags for controlling structure checks, and run status. + + Args: + self: The object instance. + + Fields: + _blocks (dict): A dictionary to store blocks, where keys are block identifiers and values are block objects. + _linked_blocks (None): Placeholder for linked block information, initially set to None. + suppress_structure_check (bool): A flag to suppress structure checks, initially set to True. + run_performed (bool): A flag indicating whether a run has been performed, initially set to False. + + Returns: + None. + + Why: + This initialization ensures that the search strategy starts with a clean slate, + ready to explore the space of possible equation structures. The flags allow + for controlling the level of validation during the search, and the block + storage will hold the building blocks of the discovered equations. + """ self._blocks = dict() self._linked_blocks = None self.suppress_structure_check = True @@ -35,14 +61,29 @@ def __init__(self): def create_linked_blocks(self, blocks = None, suppress_structure_check = False): """ - Creating sequence of `blocks` with evolution operators - - Args: - blocks (`dict`): dictionary with names and values of the evolution operator - suppress_structure_check (`boolean`): flag about checking of structure + Creates a sequence of linked evolutionary algorithm building blocks. - Returns: - None + This method arranges the provided `blocks` into a sequential structure, + preparing them for execution within the evolutionary algorithm. It also + handles checks for structural integrity (unless suppressed) to ensure + the proper functioning of the evolutionary strategy. + + Args: + blocks (`dict`, optional): A dictionary where keys are symbolic names + (e.g., "crossover", "mutation") and values are the corresponding + evolutionary operator blocks. If `None`, the blocks defined within + the strategy are used. + suppress_structure_check (`bool`, optional): A flag to disable structural + integrity checks. Defaults to `False`. Disabling this is not + recommended, as it can lead to unexpected behavior if the blocks + are not correctly configured. + + Returns: + None + + Raises: + ValueError: If `blocks` is `None` and the strategy has no defined blocks. + TypeError: If `blocks` is not a dictionary. """ self.suppress_structure_check = suppress_structure_check if self.suppress_structure_check and global_var.verbose.show_warnings: @@ -58,7 +99,23 @@ def create_linked_blocks(self, blocks = None, suppress_structure_check = False): def check_integrity(self): """ - Method for checking type on blocks of evolutional operators + Verifies the structural integrity of the evolutionary operator blocks. + + This method ensures that the blocks of evolutionary operators are correctly + structured and of the expected types before the evolutionary process begins. + It validates that the `blocks` attribute is a dictionary and the `linked_blocks` + attribute is a `LinkedBlocks` object, raising a TypeError if either condition is not met. + This is crucial for the correct functioning of the evolutionary algorithm in discovering + differential equations. + + Args: + self: The Strategy instance. + + Returns: + None + + Raises: + TypeError: If `self.blocks` is not a dictionary or `self.linked_blocks` is not a `LinkedBlocks` instance. """ if not isinstance(self.blocks, dict): raise TypeError('Blocks object must be of type dict with key - symbolic name of the block (e.g. "crossover", or "mutation", and value of type block') @@ -67,12 +124,15 @@ def check_integrity(self): def apply_block(self, label, operator_kwargs): """ - Running process of choosing block evolutionary operator - + Applies a specific evolutionary operator to a block within the equation discovery strategy. + + This method retrieves a labeled block and executes its associated evolutionary operator with the provided arguments. + This step is crucial for evolving the structure and parameters of the equation being discovered. + Args: - label (`str`): name of evolutional operator - operator_kwargs (`dict`): dictionary with argumenta of EA, where keys are names and items are values of the arguments - + label (`str`): The label of the block to which the operator will be applied. + operator_kwargs (`dict`): Keyword arguments to be passed to the evolutionary operator's `apply` method. + Returns: None """ @@ -80,6 +140,20 @@ def apply_block(self, label, operator_kwargs): @property def result(self): + """ + Return the final result produced by the strategy after the computation. + + This property provides access to the processed output, ensuring that the strategy has been successfully executed. It acts as the endpoint for retrieving the derived insights or predictions. + + Args: + None + + Returns: + The output of the linked blocks, representing the final result of the strategy. + + Raises: + ValueError: If the strategy has not been run yet, indicating that the output is not yet available. + """ if not self.run_performed: raise ValueError('Trying to get the output of the strategy before running it.') return self.linked_blocks.output diff --git a/epde/parametric/parametric_eq_translator.py b/epde/parametric/parametric_eq_translator.py index 3859163a..22713e88 100644 --- a/epde/parametric/parametric_eq_translator.py +++ b/epde/parametric/parametric_eq_translator.py @@ -5,6 +5,19 @@ def float_convertable(obj): + """ + Determines if a given object can be interpreted as a floating-point number. + + This function is crucial for ensuring data compatibility when constructing equation terms. + It verifies whether a value can be safely converted to a float, preventing errors during + equation evaluation and optimization. + + Args: + obj: The object to be checked for float convertibility. + + Returns: + bool: True if the object can be converted to a float without error, False otherwise. + """ try: float(obj) return True @@ -13,6 +26,22 @@ def float_convertable(obj): def parse_params_str(param_str): + """ + Parses a comma-separated string of parameter-value pairs into a dictionary and a list of parameters to optimize. + + This function is crucial for configuring the search space of the evolutionary algorithm. It takes a string + defining parameter values, identifies which parameters should be optimized, and prepares the data for + the equation discovery process. + + Args: + param_str (str): A string containing comma-separated parameters in the format "param1:value1,param2:value2,...". + If a value is "None", the corresponding parameter will be marked for optimization. + + Returns: + tuple: A tuple containing: + - dict: A dictionary where keys are parameter names and values are their corresponding numerical values (int or float). + - list: A list of parameter names that should be optimized (i.e., those with a value of "None" in the input string). + """ assert isinstance( param_str, str), 'Passed parameters are not in string format' params_split = param_str.split(',') @@ -33,6 +62,25 @@ def parse_params_str(param_str): def parse_parametric_factor(factor_form: str, pool): + """ + Parses a factor string, extracting its label and parameters. + + This function is crucial for interpreting the structure of factors, + determining if they contain parameters that need to be optimized during + the equation discovery process. It breaks down the factor string into + its constituent parts, preparing it for evaluation and optimization. + + Args: + factor_form: The string representation of the factor, e.g., "factor_label{param1=value1,param2=value2}". + pool: Unused parameter. + + Returns: + A tuple containing: + - A boolean indicating whether the factor is parametric (contains parameters to optimize). + - The label of the factor (string). + - A dictionary of parameter names and their values (dict). + - A list of parameter names to optimize (list). + """ label_str, params_str = tuple(factor_form.split('{')) if not '}' in params_str: raise ValueError( @@ -46,11 +94,21 @@ def parse_parametric_factor(factor_form: str, pool): def parse_parametric_equation(text_form): ''' - - Example input: '0.0 * d^3u/dx2^3{power: 1} * du/dx2{power: 1} + 0.0 * d^3u/dx1^3{power: 1} + - 0.015167810810763344 * d^2u/dx1^2{power: 1} + 0.0 * d^3u/dx2^3{power: 1} + 0.0 * du/dx2{power: 1} + - 4.261009307104081e-07 = d^2u/dx1^2{power: 1} * du/dx1{power: 1}' - + Parses a parametric equation represented as a string into a structured format. + + The equation is split into left-hand side (LHS) and right-hand side (RHS) components, + then further broken down into terms and factors. This structured representation + facilitates subsequent symbolic manipulation and numerical evaluation. + + Args: + text_form (str): A string representing the parametric equation, with terms separated by ' + ' + on the LHS and factors separated by ' * ' on both sides. The LHS and RHS + are separated by ' = '. + + Returns: + list: A list containing the parsed terms from the LHS and RHS of the equation. + Each term is further split into its constituent factors. The last element of the list + is the RHS of the equation. ''' left, right = text_form.split(' = ') left = left.split(' + ') @@ -63,6 +121,28 @@ def parse_parametric_equation(text_form): def construct_parametric_factor(label, param_equality, params_to_opt, status=None, family_type='const', params_description={'power': (1, 1)}): + """ + Constructs a parametric factor for equation discovery. + + This method creates a ParametricFactor object, a fundamental building block + in the equation discovery process. It encapsulates a potential term within + a differential equation, defining its label, parameter constraints, and + optimization settings. The factor is initialized with a status dictionary + to track its relevance and characteristics during the search for the best + equation structure. This ensures that the evolutionary algorithm can + effectively explore the space of possible equations. + + Args: + label: The label for the parametric factor. + param_equality: Equality ranges for the parameters. + params_to_opt: Parameters to be optimized. + status: A dictionary to store the status of the factor (optional). + family_type: The type of the factor family (default: 'const'). + params_description: Description of the parameters (default: {'power': (1, 1)}). + + Returns: + ParametricFactor: The constructed parametric factor object. + """ if status is None: status['meaningful'] = False status['s_and_d_merged'] = True @@ -77,6 +157,27 @@ def construct_parametric_factor(label, param_equality, params_to_opt, def construct_ordinary_factor(label, param_equality, status=None, family_type='const', params_description={'power': (1, 1)}): + """ + Constructs an ordinary factor object for equation discovery. + + This method creates a Factor object with specified properties, which represents a building block + within a differential equation. It initializes the factor's token name, status, family type, + parameter descriptions, derivative code, and equality ranges. If the status is not provided, + it initializes a default status dictionary. This ensures that each factor has a consistent + and well-defined state, which is crucial for the evolutionary search process. + + Args: + label (str): The label or name of the factor, representing a term in the equation. + param_equality (list): The equality ranges for the factor's parameters, defining constraints + on the parameter values during the search. + status (dict, optional): A dictionary containing status flags for the factor. Defaults to None. + family_type (str, optional): The family type of the factor. Defaults to 'const'. + params_description (dict, optional): A dictionary describing the parameters of the factor. + Defaults to {'power': (1, 1)}. + + Returns: + Factor: The constructed Factor object, ready to be used in the equation discovery process. + """ if status is None: status['meaningful'] = False status['s_and_d_merged'] = True @@ -90,6 +191,26 @@ def construct_ordinary_factor(label, param_equality, status=None, family_type='c def optimize_parametric_form(terms: list, pool, method='L-BFGS-B', **kwargs): + """ + Optimizes a parametric form equation within the equation discovery process. + + This method takes a list of terms, each containing factors, and constructs a + `ParametricEquation`. It distinguishes between parametric and defined factors, + using information from the provided pool to instantiate them. The resulting + equation is then optimized to find the best fit to the data. + + Args: + terms: A list of terms, where each term is a list of factors represented as strings. + pool: A pool object containing information about factor families, including evaluators and parameter constraints. + method (str): Optimization method to use (default: 'L-BFGS-B'). + **kwargs: Additional keyword arguments passed to the optimization routine, such as initial parameter values. + + Returns: + ParametricEquation: The optimized parametric equation, ready for evaluation and analysis. + + WHY: This optimization step is crucial for refining the equation structure and parameter values, + ensuring that the discovered equation accurately represents the underlying dynamics of the system. + """ assert all([isinstance(term_form, list) for term_form in terms]) # factor_constructor = partial(construct_parametric_factor, param_equality = kwargs['param_equality']), diff --git a/epde/parametric/parametric_equation.py b/epde/parametric/parametric_equation.py index b1232daf..5208dddd 100644 --- a/epde/parametric/parametric_equation.py +++ b/epde/parametric/parametric_equation.py @@ -13,7 +13,61 @@ class ParametricEquation(object): + """ + Represents a parametric equation with optimizable terms. + + This class manages a collection of terms in an equation, allowing for + optimization of their parameters to fit a desired outcome. It handles + parameter mapping, equation evaluation, and gradient calculation. + + Class Methods: + - __init__ + - optimize_equations + - parse_eq_terms + - parse_opt_params + - set_term_params + - evaluate_with_params + - evaluate_grad + - equation + - set_equation + - get_term_for_param + - _ + - _ + + Attributes: + - pool: The pool associated with the object. + - terms: A list or tuple of terms. + - total_params_count: A list containing the number of optimizable parameters + for each term in `terms`. + - param_term_beloning: A dictionary mapping a global parameter index to a + tuple containing the index of the term it belongs to and its local + index within that term. + - rpi: The index of the right part. + - _optimization_held: A boolean flag indicating whether optimization is held. + """ + def __init__(self, pool, terms: Union[list, tuple], right_part_index: int = -1): + """ + Initializes a new instance of the ParametricEquation class. + + This method prepares the equation structure for the search process. It takes a pool of equation components (terms), calculates the number of optimizable parameters within each term, and establishes a mapping between global parameter indices and their corresponding term and local index. This mapping is crucial for efficiently managing and optimizing the equation's parameters during the equation discovery process. + + Args: + pool: The pool associated with the object. + terms: A list or tuple of terms representing the equation's components. + right_part_index: An optional index for the right-hand side term. If negative, it's calculated as an offset from the end of the terms list. Defaults to -1. + + Fields: + pool: The pool associated with the object. + terms: A list or tuple of terms. + total_params_count: A list containing the number of optimizable parameters for each term in `terms`. + param_term_beloning: A dictionary mapping a global parameter index to a tuple containing the index of the term it belongs to and its local index within that term. + rpi: The index of the right part. + _optimization_held: A boolean flag indicating whether optimization is held. Initialized to False. + + Returns: + None + """ self.pool = pool self.terms = terms @@ -33,6 +87,27 @@ def __init__(self, pool, terms: Union[list, tuple], right_part_index: int = -1): self._optimization_held = False def optimize_equations(self, initial_params=None, method='L-BFGS-B'): + """ + Optimizes the equation parameters to best represent the underlying dynamics of the system. + + This method employs optimization algorithms to fine-tune the parameters + within the equation terms. By minimizing the discrepancy between the + equation's output and the observed data, it seeks to accurately + capture the system's behavior. This is crucial for identifying the + governing equations from data. + + Args: + initial_params (np.ndarray, optional): Initial values for the parameters. + If None, defaults to an array of zeros. + method (str, optional): The optimization method to use. + Currently supports 'L-BFGS-B' and 'GD' (Gradient Descent). + Defaults to 'L-BFGS-B'. + + Returns: + None. The method updates the internal parameters of the equation terms + with the optimized values, refining the equation's representation + of the system. + """ def opt_func(params, *variables): ''' @@ -86,6 +161,24 @@ def opt_grd(params): return opt_fun_grad(params, optimizational_copy) self.equation_set = False def parse_eq_terms(self): + """ + Parses the equation terms to extract weights and equivalent terms. + + This method iterates through the terms, extracts the weight and + equivalent term for each, and compiles them into separate lists. + The weight for the right-hand side term is explicitly set to 0. + This is a crucial step in preparing the equation for further analysis + and processing within the EPDE framework, ensuring that each term + contributes correctly to the overall equation structure. + + Args: + self: The instance of the class. + + Returns: + tuple[list[float], list[str]]: A tuple containing two lists: + - weights: A list of numerical weights corresponding to each term. + - equation_term: A list of equivalent terms. + """ weights = [] equation_term = [] for idx, term in enumerate(self.terms): @@ -98,6 +191,23 @@ def parse_eq_terms(self): return weights, equation_term def parse_opt_params(self, params): + """ + Parses and organizes optional parameters for each term in the equation. + + This method iterates through the terms defined in the parametric equation, + extracting and assigning the appropriate number of optional parameters + from the provided list to each term. This ensures that each term + has the correct parameter set for evaluation and contributes accurately + to the overall equation. + + Args: + params: A list of numerical parameters to be parsed and assigned to the terms. + + Returns: + OrderedDict: An ordered dictionary where keys are term IDs and + values are the parsed optional parameters for each term. The order + matches the order of terms in the equation. + """ params_parsed = OrderedDict() cur_idx = 0 for term in self.terms: @@ -107,11 +217,40 @@ def parse_opt_params(self, params): return params_parsed def set_term_params(self, params): + """ + Sets the parameters for each term in the equation model. + + The method iterates through the terms of the equation and applies the corresponding optimized parameters, ensuring each term contributes appropriately to the overall equation's behavior. This step is crucial for refining the equation's fit to the observed data. + + Args: + params (dict): A dictionary where keys are term IDs and values are dictionaries of optimized parameters for that term. + + Returns: + None + """ params_parsed = self.parse_opt_params(params) for term in self.terms: term.use_params(params_parsed[term.term_id]) def evaluate_with_params(self, params): + """ + Evaluates the parametric equation with the provided parameters. + + This method sets the parameters for each term in the equation and then + calculates the overall value. It achieves this by summing the evaluated + values of all terms, excluding the term at the reduction point index (`rpi`), + and subsequently subtracting the evaluated value of the term at `rpi`. + This approach allows to find the equation that best describes the data by + comparing the result to the target value, effectively minimizing the error + introduced by the equation's structure and parameters. + + Args: + params (list): A list of parameters to be assigned to each term in the equation. + + Returns: + float: The result of the evaluation, representing the difference between the sum of + the terms (excluding the reduction point term) and the reduction point term's value. + """ self.set_term_params(params) if self.rpi < 0: val1 = np.add.reduce([term.evaluate() for term_idx, term in enumerate(self.terms) @@ -123,6 +262,21 @@ def evaluate_with_params(self, params): return val1 - val2 def evaluate_grad(self, params, param_in_term_props): + """ + Evaluates the gradient of the equation with respect to a specific parameter. + + This is crucial for optimization algorithms used to fit the equation to the data. + By calculating the gradient, we determine how much the equation's output changes + with respect to a small change in the parameter's value, guiding the optimization + process towards the best parameter values. + + Args: + params: A dictionary of parameter values. + param_in_term_props: A tuple containing the index of the term and the index of the parameter within that term. + + Returns: + The gradient of the equation with respect to the specified parameter. + """ param_label = self.terms[param_in_term_props[0]].all_params[param_in_term_props[1]] param_grad_vals = 2 * self.evaluate_with_params(params) * \ self.terms[param_in_term_props[0]].evaluate_grad(param_label).reshape(-1) @@ -136,6 +290,24 @@ def evaluate_grad(self, params, param_in_term_props): @property def equation(self): + """ + Return the equation representing the discovered differential equation. + + This property provides access to the equation that has been identified as the best fit for the data. + If the optimization process is still active and the equation hasn't been explicitly set, it automatically + constructs the equation from the current state of the optimization. This ensures that the most up-to-date + equation is always available during the discovery process. + + Args: + self: The object instance. + + Returns: + str: The equation of the objective function. + + Raises: + AttributeError: If equation terms have not been initialized before calling, + indicating that the equation discovery process hasn't started. + """ if self._optimization_held: if not self.equation_set: self.set_equation() @@ -145,6 +317,33 @@ def equation(self): 'Equation terms have not been initialized before calling.') def set_equation(self, rpi: int = -1): + """ + Parses and sets the equation for the current object, preparing it for symbolic regression. + + This method orchestrates the creation of an `Equation` object by parsing the equation's terms, + setting the index of the term to be explained (target), and initializing the weights associated with each term. + This setup is crucial for the subsequent steps of symbolic regression, where the equation's structure and weights + are refined to best fit the observed data. + + Args: + rpi (int): Index of the target term. If -1, the last term is used as the target. + + Returns: + None + + Initializes: + _equation (Equation): An `Equation` object representing the equation. + It is initialized with the pool, basic structure (terms), + number of terms, and maximum factors in a term. + equation_set (bool): A boolean flag indicating whether the equation has been set. + It is set to True after the equation is created and configured. + _equation.target_idx (int): Index of the target term in the equation. + It is set to `rpi` if `rpi` is non-negative, otherwise it is set to the index of the last term. + _equation.weights_internal (np.ndarray): Internal weights of the equation terms. + Initialized with the parsed weights. + _equation.weights_final (np.ndarray): Final weights of the equation terms. + Initialized with the parsed weights. + """ weights, terms = self.parse_eq_terms() self._equation = Equation(pool=self.pool, basic_structure=terms, terms_number=len(terms), max_factors_in_term=max([len(term.structure) for term in terms])) @@ -155,14 +354,58 @@ def set_equation(self, rpi: int = -1): @singledispatchmethod def get_term_for_param(self, param): + """ + Retrieves the symbolic term associated with a given parameter. + + This method serves as a base implementation that raises a NotImplementedError. + Subclasses should override this method to define how a symbolic term + is obtained based on the provided parameter. This is essential for + constructing equation candidates during the equation discovery process. + + Args: + param: The parameter (index or label) for which to retrieve the term. + + Returns: + The symbolic term associated with the parameter. + + Raises: + NotImplementedError: Always raised, indicating that the + method must be implemented by a subclass to provide + specific term retrieval logic. This ensures that the + equation discovery process can correctly build and evaluate + equation candidates. + """ raise NotImplementedError( 'The term must be called by parameter index or label') @get_term_for_param.register def _(self, param: str): + """ + Finds the first term in the equation that contains the given parameter. + This is used to locate specific parts of the equation for manipulation or analysis. + + Args: + param: The string to search for within the terms of the equation. + + Returns: + str: The first term in the equation that contains the parameter. + """ term_index = [idx for idx, term in enumerate(self.terms) if param in term][0] return self.terms[term_index] @get_term_for_param.register def _(self, param: int): + """ + Retrieves a term relevant to the equation based on the provided parameter index. + + Args: + param (int): An integer representing the index of the parameter. + + Returns: + The term in the equation corresponding to the given parameter index. + + Why: + This method is used to access specific terms within the discovered equation based on the parameter index. + It allows the system to dynamically retrieve and manipulate equation components during the evolutionary process. + """ return self.terms[self.param_term_beloning[param]] diff --git a/epde/parametric/parametric_factor.py b/epde/parametric/parametric_factor.py index f386c4a3..d94c0668 100644 --- a/epde/parametric/parametric_factor.py +++ b/epde/parametric/parametric_factor.py @@ -13,6 +13,34 @@ class ParametricFactor(Factor): + """ + Represents a parametric factor with optimizable parameters. + + This class provides a base for factors that depend on parameters + which can be optimized. It includes functionalities for managing + these parameters, setting their values, and evaluating gradients. + + Class Methods: + - __deepcopy__ + - set_grad_evaluator + - grad_cache_label + - required_params + - __contains__ + - use_params + - set_defined_params + - reset_saved_state + - eval_grad + + Attributes: + - params_defined: A flag indicating whether parameters are defined (initialized to False). + - params_to_optimize: Parameters to be optimized, initialized with the value of the `params_to_optimize` argument. + - params_description: A dictionary containing parameter descriptions, where keys are parameter indices and values are dictionaries with 'name' and 'bounds'. + - params_description_odict: Original parameter descriptions (copy of params_description). + - equality_ranges: Equality ranges, initialized with the value of the `equality_ranges` argument. + - defined_params_passed: A flag indicating whether defined parameters have been passed (initialized to False). + - params_predefined: A dictionary to store predefined parameters (initialized as an empty dictionary). + """ + __slot__ = ['_params', '_params_description', '_hash_val', 'label', 'type', 'grid_set', 'grid_idx', 'is_deriv', 'deriv_code', 'cache_linked', '_status', 'equality_ranges', '_evaluator', 'saved', @@ -20,6 +48,36 @@ class ParametricFactor(Factor): def __init__(self, token_name: str, status: dict, family_type: str, params_description=None, params_to_optimize=None, deriv_code=None, equality_ranges=None): + """ + Initializes a ParametricFactor object, preparing it for equation discovery by defining its parameters. + + This constructor sets up the factor with its token name, status, family type, and most importantly, + parameter descriptions and optimization settings. It also resets the saved state to ensure a clean + slate for each new equation discovery attempt. The parameter descriptions are converted into a numerical + index-based format for efficient handling during the evolutionary search. This setup is crucial for + exploring the space of possible equation structures and parameter values. + + Args: + token_name (str): The name of the token representing this factor in the equation. + status (dict): A dictionary containing the status of the token. + family_type (str): The family type of the token. + params_description (dict): A dictionary describing the parameters, where keys are parameter names and values are dictionaries with 'name' and 'bounds'. + params_to_optimize (any): Parameters to be optimized during the equation discovery process. + deriv_code (any): Derivative code associated with this factor. + equality_ranges (any): Equality ranges for constraints on the parameters. + + Returns: + None + + Class Fields: + params_defined (bool): A flag indicating whether parameters are defined (initialized to False). + params_to_optimize (any): Parameters to be optimized, initialized with the value of the `params_to_optimize` argument. + params_description (dict): A dictionary containing parameter descriptions, where keys are parameter indices and values are dictionaries with 'name' and 'bounds'. + params_description_odict (any): Original parameter descriptions (copy of params_description). + equality_ranges (any): Equality ranges, initialized with the value of the `equality_ranges` argument. + defined_params_passed (bool): A flag indicating whether defined parameters have been passed (initialized to False). + params_predefined (dict): A dictionary to store predefined parameters (initialized as an empty dictionary). + """ self.params_defined = False self.params_to_optimize = params_to_optimize super().__init__(token_name, status, family_type, False, @@ -39,6 +97,23 @@ def __init__(self, token_name: str, status: dict, family_type: str, params_descr self.reset_saved_state() def __deepcopy__(self, memo): + """ + Creates a deep copy of the ParametricFactor object. + + This method is essential for preserving the integrity of equation structures + during evolutionary operations. It ensures that when a ParametricFactor + is modified (e.g., during mutation or crossover), the original object + remains unchanged, and a new, independent copy is created with the + desired modifications. This prevents unintended side effects and maintains + the correctness of the evolutionary search process. + + Args: + self: The ParametricFactor object to be copied. + memo: The memo dictionary used by `deepcopy` to prevent infinite recursion. + + Returns: + A new ParametricFactor object that is a deep copy of the original. + """ cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result @@ -60,22 +135,94 @@ def __deepcopy__(self, memo): return result def set_grad_evaluator(self, evaluator): + """ + Sets the gradient evaluator for parameter optimization. + + The gradient evaluator is crucial for efficiently adjusting the factor's parameters + during the equation discovery process. By setting it, we enable the factor to + compute gradients and refine its parameters to better fit the data. + + Args: + evaluator: The gradient evaluator to be set. + + Returns: + None. + + This method initializes the following class fields: + - _grad_evaluator: The gradient evaluator used for computing gradients. + """ self._grad_evaluator = evaluator @property def grad_cache_label(self): + """ + Generates a label for the gradient cache. + + This label is used to identify and retrieve cached gradient computations, + improving efficiency by avoiding redundant calculations when exploring + the parameter space of potential equation structures. The label is + constructed from the factor parameters, with a '_grad' suffix appended + to the first element to distinguish it as a gradient cache label. + + Args: + self: The instance of the class. + + Returns: + tuple: A tuple representing the label for the gradient cache. + """ grad_cache_label = list(factor_params_to_str(self)) grad_cache_label[0] += '_grad' return tuple(grad_cache_label) @property def required_params(self): + """ + Returns the required parameters for optimization. + + This method provides the necessary information for the optimization process, + specifically the equation's structure (hash description) and the parameters + that need to be tuned to best fit the data. This is crucial for the evolutionary + algorithm to effectively search for the optimal equation. + + Args: + None + + Returns: + tuple: A tuple containing: + - hash_descr (str): The hash description representing the equation's structure. + - params_to_optimize (list): The parameters to optimize within the equation. + """ return self.hash_descr, self.params_to_optimize def __contains__(self, element): + """ + Checks if a given parameter is marked for optimization within the factor. + + This is crucial for the optimization process, ensuring that only relevant parameters are adjusted to improve the model's fit to the data. + + Args: + element: The parameter to check for inclusion in the optimization set. + + Returns: + bool: True if the parameter is to be optimized, False otherwise. + """ return element in self.params_to_optimize def use_params(self, params): + """ + Uses the provided parameters to update the factor model. + + This method takes a list of optimized parameters and updates the factor model's + internal parameters based on these values. It also handles predefined parameters, + ensuring the factor is correctly configured for subsequent calculations of the objective function. + This ensures that the factor model accurately reflects the current state of the optimization process. + + Args: + params (list): A list of tuples, where each tuple contains the parameter name and its optimized value. + + Returns: + None + """ self.reset_saved_state() assert len(params) == len(self.params_to_optimize), 'The number of the passed parameters does not match declared problem' _params = np.ones(shape=len(self.params_description)) @@ -96,6 +243,24 @@ def use_params(self, params): self.equality_ranges, random=False, **_kw_params) def set_defined_params(self, defined_params: dict): + """ + Sets specific parameter values to be used during the equation discovery process. + + This ensures that certain parameters are fixed to particular values, + allowing the search to focus on other aspects of the equation structure. + This is useful when some parameters are already known or constrained by prior knowledge. + + Args: + defined_params (dict): A dictionary where keys are parameter labels (strings) + and values are the numerical values to which these parameters should be fixed. + + Raises: + ValueError: If any of the provided parameter values is None, as this indicates + an undefined or missing value that cannot be used. + + Returns: + None: This method modifies the internal state of the ParametricFactor object. + """ for param_label, val in defined_params.items(): if val is None: raise ValueError('Trying to set the parameter with None value') @@ -103,11 +268,35 @@ def set_defined_params(self, defined_params: dict): self.defined_params_passed = True def reset_saved_state(self): + """ + Resets the internal state tracking which components of the loss function have been computed. + + This ensures that during the evolutionary search for the optimal equation, + loss and gradient calculations are performed when necessary, avoiding + redundant computations and ensuring that changes to parameters are properly + reflected in the optimization process. This is crucial for the evolutionary + algorithm to efficiently explore the search space of possible equations. + + Args: + self: The object instance. + + Returns: + None. + """ deriv_eval_dict = {label: False for label in self.params_to_optimize} self.saved = {'base': False, 'deriv': deriv_eval_dict, 'structural': False} def eval_grad(self, param_label: str): + """ + Evaluates the gradient of a specified parameter. This is a crucial step in refining the equation structure by quantifying the sensitivity of the loss function with respect to changes in the parameter. + + Args: + param_label: The label of the parameter for which to evaluate the gradient. + + Returns: + The evaluated gradient of the specified parameter. + """ # if self.saved['deriv'][param_label]: # return global_var.tensor_cache.get(self.grad_cache_label, # structural = False) @@ -118,11 +307,64 @@ def eval_grad(self, param_label: str): class ParametricTerm(Term): + """ + Represents a term in a factor graph with parametric factors. + + This class manages parametric and defined factors, providing methods + for evaluation, gradient calculation, and parameter handling. + + Methods: + - __deepcopy__ + - term_id + - parse_opt_params + - opt_params_num + - use_params + - evaluate + - evaluate_grad + - equivalent_common_term + - __contains__ + - _ + - _ + - _ + + Attributes: + - parametric_factors: A dictionary storing parametric factors. + - defined_factors: A dictionary storing defined factors. + - all_params: A list of all parameters to optimize, extracted from the + parametric factors. + - pool: A multiprocessing pool for parallel computations. + - operator: The operator used to combine the results of individual factors. + """ + __slots__ = ['_history', 'structure', 'interelement_operator', 'saved', 'saved_as', 'pool', 'max_factors_in_term', 'cache_linked', 'occupied_tokens_labels', 'parametric_factors', 'defined_factors', 'params_to_optimize', 'all_params'] def __init__(self, pool, parametric_factors: dict, defined_factors: dict, interelement_operator=np.multiply): + """ + Initializes a ParametricTerm object. + + This method sets up a term within an equation, preparing it for evaluation + by storing its parametric and defined factors, extracting optimizable parameters, + and configuring the multiprocessing pool and inter-element operator. + + Args: + pool: A multiprocessing pool for parallel computations. + parametric_factors: A dictionary of parametric factors. Keys are factor names, + values are the corresponding factor objects. + defined_factors: A dictionary of defined factors. Keys are factor names, + values are the corresponding factor objects. + interelement_operator: The operator used to combine the results of + individual factors (default: numpy.multiply). + + Returns: + None. + + Why: + This initialization is crucial for structuring the term, ensuring that all + necessary components (factors, parameters, computational resources) are + properly configured before the term is evaluated during the equation discovery process. + """ self.parametric_factors = parametric_factors self.defined_factors = defined_factors # print('parametric factors:', self.parametric_factors) @@ -132,6 +374,23 @@ def __init__(self, pool, parametric_factors: dict, defined_factors: dict, intere self.operator = interelement_operator def __deepcopy__(self, memo): + """ + Creates a deep copy of the ParametricTerm object. + + This method is essential for preserving the integrity of equation structures + during evolutionary processes. It ensures that when a ParametricTerm is + modified (e.g., during crossover or mutation), the original structure + remains unchanged, preventing unintended side effects in the equation + discovery process. + + Args: + self: The ParametricTerm object to copy. + memo (dict): A dictionary used to keep track of objects that have + already been copied, to prevent infinite recursion. + + Returns: + ParametricTerm: A deep copy of the ParametricTerm object. + """ # print('while copying factor:') # print('properties of self') # pprint(vars(self)) @@ -167,11 +426,43 @@ def __deepcopy__(self, memo): @property def term_id(self) -> int: + """ + Calculates a unique ID for the term based on its parametric factors. + + The term ID is calculated by summing the hash descriptions of all + parametric factors associated with the term. This ID serves to + uniquely identify the term within the equation discovery process, + allowing for efficient comparison and manipulation of terms during + the evolutionary search for the best equation structure. + + Args: + self: The instance of the class. + + Returns: + int: The calculated term ID. + """ _term_id = sum([factor.hash_descr for factor in self.parametric_factors.values()]) # print('_term_id', _term_id) return _term_id def parse_opt_params(self, params: np.ndarray): + """ + Parses optimization parameters for each parametric factor. + + This method iterates through the parametric factors, extracts the + required parameters for each factor, and associates them with the + corresponding values from the input `params` array. The results are + stored in an ordered dictionary, allowing to associate optimized values with corresponding parametric factors. + + Args: + params: A NumPy array containing the optimization parameters. + + Returns: + OrderedDict: An ordered dictionary where keys are hash descriptions + of the parametric factors and values are lists of tuples. Each + tuple contains the parameter name and its corresponding value + from the `params` array. + """ params_dict = OrderedDict() init_idx = 0 for factor in self.parametric_factors.values(): @@ -181,20 +472,72 @@ def parse_opt_params(self, params: np.ndarray): return params_dict def opt_params_num(self): + """ + Returns the number of optimizable parameters. + + This count is crucial for assessing the complexity of the equation and guiding the optimization process during equation discovery. + + Args: + self: The ParametricTerm instance. + + Returns: + int: The number of optimizable parameters in the term. + """ return len(self.all_params) # sum([len(params) for params in self.parse_opt_params().values()]) def use_params(self, params: dict): + """ + Applies updated parameter values to the underlying parametric factors. + + The method iterates through a dictionary of parameter updates, applying + each update to the corresponding parametric factor. This ensures that + the model reflects the latest parameter estimates obtained during the + equation discovery process. + + Args: + params: A dictionary where keys are factor labels and values are + dictionaries of parameters for each factor. These parameters + represent the optimized coefficients and other relevant values + for each term in the equation. + + Returns: + None. The method modifies the internal state of the `parametric_factors` + attribute by updating the parameters of its constituent factors. + """ for factor_label, factor_params in params.items(): # print('setting params:', params) self.parametric_factors[factor_label].use_params(factor_params) def evaluate(self): + """ + Evaluates the joint probability distribution. + + This method computes the joint probability distribution by multiplying the evaluated values of all parametric and defined factors associated with this term. This represents the overall probability of a specific configuration of variables within the model. + + Args: + self: The object instance. + + Returns: + np.ndarray: A 1D numpy array representing the joint probability + distribution. + """ value = np.multiply.reduce([factor.evaluate() for factor in self.parametric_factors.values()] + [factor.evaluate() for factor in self.defined_factors.values()]) # , initial = return value.reshape(-1) def evaluate_grad(self, parameter): + """ + Evaluates the gradient of the term with respect to a given parameter. + + This method computes the gradient of the term to refine the equation discovery process. It identifies the parametric factor associated with the specified parameter, evaluates the remaining factors within the term, and combines them with the gradient of the identified factor. This ensures accurate gradient calculation for optimization. + + Args: + parameter: The parameter with respect to which the gradient is evaluated. + + Returns: + The gradient of the term with respect to the given parameter. + """ # print(self.parametric_factors, parameter) param_factor_idxs = [idx for idx, factor in enumerate(self.parametric_factors.values()) if parameter in factor] # print(param_factor_idxs) @@ -204,6 +547,22 @@ def evaluate_grad(self, parameter): [factor.evaluate() for factor in self.defined_factors.values()]) * list(self.parametric_factors.values())[param_factor_idxs[0]].eval_grad(parameter) def equivalent_common_term(self): + """ + Generates a simplified term representation by merging parametric and defined factors. + + This method consolidates the factors, treating constant factors separately to + facilitate efficient equation discovery. By combining these factors into a single + term, the search space for potential differential equations is reduced, + improving the efficiency of the evolutionary algorithm. + + Args: + self: The instance of the ParametricTerm class. + + Returns: + tuple: A tuple containing: + - const_val: The value of the constant factor (or 1 if no constant factor is present). + - equilvalent_term: A Term object representing the combined factors. + """ factors_to_convert = [] const_set = False for factor in self.parametric_factors.values(): @@ -224,17 +583,74 @@ def equivalent_common_term(self): @singledispatchmethod def __contains__(self, element): + """ + Checks if a given element is contained within the parametric term. + + This method serves as a placeholder and is designed to be overridden by subclasses. + Subclasses should implement this method to define how membership is determined + based on the specific structure and properties of the parametric term. + For example, a subclass representing a polynomial term might check if the element + is a valid variable or coefficient within the polynomial. + + Args: + element: The element to check for membership. The type of this element + depends on the specific parametric term implementation. + + Returns: + bool: Always raises a NotImplementedError. + + Raises: + NotImplementedError: Always raised, indicating that the method + must be implemented by a subclass to provide meaningful + membership checking logic. This ensures that the behavior is + tailored to the specific type of parametric term. + """ raise NotImplementedError( 'Incorrect type of the requested item for the __contains__ method') @__contains__.register def _(self, element: str): + """ + Checks if a given parameter should be optimized during the equation discovery process. + + This is crucial for focusing the search on relevant parameters and improving the efficiency + of the equation discovery process. By selectively optimizing parameters, the algorithm can + avoid overfitting and identify more parsimonious and interpretable equation structures. + + Args: + element (str): The name of the parameter to check. + + Returns: + bool: True if the parameter is in the set of parameters to be optimized, False otherwise. + """ return element in self.params_to_optimize @__contains__.register def _(self, element: ParametricFactor): + """ + Checks if a given parametric factor is already included within the managed set of parametric factors. + + This check ensures that redundant factors are not added, maintaining the uniqueness and integrity of the parametric term's structure. + + Args: + element (ParametricFactor): The parametric factor to check for existence. + + Returns: + bool: True if the parametric factor is already present, False otherwise. + """ return element in self.parametric_factors.values() @__contains__.register def _(self, element: Factor): + """ + Checks if a given factor is already defined within the parametric term's factor graph. + + This check is crucial for ensuring that the evolutionary process, which searches for the optimal equation structure, does not introduce redundant or conflicting factors into the model. By verifying the existence of a factor, the algorithm avoids unnecessary computations and maintains the integrity of the equation being constructed. + + Args: + element: The factor to check for existence. + + Returns: + bool: True if the factor is already defined, False otherwise. + """ return element in self.defined_factors.values() diff --git a/epde/preprocessing/cheb.py b/epde/preprocessing/cheb.py index 585409ec..5204c215 100644 --- a/epde/preprocessing/cheb.py +++ b/epde/preprocessing/cheb.py @@ -12,6 +12,28 @@ def get_cheb_for_point(matrix, axis, idx, grid, max_der_order=3, points=9, poly_order=None): + """ + Computes a Chebyshev polynomial fit at a specific location within a multi-dimensional array. + + This method isolates a subset of data points along a given axis centered on a specified index, + and subsequently fits a Chebyshev polynomial to these points. This provides a smooth, + continuous representation of the data in the neighborhood of the point, which can be used + for tasks such as estimating derivatives or interpolating values. + + Args: + matrix (numpy.ndarray): The multi-dimensional array containing the data. + axis (int): The axis along which to extract the data points. + idx (tuple or array-like): A tuple or array of indices specifying the location of the point. + grid (list of numpy.ndarray): A list of arrays, where each array represents the coordinate grid for each axis. + max_der_order (int, optional): The maximum derivative order to consider when automatically determining the polynomial order. Defaults to 3. + points (int, optional): The number of points to use for the Chebyshev fit. Defaults to 9. + poly_order (int, optional): The order of the Chebyshev polynomial. If None, it is determined based on max_der_order. Defaults to None. + + Returns: + tuple: A tuple containing: + - The x-coordinate of the central point (float). + - The Chebyshev polynomial object (numpy.polynomial.chebyshev.Chebyshev) fitted to the data. + """ if poly_order is None: max_power = max_der_order + 1 else: @@ -38,6 +60,29 @@ def get_cheb_for_point(matrix, axis, idx, grid, max_der_order=3, points=9, poly_ def process_point_cheb(args): + """ + Processes a single point to calculate derivatives using Chebyshev polynomials or finite differences. + + This method strategically chooses between Chebyshev polynomials and finite difference approximations + to compute derivatives at a specific point within a multi-dimensional data tensor. It prioritizes + Chebyshev polynomials for points within a defined boundary to leverage their accuracy and efficiency, + reverting to finite differences for points outside this boundary. This hybrid approach ensures + accurate derivative calculations across the entire data domain. + + Args: + args: A list containing the following elements: + idx (numpy.ndarray): The index of the point in the matrix. + matrix (numpy.ndarray): The multi-dimensional data tensor. + grid (numpy.ndarray): The grid spacing for each dimension. + points (numpy.ndarray): The points at which to evaluate the Chebyshev polynomials. + n_der (int or tuple/list of int): The order of derivatives to calculate for each dimension. + poly_bound (int): The boundary within which to use Chebyshev polynomials. + poly_order (int): The order of the Chebyshev polynomials to use. + + Returns: + numpy.ndarray: An array containing the calculated derivatives. The length of the array is the sum of the + derivative orders for each dimension. + """ global PolyBoundary idx = np.array(args[0]) matrix = args[1] diff --git a/epde/preprocessing/deriv_calculators.py b/epde/preprocessing/deriv_calculators.py index 9cc40937..ad51ce49 100644 --- a/epde/preprocessing/deriv_calculators.py +++ b/epde/preprocessing/deriv_calculators.py @@ -18,6 +18,28 @@ from epde.preprocessing.cheb import process_point_cheb def Heatmap(Matrix, interval = None, area = ((0, 1), (0, 1)), xlabel = '', ylabel = '', figsize=(8,6), filename = None, title = ''): + """ + Generates and displays a heatmap from a given matrix. + + This function visualizes the relationships and patterns within a matrix, + allowing for the identification of key areas and trends. The heatmap + representation aids in understanding the structure and distribution of + data, which is crucial for tasks such as model validation and feature + selection. + + Args: + Matrix (numpy.ndarray): The input matrix to be visualized as a heatmap. + interval (tuple, optional): The colorbar interval (vmin, vmax). If None, it's automatically determined based on the matrix values. Defaults to None. + area (tuple, optional): The area covered by the heatmap ((ymin, ymax), (xmin, xmax)). Defaults to ((0, 1), (0, 1)). + xlabel (str, optional): Label for the x-axis. Defaults to ''. + ylabel (str, optional): Label for the y-axis. Defaults to ''. + figsize (tuple, optional): Figure size (width, height) in inches. Defaults to (8,6). + filename (str, optional): If provided, the plot is saved to a file with this name ('.eps' extension added). Defaults to None. + title (str, optional): The title of the heatmap plot. Defaults to ''. + + Returns: + None. Displays the heatmap plot and optionally saves it to a file. The heatmap visually represents the input matrix. + """ y, x = np.meshgrid(np.linspace(area[0][0], area[0][1], Matrix.shape[0]), np.linspace(area[1][0], area[1][1], Matrix.shape[1])) fig, ax = plt.subplots(figsize = figsize) plt.xlabel(xlabel) @@ -36,19 +58,112 @@ def Heatmap(Matrix, interval = None, area = ((0, 1), (0, 1)), xlabel = '', ylabe if type(filename) != type(None): plt.savefig(filename + '.eps', format='eps') class AbstractDeriv(ABC): + """ + Abstract base class for differentiation objects. + + This class serves as a template for implementing different differentiation methods. + It defines the basic interface that all differentiation objects should implement. + + Class Methods: + - __call__: + """ + def __init__(self, *args, **kwargs): + """ + Initializes the AbstractDeriv object. + + This base class provides a foundation for representing differential equation terms. + Subclasses will extend this to define specific term structures and their derivatives. + This initialization currently performs no operations, but serves as a placeholder + for future common initialization logic across different derivative terms. + + Args: + *args: Variable length argument list. These arguments are passed to the superclass constructor. + **kwargs: Arbitrary keyword arguments. These keyword arguments are passed to the superclass constructor. + + Returns: + None. + """ pass def __call__(self, data: np.ndarray, grid: list, max_order: Union[int, list], *args, **kwargs): + """ + Applies the abstract differentiation operator. + + This method serves as the entry point for applying a specific differentiation scheme + to the input data. It ensures that all concrete differentiation methods implement + a consistent interface, facilitating their use within the equation discovery process. + Since this is an abstract method, it raises a NotImplementedError to enforce + implementation in derived classes. + + Args: + data (np.ndarray): The input data to be differentiated. + grid (list): The grid points at which the data is defined. + max_order (Union[int, list]): The maximum order of derivatives to compute. + *args: Additional positional arguments passed to the differentiation method. + **kwargs: Additional keyword arguments passed to the differentiation method. + + Returns: + None + + Raises: + NotImplementedError: Always raised as this is an abstract method. + """ raise NotImplementedError('Calling abstract differentiation object') class AdaptiveFiniteDeriv(AbstractDeriv): + """ + A class for calculating finite derivatives of data with adaptive grid refinement. + + Attributes: + stencil_width: The width of the stencil used for derivative calculation. + accuracy_order: The accuracy order of the finite difference scheme. + step_ratio_limit: The maximum allowed ratio between consecutive grid spacings. + """ + def __init__(self): + """ + Initializes the AdaptiveFiniteDeriv class. + + This class is designed to dynamically adjust finite difference approximations + for derivative estimation. Initialization prepares the object for subsequent + derivative calculations without requiring any initial parameters. + + Args: + self: The object instance. + + Returns: + None. + + Why: + The class needs to be initialized before it can be used to estimate derivatives. + This initialization sets up the internal state of the object, preparing it for + subsequent calculations. + """ pass def differentiate(self, data: np.ndarray, max_order: Union[int, list], mixed: bool = False, axis=None, *grids) -> list: + """ + Calculates derivatives of the input data up to a specified order. + + This method computes derivatives of a given data array with respect to specified grids. + It can compute derivatives along a specific axis or mixed derivatives (under certain conditions). + The computed derivatives are essential for constructing candidate differential equations. + + Args: + data (np.ndarray): The input data as a NumPy array. + max_order (Union[int, list]): The maximum order of derivative to compute. It can be an integer or a list of integers, + where each element corresponds to the maximum order along each axis. + mixed (bool, optional): A boolean flag indicating whether to compute mixed derivatives. Defaults to False. + axis (optional): The axis along which to calculate the derivative. Defaults to None. + *grids: Variable number of grid spacings for each dimension. + + Returns: + list: A list of NumPy arrays containing the computed derivatives. + The order of derivatives in the list corresponds to the order in which they are calculated. + """ if isinstance(max_order, int): max_order = [max_order,] * data.ndim if any([ord_ax != max_order[0] for ord_ax in max_order]) and mixed: @@ -94,6 +209,22 @@ def differentiate(self, data: np.ndarray, max_order: Union[int, list], def __call__(self, data: np.ndarray, grid: list, max_order: Union[int, list, tuple], mixed: bool = False) -> np.ndarray: + """ + Computes derivatives of the input data using adaptive finite difference schemes. + + This method is essential for estimating the rates of change within the provided data, + which is a crucial step in identifying the underlying differential equations. + The derivatives are calculated based on the provided grid points and user-specified derivative orders. + + Args: + data (np.ndarray): Input data to differentiate. + grid (list): Grid points along each axis. + max_order (Union[int, list, tuple]): Maximum order of derivative to compute. + mixed (bool): Whether to compute mixed derivatives. + + Returns: + np.ndarray: Array of derivatives. + """ grid_unique = [np.unique(ax_grid) for ax_grid in grid] derivs = self.differentiate(data, max_order, mixed, None, *grid_unique) @@ -109,11 +240,49 @@ def __call__(self, data: np.ndarray, grid: list, max_order: Union[int, list, tup # mixed: bool = False, axis=None, *grids) -> list: class PolynomialDeriv(AbstractDeriv): + """ + Calculates derivatives of data using Chebyshev polynomials. + """ + def __init__(self): + """ + Initializes a new instance of the PolynomialDeriv class. + + This class is designed to represent and manipulate polynomial derivatives within the EPDE framework. + The initialization prepares the object for subsequent operations related to symbolic differentiation + and equation discovery. + + Args: + self: The object instance. + + Returns: + None. + """ pass def __call__(self, data: np.ndarray, grid: list, max_order: Union[int, list, tuple], mp_poolsize: int, polynomial_window: int, poly_order: int) -> np.ndarray: + """ + Computes derivatives of the input data using local polynomial approximations. + + This method calculates derivatives at each point by fitting Chebyshev polynomials + within a specified window. This approach allows for accurate estimation of derivatives, + even in the presence of noise or irregularities in the data. Multiprocessing is used + to accelerate the computation when a pool size greater than 1 is specified. + This is a crucial step in identifying the underlying differential equations + that govern the data. + + Args: + data (np.ndarray): Input data as a NumPy array. + grid (list): Grid coordinates corresponding to the data points. + max_order (Union[int, list, tuple]): Maximum order of the derivative to compute. Can be an integer, list, or tuple. + mp_poolsize (int): Size of the multiprocessing pool. If greater than 1, multiprocessing is used. + polynomial_window (int): Size of the window used for polynomial fitting. + poly_order (int): Order of the polynomial to fit. + + Returns: + np.ndarray: NumPy array containing the computed derivatives for each point in the input data. + """ polynomial_boundary = polynomial_window//2 + 1 index_array = [] @@ -137,18 +306,41 @@ def __call__(self, data: np.ndarray, grid: list, max_order: Union[int, list, tup class SpectralDeriv(AbstractDeriv): ''' Класс спектральной производной by https://github.com/KimKitsurag1 - adapted to match AbstractDeriv + adapted to match AbstractDeriv ''' + def __init__(self): + """ + Initializes a `SpectralDeriv` object. + + This class is designed to work with spectral representations of data, + preparing it for subsequent differential equation discovery. + The initialization sets up the object to handle spectral data, + allowing for efficient computation of derivatives in the spectral domain, + which is a crucial step in identifying underlying differential equations. + + Args: + self: The object instance. + + Returns: + None. + """ pass @staticmethod def butterworth_filter(freqs, number_of_freqs, steepness): - ''' - Фильтр Баттерворта с порядком крутизны steepness, гасящий высокочастотные - составляющие с частотой выше number_of_freqs-ой преобразования Фурье - ''' + """ + Applies a Butterworth filter to dampen high-frequency components in a frequency spectrum. This helps to smooth the spectrum and reduce noise, which is crucial for identifying the dominant frequencies that contribute most to the underlying dynamics of the system. + + Args: + freqs (np.ndarray): Array of frequencies. + number_of_freqs (int): Cutoff frequency index; frequencies above this index are attenuated. + steepness (int): Order of the Butterworth filter, controlling the sharpness of the cutoff. + + Returns: + np.ndarray: Filtered frequency array. + """ freqs_copy = np.copy(freqs) freqs_copy = np.abs(freqs_copy) freqs_copy.sort() @@ -161,7 +353,22 @@ def butterworth_filter(freqs, number_of_freqs, steepness): return freqs * butterworth_filter_multiplier def spectral_derivative_1d(self, func: np.ndarray, grid: np.ndarray, n=None, steepness=1): - '''Одномерная спектральная производная,принимает на вход количество частот и крутизну для фильтра Баттерворта, если они не указаны - фильтрация не производится''' + """ + Applies a spectral derivative operator to a 1D function. + + This method transforms the input function to the frequency domain, applies a derivative operator, and then transforms the result back to the spatial domain. + Optionally, a Butterworth filter can be applied in the frequency domain to reduce noise. + This is useful for calculating derivatives of noisy data, where direct differentiation may amplify noise. + + Args: + func (np.ndarray): The input 1D function values. + grid (np.ndarray): The grid coordinates corresponding to the function values. + n (int, optional): The number of frequencies to use for the Butterworth filter. If None, no filtering is applied. Defaults to None. + steepness (float, optional): The steepness of the Butterworth filter. Defaults to 1. + + Returns: + np.ndarray: The spectral derivative of the input function. + """ # print(func.shape, grid.shape) # if isinstance(func, type(None)) and isinstance(grid, type(None)): @@ -179,7 +386,24 @@ def spectral_derivative_1d(self, func: np.ndarray, grid: np.ndarray, n=None, ste def spectral_derivative_nd(self, func : np.ndarray, grid : list, n = None, steepness = 1, deriv_hist : list = []): - '''Многомерная спектральная производная,принимает на вход количество частот по каждой размерности и крутизну для фильтра Баттерворта, если они не указаны-фильтрация не производится''' + """ + Computes the spectral derivative of a function along specified grid dimensions. This method leverages Fourier transforms to estimate derivatives, providing a way to analyze the rate of change within the function's spectral representation. + + Args: + func (np.ndarray): The input function represented as a multi-dimensional array. + grid (list): A list of arrays, where each array represents the grid coordinates along a specific dimension. + n (int or None): The number of frequencies to consider for the Butterworth filter. If None, it defaults to the minimum dimension size of the input function. + steepness (int): The steepness parameter for the Butterworth filter, controlling the filter's sharpness. + deriv_hist (list): A list to keep track of the derivative dimensions. Defaults to an empty list. + + Returns: + list: A list of tuples, where each tuple contains: + - A tuple describing the derivative dimensions. + - The derivative of the function along the corresponding dimension. + + Why: + This method calculates spectral derivatives to analyze the function's rate of change in the frequency domain, which is useful for identifying dominant modes and patterns, and ultimately, for constructing differential equation models. + """ if isinstance(n, int): n = np.full(shape=len(grid), fill_value=n) @@ -202,6 +426,31 @@ def spectral_derivative_nd(self, func : np.ndarray, grid : list, n = None, steep def differentiate(self, field : np.ndarray, grid : list, max_order : Union[int, list], mixed : bool = False, n = None, steepness = 1, deriv_hist : list = []) -> list: + """ + Calculates derivatives of a field using spectral differentiation. + + This method computes derivatives of a given field with respect to + the spatial grid up to a specified order. It supports both single-axis and mixed derivatives. + The derivatives are calculated to facilitate the discovery of underlying differential equations + that govern the behavior of the input field. + + Args: + field: The input field to differentiate (numpy ndarray). + grid: The spatial grid coordinates (list of numpy arrays). + max_order: The maximum order of derivative to compute. Can be an + integer or a list specifying the maximum order for each axis. + mixed: A boolean indicating whether to compute mixed derivatives. + Defaults to False. + n: Number of modes to use in spectral derivative. Defaults to None. + steepness: Steepness parameter for spectral derivative. Defaults to 1. + deriv_hist: A list to store the history of derivatives taken. + Defaults to []. + + Returns: + A dictionary where keys are tuples representing the derivative + orders along each axis, and values are the corresponding derivative + fields (numpy ndarrays). + """ if isinstance(max_order, int): max_order = [max_order,] * field.ndim else: @@ -245,6 +494,25 @@ def num_of_derivs_with_ord(ords): def __call__(self, data : np.ndarray, grid : list, max_order : Union[int, list], mixed : bool = False, n = None, steepness = 1) -> np.ndarray: + """ + Computes derivatives of the input data using spectral methods to facilitate equation discovery. + + This method calculates derivatives of the input data with respect to the given grid, up to a specified order. + These derivatives are essential components for constructing candidate differential equations within the EPDE framework. + The method supports mixed derivatives and allows for adjusting the steepness parameter to fine-tune the differentiation process. + It is used to generate features that represent different derivative terms, which are then used in the equation discovery process. + + Args: + data (np.ndarray): Input data as a numpy array. + grid (list): Grid points for differentiation. + max_order (Union[int, list]): Maximum order of derivative to compute. + mixed (bool, optional): Flag to include mixed derivatives. Defaults to False. + n (optional): Order of the derivative for each dimension. If None, defaults to 0. + steepness (int, optional): Steepness parameter for the derivative calculation. Defaults to 1. + + Returns: + np.ndarray: Derivatives of the data as a numpy array. + """ def make_unsparse_sparse(*grids): # TODO^ find more e;egant solution unique_vals = [np.unique(grid) for grid in grids] return np.meshgrid(*unique_vals, sparse = True, indexing = 'ij') @@ -261,6 +529,29 @@ def make_unsparse_sparse(*grids): # TODO^ find more e;egant solution def spectral_derivative_high_ord(self, func : np.ndarray, grid : list, axis : int = 0, max_order : int = 1, n = None, steepness = 1) -> list: + """ + Computes spectral derivatives of a function along a specified axis. + + This method calculates derivatives of a given function using the spectral + method, which involves transforming the function to Fourier space, + multiplying by the corresponding frequencies, and then transforming back + to real space. It applies a Butterworth filter to the frequencies to + reduce noise, ensuring stable and accurate derivative estimation, which is crucial for equation discovery. + This is done to accurately represent the function's behavior in the frequency domain, enabling precise derivative calculations required for identifying underlying differential equations. + + Args: + func: The input function as a NumPy array. + grid: A list of arrays representing the grid coordinates. + axis: The axis along which to compute the derivative (default: 0). + max_order: The maximum order of the derivative to compute (default: 1). + n: Cutoff frequency for the butterworth filter. If None, defaults to the minimum dimension of func. + steepness: Steepness of the butterworth filter. + + Returns: + A list of tuples, where each tuple contains: + - A tuple describing the derivative (axis, repeated deriv_idx times). + - The derivative as a NumPy array. + """ derivs = [] cur_deriv = func func_projection = np.fft.fftn(func) @@ -283,8 +574,46 @@ def spectral_derivative_high_ord(self, func : np.ndarray, grid : list, axis : in return derivs class TotalVariation(AbstractDeriv): + """ + Total variation (TV) regularization. + + This class implements total variation regularization, a technique used to + smooth images or signals while preserving edges. It leverages the + Alternating Direction Method of Multipliers (ADMM) to solve the optimization + problem. + + Methods: + - initial_guess + - admm_step + - optimize_with_admm + """ + @staticmethod def initial_guess(data: np.ndarray, dimensionality: tuple): + """ + Generates initial estimates for optimization variables. + + This method computes initial values for the gradient (`grad`), an approximation + of the Hessian (`w`), and the Laplacian multiplier (`lap_mul`). These initial + values are crucial for the subsequent optimization process, guiding the search + towards a solution that minimizes the total variation while adhering to the + constraints imposed by the data. The gradient is computed using `np.gradient` + along each dimension of the input data. The 'w' is calculated as the gradient + of the gradient along each dimension. The Laplacian multiplier is initialized + as an array of zeros with the same shape as 'w'. These initial values provide + a starting point for refining the solution and discovering the underlying + differential equation. + + Args: + data: The input data as a multi-dimensional numpy array. + dimensionality: A tuple representing the dimensions of the input data. + + Returns: + A tuple containing: + - grad: The gradient of the input data. + - w: An approximation of the Hessian matrix. + - lap_mul: The initialized Laplacian multiplier (zeros). + """ grad = np.array([np.gradient(data, axis=dim_idx) for dim_idx, dim in enumerate(dimensionality)]) # print(grad.shape) # w = np.array([np.zeros(dimensionality) for idx in np.arange(len(dimensionality)**2)], @@ -301,10 +630,27 @@ def initial_guess(data: np.ndarray, dimensionality: tuple): @staticmethod def admm_step(data: np.ndarray, steps: list, initial_u: np.ndarray, initial_w: np.ndarray, initial_lap: np.ndarray, lbd: float, reg_strng: float, c_const: float) -> tuple: - ''' - *data* has to be already Fourier-transformed - All inputs initial_u, initial_w & initial_lap have to be transformed by DFT. - ''' + """ + Performs a single ADMM (Alternating Direction Method of Multipliers) step to refine the solution. + This step optimizes the variables `u`, `w`, and `lap` in an iterative manner to minimize the total variation of the input `data`, + subject to constraints that enforce consistency between the variables. + + Args: + data (np.ndarray): The input data, which should already be transformed into the Fourier domain. + steps (list): List of stepsizes for each dimension + initial_u (np.ndarray): Initial estimate of the variable `u` in Fourier space. + initial_w (np.ndarray): Initial estimate of the variable `w` in Fourier space. + initial_lap (np.ndarray): Initial estimate of the Lagrange multiplier `lap` in Fourier space. + lbd (float): Regularization parameter controlling the strength of the total variation penalty. + reg_strng (float): Regularization strength parameter. + c_const (float): Parameter for updating the Lagrange multiplier. + + Returns: + tuple: A tuple containing the updated values of `u`, `w`, and `lap` after the ADMM step. + These updated values represent a refined solution that better fits the data while minimizing its total variation. + Why: + This method refines the estimates of the variables u, w, and lap, driving the solution towards a state that minimizes the total variation of the input data while adhering to the constraints imposed by the problem formulation. + """ def soft_thresholding(arg: np.ndarray, lbd: float) -> np.ndarray: norm = np.linalg.norm(arg) return max(norm - lbd, 0) * arg / norm @@ -380,6 +726,21 @@ def take(arr: np.ndarray, taking_args: dict = taking_args): return initial_u, initial_w, initial_lap def optimize_with_admm(self, data, lbd: float, reg_strng: float, c_const: float, nsteps: int = 1e5): + """ + Optimizes the given data using the Alternating Direction Method of Multipliers (ADMM). + + This method refines an estimate of the underlying signal by iteratively updating primal (u), dual (w) variables, and the Lagrangian multiplier (lap_mul) based on the ADMM algorithm. This optimization process helps in identifying the governing differential equations from the input data. + + Args: + data: The input data to be optimized. + lbd: Lambda, a regularization parameter. + reg_strng: Regularization strength parameter. + c_const: Constant parameter used in the ADMM update. + nsteps: The number of ADMM iterations to perform. Defaults to 1e5. + + Returns: + The optimized primal variable (u) after the ADMM iterations. This represents a refined solution that aids in discovering the underlying differential equations. + """ u, w, lap_mul = self.initial_guess(data=data, dimensionality=data.shape) data_fft = np.fft.fftn(data) print(f'For some reason has to be abysmal: {np.min(np.real(data_fft)), np.max(np.real(data_fft))}') diff --git a/epde/preprocessing/derivatives.py b/epde/preprocessing/derivatives.py index 82f176f5..151dce64 100644 --- a/epde/preprocessing/derivatives.py +++ b/epde/preprocessing/derivatives.py @@ -22,38 +22,31 @@ def preprocess_derivatives_poly(field, grid=None, steps=None, data_name=None, output_file_name=None, smooth=True, sigma=9, mp_poolsize=4, max_order=2, polynomial_window=9, poly_order=None, scaling=False, include_time = False): - ''' - - Main preprocessing function for the calculation of derivatives on uniform grid - - Parameters (old): - --------- - - field : numpy.ndarray - The values of studied field on uniform grid. The dimensionality of the tensor is not restricted; - - output_file_name : string, optional - Name of the file, in which the tensors of caluclated derivatives will be saved; if it is not given, function returns the tensor - - mp_poolsize : integer, optional - The number of workers for multiprocessing.pool, that would be created for derivative evaluation; - - max_order : integer, optional - The maximum order of the derivatives to be calculated; - - polynomial_window : integer, optional - The number of points, for which the polynmial will be fitted, in order to later analytically differentiate it and obtain the derivatives. - Shall be defined with odd number or if it is even, expect polynomial_window + 1 - number of points to be used. - - Returns: - -------- - - derivatives : np.ndarray - If the output file name is not defined, or set as None, - tensor of derivatives, where the first dimentsion is the order - and the axis of derivative in such manner, that at first, all derivatives for first axis are returned, secondly, all - derivatives for the second axis and so on. The next dimensions match the dimensions of input field. - - ''' + """ + Main preprocessing function for calculating derivatives on a uniform grid using polynomial fitting. + + This function prepares the input data for the equation discovery process by calculating derivatives + using local polynomial approximations. This approach allows to estimate derivatives even from noisy data. + + Args: + field (numpy.ndarray): The values of the field on a uniform grid. The dimensionality is unrestricted. + grid (numpy.ndarray, optional): The grid coordinates. If None, it's assumed to be a unit grid. Defaults to None. + steps (numpy.ndarray, optional): The grid steps in each dimension. If None, it's assumed to be a unit step. Defaults to None. + data_name (str, optional): Name for saving the input field data. Defaults to None. + output_file_name (str, optional): Name for saving the calculated derivatives. If None, the derivatives are returned. Defaults to None. + smooth (bool, optional): Whether to apply Gaussian smoothing to the field. Defaults to True. + sigma (int, optional): The standard deviation for Gaussian smoothing. Defaults to 9. + mp_poolsize (int, optional): The number of workers for multiprocessing. Defaults to 4. + max_order (int, optional): The maximum order of derivatives to calculate. Defaults to 2. + polynomial_window (int, optional): The number of points for polynomial fitting. Must be odd. Defaults to 9. + poly_order (int, optional): The order of the polynomial to fit. If None, it defaults to max_order. Defaults to None. + scaling (bool, optional): Whether to scale the data. Defaults to False. + include_time (bool, optional): Whether to include time dimension. Defaults to False. + + Returns: + tuple (numpy.ndarray, numpy.ndarray): A tuple containing the (potentially smoothed) input field and the calculated derivatives. + The derivatives are structured such that the first dimension represents the order and axis of differentiation. + """ t1 = datetime.datetime.now() polynomial_boundary = polynomial_window//2 + 1 @@ -103,6 +96,23 @@ def preprocess_derivatives_poly(field, grid=None, steps=None, data_name=None, ou def init_ann(dim): + """ + Initializes an artificial neural network (ANN) model. + + This method constructs a sequential ANN model with several linear layers + and Tanh activation functions. The architecture is designed to map an + input of a given dimension to a single output value. This ANN serves as a component + in the equation discovery process, specifically for learning coefficients or + terms within the differential equation. The network's structure is designed + to provide a flexible function approximation for representing these unknown elements. + + Args: + dim: The input dimension of the ANN model. This corresponds to the number of + variables the equation depends on. + + Returns: + torch.nn.Sequential: The initialized ANN model. + """ model = torch.nn.Sequential( torch.nn.Linear(dim, 256), torch.nn.Tanh(), @@ -120,6 +130,26 @@ def init_ann(dim): def differentiate(data, order: Union[int, list], mixed: bool = False, axis=None, *grids): + """ + Computes numerical derivatives of the input data. + + Calculates derivatives of the input data using the gradient method to enable equation discovery. + It can compute mixed derivatives and handle data with multiple dimensions, which is essential + for identifying complex relationships in multi-dimensional systems. + + Args: + data: The input data to differentiate. + order: The order of the derivative to compute. It can be an integer + or a list of integers specifying the order along each axis. + mixed: A boolean indicating whether to compute mixed derivatives. + Defaults to False. + axis: The axis along which to compute the derivative. Defaults to None. + *grids: Grid spacing(s) for numerical differentiation. + + Returns: + A list of arrays representing the computed derivatives. These derivatives are + used to construct candidate differential equations. + """ print('order', order) if isinstance(order, int): order = [order,] * data.ndim @@ -167,6 +197,34 @@ def differentiate(data, order: Union[int, list], mixed: bool = False, axis=None, def preprocess_derivatives_ANN(field, grid, max_order, test_output=False, epochs_max=1e3, loss_mean=1000, batch_frac=0.5, return_ann: bool = False): + """ + Preprocesses field derivatives using an Artificial Neural Network (ANN). + + This method leverages an ANN to create a smooth, differentiable representation + of the input field. This is particularly useful when the original field data + is noisy or discrete, as it allows for more accurate and stable derivative + calculations, which are essential for discovering underlying differential + equations. + + Args: + field (np.ndarray): The field data to approximate. + grid (list or np.ndarray): The grid coordinates corresponding to the field data. + max_order (int): The maximum order of derivatives to calculate. + test_output (bool, optional): A flag for testing purposes. Defaults to False. + epochs_max (float, optional): The maximum number of epochs for ANN training. Defaults to 1e3. + loss_mean (int, optional): Threshold for loss function. Defaults to 1000. + batch_frac (float, optional): Fraction of data used for batch training. Defaults to 0.5. + return_ann (bool, optional): Whether to return the trained ANN model. Defaults to False. + + Returns: + tuple: A tuple containing the ANN approximation of the field and the + calculated derivatives. If `return_ann` is True, the tuple also + includes the trained ANN model. Specifically, it returns: + - approximation (np.ndarray): The ANN approximation of the field. + - derivs (np.ndarray): The calculated derivatives of the approximation. + - best_model (tf.keras.Model, optional): The trained ANN model, + returned only if `return_ann` is True. + """ assert grid is not None, 'Grid needed for derivatives preprocessing with ANN' if isinstance(grid, np.ndarray): grid = [grid,] @@ -192,6 +250,22 @@ def preprocess_derivatives_ANN(field, grid, max_order, test_output=False, def preprocess_derivatives(field, method, method_kwargs): + """ + Preprocesses a derivative field using a specified method. + + This method applies a preprocessing technique to a given derivative + field, preparing it for subsequent equation discovery. Different preprocessing + methods can enhance the signal or reduce noise in the derivative data, + improving the accuracy and efficiency of the equation search process. + + Args: + field: The derivative field to preprocess. + method: The name of the preprocessing method to apply. + method_kwargs: Keyword arguments to pass to the preprocessing method. + + Returns: + The preprocessed derivative field. + """ if method not in implemented_methods.keys(): raise NotImplementedError( 'Called preprocessing method has not been implemented yet. Use one of {implemented_methods}') diff --git a/epde/preprocessing/domain_pruning.py b/epde/preprocessing/domain_pruning.py index 6fad78db..016abf30 100644 --- a/epde/preprocessing/domain_pruning.py +++ b/epde/preprocessing/domain_pruning.py @@ -15,22 +15,42 @@ def scale_values(tensor): """ - Method for normalization of data + Scales the input tensor by dividing it by its maximum value. + + This ensures that all values in the tensor are normalized to the range [0, 1]. + + Args: + tensor (np.ndarray): The input tensor to be scaled. + + Returns: + np.ndarray: The scaled tensor with values in the range [0, 1]. + + Why: + This scaling is crucial for ensuring stable and efficient equation discovery. + By normalizing the data, we prevent any single variable from dominating the equation search process due to its magnitude. """ return tensor/np.max(tensor) def split_tensor(tensor, fraction_along_axis: Union[int, list, tuple]): """ - Method for splitting input data into blocks for further examenation - - Args: - tensor (`np.ndarray`): input data - fraction_along_axis (`int|list|tuple): number of fraction for each axis (if this is integer than all axis are dividing by same fractions) - - Returns: - aplit_indexes_along_axis (`list`): indexes of place where tensor were divided - block_matrix (`np.ndarray`): resulting devided tensor + Method for splitting a tensor into smaller blocks along specified axes. + + This function divides the input tensor into a grid of sub-tensors (blocks) based on the provided fractions for each axis. + This is a preliminary step for parallel or distributed processing of the tensor, enabling the framework to analyze different + sections of the data independently. + + Args: + tensor (`np.ndarray`): The input tensor to be split. + fraction_along_axis (`int|list|tuple`): The number of fractions to divide each axis into. + If an integer is provided, all axes will be split into the same number of fractions. + If a list or tuple is provided, each element specifies the number of fractions for the corresponding axis. + + Returns: + split_indexes_along_axis (`list`): A list of lists, where each inner list contains the indices along each axis where the tensor was split. + These indices define the boundaries of the resulting blocks. + block_matrix (`np.ndarray`): A NumPy array (object dtype) containing the split tensor blocks. The shape of this array + corresponds to the fractions specified for each axis. Each element of this array holds a sub-tensor (block) of the original tensor. """ assert isinstance(tensor, np.ndarray) fragments = [tensor,] @@ -72,6 +92,22 @@ def split_tensor(tensor, fraction_along_axis: Union[int, list, tuple]): def majority_rule(tensors: Union[list, np.ndarray], threshold: float = 1e-2): + """ + Applies majority rule to a list of tensors based on a non-constancy condition. + + This method checks if a majority of the input tensors exhibit sufficient variation, + indicating they are not constant. This is crucial for identifying active terms + in a differential equation. The non-constancy condition checks if the norm + of a tensor is greater than a threshold times its size. + + Args: + tensors: A list or numpy array of tensors to evaluate. + threshold: A threshold value used to determine non-constancy. + + Returns: + bool: True if the majority of tensors satisfy the non-constancy condition, + False otherwise. + """ def non_constancy_cond(x): return np.linalg.norm(x) > (threshold * x.size) return sum([non_constancy_cond(x_elem) for x_elem in tensors])/len(tensors) >= 0.5 @@ -79,18 +115,20 @@ def non_constancy_cond(x): return np.linalg.norm(x) > (threshold * x.size) def get_subdomains_mask(tensor, division_fractions: Union[int, list, tuple], domain_selector: Callable, domain_selector_kwargs: dict, time_axis: int): """ - Method for getting mask for further cutting - - Args: - tensor (`np.ndarray`): input data - division_fractions (`int|list|tuple): number of fraction for each axis (if this is integer than all axis are dividing by same fractions) - domain_selector (`callable`): method for select domain - domain_selector_kwargs (`dict`): args for method `domain_selector` - time_axis (`int`): index of place axis with time - - Returns: - split_idxs (`list`): indexes of place where tensor were divided - accepted_spatial_domains (`np.ndarray with boolen values`): mask with values of True in valuable subdomains + Method for creating a mask that identifies valuable subdomains within the input tensor, which are then used for equation discovery. + + Args: + tensor (`np.ndarray`): The input data tensor. + division_fractions (`int|list|tuple`): The number of divisions for each axis (or a single number for all non-time axes). + domain_selector (`callable`): A function to select valuable subdomains. + domain_selector_kwargs (`dict`): Keyword arguments for the `domain_selector` function. + time_axis (`int`): The index of the time axis in the tensor. + + Returns: + split_idxs (`list`): The indices where the tensor was split. + accepted_spatial_domains (`np.ndarray with boolen values`): A boolean mask indicating the valuable subdomains. + + WHY: This method divides the input tensor into smaller subdomains and uses a domain selector function to identify those subdomains that are most informative for discovering the underlying differential equations. The mask is then used to focus the equation discovery process on the most relevant parts of the data. """ if isinstance(division_fractions, int): sd_shape = tuple([division_fractions for idx in range( @@ -125,6 +163,23 @@ def pruned_domain_boundaries(mask: np.ndarray, split_idxs: list, time_axis: int, Method for finding boundaries by mask Args: + """ + Method for determining the spatial and temporal boundaries of a subdomain based on a significance mask. + + This method identifies the extent of a region where the dynamics are considered significant, + effectively pruning the domain to focus on areas of interest. This is achieved by filtering + slices along each axis based on the density of significant points within them. + + Args: + mask (`np.ndarray`): Boolean mask indicating the significance of dynamics within the subdomain. + split_idxs (`list`): Indices defining the divisions along each axis. + time_axis (`int`): Index of the time axis. + rectangular (`bool`): Flag indicating whether the area is rectangular (default: True). + threshold (`float`): Percentage of significant data required in a slice to be considered part of the boundary (default: 0.5). + + Returns: + boundaries (`list`): A list of tuples, where each tuple represents the start and end indices of the significant region along each axis. + """ mask (`np.ndarray`): boolean mask containg flags about significance all the dynamics in subdomain split_idxs (`list`): indexs of place of division into fraction time_axis (`int`): number of time axis @@ -159,15 +214,34 @@ def pruned_domain_boundaries(mask: np.ndarray, split_idxs: list, time_axis: int, class DomainPruner(object): """ - Class with methods for selecting domain region. - - Attribites: - domain_selector (`callable`): default - majority_rule. - Rule for selecting region. - domain_selector_kwargs (`dict`): parameters for method in `domain_selector`. - bds_init (`int`): flag for marking execution of the selector + Class for pruning the domain based on specified criteria, focusing on data selection and preparation for equation discovery. + + + Attribites: + domain_selector (`callable`): default - majority_rule. + Rule for selecting region. + domain_selector_kwargs (`dict`): parameters for method in `domain_selector`. + bds_init (`int`): flag for marking execution of the selector """ + def __init__(self, domain_selector: Callable = majority_rule, domain_selector_kwargs: dict = dict()): + """ + Initializes the SimpleDomainSelector with a specific domain selection strategy. + + This class sets up the domain selection mechanism used to refine the search space + during the equation discovery process. By storing the provided domain selector + function and its associated keyword arguments, it prepares for subsequent + domain pruning operations. This initialization is crucial for focusing the search + on the most promising areas of the solution space, thereby improving the + efficiency and accuracy of the equation discovery process. + + Args: + domain_selector: The function to use for selecting the domain. Defaults to majority_rule. + domain_selector_kwargs: Keyword arguments to pass to the domain selector function. Defaults to an empty dictionary. + + Returns: + None + """ self.domain_selector = domain_selector self.domain_selector_kwargs = domain_selector_kwargs self.bds_init = False @@ -175,15 +249,29 @@ def __init__(self, domain_selector: Callable = majority_rule, domain_selector_kw def get_boundaries(self, pivotal_tensor: np.ndarray, division_fractions: Union[int, list, tuple] = 3, time_axis=None, rectangular: bool = True): """ - Method for getting a boundaries of the pruned domain + Method for determining the boundaries of the refined domain based on significant regions in the data. - Args: - pivotal_tensor (`np.ndarray`): pattern that guides the domain pruning - will be cutting areas, where values of the `pivotal_tensor` are closed to zero - division_fractions (`int|list|tuple`): optional, default - 3 - number of section in domain area (when type `int` all section fractions will be the same) - time_axis (`int`): index of axis with time, default - None - rectangular (`bool`): flag indecating that area is rectangle + This method identifies and defines the boundaries of the most relevant areas within the data + by analyzing a given tensor and dividing the domain into subregions. + It focuses on isolating the portions of the data that exhibit the most prominent features, + effectively narrowing down the search space for equation discovery. + This is done to improve the efficiency and accuracy of the equation discovery process + by focusing on the most informative parts of the data. + + Args: + pivotal_tensor (`np.ndarray`): A tensor highlighting important areas within the data, + guiding the domain refinement process by emphasizing regions with significant values. + division_fractions (`int|list|tuple`): Specifies the number of subregions to divide the domain into. + If an integer is provided, the domain is divided equally along each dimension. + A list or tuple allows for specifying different division fractions for each dimension. Defaults to 3. + time_axis (`int`): The index of the axis representing time. If None, it attempts to retrieve + the time axis from a global variable. Defaults to None. + rectangular (`bool`): A flag indicating whether the refined domain should be constrained to a rectangular shape. + Defaults to True. + + Returns: + None. The method updates the internal state of the `DomainPruner` object, + specifically `self.bds` with the calculated boundaries. """ if time_axis is None: try: @@ -200,13 +288,17 @@ def get_boundaries(self, pivotal_tensor: np.ndarray, division_fractions: Union[i def prune(self, tensor): """ - Method for applying detected bounds to tensor - - Args: - tensor (`np.ndarray`): input data for pruning - - Returns: - tensor_new (`np.ndarray`): tensor without areas with no dynamics + Applies the detected domain boundaries to the input tensor, effectively focusing on the dynamically relevant regions. + + This method refines the input tensor by removing data points outside the identified domain boundaries. + This ensures that subsequent equation discovery focuses on the areas where the system exhibits meaningful dynamics, + avoiding regions with minimal or no change. + + Args: + tensor (`np.ndarray`): Input data for pruning. + + Returns: + `np.ndarray`: The tensor with areas outside the learned domain boundaries removed. """ if not self.bds_init: raise AttributeError( diff --git a/epde/preprocessing/fin_der.py b/epde/preprocessing/fin_der.py index 328689ce..ac044a58 100644 --- a/epde/preprocessing/fin_der.py +++ b/epde/preprocessing/fin_der.py @@ -10,6 +10,24 @@ def FDderivatives(matrix, axis, idx, grid, max_order, poly_bound): + """ + Calculates finite difference derivatives of a matrix along a specified axis at a given index. + + This method approximates derivatives using finite differences. It selects a stencil of points + around the given index along the specified axis and uses these points to compute the derivatives. + The order of the derivatives calculated is limited to a maximum of 3. This function is used to estimate the rate of change of the data, which is a crucial step in identifying the underlying differential equations. + + Args: + matrix (np.ndarray): The input matrix for which to calculate derivatives. + axis (int): The axis along which to calculate the derivatives. + idx (tuple): The index at which to calculate the derivatives. + grid (np.ndarray): The grid coordinates corresponding to the matrix. + max_order (int): The maximum order of derivatives to calculate (limited to 3). + poly_bound (int): A boundary parameter that defines the region near the edges where a different stencil is used. + + Returns: + np.ndarray: An array containing the calculated derivatives up to the specified `max_order`. These derivatives serve as features for discovering the underlying differential equation. + """ assert idx[axis] < poly_bound or idx[axis] > matrix.shape[axis] - poly_bound if idx[axis] < poly_bound: I = idx[axis] + np.arange(6) diff --git a/epde/preprocessing/interpolation_oversampling.py b/epde/preprocessing/interpolation_oversampling.py index 0e621821..ad480204 100644 --- a/epde/preprocessing/interpolation_oversampling.py +++ b/epde/preprocessing/interpolation_oversampling.py @@ -12,7 +12,46 @@ class BesselInterpolator(object): + """ + Represents an interpolator using Bessel functions. + + This class approximates a function using a linear combination of Bessel + functions of the first kind. It fits the coefficients of the Bessel + functions to the given data. + + Class Methods: + - __init__ + - bf_vals + - approximate + + Class Fields: + x (np.ndarray): The independent variable data. + data (np.ndarray): The dependent variable data. + max_order (int): The maximum order of the Bessel functions to use. + coef_ (np.ndarray): The coefficients of the fitted Bessel function expansion. + """ + def __init__(self, x, data, max_order=5): + """ + Initializes the interpolator by fitting a linear regression model to the data using Bessel basis functions. + + This method prepares the interpolator to approximate a function by determining the coefficients + that best combine the Bessel basis functions to fit the provided data. The linear regression model + is fitted using the provided 'x' values as the independent variable and 'data' as the dependent variable. + Fitting happens during initialization to precompute regression coefficients for faster interpolation later. + + Args: + x (np.ndarray): The independent variable data (1D array). + data (np.ndarray): The dependent variable data (array with the same shape as x). + max_order (int): The maximum order of the Bessel functions to use as basis functions (default is 5). + + Returns: + None + + Class Fields: + max_order (int): The maximum order of the Bessel basis functions. + coef_ (np.ndarray): The coefficients of the fitted linear regression model, representing the weights of each basis function. + """ assert x.ndim == 1, 'Prototype for 1D - data' assert data.shape == x.shape @@ -24,6 +63,26 @@ def __init__(self, x, data, max_order=5): self.coef_ = model.coef_ def bf_vals(self, arg): + """ + Calculates Bessel function values required for basis function expansion. + + This method computes a list of Bessel function values of the first kind (jv) + for orders 0 up to `self.max_order`, evaluated at the input `arg`. These values + serve as the foundation for constructing a basis set used in representing + solutions to differential equations. A list of ones is appended to provide + a constant term in the basis. + + Args: + arg: The argument at which to evaluate the Bessel functions. + Can be a scalar (int or float) or a NumPy array. + + Returns: + A list of NumPy arrays. The first `self.max_order` elements are the + Bessel function values of the first kind for orders 0 to + `self.max_order - 1`. The last element is a NumPy array of ones + with the same size as `arg` if `arg` is an array, or a list containing + a single float 1.0 if `arg` is a scalar. + """ ones = [1.,] if isinstance(arg, (int, float)) else [np.ones(arg.size),] res = ([jv(order, arg) for order in range(self.max_order)] + # [yv(order, arg) for order in range(self.max_order)] + ones) # Конкат. листов - неоптимальная операция @@ -31,4 +90,15 @@ def bf_vals(self, arg): return res def approximate(self, point): + """ + Approximates the function value at a specified point using a linear combination of basis functions. + + This method leverages pre-computed coefficients and basis function values to efficiently estimate the function's value at the given point. This is a core step in reconstructing the underlying function based on its representation in the basis function space. + + Args: + point (float or array-like): The point at which to evaluate the approximation. + + Returns: + float: The approximated function value at the given point. + """ return np.dot(self.coef_, np.array(self.bf_vals(point))) diff --git a/epde/preprocessing/preprocessor.py b/epde/preprocessing/preprocessor.py index 6fcf5762..cbb07bda 100644 --- a/epde/preprocessing/preprocessor.py +++ b/epde/preprocessing/preprocessor.py @@ -14,40 +14,200 @@ class GeneralizedPrepBuilder(ABC): + """ + A base class for building data preparation pipelines. + + This class provides a structure for creating data preparation pipelines, + including methods for setting smoothers, derivative calculators, and + checking the correctness of preprocessing steps. It is intended to be + subclassed by more specific pipeline builders. + + Class Methods: + - prep_pipeline + - set_smoother + - set_deriv_calculator + - check_preprocessing_correctness + """ + def __init__(self): + """ + Initializes a new instance of the GeneralizedPrepBuilder class. + + This class facilitates the preparation of data for the equation discovery process. + It sets up the necessary data structures and configurations required for subsequent steps + like data preprocessing and feature engineering, ensuring a smooth transition into the + equation search phase. + + Args: + self: The object instance. + + Returns: + None. + """ pass @abstractproperty def prep_pipeline(self): + """ + Prepare the data processing pipeline. + + This abstract property must be implemented by subclasses to define the specific steps for preparing the data pipeline. + Subclasses should configure data loading, preprocessing, and any other transformations required before the equation discovery process. + + Args: + self: The object instance. + + Returns: + Pipeline: Configured data processing pipeline. + + Why: This step is essential to ensure that the data is in a suitable format for the equation discovery algorithms, enabling effective and accurate identification of underlying relationships. + """ pass @abstractmethod def set_smoother(self): + """ + Sets the smoother to be used during data preparation. + + This abstract method enforces that subclasses define how smoothing is applied + to the data before equation discovery. Smoothing helps to reduce noise and + improve the accuracy of the identified equations. + + Args: + self: The object instance. + + Returns: + None. + """ pass @abstractmethod def set_deriv_calculator(self): + """ + Sets up the derivative calculation method. + + This abstract method must be implemented by subclasses to define how derivatives are computed. + The derivative calculation is a crucial step in constructing the equation terms. + + Args: + self: The object instance. + + Returns: + None. + """ pass @abstractmethod def check_preprocessing_correctness(self): + """ + Checks the correctness of the preprocessing steps. + + This method verifies that the data preprocessing steps are correctly applied, + ensuring data integrity before equation discovery. Implementations should + raise an exception if inconsistencies or errors are found, preventing + the evolutionary algorithm from operating on flawed data. + + Args: + self: The object instance. + + Returns: + None. + + Why: To guarantee the reliability of the equation discovery process by + validating the consistency and correctness of the preprocessed data. + """ pass class ConcretePrepBuilder(GeneralizedPrepBuilder): + """ + A builder class for constructing a preprocessing pipeline for concrete data. + + This class provides methods to set the derivative calculator, smoother, + and output tests for the preprocessing pipeline. It also includes a method + to check the correctness of the configured preprocessing steps. + + Class Methods: + - __init__ + - reset + - set_tests + - set_smoother + - set_deriv_calculator + - check_preprocessing_correctness + - prep_pipeline + """ + def __init__(self): + """ + Initializes a ConcretePrepBuilder object. + + This constructor prepares the object for configuring and creating data preprocessing pipelines. + It ensures a clean state by calling the reset method, allowing for the construction of a new pipeline. + + Args: + None + + Returns: + None + """ self.reset() def reset(self): + """ + Resets the preprocessing pipeline to its initial state. + + This method prepares the builder for constructing a new preprocessing pipeline by clearing any previously configured steps. It ensures a clean slate for defining a new sequence of preprocessing operations. This is essential when exploring different preprocessing strategies during the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. The method modifies the internal state of the `ConcretePrepBuilder` object. + """ self._prep_pipeline = PreprocessingPipe() self.deriv_calc_set = False self.smoother_set = False self.output_tests = [] def set_tests(self, tests): + """ + Sets the tests to be used for evaluating the generated equation. + + This ensures that the discovered equation is rigorously validated against a predefined set of test cases. + + Args: + tests (list): A list of test functions or data points to evaluate the equation's performance. + + Returns: + None + + Class Fields: + output_tests (list): The list of tests to be outputted. + """ self.output_tests = tests def set_smoother(self, smoother, *args, **kwargs): + """ + Sets the smoothing method for the data preprocessing pipeline. + + This method configures the data preprocessing stage by specifying the smoothing technique to be applied. + It initializes the smoother with the given arguments and stores it within the pipeline. + After setting the smoother, the method verifies the consistency of the preprocessing steps if both the derivative calculation and smoothing methods have been defined. + This ensures that the data is properly prepared for subsequent equation discovery. + + Args: + smoother: The smoother class to be used. + *args: Positional arguments to be passed to the smoother's constructor. + **kwargs: Keyword arguments to be passed to the smoother's constructor. + + Returns: + None. + + Class Fields Initialized: + _prep_pipeline.smoother: An instance of the smoother class. + _prep_pipeline.smoother_args: The positional arguments passed to the smoother. + _prep_pipeline.smoother_kwargs: The keyword arguments passed to the smoother. + """ self._prep_pipeline.smoother = smoother() self._prep_pipeline.smoother_args = args self._prep_pipeline.smoother_kwargs = kwargs @@ -55,6 +215,32 @@ def set_smoother(self, smoother, *args, **kwargs): self.check_preprocessing_correctness() def set_deriv_calculator(self, deriv_calculator, *args, **kwargs): + """ + Sets the derivative calculator for the preprocessing pipeline. + + This method configures the derivative calculation step within the preprocessing pipeline. + It initializes the specified derivative calculator with provided arguments, + preparing it to estimate derivatives from the input data. This is a crucial step + in transforming raw data into a format suitable for equation discovery, as + accurate derivative estimation is essential for identifying the relationships + between variables and their rates of change. After setting the derivative calculator, + the method checks if both the derivative calculator and smoother have been set, + and if so, triggers a check for preprocessing correctness to ensure that the + data is being prepared appropriately for the subsequent equation discovery process. + + Args: + deriv_calculator: The derivative calculator class to be instantiated. + *args: Positional arguments to be passed to the derivative calculator. + **kwargs: Keyword arguments to be passed to the derivative calculator. + + Returns: + None. + + Class Fields: + _prep_pipeline.deriv_calculator: An instance of the derivative calculator class. + _prep_pipeline.deriv_calculator_args: Positional arguments to be passed to the derivative calculator. + _prep_pipeline.deriv_calculator_kwargs: Keyword arguments to be passed to the derivative calculator. + """ self._prep_pipeline.deriv_calculator = deriv_calculator() self._prep_pipeline.deriv_calculator_args = args self._prep_pipeline.deriv_calculator_kwargs = kwargs @@ -62,6 +248,20 @@ def set_deriv_calculator(self, deriv_calculator, *args, **kwargs): self.check_preprocessing_correctness() def check_preprocessing_correctness(self): + """ + Checks the correctness of the preprocessing tool. + + It validates that the chosen preprocessing steps are compatible with the data and the subsequent equation discovery process. + It attempts to run the preprocessing pipeline with a test input and applies a series of output tests to verify the result. + If any error occurs during the process, it indicates an incorrect selection of preprocessing tools, + preventing issues during the equation search phase. + + Args: + self: The object instance. + + Returns: + None. + """ print("Checking correctness of the preprocessing tool:") try: test_call = self._prep_pipeline.run() @@ -73,6 +273,17 @@ def check_preprocessing_correctness(self): @property def prep_pipeline(self): + """ + Prepare the pipeline for equation discovery. + + This method retrieves the prepared pipeline and resets the internal state to ensure a clean slate for the next equation search. This is done to avoid any potential contamination from previous runs and to guarantee the reproducibility of results. + + Args: + self: The object instance. + + Returns: + The prepared pipeline. + """ pipeline = self._prep_pipeline self.reset() return pipeline @@ -80,18 +291,50 @@ def prep_pipeline(self): class PreprocessingPipe(object): """ - Class with instruments for preprocessing input data and calculate derivatives. - - Attributes: - smoother (`callable`): method for smoothing input data before calculate derivatives - smoother_args (`list`): args for `self.smoother` - smoother_kwargs (`dict`): kwargs fot `self.smoother` - - deriv_calculator (`callable`): method for calculating derivatives from data - deriv_calculator_args (`list`): args for `self.deriv_calculator` - deriv_calculator_kwargs (`dict`): kwargs for `self.deriv_calculator` + Class for preparing data for equation discovery, including scaling, smoothing, and derivative calculation. + + + Attributes: + smoother (`callable`): method for smoothing input data before calculate derivatives + smoother_args (`list`): args for `self.smoother` + smoother_kwargs (`dict`): kwargs fot `self.smoother` + + deriv_calculator (`callable`): method for calculating derivatives from data + deriv_calculator_args (`list`): args for `self.deriv_calculator` + deriv_calculator_kwargs (`dict`): kwargs for `self.deriv_calculator` """ + def __init__(self): + """ + Initializes the PreprocessingPipe with tools for data preparation. + + The PreprocessingPipe utilizes a smoother and derivative calculator to prepare data + for differential equation discovery. This initialization sets up these tools + with their respective arguments and keyword arguments, readying the pipeline + for data processing. + + Args: + None + + Returns: + None + + Attributes: + smoother: The smoother object, initially set to None. Used for smoothing data. + deriv_calculator: The derivative calculator object, initially set to None. Used for calculating derivatives. + smoother_args: Positional arguments for the smoother, initially set to None. + smoother_kwargs: Keyword arguments for the smoother, initialized as an empty dictionary. + deriv_calculator_kwargs: Keyword arguments for the derivative calculator, initialized as an empty dictionary. + deriv_calculator_args: Positional arguments for the derivative calculator, initially set to None. + + Why: + Initializing these attributes allows the PreprocessingPipe to be configured + with specific smoothing and derivative calculation methods. This is crucial + for preparing the data in a way that facilitates accurate discovery of + underlying differential equations. The flexibility in configuring these + tools enables the pipeline to adapt to different data characteristics + and equation discovery tasks. + """ self.smoother = None self.deriv_calculator = None @@ -103,11 +346,16 @@ def __init__(self): def use_grid(self, grid): """ - Method to set parameter 'grid' to kwargs of methods for smoothing and derivative's calculating. - + Set the grid used for smoothing and derivative calculations. + + This method ensures that both the smoother and derivative calculator utilize the same grid, + which is crucial for maintaining consistency and accuracy in subsequent equation discovery steps. + By aligning the grid, we ensure that the derivatives and smoothed data are computed on the same + domain, preventing potential artifacts or inaccuracies in the identified equations. + Args: - grid (`np.ndarray`): value of grid - + grid (`np.ndarray`): The grid on which smoothing and derivative calculations will be performed. + Returns: None """ @@ -118,16 +366,16 @@ def use_grid(self, grid): def run(self, data, grid=None, max_order: Union[list, int] = 1): """ - Method that runs process of calculation derivatives. - - Args: - data (`np.ndarray`): values from which derivatives are calculated - grid (`np.ndarray`, optional): the grid on which the data is viewed - max_order (`list`|`int`, optional): max order of derivatives - - Returns: - np.ndarray: smoothing data if `self.smoother` is not None, else - original data - np.ndarray: calculated derivatives + Calculates derivatives of the input data, optionally smoothing it beforehand. This is a crucial step in identifying the underlying differential equations, as derivatives are fundamental components of such equations. + + Args: + data (`np.ndarray`): The input data for derivative calculation. + grid (`np.ndarray`, optional): The grid on which the data is defined. If provided, it's used for derivative calculations. Defaults to None. + max_order (`list`|`int`, optional): The maximum order of derivatives to compute. Defaults to 1. + + Returns: + `np.ndarray`: The smoothed data if a smoother is applied; otherwise, the original data. + `np.ndarray`: The calculated derivatives of the input data. """ self.deriv_calculator_kwargs['max_order'] = max_order if grid is not None: diff --git a/epde/preprocessing/preprocessor_setups.py b/epde/preprocessing/preprocessor_setups.py index b4171006..7c213628 100644 --- a/epde/preprocessing/preprocessor_setups.py +++ b/epde/preprocessing/preprocessor_setups.py @@ -14,19 +14,105 @@ class PreprocessorSetup: + """ + Sets up preprocessing configurations for different derivative calculation methods. + + This class provides methods to configure preprocessing steps for various + derivative calculation techniques, including ANN-based, finite difference, + spectral, and polynomial differentiation methods. It uses a builder pattern + to construct the preprocessing pipeline. + + Class Methods: + - __init__ + - builder + - build_ANN_preprocessing + - build_FD_preprocessing + - build_spectral_preprocessing + - build_poly_diff_preprocessing + """ + def __init__(self): + """ + Initializes the PreprocessorSetup object. + + This method prepares the object for configuring and managing data preprocessing steps. It sets up the internal state required to build a preprocessing pipeline. + + Args: + self: The PreprocessorSetup instance. + + Returns: + None. + + Class Fields: + _builder: An internal builder object, initially set to None. This builder will be used to construct the preprocessing pipeline. + """ self._builder = None @property def builder(self): + """ + Returns the equation builder associated with this setup. + + This builder is responsible for constructing and managing the symbolic + representation of the differential equations. It encapsulates the + grammar and available operations used in the equation discovery process. + Accessing the builder allows for customization and extension of the + equation search space. + + Returns: + EquationBuilder: The builder object. + + Why: + The equation builder is a core component for defining the search space + of possible differential equations. Accessing it allows users to + inspect and modify the building blocks used in the equation discovery + process, such as available operators, functions, and variables. + """ return self._builder @builder.setter def builder(self, builder: ConcretePrepBuilder): + """ + Sets the concrete builder responsible for constructing the preprocessing pipeline. + + This method configures the setup with a specific builder, which defines the steps + involved in preparing the data for equation discovery. By setting the builder, + the setup ensures that the data is properly transformed and formatted before + being used to identify underlying differential equations. + + Args: + builder: The concrete builder to use for constructing the preprocessing pipeline. + + Returns: + None. + + Class Fields: + _builder: The builder object used to construct the product. + """ self._builder = builder def build_ANN_preprocessing(self, test_output=False, epochs_max=1e5, loss_mean=1000, batch_frac=0.8): + """ + Builds the preprocessing steps using an Artificial Neural Network (ANN) model. + + This method configures the smoother and derivative calculator components + of the data preprocessing pipeline using ANN-based methods. It sets up + an `ANNSmoother` for smoothing and an `AdaptiveFiniteDeriv` for + derivative calculation. This is a crucial step in preparing the data + for equation discovery, as it reduces noise and provides accurate + derivative estimates, which are essential for identifying the underlying + differential equations. + + Args: + test_output: A flag for testing the output (currently unused). + epochs_max: The maximum number of epochs for training the ANN smoother. + loss_mean: The target mean loss value for training the ANN smoother. + batch_frac: The fraction of data to use in each batch during ANN smoother training. + + Returns: + None + """ smoother_args = () smoother_kwargs = {'grid': None, 'epochs_max': epochs_max, 'loss_mean': loss_mean, 'batch_frac': batch_frac} @@ -39,6 +125,22 @@ def build_ANN_preprocessing(self, test_output=False, epochs_max=1e5, **deriv_calculator_kwargs) def build_FD_preprocessing(self): + """ + Builds the preprocessing steps for functional data within the EPDE framework. + + This method configures the smoother and derivative calculator + used in the preprocessing pipeline. It sets the smoother to a + PlaceholderSmoother and the derivative calculator to an + AdaptiveFiniteDeriv. This ensures that the data is properly prepared + for subsequent equation discovery by providing a standardized and + flexible preprocessing approach. + + Args: + self: The instance of the class. + + Returns: + None. + """ smoother_args = () smoother_kwargs = {} @@ -50,6 +152,21 @@ def build_FD_preprocessing(self): **deriv_calculator_kwargs) def build_spectral_preprocessing(self, n=None, steepness=1): + """ + Builds the spectral preprocessing pipeline for derivative estimation. + + This method configures the smoother and derivative calculator within the + builder to use spectral methods. It sets a placeholder smoother and a spectral derivative calculator. + This is a crucial step in preparing the data for the equation discovery process, + as accurate derivative estimation is essential for identifying the underlying differential equations. + + Args: + n: The number of points to use in the spectral derivative calculation. + steepness: A parameter controlling the steepness of the spectral filter. + + Returns: + None + """ smoother_args = () smoother_kwargs = {} @@ -62,6 +179,22 @@ def build_spectral_preprocessing(self, n=None, steepness=1): def build_poly_diff_preprocessing(self, use_smoothing=False, sigma=1, mp_poolsize=4, polynomial_window=9, poly_order=None, include_time=False): + """ + Builds a preprocessing pipeline optimized for polynomial differentiation, a crucial step in revealing underlying patterns from noisy data. + + This method configures the preprocessing steps, setting up a smoother (Gaussian or a placeholder for no smoothing) and a polynomial derivative calculator. These components work together to prepare the data for accurate derivative estimation, which is essential for discovering the underlying differential equations. + + Args: + use_smoothing (bool): Whether to use Gaussian smoothing before differentiation to reduce noise. + sigma (int): Standard deviation for Gaussian smoothing (if used). + mp_poolsize (int): Number of processes to use for multiprocessing in derivative calculation, speeding up the computation. + polynomial_window (int): Size of the window for polynomial fitting, influencing the smoothness of the derivative. + poly_order (int, None): Order of the polynomial to fit. If None, it will be automatically determined. + include_time (bool): Whether to include time as a feature in the Gaussian smoothing, useful for time-series data. + + Returns: + None: This method configures the internal state of the `builder` object. + """ smoother_args = () smoother_kwargs = {'sigma': sigma, 'include_time' : include_time} diff --git a/epde/preprocessing/smoothers.py b/epde/preprocessing/smoothers.py index f9ed5ecc..db7e55d9 100644 --- a/epde/preprocessing/smoothers.py +++ b/epde/preprocessing/smoothers.py @@ -17,22 +17,121 @@ import epde.globals as global_var class AbstractSmoother(ABC): + """ + Abstract base class for smoothers. + + This class defines the interface for all smoother classes. + It provides a common structure for implementing smoothing algorithms. + + Class Methods: + - __call__: + """ + def __init__(self, *args, **kwargs): + """ + Initializes the smoother object. + + This method serves as a placeholder in the abstract class. + Subclasses should override this method to implement specific + initialization procedures required for different smoothing techniques. + + Args: + *args: Variable length argument list. These arguments will be passed to the underlying smoother. + **kwargs: Arbitrary keyword arguments. These keyword arguments will be passed to the underlying smoother. + + Returns: + None. + + Why: This initialization ensures that all concrete smoother classes have a consistent interface, + allowing them to be used interchangeably within the equation discovery process. + """ pass def __call__(self, data, *args, **kwargs): + """ + Applies a smoothing operation to the input data. + + This method is part of the abstract base class for all smoothing techniques + within the EPDE framework. It ensures that all concrete smoother + implementations provide a consistent interface for applying smoothing + operations. + + Raises: + NotImplementedError: Always raised, as this is an abstract method and + must be implemented by subclasses. + + Args: + data (array-like): The input data to be smoothed. This can be a + NumPy array or any other array-like object that can be processed + by the smoothing algorithm. + *args: Variable length argument list. These arguments are passed + to the underlying smoothing implementation. + **kwargs: Arbitrary keyword arguments. These keyword arguments are + passed to the underlying smoothing implementation. + + Returns: + None: This method always raises an error, as it is an abstract method. + """ raise NotImplementedError('Calling abstract smoothing object') class PlaceholderSmoother(AbstractSmoother): + """ + A placeholder smoother that does nothing. + + This smoother simply returns the input data without modification. + It is useful as a default or when no smoothing is desired. + """ + def __init__(self): + """ + Initializes the PlaceholderSmoother. + + This class serves as a placeholder for more sophisticated smoothing techniques. + Currently, it performs no smoothing, acting as an identity operation. + It is used as a base or default option within the EPDE framework when smoothing is not required. + + Args: + self: The object instance. + + Returns: + None. + """ pass def __call__(self, data, *args, **kwargs): + """ + Applies a placeholder transformation to the input data. + + This transformation serves as a stand-in when no actual smoothing or data modification is required. It ensures that the data pipeline remains consistent and functional even when a smoothing step is not necessary. + + Args: + data: The input data, which can be of any type. + + Returns: + The input data, returned without any modifications. This maintains data integrity when smoothing is not needed. + """ return data def baseline_ann(dim): + """ + Creates a baseline artificial neural network (ANN) model for approximating differential equation solutions. + + This method constructs a simple feedforward neural network using PyTorch. + The network consists of several linear layers with ReLU activation functions. + The input dimension is determined by the 'dim' parameter, and the output is a single value, + representing the approximated solution at a given point. This baseline model serves as a + fundamental building block for more complex equation discovery and solution approximation tasks + within the EPDE framework. + + Args: + dim: The input dimension of the first linear layer, corresponding to the number of independent + variables in the differential equation. + + Returns: + torch.nn.Sequential: A PyTorch Sequential model representing the ANN. + """ model = torch.nn.Sequential( torch.nn.Linear(dim, 256), torch.nn.ReLU(), @@ -47,9 +146,56 @@ def baseline_ann(dim): return model class Rational(torch.nn.Module): + """ + Represents a rational number with a numerator and denominator. + + Class Methods: + - __init__: Initializes a Rational object. + - __str__: Returns a string representation of the Rational object. + - __repr__: Returns a string representation of the Rational object for debugging. + - __eq__: Checks if two Rational objects are equal. + - __ne__: Checks if two Rational objects are not equal. + - __lt__: Checks if one Rational object is less than another. + - __gt__: Checks if one Rational object is greater than another. + - __le__: Checks if one Rational object is less than or equal to another. + - __ge__: Checks if one Rational object is greater than or equal to another. + - __add__: Adds two Rational objects. + - __sub__: Subtracts two Rational objects. + - __mul__: Multiplies two Rational objects. + - __truediv__: Divides two Rational objects. + - __float__: Converts the Rational object to a float. + + Attributes: + numerator (int): The numerator of the rational number. + denominator (int): The denominator of the rational number. + """ + def __init__(self, Data_Type = torch.float32, Device = torch.device('cpu')): + """ + Initializes the Rational activation function with trainable numerator and denominator coefficients. + + This initialization sets up the parameters that define the rational function, + allowing the network to learn an appropriate non-linear activation. The + coefficients are initialized to values that provide a good starting point + for approximating ReLU, enabling effective gradient-based optimization + during training. + + Args: + Data_Type (torch.dtype): The data type for the coefficients (e.g., torch.float32). + Device (torch.device): The device to store the coefficients on (e.g., 'cpu' or 'cuda'). + + Returns: + None + + Class Fields: + a (torch.nn.parameter.Parameter): Numerator coefficients, initialized to (1.1915, 1.5957, 0.5, .0218). Requires gradient tracking. + b (torch.nn.parameter.Parameter): Denominator coefficients, initialized to (2.3830, 0.0, 1.0). Requires gradient tracking. + + Why: + Initializing the Rational activation function with trainable coefficients allows the model to learn complex, non-linear relationships within the data, which is crucial for accurately representing the dynamics described by differential equations. The initial values are chosen to provide a good starting point for optimization, facilitating the discovery of governing equations from data. + """ # This activation function is based on the following paper: # Boulle, Nicolas, Yuji Nakatsukasa, and Alex Townsend. "Rational neural # networks." arXiv preprint arXiv:2004.01902 (2020). @@ -72,15 +218,17 @@ def __init__(self, self.b.requires_grad_(True) def forward(self, X : torch.tensor): - """ This function applies a rational function to each element of X. - ------------------------------------------------------------------------ - Arguments: - X: A tensor. We apply the rational function to every element of X. - ------------------------------------------------------------------------ - Returns: - Let N(x) = sum_{i = 0}^{3} a_i x^i and D(x) = sum_{i = 0}^{2} b_i x^i. - Let R = N/D (ignoring points where D(x) = 0). This function applies R - to each element of X and returns the resulting tensor. """ + """ + Applies a rational function, defined by learnable parameters, element-wise to the input tensor. + + The rational function is a ratio of two polynomials, allowing the model to approximate complex functions and relationships within the data. This transformation enhances the model's ability to capture non-linear dynamics present in the data. + + Args: + X (torch.tensor): The input tensor to which the rational function will be applied. + + Returns: + torch.tensor: A tensor of the same shape as X, with each element transformed by the rational function. + """ # Create aliases for self.a and self.b. This makes the code cleaner. a = self.a @@ -95,14 +243,50 @@ def forward(self, X : torch.tensor): return N_X/D_X class Sin(torch.nn.Module): + """ + Applies the sine function to the input tensor. + + Class Methods: + - forward: + """ + def __init__(self): + """ + Initializes a new instance of the Sin class. + + This constructor prepares the `Sin` object for symbolic manipulation and equation discovery. It ensures that the object is properly set up to represent a sine function within the broader equation search space. + + Args: + self: The object instance. + + Returns: + None + """ super(Sin, self).__init__() def forward(self, x): + """ + Applies the sine function element-wise to the input tensor. + + This operation is a fundamental building block for constructing more complex equation structures within the evolutionary search process. By applying sine, the framework can explore non-linear relationships and periodic behaviors in the data, which are common in many physical and biological systems modeled by differential equations. + + Args: + x (torch.Tensor): The input tensor. + + Returns: + torch.Tensor: A tensor with the same shape as input, containing the sine of each element in `x`. + """ x = torch.sin(x) return x class NN(torch.nn.Module): + """ + A feedforward neural network class. + + Class Methods: + - __init__: + """ + def __init__(self, Num_Hidden_Layers : int = 3, Neurons_Per_Layer : int = 20, # Neurons in each Hidden Layer @@ -112,6 +296,24 @@ def __init__(self, Device : torch.device = torch.device('cpu'), Activation_Function : str = "Tanh", Batch_Norm : bool = False): + """ + Initializes the neural network architecture. + + This method sets up the layers, activation functions, and normalization layers (if specified) of the neural network. The architecture is designed to provide a flexible foundation for representing functions, allowing the framework to approximate solutions to differential equations. The dimensions of the input and output layers, as well as the number of hidden layers and neurons per layer, are configurable to suit the complexity of the target function. + + Args: + Num_Hidden_Layers (int): The number of hidden layers in the network. + Neurons_Per_Layer (int): The number of neurons in each hidden layer. + Input_Dim (int): The dimension of the input. + Output_Dim (int): The dimension of the output. + Data_Type (torch.dtype): The data type to use for the network's parameters (e.g., torch.float32). + Device (torch.device): The device to use for the network's computations (e.g., torch.device('cpu') or torch.device('cuda')). + Activation_Function (str): The activation function to use for the hidden layers (e.g., "Tanh", "Sin", "Rational"). + Batch_Norm (bool): Whether to use batch normalization. + + Returns: + None + """ # For the code below to work, Num_Hidden_Layers, Neurons_Per_Layer, # Input_Dim, and Output_Dim must be positive integers. assert(Num_Hidden_Layers > 0), "Num_Hidden_Layers must be positive. Got %du" % Num_Hidden_Layers; @@ -204,23 +406,19 @@ def __init__(self, exit(); def forward(self, X: torch.Tensor) -> torch.Tensor: - """ Forward method for the NN class. Note that the user should NOT call - this function directly. Rather, they should call it through the __call__ - method (using the NN object like a function), which is part of the - module class and calls forward. - - ------------------------------------------------------------------------ - Arguments: - - X: A batch of inputs. This should be a B by Input_Dim tensor, where B - is the batch size. The ith row of X should hold the ith input. - - ------------------------------------------------------------------------ + """ + Forward pass through the neural network. This method propagates the input tensor `X` through the network's layers, applying batch normalization (if enabled) and activation functions to each hidden layer. The final layer's output is then returned. Note that the user should NOT call this function directly. Rather, they should call it through the __call__ method (using the NN object like a function), which is part of the module class and calls forward. + + The forward pass is a crucial step in both training and inference, as it determines the network's output for a given input. By structuring the network in this way, we can approximate complex functions and discover underlying relationships within the data, which is essential for identifying governing differential equations. + + Args: + X: A batch of inputs. This should be a B by Input_Dim tensor, where B + is the batch size. The ith row of X should hold the ith input. + Returns: - - If X is a B by Input_Dim tensor, then the output of this function is a - B by Output_Dim tensor, whose ith row holds the value of the network - applied to the ith row of X. """ + A B by Output_Dim tensor, whose ith row holds the value of the network + applied to the ith row of X. + """ # If we are using batch normalization, then normalize the inputs. if (self.Batch_Norm == True): @@ -234,12 +432,57 @@ def forward(self, X: torch.Tensor) -> torch.Tensor: return self.Layers[self.Num_Hidden_Layers](X); class ANNSmoother(AbstractSmoother): + """ + Applies smoothing to data using an Artificial Neural Network (ANN). + + Class Methods: + - __init__: + """ + def __init__(self): + """ + Initializes the ANNSmoother object. + + This method initializes the base class and sets the internal model to None. + The model will be later populated with a suitable approximation method + to represent the discovered equation. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + model (object): The approximation model used for smoothing. Initialized to None. + """ super().__init__() # Optional depending on AbstractSmoother self.model = None def __call__(self, data, grid, epochs_max=1000, loss_mean=1000, loss_threshold=1e-8, batch_frac=0.5, val_frac=0.1, learning_rate=1e-3, return_ann=False, device='cpu'): + """ + Applies a trained neural network to smooth the input data and approximate the underlying function. + + This method leverages a neural network to learn the relationship between the input grid and the corresponding data values. By training the network on flattened data, it captures the essential features and dependencies, effectively smoothing out noise and irregularities. The trained network is then used to predict values on the original grid, providing a smoothed representation of the data. Early stopping is employed to prevent overfitting and optimize the network's generalization ability. + + Args: + data (np.ndarray): The input data to be smoothed. + grid (tuple[np.ndarray]): The grid corresponding to the input data. Each element of the tuple represents a dimension of the grid. + epochs_max (int, optional): The maximum number of training epochs. Defaults to 1000. + loss_mean (float, optional): Unused parameter. Defaults to 1000. + loss_threshold (float, optional): The loss threshold for early stopping. Defaults to 1e-8. + batch_frac (float, optional): Unused parameter. Defaults to 0.5. + val_frac (float, optional): The fraction of data to use for validation. Defaults to 0.1. + learning_rate (float, optional): The learning rate for the optimizer. Defaults to 1e-3. + return_ann (bool, optional): Whether to return the trained ANN model. Defaults to False. + device (str, optional): The device to use for training (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + + Returns: + np.ndarray: The smoothed data as a NumPy array with the same shape as the input data. + + WHY: This smoothing operation is crucial for preparing data for equation discovery. By reducing noise and highlighting underlying patterns, it enables the evolutionary algorithm to identify more accurate and meaningful differential equation models. + """ if torch.cuda.is_available(): device = "cuda" # Convert to int if passed as float @@ -337,10 +580,52 @@ def __call__(self, data, grid, epochs_max=1000, loss_mean=1000, loss_threshold=1 class GaussianSmoother(AbstractSmoother): + """ + Applies a Gaussian smoothing filter to data. + + Attributes: + sigma: The standard deviation for the Gaussian kernel. + truncate: Truncate the filter at this many standard deviations. + """ + def __init__(self): + """ + Initializes a new instance of the GaussianSmoother class. + + This constructor prepares the Gaussian Smoother for subsequent operations. + Currently, it performs no specific initialization, deferring setup to later + stages when data and smoothing parameters are available. + + Args: + self: The object instance. + + Returns: + None. + + Why: + The GaussianSmoother is initialized without any specific parameters. + This allows for a flexible setup where data and smoothing parameters + can be provided later, adapting the smoother to different datasets + and noise levels, which is essential for discovering underlying + differential equations from potentially noisy data. + """ pass def __call__(self, data, kernel_fun='gaussian', **kwargs): + """ + Applies a Gaussian smoothing kernel to the input data. + + This method smooths the input data using a Gaussian filter, effectively reducing noise and highlighting underlying trends. + The smoothing is applied either across the entire dataset or independently for each time point, depending on the input parameters. + + Args: + data (np.ndarray): The data to be smoothed. + kernel_fun (str, optional): The type of smoothing kernel to apply. Currently, only 'gaussian' is supported. Defaults to 'gaussian'. + **kwargs: Keyword arguments to pass to the Gaussian filter, such as 'sigma' (the standard deviation for Gaussian kernel) and 'include_time' (boolean flag to indicate whether to smooth across time). + + Returns: + np.ndarray: The smoothed data. + """ smoothed_data = np.empty_like(data) if kernel_fun == 'gaussian': if not kwargs['include_time'] and np.ndim(data) > 1: diff --git a/epde/preprocessing/smoothing.py b/epde/preprocessing/smoothing.py index c80e1d61..3c1c387e 100644 --- a/epde/preprocessing/smoothing.py +++ b/epde/preprocessing/smoothing.py @@ -11,6 +11,18 @@ def smoothing(data, kernel_fun, **kwargs): + """ + Applies a smoothing filter to the input data, enhancing the signal and reducing noise before equation discovery. + + Args: + data (np.ndarray): The data to be smoothed. + kernel_fun (str): The type of smoothing kernel to use (e.g., 'gaussian'). Currently only 'gaussian' is supported. + **kwargs: Keyword arguments to pass to the smoothing function. + Must include 'sigma' for gaussian kernel. If 'include_time' is False and the data has more than one dimension, smoothing is applied along each time slice. + + Returns: + np.ndarray: The smoothed data. + """ smoothed_data = np.empty_like(data) if kernel_fun == 'gaussian': if kernel_fun == 'gaussian': diff --git a/epde/solver/cache.py b/epde/solver/cache.py index c3fbedf3..002a9227 100644 --- a/epde/solver/cache.py +++ b/epde/solver/cache.py @@ -20,6 +20,20 @@ def count_output(model): + """ + Counts the output features of the last relevant layer in a given model. + + This method iterates through the layers of a model in reverse order, searching for the + 'out_features' attribute. It returns the 'out_features' of the first layer found with this attribute. + This is useful for determining the dimensionality of the solution space when constructing + equation candidates. + + Args: + model: The model to analyze. + + Returns: + int: The output features of the last layer with 'out_features' attribute, or None if no such layer is found. + """ modules, output_layer = list(model.modules()), None for layer in reversed(modules): if hasattr(layer, 'out_features'): @@ -29,6 +43,17 @@ def count_output(model): def create_random_fn(eps): + """ + Creates a function that adds random noise to the parameters of linear and convolutional layers. + This helps to explore the model space by slightly perturbing the weights and biases, + allowing the evolutionary algorithm to discover new and potentially better-performing equation structures. + + Args: + eps (float): The magnitude of the random noise to add to the parameters. + + Returns: + function: A function that takes a PyTorch module as input and adds random noise to the weights and biases of its linear and convolutional layers. + """ def randomize_params(m): if type(m) == torch.nn.Linear or type(m) == torch.nn.Conv2d: m.weight.data = m.weight.data + \ @@ -38,6 +63,15 @@ def randomize_params(m): return randomize_params def remove_all_files(folder): + """ + Removes all files and subdirectories within a specified folder to ensure a clean state before or after equation discovery processes. This is crucial for managing temporary files and directories created during the search for the best equation structure. + + Args: + folder (str): The path to the folder whose contents should be removed. + + Returns: + None + """ for filename in os.listdir(folder): file_path = os.path.join(folder, filename) try: @@ -51,11 +85,28 @@ def remove_all_files(folder): class Model_prepare(): """ Prepares initial model. Serves for computing acceleration.\n - Saves the trained model to the cache, and subsequently it is possible to use pre-trained model (if \\\ - it saved and if the new model is structurally similar) to sped up computing.\n - If there isn't pre-trained model in cache, the training process will start from the beginning. + Saves the trained model to the cache, and subsequently it is possible to use pre-trained model (if \\\ + it saved and if the new model is structurally similar) to sped up computing.\n + If there isn't pre-trained model in cache, the training process will start from the beginning. """ + def __init__(self, grid, equal_cls, model, mode, weak_form): + """ + Initializes the Model_prepare instance, setting up the computational environment for equation discovery. + + This involves associating the provided grid, equation class, model, mode of operation, and weak form setting with the instance. + The cache directory is initialized to store intermediate results, which speeds up the equation discovery process by avoiding redundant computations. + + Args: + grid (object): The spatial or temporal grid on which the data is defined. + equal_cls (object): The class defining the equation structure and its parameters. + model (object): The model object containing data and any prior assumptions. + mode (str): The mode of operation (e.g., training, validation). + weak_form (bool): A flag indicating whether to use the weak form of the equation. + + Returns: + None + """ self.grid = grid self.equal_cls = equal_cls self.model = model @@ -68,10 +119,32 @@ def __init__(self, grid, equal_cls, model, mode, weak_form): self.cache_dir = os.path.normpath((os.path.join(os.path.dirname(file), '..','cache'))) def change_cache_dir(self, string): + """ + Changes the directory where intermediate results and processed data are stored. + + This allows to control where the framework saves its working files, + ensuring reproducibility and efficient management of storage space + during the equation discovery process. + + Args: + string (str): The new path to the cache directory. + + Returns: + None + """ self.cache_dir=string return None def clear_cache_dir(self, directory=None): + """ + Clears the specified cache directory, removing all files within it. This ensures that outdated or irrelevant cached data does not interfere with subsequent equation discovery runs, promoting accurate and efficient model generation. + + Args: + directory (str, optional): The path to the cache directory to clear. If None, the default cache directory associated with the `Model_prepare` instance is cleared. Defaults to None. + + Returns: + None + """ if directory==None: remove_all_files(self.cache_dir) else: @@ -80,6 +153,22 @@ def clear_cache_dir(self, directory=None): @staticmethod def cache_files(files, nmodels): + """ + Caches a subset of files to optimize the search for the best equation structure. + + When exploring a large space of possible differential equations, + it can be beneficial to work with a subset of the available models + to reduce computational cost. This method selects a subset of files + representing these models. + + Args: + files: A list of files to consider for caching. + nmodels: The number of models to randomly select from the cache. + If None, all files are used. + + Returns: + A NumPy array of indices representing the cached files. + """ # at some point we may want to reduce the number of models that are # checked for the best in the cache @@ -93,6 +182,20 @@ def cache_files(files, nmodels): return cache_n def grid_model_mat(self, cache_model): + """ + Generates a grid representing the input space and prepares a neural network model for approximating the solution. + + This method constructs a grid from the object's grid attribute, which represents the domain where the differential equation is defined. It also initializes or reuses a neural network model that will learn to approximate the solution of the differential equation over this grid. + + Args: + cache_model: An optional pre-existing neural network model. If provided, this model will be used; otherwise, a new model will be created. + + Returns: + tuple: A tuple containing the grid matrix (NN_grid) and the neural network model (cache_model). If cache_model was None, a new model is created and returned; otherwise, the original cache_model is returned. + + Why: + The grid is needed to discretize the domain of the differential equation, allowing the neural network to learn the solution at specific points. The neural network model serves as a function approximator, learning to map the grid points to the corresponding solution values. + """ NN_grid = torch.vstack([self.grid[i].reshape(-1) for i in \ range(self.grid.shape[0])]).T.float() out = self.model.shape[0] @@ -111,6 +214,22 @@ def grid_model_mat(self, cache_model): @staticmethod def mat_op_coeff(operator): + """ + Applies necessary transformations to the coefficients of operators. + + This method iterates through the provided operator(s) and reshapes the + coefficient of each term within the operator(s) if it's a PyTorch tensor to ensure compatibility + with subsequent mathematical operations. It also issues a warning if a coefficient is callable, + as this might interfere with the caching mechanism and lead to unexpected behavior during + equation discovery. This ensures that coefficients are in the correct format for equation processing + and alerts the user to potential issues with dynamically defined coefficients. + + Args: + operator: The operator to process. It can be a single operator (dict) or a list of operators. + + Returns: + The processed operator(s) with reshaped coefficients (if applicable). + """ if type(operator) is not list: operator = [operator] for op in operator: @@ -126,16 +245,17 @@ def mat_op_coeff(operator): @staticmethod def model_reform(init_model, model): """ - As some models are nn.Sequential class objects, - but another models are nn.Module class objects. - This method does checking the solver model (init_model) - and the cache model (model). - Args: - init_model: [nn.Sequential or class(nn.Module)]. - model: [nn.Sequential or class(nn.Module)]. - Returns: - * **init_model** -- [nn.Sequential or nn.ModuleList] \n - * **model** -- [nn.Sequential or nn.ModuleList]. + Reformats the initial and evolved models to ensure compatibility with subsequent operations. + + This method checks if the provided models (`init_model` and `model`) are directly indexable (e.g., `nn.Sequential`) or if they are encapsulated within a `model` attribute (e.g., a custom `nn.Module`). If the models are encapsulated, it extracts the underlying model. This ensures that both models can be consistently accessed as indexable structures, facilitating operations like layer-wise comparisons or modifications during the evolutionary process. + + Args: + init_model: The initial model (either `nn.Sequential` or a custom `nn.Module` with a `model` attribute). + model: The evolved model (either `nn.Sequential` or a custom `nn.Module` with a `model` attribute). + + Returns: + * **init_model** -- The reformed initial model (either `nn.Sequential` or `nn.ModuleList`). + * **model** -- The reformed evolved model (either `nn.Sequential` or `nn.ModuleList`). """ try: model[0] @@ -154,16 +274,22 @@ def cache_lookup(self, lambda_operator: float = 1., lambda_bound: float = 0.001, nmodels: Union[int, None] = None, save_graph: bool = False, cache_verbose: bool = False, return_normalized_loss: bool = False) -> Tuple[dict, torch.Tensor]: """ - Looking for a saved cache. - Args: - lambda_bound: an arbitrary chosen constant, influence only convergence speed. - save_graph: boolean constant, responsible for saving the computational graph. - cache_dir: directory where saved cache in. - nmodels: maximal number of models that are looked before optimization - cache_verbose: more detailed info about models in cache. - Returns: - * **best_checkpoint** -- best model with optimizator state.\n - * **min_loss** -- minimum error in pre-trained error. + Looks for a pre-trained model in the cache to initialize the optimization process. + + This function searches for previously saved models in the specified cache directory and evaluates their performance on the target problem. The best-performing model, based on the loss function, is then selected to provide a warm start for the optimization, potentially accelerating convergence and improving the final solution. + + Args: + lambda_operator (float, optional): Weight for the operator loss term. Defaults to 1.0. + lambda_bound (float, optional): Weight for the boundary loss term. Defaults to 0.001. + nmodels (Union[int, None], optional): Maximum number of models to consider from the cache. If None, all models are considered. Defaults to None. + save_graph (bool, optional): Whether to save the computational graph during evaluation. Defaults to False. + cache_verbose (bool, optional): Whether to print detailed information about the models in the cache. Defaults to False. + return_normalized_loss (bool, optional): Whether to return the normalized loss instead of the raw loss. Defaults to False. + + Returns: + Tuple[dict, torch.Tensor]: A tuple containing: + * **best_checkpoint** (dict or None): A dictionary containing the best model's state, including the model itself, its state dictionary, and the optimizer's state dictionary. Returns None if no suitable model is found in the cache. + * **min_loss** (torch.Tensor): The minimum loss achieved by the best model in the cache. """ files = glob.glob(self.cache_dir + '\*.tar') if len(files) == 0: @@ -220,13 +346,16 @@ def cache_lookup(self, lambda_operator: float = 1., lambda_bound: float = 0.001, def save_model(self, prep_model: Any, state: dict, optimizer_state: dict, name: Union[str, None] = None): """ - Saved model in a cache (uses for 'NN' and 'autograd' methods). - Args: - prep_model: model to save. - state: a dict holding current model state (i.e., dictionary that maps each layer to its parameter tensor). - optimizer_state: a dict holding current optimization state (i.e., values, hyperparameters). - cache_dir: directory where saved cache in. - name: name for a model. + Saves the trained model, its state, and the optimizer's state to a cache file. This allows for later reuse of the trained model without retraining, which is useful for comparing different equation structures and optimization strategies. + + Args: + prep_model: The trained model to be saved. + state: A dictionary containing the model's state (layer-to-parameter tensor mapping). + optimizer_state: A dictionary containing the optimizer's state (values, hyperparameters). + name: An optional name for the saved model file. If None, a timestamp is used. + + Returns: + None. The model is saved to a file in the cache directory. """ if name == None: name = str(datetime.datetime.now().timestamp()) @@ -248,12 +377,18 @@ def save_model(self, prep_model: Any, state: dict, optimizer_state: dict, name: def save_model_mat(self, name: None = None, cache_model: None = None, cache_verbose: bool=False): """ - Saved model in a cache (uses for 'mat' method). - + Fine-tunes a simplified model to mimic the behavior of a more complex, pre-trained model. + + This process involves training a smaller, more efficient model to approximate the output of a larger, pre-trained model on a specific grid. + This is done to create a computationally cheaper representation of the original model, suitable for tasks where speed and memory efficiency are critical. + Args: - cache_dir: a directory where saved cache in. - name: name for a model - cache_model: model to save + name (str, optional): A name to assign to the saved model. Defaults to None. + cache_model (torch.nn.Module, optional): The simplified model to be trained. Defaults to None. + cache_verbose (bool, optional): If True, prints the training loss at each iteration. Defaults to False. + + Returns: + None """ NN_grid, cache_model = self.grid_model_mat(cache_model) @@ -280,14 +415,17 @@ def closure(): def scheme_interp(self, trained_model: Any, cache_verbose: bool = False) -> Tuple[Any, dict]: """ - Smth - - Args: - trained_model: smth - cache_verbose: detailed info about models in cache. - Returns: - * **model** -- NN or mat.\n - * **optimizer_state** -- dict. + Interpolates the model's parameters to match the behavior of a pre-trained model. + + This method refines the current model by minimizing the difference between its output and the output of a `trained_model` on a given grid. It uses an optimization loop to adjust the model's parameters until the loss (mean squared error) falls below a threshold or a maximum number of iterations is reached. This is useful for adapting a general model to a specific solution learned by another model. + + Args: + trained_model: A pre-trained model whose behavior the current model should mimic. + cache_verbose: If True, prints the loss at each iteration of the optimization loop. + + Returns: + * **model**: The refined model, with parameters adjusted to approximate the `trained_model`. + * **optimizer_state**: A dictionary containing the state of the optimizer after the interpolation process. """ optimizer = torch.optim.Adam(self.model.parameters(), lr=0.001) @@ -316,13 +454,20 @@ def closure(): def cache_retrain(self, cache_checkpoint, cache_verbose: bool = False) -> Union[ Tuple[Any, None], Tuple[Any, Union[dict, Any]]]: """ - Smth - Args: - cache_checkpoint: smth - cache_verbose: detailed info about models in cache. - Returns: - * **model** -- model.\n - * **optimizer_state** -- smth + Refines the model using a cached model if available, otherwise performs a retraining. + + This method attempts to leverage a previously cached model to accelerate the training process. + If a compatible cached model exists (same structure), it's loaded and used directly. + Otherwise, the input model is retrained, potentially using the cached model as a starting point. + This is done to avoid training models from scratch every time, saving computational resources and time. + + Args: + cache_checkpoint: A dictionary containing the cached model's state, optimizer state, and model architecture. If None, caching is skipped. + cache_verbose: If True, prints a message indicating whether the model was loaded from the cache. + + Returns: + * **model**: The refined model, either loaded from the cache or retrained. + * **optimizer_state**: The optimizer state, loaded from the cache if a compatible model was found, otherwise None. """ # do nothing if cache is empty @@ -355,19 +500,21 @@ def cache_nn(self, nmodels: Union[int, None], lambda_operator: float, lambda_bou cache_verbose: bool,model_randomize_parameter: Union[float, None], cache_model: torch.nn.Sequential, return_normalized_loss: bool = False): """ - Restores the model from the cache and uses it for retraining. - Args: - cache_dir: a directory where saved cache in. - nmodels: smth - lambda_bound: an arbitrary chosen constant, influence only convergence speed. - cache_verbose: more detailed info about models in cache. - model_randomize_parameter: Creates a random model parameters (weights, biases) multiplied with a given - randomize parameter. - cache_model: cached model - Returns: - * **model** -- NN.\n - * **min_loss** -- min loss as is. - """ + Restores a pre-trained model from the cache and refines it to better fit the specific problem. This leverages prior knowledge encoded in the cache to accelerate the model fitting process. + + Args: + nmodels: The number of models to consider from the cache. + lambda_operator: Regularization strength for operators in the equation. + lambda_bound: Bound for the regularization strength. + cache_verbose: Whether to print detailed information about the models loaded from the cache. + model_randomize_parameter: If provided, randomizes the model parameters (weights, biases) by multiplying them with this value. + cache_model: The cached model to restore. + return_normalized_loss: Whether to return the normalized loss. + + Returns: + * **model** -- The refined neural network model. + * **min_loss** -- The minimum loss achieved by the restored model. + """ r = create_random_fn(model_randomize_parameter) cache_checkpoint, min_loss = self.cache_lookup(nmodels=nmodels, cache_verbose=cache_verbose, @@ -386,19 +533,24 @@ def cache_mat(self, nmodels: Union[int, None],lambda_operator: float, lambda_bou cache_verbose: bool,model_randomize_parameter: Union[float, None], cache_model: torch.nn.Sequential, return_normalized_loss: bool = False): """ - Restores the model from the cache and uses it for retraining. - Args: - cache_dir: a directory where saved cache in. - nmodels: smth - lambda_bound: an arbitrary chosen constant, influence only convergence speed. - cache_verbose: more detailed info about models in cache. - model_randomize_parameter: Creates a random model parameters (weights, biases) multiplied with a given - randomize parameter. - cache_model: cached model - Returns: - * **model** -- mat.\n - * **min_loss** -- min loss as is. - """ + Restores a model from a pre-computed cache, potentially refining it to better fit the specific problem instance. This leverages prior computations to accelerate the solution process. + + Args: + nmodels (Union[int, None]): Number of models to consider from the cache. If None, all cached models are used. + lambda_operator (float): Weighting factor for the operator loss term. + lambda_bound (float): Weighting factor for the boundary condition loss term. Influences convergence speed. + cache_verbose (bool): Enables more detailed logging of cache operations. + model_randomize_parameter (Union[float, None]): If provided, randomizes the model parameters (weights, biases) by multiplying them with a random value. + cache_model (torch.nn.Sequential): The cached model to be used as a starting point. + return_normalized_loss (bool, optional): If True, returns the normalized loss. Defaults to False. + + Returns: + torch.Tensor: The refined model, adapted to the current problem. + float: The minimum loss achieved by the refined model. + + Why: + This method aims to improve the efficiency of solving differential equations by reusing previously computed solutions. By starting from a cached model and fine-tuning it to the specific problem instance, the method can converge faster and achieve better accuracy than training a model from scratch. + """ NN_grid, cache_model = self.grid_model_mat(cache_model) operator = deepcopy(self.equal_cls.operator) @@ -435,18 +587,19 @@ def cache(self, nmodels: Union[int, None],lambda_operator, lambda_bound: float, cache_model: torch.nn.Sequential, return_normalized_loss: bool = False): """ - Restores the model from the cache and uses it for retraining. - Args: - cache_dir: a directory where saved cache in. - nmodels: number cached models. - lambda_bound: an arbitrary chosen constant, influence only convergence speed. - cache_verbose: more detailed info about models in cache. - model_randomize_parameter: Creates a random model parameters (weights, biases) multiplied with a given - randomize parameter. - cache_model: cached model - - Returns: - cache.cache_nn or cache.cache_mat + Restores a pre-trained model from the cache to refine its parameters based on new data. This leverages prior knowledge to accelerate the model fitting process and potentially improve generalization. + + Args: + nmodels: The number of cached models to consider. + lambda_operator: regularization parameter for operators + lambda_bound: A constant influencing the convergence speed of the retraining process. + cache_verbose: Enables more detailed logging of the cached models. + model_randomize_parameter: Introduces slight variations to the model's initial parameters (weights, biases) by multiplying them with a random factor. + cache_model: The pre-trained model loaded from the cache. + return_normalized_loss: if true returns normalized loss + + Returns: + The refined model, either a neural network (`cache.cache_nn`) or a matrix-based model (`cache.cache_mat`), depending on the configuration. """ if self.mode != 'mat': diff --git a/epde/solver/callbacks/adaptive_lambda.py b/epde/solver/callbacks/adaptive_lambda.py index f2844521..3eba6e3f 100644 --- a/epde/solver/callbacks/adaptive_lambda.py +++ b/epde/solver/callbacks/adaptive_lambda.py @@ -10,14 +10,25 @@ class AdaptiveLambda(Callback): """ Serves for computing adaptive lambdas. """ + def __init__(self, sampling_N: int = 1, second_order_interactions = True): """ - + Initializes the AdaptiveLambda module. + + This module dynamically adjusts a symbolic expression (lambda) based on data, refining its structure and parameters to improve accuracy. + Args: - sampling_N (int, optional): essentially determines how often the lambda will be re-evaluated. Defaults to 1. - second_order_interactions (bool, optional): Calculate second-order sensitivities. Defaults to True. + sampling_N (int, optional): Controls the frequency of re-evaluating the lambda expression. Higher values lead to more frequent updates. Defaults to 1. + second_order_interactions (bool, optional): Enables the calculation of second-order sensitivities, capturing more complex relationships within the data. Defaults to True. + + Returns: + None + + Why: + The sampling_N parameter controls how often the symbolic expression is refined based on the data. + The second_order_interactions parameter enables the discovery of more complex relationships in the data by considering second-order sensitivities. """ super().__init__() self.second_order_interactions = second_order_interactions @@ -25,15 +36,20 @@ def __init__(self, @staticmethod def lambda_compute(pointer: int, length_list: list, ST: np.ndarray) -> torch.Tensor: - """ Computes lambdas. - + """ + Computes adaptive lambda values for each group of parameters. + + This function calculates lambda values based on the Sobol indices (ST) obtained from sensitivity analysis. + These lambdas are used to adaptively adjust the learning rates during the optimization process, + giving more weight to parameters that have a greater impact on the model's output. + Args: - pointer (int): the label to calculate the lambda for the corresponding parameter. - length_list (list): dict where values are lengths. - ST (np.ndarray): result of SALib.ProblemSpec(). - + pointer (int): Starting index in the ST array for the current group of parameters. + length_list (list): A list containing the number of parameters in each group. + ST (np.ndarray): Array of Sobol indices (total effect indices) for all parameters. + Returns: - torch.Tensor: calculated lambdas written as vector + torch.Tensor: A tensor containing the calculated lambda values for each group of parameters. """ lambdas = [] @@ -46,16 +62,19 @@ def update(self, op_length: List, bval_length: List, sampling_D: int) -> Tuple[torch.Tensor, torch.Tensor]: - """ Updates all lambdas (operator and boundary). - - Args: - op_length (list): list with lengths of operator solution. - bval_length (list): list with lengths of boundary solution. - sampling_D (int): sum of op_length and bval_length. - - Returns: - lambda_op (torch.Tensor): values of lambdas for operator. - lambda_bound (torch.Tensor): values of lambdas for boundary. + """ + Updates the lambda values for the operator and boundary conditions based on sensitivity analysis. + + The method performs a sensitivity analysis using the provided solution lengths and total sampling dimension to determine the influence of each variable on the loss function. These sensitivity indices are then used to update the lambda values, effectively weighting the contribution of each term in the operator and boundary conditions based on its importance in minimizing the loss. This ensures that the optimization process focuses on the most relevant terms, leading to a more efficient and accurate discovery of the underlying differential equation. + + Args: + op_length (list): A list containing the lengths of each term in the operator solution. + bval_length (list): A list containing the lengths of each term in the boundary condition solution. + sampling_D (int): The total sampling dimension, which is the sum of `op_length` and `bval_length`. + + Returns: + lambda_op (torch.Tensor): Updated lambda values for the operator terms, reflecting their sensitivity. + lambda_bound (torch.Tensor): Updated lambda values for the boundary condition terms, reflecting their sensitivity. """ op_array = np.array(self.op_list) @@ -85,7 +104,14 @@ def update(self, return lambda_op, lambda_bnd def lambda_update(self): - """ Method for lambdas calculation. + """ + Calculates and updates the Lagrangian multipliers (lambdas) for the equation and boundary conditions. These lambdas are crucial for adjusting the optimization process, balancing the influence of the equation and boundary conditions in satisfying the underlying differential equation. The method accumulates information about the equation's operator values, boundary values, and loss, and then updates the lambdas when enough samples are collected. This adaptive adjustment ensures that the solution adheres to both the equation and the specified boundary conditions. + + Args: + None + + Returns: + None """ sln_cls = self.model.solution_cls bval = sln_cls.bval @@ -122,4 +148,15 @@ def lambda_update(self): lambda_print(sln_cls.lambda_bound, bval_keys) def on_epoch_end(self, logs=None): + """ + Updates the lambda value at the end of each epoch. + + This update is crucial for adapting the equation discovery process based on the performance of the current population of equations. By adjusting lambda, the algorithm can refine its search strategy and converge towards more accurate and relevant differential equation models. + + Args: + logs: Contains information about the current epoch, such as loss values. + + Returns: + None. + """ self.lambda_update() diff --git a/epde/solver/callbacks/cache.py b/epde/solver/callbacks/cache.py index 9eb13c43..66b3bb02 100644 --- a/epde/solver/callbacks/cache.py +++ b/epde/solver/callbacks/cache.py @@ -20,13 +20,18 @@ def count_output(model: torch.Tensor) -> int: - """ Determine the out features of the model. - + """ + Determine the number of output features of the model's final layer. + + This is crucial for adapting the discovered equation to the specific data being modeled. + By identifying the output features, the framework ensures that the equation's structure aligns + with the dimensionality of the target variables. + Args: - model (torch.Tensor): torch neural network. - + model (torch.Tensor): The neural network model. + Returns: - int: number of out features. + int: The number of output features in the final layer. """ modules, output_layer = list(model.modules()), None for layer in reversed(modules): @@ -37,28 +42,38 @@ def count_output(model: torch.Tensor) -> int: class CachePreprocessing: - """class for preprocessing cache files. """ + class for preprocessing cache files. + """ + def __init__(self, model: Model ): """ - Args: - model (Model): object of Model class + Initializes the CachePreprocessing object with a model. + + This ensures that the preprocessing steps are aligned with the specific solution class + defined within the provided model, facilitating consistent data handling throughout the equation discovery process. + + Args: + model (Model): The model containing the solution class to be used for preprocessing. + + Returns: + None """ self.solution_cls = model.solution_cls @staticmethod def _cache_files(files: list, nmodels: Union[int, None]=None) -> np.ndarray: - """ At some point we may want to reduce the number of models that are - checked for the best in the cache. - - Args: - files (list): list with all model names in cache. - nmodels (Union[int, None], optional): models quantity for checking. Defaults to None. - - Returns: - cache_n (np.ndarray): array with random cache files names. + """ + Reduces the number of cached models to be evaluated, potentially improving the efficiency of the equation discovery process. By limiting the number of models considered, the search for the best equation can be accelerated. + + Args: + files (list): A list of all model names available in the cache. + nmodels (Union[int, None], optional): The desired number of models to select for evaluation. If None, all models are selected. Defaults to None. + + Returns: + np.ndarray: An array containing the indices of the selected cache files. These indices can be used to access the corresponding model data. """ if nmodels is None: @@ -74,16 +89,17 @@ def _cache_files(files: list, nmodels: Union[int, None]=None) -> np.ndarray: def _model_reform(init_model: Union[torch.nn.Sequential, torch.nn.ModuleList], model: Union[torch.nn.Sequential, torch.nn.ModuleList]): """ - As some models are nn.Sequential class objects, - but another models are nn.Module class objects. - This method does checking the solver model (init_model) - and the cache model (model). - Args: - init_model (nn.Sequential or nn.ModuleList): solver model. - model (nn.Sequential or nn.ModuleList): cache model. - Returns: - init_model (nn.Sequential or nn.ModuleList): checked init_model. - model (nn.Sequential or nn.ModuleList): checked model. + Checks and adjusts the structure of the initial and cached models to ensure compatibility. + + This function verifies if the provided models are directly indexable (e.g., `nn.Sequential`) or if they are nested within a `model` attribute. If a model is nested, it extracts the underlying model. This ensures that subsequent operations can correctly access the model's layers, regardless of its initial structure. This adjustment is crucial for consistent handling of different model types within the caching mechanism. + + Args: + init_model (nn.Sequential or nn.ModuleList): The initial model used by the solver. + model (nn.Sequential or nn.ModuleList): The cached model to be checked. + + Returns: + init_model (nn.Sequential or nn.ModuleList): The adjusted initial model. + model (nn.Sequential or nn.ModuleList): The adjusted cached model. """ try: model[0] @@ -102,16 +118,21 @@ def cache_lookup(self, nmodels: Union[int, None] = None, save_graph: bool = False, cache_verbose: bool = False) -> Union[None, dict, torch.nn.Module]: - """Looking for the best model (min loss) model from the cache files. - - Args: - cache_dir (str): folder where system looks for cached models. - nmodels (Union[int, None], optional): maximal number of models that are taken from cache dir. Defaults to None. - save_graph (bool, optional): responsible for saving the computational graph. Defaults to False. - cache_verbose (bool, optional): verbose cache operations. Defaults to False. - - Returns: - Union[None, dict, torch.Tensor]: best model with optimizator state. + """ + Looks for the best performing model within the cached models based on validation loss. + + This method iterates through the cached models, evaluates their performance, and selects the one with the lowest validation loss. + It ensures compatibility between the cached model and the current solver's model before evaluation. + This is done to efficiently reuse previously trained models and accelerate the equation discovery process. + + Args: + cache_dir (str): Folder where the system looks for cached models. + nmodels (Union[int, None], optional): Maximal number of models to consider from the cache directory. Defaults to None, meaning all models are considered. + save_graph (bool, optional): Whether to save the computational graph during evaluation. Defaults to False. + cache_verbose (bool, optional): Enables verbose output for cache operations. Defaults to False. + + Returns: + Union[None, dict, torch.nn.Module]: The best model found in the cache, along with its optimizer state, or None if no suitable model is found. """ files = glob.glob(cache_dir + '\*.tar') @@ -183,14 +204,15 @@ def cache_lookup(self, def scheme_interp(self, trained_model: torch.nn.Module, cache_verbose: bool = False) -> torch.nn.Module: - """ If the cache model has another arcitechure to user's model, - we will not be able to use it. So we train user's model on the - outputs of cache model. - - Args: - trained_model (torch.nn.Module): the best model (min loss) from cache. - cache_verbose (bool, optional): verbose on/off of cache operations. Defaults to False. - + """ + Trains the user's model to mimic the behavior of a pre-trained model from the cache, effectively transferring knowledge and adapting to potential architectural differences. This ensures compatibility and leverages prior learning when the cache model's structure differs from the user's specified model. + + Args: + trained_model (torch.nn.Module): The pre-trained model from the cache whose behavior will be mimicked. + cache_verbose (bool, optional): Enables verbose output during the training process. Defaults to False. + + Returns: + torch.nn.Module: The user's model, fine-tuned to approximate the behavior of the `trained_model`. """ grid = self.solution_cls.grid @@ -223,14 +245,18 @@ def closure(): def cache_retrain(self, cache_checkpoint: dict, cache_verbose: bool = False) -> torch.nn.Module: - """ The comparison of the user's model and cache model architecture. - If they are same, we will use model from cache. In the other case - we use interpolation (scheme_interp method) - - Args: - cache_checkpoint (dict): checkpoint of the cache model - cache_verbose (bool, optional): on/off printing cache operations. Defaults to False. - + """ + Compares the user-provided model architecture with the cached model architecture to determine if the cached model can be directly used or if interpolation is required to transfer knowledge. + + Args: + cache_checkpoint (dict): Checkpoint containing the cached model's architecture and state. + cache_verbose (bool, optional): Enables verbose logging of cache operations. Defaults to False. + + Returns: + torch.nn.Module: Returns the model. It returns None if the cache is empty, returns cache model if architectures are the same, and returns the user defined model after interpolation in other cases. + + Why: + This method aims to accelerate the model training process by reusing previously learned knowledge from similar models. If the architectures match, the cached model's state is directly loaded. Otherwise, interpolation is used to adapt the cached knowledge to the current model, leveraging prior learning to guide the training process. """ model = self.solution_cls.model @@ -261,11 +287,12 @@ def cache_retrain(self, class Cache(Callback): """ Prepares user's model. Serves for computing acceleration.\n - Saves the trained model to the cache, and subsequently it is possible to use pre-trained model - (if it saved and if the new model is structurally similar) to sped up computing.\n - If there isn't pre-trained model in cache, the training process will start from the beginning. + Saves the trained model to the cache, and subsequently it is possible to use pre-trained model + (if it saved and if the new model is structurally similar) to sped up computing.\n + If there isn't pre-trained model in cache, the training process will start from the beginning. """ + def __init__(self, nmodels: Union[int, None] = None, cache_dir: str = 'tedeous_cache', @@ -275,15 +302,17 @@ def __init__(self, clear_cache: bool = False ): """ - Args: - nmodels (Union[int, None], optional): maximal number of models that are taken from cache dir. Defaults to None. Defaults to None. - cache_dir (str, optional): directory with cached models. Defaults to '../tedeous_cache/' in temporary directoy of user system. - If cache_dir is custom, then file will be searched in *torch_de_solver* directory. - cache_verbose (bool, optional): printing cache operations. Defaults to False. - cache_model (Union[torch.nn.Sequential, None], optional): model for mat method, which will be saved in cache. Defaults to None. - model_randomize_parameter (Union[int, float], optional): creates a random model parameters (weights, biases) - multiplied with a given randomize parameter.. Defaults to 0. - clear_cache (bool, optional): clear cache directory. Defaults to False. + Initializes the Cache object for managing pre-trained models. + + The cache facilitates the reuse of previously trained models, potentially reducing the computational cost of solving similar problems. + + Args: + nmodels (Union[int, None], optional): The maximum number of models to keep in the cache directory. If None, all models are kept. Defaults to None. + cache_dir (str, optional): The directory where cached models are stored. Defaults to 'tedeous_cache', which resolves to a subdirectory within the system's temporary directory. If a custom directory is specified, it is resolved relative to the *torch_de_solver* directory. + cache_verbose (bool, optional): Enables verbose output for cache operations, providing more detailed information about loading and saving models. Defaults to False. + cache_model (Union[torch.nn.Sequential, None], optional): A PyTorch model to be immediately saved to the cache. Defaults to None. + model_randomize_parameter (Union[int, float], optional): A factor to randomize the model's initial parameters (weights and biases) before saving it to the cache. This can help explore different regions of the solution space. Defaults to 0. + clear_cache (bool, optional): If True, the cache directory is emptied upon initialization. Defaults to False. """ self.nmodels = nmodels @@ -308,7 +337,14 @@ def __init__(self, remove_all_files(self.cache_dir) def _cache_nn(self): - """ take model from cache as initial guess for *NN, autograd* modes. + """ + Utilizes a model from the cache as an initial starting point for neural network-based equation discovery. This approach leverages previously learned information to accelerate the search process and potentially improve the quality of the discovered equations by initializing the model with a promising configuration. + + Args: + self: Instance of the Cache class. + + Returns: + None """ cache_preproc = CachePreprocessing(self.model) @@ -324,7 +360,14 @@ def _cache_nn(self): self.model.solution_cls.model.apply(r) def _cache_mat(self) -> torch.Tensor: - """ take model from cache as initial guess for *mat* mode. + """ + Refines the initial guess for the *mat* mode solution by leveraging a cache of previously trained models. It retrieves a suitable model from the cache, retrains it to better align with the current problem, and uses its output as a starting point for the *mat* mode solution. This approach accelerates the solution process by providing a pre-trained model that captures general solution characteristics. + + Args: + self: The Cache instance. + + Returns: + torch.Tensor: A refined initial guess for the *mat* mode solution, obtained from the cache and adapted to the current problem. """ net = self.model.net @@ -364,7 +407,20 @@ def _cache_mat(self) -> torch.Tensor: self.model.solution_cls._model_change(model.requires_grad_()) def cache(self): - """ Wrap for cache_mat and cache_nn methods. + """ + Caches the model's computations based on its mode. + + This method acts as a dispatcher, selecting the appropriate caching strategy + based on whether the model is operating in 'mat' mode or another mode (presumably 'nn'). + It ensures that the computationally intensive parts of the model are cached for later use, + potentially speeding up subsequent calculations or analyses. + + Args: + self: The Cache instance. + + Returns: + The result of either `self._cache_mat()` if the model is in 'mat' mode, + or `self._cache_nn()` otherwise. """ if self.model.mode != 'mat': @@ -373,5 +429,16 @@ def cache(self): return self._cache_mat() def on_train_begin(self, logs=None): + """ + Called at the beginning of training. + + Caches the model and sets the save directory. This ensures that the discovered equation and its associated parameters can be efficiently stored and retrieved during the evolutionary search process, facilitating faster exploration of the solution space. + + Args: + logs: Contains the logs. + + Returns: + None + """ self.cache() self.model._save_dir = self.cache_dir diff --git a/epde/solver/callbacks/callback.py b/epde/solver/callbacks/callback.py index b35b4c38..83ee185e 100644 --- a/epde/solver/callbacks/callback.py +++ b/epde/solver/callbacks/callback.py @@ -1,76 +1,156 @@ from abc import ABC, abstractmethod class Callback(ABC): - """Base class used to build new callbacks. """ + Base class used to build new callbacks. + """ + def __init__(self): + """ + Initializes the KerasClassifierTrainer. + + This method sets up the trainer with default configurations, preparing it for fitting a Keras model. + It's crucial for ensuring a clean state before training, allowing for consistent and reproducible results + when discovering differential equations from data using neural networks. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + print_every (None): Frequency of printing training metrics. Initialized to None. + verbose (int): Verbosity level during training. Initialized to 0. + validation_data (None): Validation data to be used during training. Initialized to None. + _model (None): The Keras model to be trained. Initialized to None. + """ self.print_every = None self.verbose = 0 self.validation_data = None self._model = None def set_params(self, params): + """ + Sets the parameters of the callback. + + This allows customization of the callback's behavior during the equation discovery process. + + Args: + params (dict): A dictionary containing the parameters to set for the callback. + + Returns: + None + """ self.params = params def set_model(self, model): + """ + Sets the model to be used by the callback during training. + + This allows the callback to interact with and monitor the training process of a specific model. + The model is stored internally for later use by the callback methods. + + Args: + model: The model instance to be associated with the callback. + + Returns: + None. + """ self._model = model @property def model(self): + """ + Gets the model associated with this callback. + + This allows access to the underlying model being used for equation discovery, + enabling inspection of its architecture and parameters during the evolutionary process. + + Args: + None + + Returns: + str: The model. + """ return self._model def on_epoch_begin(self, logs=None): - """Called at the start of an epoch. - - Subclasses should override for any actions to run. This function should - only be called during TRAIN mode. - + """ + Called at the start of each training epoch. + + Subclasses can override this method to implement custom logic that needs to be executed at the beginning of each epoch. + This is useful for tasks such as adjusting learning rates, modifying training data, or performing other epoch-specific initializations. + Args: - epoch: Integer, index of epoch. logs: Dict. Currently no data is passed to this argument for this method but that may change in the future. """ pass def on_epoch_end(self, logs=None): - """Called at the end of an epoch. - - Subclasses should override for any actions to run. This function should - only be called during TRAIN mode. - + """ + Called at the end of an epoch to perform actions that contribute to finding the best differential equation. + + Subclasses should override this method to implement custom logic that needs to be executed after each training epoch. + This function is designed to be called only during the training phase. It allows for actions such as updating the population of equation candidates, + evaluating their performance, and adjusting search parameters based on the epoch's results. + Args: - epoch: Integer, index of epoch. + epoch: Integer, index of the epoch. logs: Dict, metric results for this training epoch, and for the validation epoch if validation is performed. Validation result keys are prefixed with `val_`. For training epoch, the values of the `Model`'s metrics are returned. Example: `{'loss': 0.2, 'accuracy': 0.7}`. + + Returns: + None """ pass def on_train_begin(self, logs=None): - """Called at the beginning of training. - - Subclasses should override for any actions to run. - + """ + Called at the beginning of the training process. + + This method is designed to be overridden by subclasses to implement custom initialization or setup procedures before the training loop starts. + It allows users to define specific actions that should be executed once at the beginning of the training process, such as initializing variables, setting up logging, or preparing data. + Args: - logs: Dict. Currently no data is passed to this argument for this - method but that may change in the future. + logs (dict, optional): A dictionary containing information about the training process. + Currently, no data is passed to this argument, but it may be populated in future versions. Defaults to None. + + Returns: + None """ pass def on_train_end(self, logs=None): - """Called at the end of training. - - Subclasses should override for any actions to run. - + """ + Called at the end of training. + + Subclasses should override for any actions to run after the training process is complete. + This can be useful for tasks such as saving the discovered equation or performing final evaluations. + Args: logs: Dict. Currently the output of the last call to `on_epoch_end()` is passed to this argument for this method but that may change in the future. + + Returns: + None """ pass def during_epoch(self, logs=None): + """ + This method is called at the end of each epoch to perform any necessary updates or evaluations after the model has been trained on the entire dataset for that epoch. + + Args: + logs (dict, optional): The logs returned from the previous epoch, containing information such as loss and metrics. Defaults to None. + + Returns: + None + """ pass \ No newline at end of file diff --git a/epde/solver/callbacks/callback_list.py b/epde/solver/callbacks/callback_list.py index 42ab6b6c..57ab4aa2 100644 --- a/epde/solver/callbacks/callback_list.py +++ b/epde/solver/callbacks/callback_list.py @@ -3,24 +3,33 @@ # import tree class CallbackList(Callback): - """Container abstracting a list of callbacks.""" + """ + Container abstracting a list of callbacks. + """ + def __init__( self, callbacks=None, model=None, **params, ): - """Container for `Callback` instances. - - This object wraps a list of `Callback` instances, making it possible - to call them all at once via a single endpoint - (e.g. `callback_list.on_epoch_end(...)`). - + """ + Initializes a container for managing and invoking a set of callbacks. + + This class streamlines the execution of multiple callbacks during + different stages of a process (e.g., training epochs). It aggregates + a list of `Callback` instances, enabling their collective invocation + through a single method call (e.g., `callback_list.on_epoch_end(...)`). + This ensures that all relevant actions are performed at each stage, + simplifying the management of complex workflows. + Args: - callbacks: List of `Callback` instances. - model: The `Model` these callbacks are used with. - **params: If provided, parameters will be passed to each `Callback` - via `Callback.set_params`. + callbacks: A list of `Callback` instances to be managed. Defaults to an empty list if None is provided. + model: The associated `Model` instance that these callbacks will interact with. + **params: Optional keyword arguments containing parameters to be passed to each `Callback` instance via the `set_params` method. + + Returns: + None """ self.callbacks = callbacks if callbacks else [] @@ -30,34 +39,124 @@ def __init__( self.set_params(params) def set_model(self, model): + """ + Sets the Keras model for the callback and its children. + + This method propagates the Keras model instance to the callback and all its children, + ensuring that all callbacks have access to the model for tasks such as monitoring + training progress, modifying training behavior, or accessing model internals. + + Args: + model: The Keras model instance. + + Returns: + None. + """ super().set_model(model) for callback in self.callbacks: callback.set_model(model) def append(self, callback): + """ + Appends a callback function to the list of callbacks. + + This allows to execute custom code during the equation discovery process, for example, to track progress or modify search parameters. + + Args: + callback: The callback function to append. + + Returns: + None. + """ self.callbacks.append(callback) def set_params(self, params): + """ + Sets the parameters for the object and propagates them to the individual callbacks. + + This ensures that all callbacks have access to the same set of parameters, + allowing them to consistently tailor their behavior during the equation discovery process. + + Args: + params (dict): The parameters to set. These parameters might control aspects + of the evolutionary search, data preprocessing, or equation evaluation. + + Returns: + None. + + Class fields (object properties) that are initialized: + params (dict): The parameters of the object. + """ self.params = params for callback in self.callbacks: callback.set_params(params) def on_epoch_begin(self, logs=None): + """ + Called at the beginning of an epoch. + + This method iterates through the registered callbacks and calls their `on_epoch_begin` methods, allowing each callback to perform actions at the start of each epoch, such as resetting metrics or updating internal states. This ensures that all necessary preparations are made before the training epoch begins. + + Args: + logs: Log data. + + Returns: + None + """ logs = logs or {} for callback in self.callbacks: callback.on_epoch_begin(logs) def on_epoch_end(self, logs=None): + """ + Called at the end of an epoch to notify callbacks. + + This method iterates through the registered callbacks and calls their `on_epoch_end` methods, + allowing them to perform actions or record information at the end of each epoch. This is + crucial for tasks such as tracking metrics, adjusting hyperparameters, or saving model + checkpoints during the training process. + + Args: + logs (dict, optional): Metric results for this epoch. Defaults to None. + + Returns: + None + """ logs = logs or {} for callback in self.callbacks: callback.on_epoch_end(logs) def on_train_begin(self, logs=None): + """ + Called at the beginning of training. + + This method is called to signal the start of the training process, + allowing callbacks to prepare or initialize any resources they need + before the training loop begins. It iterates through the list of callbacks + and calls their respective `on_train_begin` methods. + + Args: + logs (dict, optional): Dictionary of logs. Defaults to None. + + Returns: + None + """ logs = logs or {} for callback in self.callbacks: callback.on_train_begin(logs) def on_train_end(self, logs=None): + """ + Propagates the `on_train_end` event to all callbacks in the list. + + This method is called at the very end of the training process. It ensures that each callback has the opportunity to perform any final actions or cleanup operations, such as logging final results or saving model parameters. + + Args: + logs (dict, optional): Dictionary of logs collected during training. Defaults to None. + + Returns: + None + """ logs = logs or {} for callback in self.callbacks: callback.on_train_end(logs) diff --git a/epde/solver/callbacks/early_stopping.py b/epde/solver/callbacks/early_stopping.py index 86b7d70b..2c082e7f 100644 --- a/epde/solver/callbacks/early_stopping.py +++ b/epde/solver/callbacks/early_stopping.py @@ -7,8 +7,10 @@ class EarlyStopping(Callback): - """ Class for using adaptive stop criterias at training process. """ + Class for using adaptive stop criterias at training process. + """ + def __init__(self, eps: float = 1e-5, loss_window: int = 100, @@ -21,23 +23,24 @@ def __init__(self, verbose: bool = True, save_best: bool = False ): - """_summary_ - - Args: - eps (float, optional): arbitrarily small number that uses for loss comparison criterion. Defaults to 1e-5. - loss_window (int, optional): width of losses window which is used for average loss estimation. Defaults to 100. - no_improvement_patience (int, optional): number of iterations during which - the loss may not improve.. Defaults to 1000. - patience (int, optional): maximum number of times the stopping criterion - can be satisfied.. Defaults to 5. - abs_loss (Union[float, None], optional): absolute loss value using in _absloss_check().. Defaults to None. - normalized_loss (bool, optional): calculate loss with all lambdas=1. Defaults to False. - randomize_parameter (float, optional): some error for resulting - model weights to to avoid local optima. Defaults to 1e-5. - info_string_every (Union[int, None], optional): prints the loss state after every *int* - step. Defaults to None. - verbose (bool, optional): print or not info about loss and current state of stopping criteria. Defaults to True. - save_best (bool, optional): model with least loss is saved during the training and returned at the end as a result + """ + Initializes the EarlyStopping callback. + + This callback monitors the loss function during training and stops the training process when the loss stops improving, + potentially saving the best model encountered during training. This prevents overfitting and saves computational resources + by avoiding unnecessary iterations. + + Args: + eps (float, optional): A small threshold to consider an improvement in loss. Defaults to 1e-5. + loss_window (int, optional): The number of recent losses to average for trend estimation. Defaults to 100. + no_improvement_patience (int, optional): How many iterations to wait after the best loss before stopping. Defaults to 1000. + patience (int, optional): How many times the `no_improvement_patience` can be reached before stopping. Defaults to 5. + abs_loss (Union[float, None], optional): An absolute target loss value. Training stops if this loss is reached. Defaults to None. + normalized_loss (bool, optional): Whether to use a normalized loss function (all lambdas=1) for early stopping. Defaults to False. + randomize_parameter (float, optional): A small value to randomize model weights to escape local optima. Defaults to 1e-5. + info_string_every (Union[int, None], optional): How often (in iterations) to print the current loss and early stopping status. Defaults to None. + verbose (bool, optional): Whether to print information about the loss and early stopping status. Defaults to True. + save_best (bool, optional): Whether to save the model with the best loss encountered during training. Defaults to False. """ super().__init__() self.eps = eps @@ -57,14 +60,33 @@ def __init__(self, def _line_create(self): - """ Approximating last_loss list (len(last_loss)=loss_oscillation_window) by the line. - + """ + Approximates the trend of recent loss values using linear regression. + + This helps to identify whether the loss is consistently decreasing, increasing, or oscillating, + which is used to determine when to stop training to prevent overfitting. + + Args: + None + + Returns: + None """ self._line = np.polyfit(range(self.loss_window), self.last_loss, 1) def _window_check(self): - """ Stopping criteria. We divide angle coeff of the approximating - line (line_create()) on current loss value and compare one with *eps* + """ + Checks for early stopping based on the trend of the loss function within a window. + + This method assesses whether the rate of change of the loss, normalized by the current loss value, + falls below a specified threshold (*eps*). If this condition is met, it indicates that the training + is no longer significantly improving the model's performance and may be converging. + + Args: + None + + Returns: + None """ if self.t % self.loss_window == 0 and self._check is None: self._line_create() @@ -75,10 +97,21 @@ def _window_check(self): self._check = 'window_check' def _patience_check(self): - """ Stopping criteria. We control the minimum loss and count steps - when the current loss is bigger then min_loss. If these steps equal to - no_improvement_patience parameter, the stopping criteria will be achieved. - + """ + Checks if the training should be stopped based on the patience criterion. + + The patience mechanism monitors the training loss and stops the training process if the loss + does not improve for a specified number of epochs. This helps to prevent overfitting and + improve the generalization ability of the discovered equations. The method checks if the + number of epochs since the last improvement exceeds the `no_improvement_patience` threshold. + If it does, and a check hasn't already been triggered, it increments the stop counter, resets + the improvement start time, and potentially restores the best model found so far. + + Args: + None + + Returns: + None """ if (self.t - self._t_imp_start) == self.no_improvement_patience and self._check is None: self._stop_dings += 1 @@ -90,15 +123,31 @@ def _patience_check(self): self._check = 'patience_check' def _absloss_check(self): - """ Stopping criteria. If current loss absolute value is lower then *abs_loss* param, - the stopping criteria will be achieved. + """ + Check if the absolute loss is below the specified threshold. + + This check is part of the early stopping mechanism, which aims to prevent overfitting by monitoring the loss function and stopping the training process when the loss reaches a satisfactory level. + The training stops to avoid unnecessary computations when the model performance, measured by the loss function, is already good enough. + + Args: + self (EarlyStopping): The EarlyStopping instance. + + Returns: + None """ if self.abs_loss is not None and self.model.cur_loss < self.abs_loss and self._check is None: self._stop_dings += 1 self._check = 'absloss_check' def verbose_print(self): - """ print info about loss and stopping criteria. + """ + Prints information about the current loss, stopping criteria, and normalized loss line to monitor the training process and understand the behavior of the equation discovery. + + Args: + None + + Returns: + None """ if self._check == 'window_check': @@ -122,6 +171,21 @@ def verbose_print(self): print(info) def on_epoch_end(self, logs=None): + """ + Handles end-of-epoch actions, focusing on early stopping to optimize equation discovery. + + This method orchestrates checks for early stopping based on loss trends, + patience criteria, and absolute loss thresholds. It saves the best model + encountered so far and provides verbose output for monitoring progress. + The goal is to efficiently identify the most promising equation structures + during the evolutionary search process. + + Args: + logs: Metric results for the current epoch, used to evaluate model performance. + + Returns: + None + """ self._window_check() self._patience_check() self._absloss_check() @@ -141,6 +205,20 @@ def on_epoch_end(self, logs=None): self._check = None def on_epoch_begin(self, logs=None): + """ + Updates the callback's internal state at the start of each epoch. + + This method retrieves the current training step, mode, and check flag from the model to track the training progress. + It also maintains a history of recent loss values, which is used to determine when early stopping criteria are met. + By tracking these values, the callback can make informed decisions about when to terminate training, + preventing overfitting and saving computational resources. + + Args: + logs: Optional dictionary of logs. Will be passed to all callbacks. + + Returns: + None + """ self.t = self.model.t self.mode = self.model.mode self._check = self.model._check diff --git a/epde/solver/callbacks/inverse_task.py b/epde/solver/callbacks/inverse_task.py index d60dcd8a..4531bb63 100644 --- a/epde/solver/callbacks/inverse_task.py +++ b/epde/solver/callbacks/inverse_task.py @@ -6,22 +6,39 @@ class InverseTask(Callback): - """Class for printing the parameters during inverse task solution. """ + Class for printing the parameters during inverse task solution. + """ + def __init__(self, parameters: dict, info_string_every: Union[int, None] = None): """ - Args: - parameters (dict): dictioanry with initial guess of parameters. - info_string_every (Union[int, None], optional): print parameters after every *int* step. Defaults to None. + Initializes the inverse problem with parameter guesses and logging frequency. + + Args: + parameters (dict): Dictionary containing the initial guesses for the parameters of the differential equation. + info_string_every (Union[int, None], optional): Frequency (in iterations) at which to print the current parameter values during the optimization process. If None, parameter values are not printed. Defaults to None. + + Returns: + None + + Why: + This initialization sets up the inverse task by storing the initial parameter guesses and the frequency at which to log parameter values during the optimization process. This allows the evolutionary algorithm to start with reasonable parameter values and provides a way to monitor the progress of the optimization. """ super().__init__() self.parameters = parameters self.info_string_every = info_string_every def str_param(self): - """ printing the inverse parameters. + """ + Prints the values of specified network parameters to monitor their evolution during the training process. This helps in understanding how the parameters are being adjusted as the inverse problem is solved. + + Args: + self: The instance of the InverseTask class. + + Returns: + None. The method prints the parameter values to the console. """ if self.info_string_every is not None and self.model.t % self.info_string_every == 0: param = list(self.parameters.keys()) @@ -34,4 +51,15 @@ def str_param(self): print(param_str) def on_epoch_end(self, logs=None): + """ + Called at the end of each epoch to perform string parameter updates. + + This ensures that the evolutionary process adapts based on the model's performance during training. + + Args: + logs: The logs returned by the Keras model. + + Returns: + None. + """ self.str_param() \ No newline at end of file diff --git a/epde/solver/callbacks/plot.py b/epde/solver/callbacks/plot.py index 6b0770a3..d00e0a63 100644 --- a/epde/solver/callbacks/plot.py +++ b/epde/solver/callbacks/plot.py @@ -10,7 +10,10 @@ class Plots(Callback): - """Class for ploting solutions.""" + """ + Class for ploting solutions. + """ + def __init__(self, print_every: Union[int, None] = 500, @@ -18,11 +21,18 @@ def __init__(self, title: str = None, img_dir: str = None): """ - Args: - print_every (Union[int, None], optional): print plots after every *print_every* steps. Defaults to 500. - save_every (Union[int, None], optional): save plots after every *print_every* steps. Defaults to 500. - title (str, optional): plots title. Defaults to None. - img_dir (str, optional): directory title where plots are being saved. Defaults to None. + Initializes the plotting utility. + + The plotting utility provides functionality to visualize the identified equations during the search process. + It allows to track the progress of the algorithm and assess the quality of the discovered equations. + The frequency of printing and saving plots can be controlled to balance between real-time monitoring + and storage overhead. + + Args: + print_every (Union[int, None], optional): Print plots after every *print_every* steps. Defaults to 500. + save_every (Union[int, None], optional): Save plots after every *save_every* steps. Defaults to 500. + title (str, optional): Title for the plots. Defaults to None. + img_dir (str, optional): Directory to save the plots. Defaults to None. """ super().__init__() self.print_every = print_every if print_every is not None else 0.1 @@ -32,8 +42,13 @@ def __init__(self, def _print_nn(self): """ - Solution plot for *NN, autograd* mode. - + Generates a solution plot for neural network models, visualizing the learned function across the input grid. This is particularly useful in the context of equation discovery, as it allows to examine the behavior of the neural network solution and compare it with the underlying dynamics of the target equation. + + Args: + self: The Plots class instance, containing the trained neural network (self.net) and the input grid (self.grid). + + Returns: + None. Displays the generated plot using matplotlib. """ attributes = {'model': ['out_features', 'output_dim', 'width_out'], @@ -79,7 +94,13 @@ def _print_nn(self): def _print_mat(self): """ - Solution plot for mat mode. + Plots the solution surface or scatter plot for each variable of the identified differential equation, visualizing the relationship between the input grid and the model's output. This helps in understanding the behavior of the discovered equation across the input space. + + Args: + self (Plots): The Plots object containing the grid and network data. + + Returns: + None: Displays the plot using matplotlib. """ nparams = self.grid.shape[0] @@ -105,13 +126,19 @@ def _print_mat(self): ax1.set_ylabel("x2") def _dir_path(self, save_dir: str) -> str: - """ Path for save figures. - + """ + Generates a unique file path for saving a plot. + + If a `save_dir` is provided, the plot will be saved in that directory with a timestamped filename. + Otherwise, the plot will be saved in a default 'img' directory within the project, also with a timestamped filename. + This ensures that each generated plot has a unique and easily identifiable file path. + Args: - save_dir (str): directory where saves in - + save_dir (str, optional): The directory where the plot should be saved. + If None, the plot is saved to the default 'img' directory. + Returns: - str: directory where saves in + str: The absolute path to the generated plot file. """ if save_dir is None: @@ -134,7 +161,19 @@ def _dir_path(self, save_dir: str) -> str: def solution_print( self): - """ printing or saving figures. + """ + Generates and displays or saves visualizations of the solution at specified intervals. + + The method checks if the current time step aligns with the printing or saving frequency. + If so, it generates a plot of the solution (either from the matrix or neural network representation) + and either saves it to a file or displays it on the screen, or both. + This functionality allows to track the solution's evolution during the modeling process. + + Args: + self: A reference to the Plots class instance. + + Returns: + None """ print_flag = self.model.t % self.print_every == 0 save_flag = self.model.t % self.save_every == 0 @@ -154,4 +193,15 @@ def solution_print( plt.close() def on_epoch_end(self, logs=None): + """ + Called at the end of an epoch to trigger the printing of the current best solution. + + This allows for monitoring the progress of the evolutionary search for the differential equation during training. + + Args: + logs: The logs returned by the Keras model. + + Returns: + None. + """ self.solution_print() diff --git a/epde/solver/config.py b/epde/solver/config.py index 55347208..9bcfbe72 100644 --- a/epde/solver/config.py +++ b/epde/solver/config.py @@ -5,14 +5,18 @@ def read_config(name: str) -> json: """ - Read some config - + Reads a configuration file containing settings for the equation discovery process. + + This function is essential for setting up and customizing the search for differential equations. + It allows users to define parameters such as evolutionary algorithm settings, data preprocessing + options, and custom token definitions, ensuring that the equation discovery process is tailored + to the specific problem and dataset. + Args: - name: config name. - + name (str): The path to the JSON configuration file. + Returns: - json config. - + json: A dictionary containing the configuration parameters loaded from the file. """ with open(name, 'r') as config_file: config_data = json.load(config_file) @@ -62,14 +66,18 @@ def read_config(name: str) -> json: def check_module_name(module_name: str) -> bool: """ - Check correctness of the 'first' level of config parameter name - we call it module. - + Validates the module name against a predefined configuration. + + This function ensures that the provided module name is a valid top-level + configuration element, which is crucial for correct configuration processing. + By verifying the module name, the system can properly route and apply + configuration settings, preventing errors and ensuring consistent behavior. + Args: - module_name: first level of a parameter of a custom config. - + module_name: The module name to validate (first level config parameter). + Returns: - if module presents in 'default' config. + True if the module name is found in the default configuration, False otherwise. """ if module_name in default_config.keys(): return True @@ -79,15 +87,16 @@ def check_module_name(module_name: str) -> bool: def check_param_name(module_name: str, param_name: str) -> bool: """ - Check correctness of the 'first' level of config parameter name - we call it module. - + Checks if a given parameter name exists within a specified module in the default configuration. + + This function verifies that a parameter is valid for a particular module, ensuring that only recognized parameters are used during the equation discovery process. + Args: - module_name: first level of a parameter of a custom config. - param_name: specific parameter name. - + module_name (str): The name of the module to check within the default configuration. + param_name (str): The name of the parameter to validate. + Returns: - true if module presents in 'default' config. + bool: True if the parameter name is found within the specified module in the default configuration, False otherwise. """ if param_name in default_config[module_name].keys(): return True @@ -95,19 +104,31 @@ def check_param_name(module_name: str, param_name: str) -> bool: return False class Config: + """ + Represents a configuration object for the solver. + + The configuration can be initialized with default values and updated + from a custom configuration file. + + Attributes: + config_path: Path to a custom configuration file. + """ + def __init__(self, *args): """ - We initialize config with default one - - If there is passed path to a custom config, we try to load it and change - default parameters - + Initializes the configuration, prioritizing user-defined settings. + + The configuration is initialized with default parameters. If a path to a + custom configuration file is provided, the method attempts to load it and + override the default settings. This allows users to tailor the equation + discovery process to their specific problem and dataset. The method validates + the structure of the custom configuration to ensure compatibility. + Args: - config_path: path to a custom config - + config_path (str, optional): Path to a custom configuration file. If None, the default configuration is used. + Returns: - config used in solver.optimization_solver function - + dict: The configuration dictionary used by the equation discovery process. """ self.params = default_config @@ -134,15 +155,17 @@ def __init__(self, *args): def set_parameter(self, parameter_string: str, value: Union[bool, float, int, None]): """ - We may want to just change default config parameters manually, without loading - the .json - - We run checks to see we set them correctly - - Args: - parameter_string: string in format 'module.parameter'. - value: value for the parameter. - + Modifies a specific configuration parameter directly. + + This allows for fine-grained control over the configuration without needing to load a complete configuration file. + Input is validated to ensure that only existing modules and parameters are modified, preventing unintended configuration errors. + + Args: + parameter_string: A string specifying the parameter to modify, in the format 'module.parameter'. + value: The new value for the specified parameter. Can be a boolean, float, integer, or None. + + Returns: + None. The method modifies the configuration in place. """ module_name, param = parameter_string.split('.') diff --git a/epde/solver/data.py b/epde/solver/data.py index 5094e93a..e74a6480 100644 --- a/epde/solver/data.py +++ b/epde/solver/data.py @@ -11,13 +11,17 @@ def tensor_dtype(dtype: str): - """convert tensor to dtype format - + """ + Converts a string representation of a data type to its corresponding PyTorch data type. + + This function ensures that the data type used within the equation discovery process is correctly interpreted by the PyTorch backend, + allowing for seamless integration with tensor operations and numerical solvers. + Args: - dtype (str): dtype - + dtype (str): A string representing the desired data type (e.g., 'float32', 'float64', 'float16'). + Returns: - dtype: torch.dtype + torch.dtype: The corresponding PyTorch data type. """ if dtype == 'float32': dtype = torch.float32 @@ -30,14 +34,43 @@ def tensor_dtype(dtype: str): class Domain(): - """class for grid building """ + class for grid building + """ + def __init__(self, type='uniform'): + """ + Initializes a new Domain instance. + + This method sets the domain type and prepares a dictionary to hold symbolic variables. + The domain type influences how variables are sampled and used during the equation discovery process. + The variable dictionary will store symbolic representations of variables used in the discovered equations. + + Args: + type (str): The type of the domain, influencing variable sampling (default: 'uniform'). + + Returns: + None. + """ self.type = type self.variable_dict = {} @property def dim(self): + """ + Returns the dimensionality of the domain. + + This property returns the number of variables associated with the domain, + effectively representing the dimensionality of the search space for + equation discovery. This is crucial for understanding the complexity + of potential differential equations that can be constructed within this domain. + + Args: + self: The Domain instance. + + Returns: + int: The number of variables in the domain's variable dictionary. + """ return len(self.variable_dict) def variable( @@ -46,14 +79,22 @@ def variable( variable_set: Union[List, torch.Tensor], n_points: Union[None, int], dtype: str = 'float32') -> None: - """ determine varibles for grid building. - + """ + Initializes a spatial variable within the domain. + + This method creates a tensor representing the spatial discretization of a given variable. + It supports both uniform sampling between specified bounds and the use of pre-defined tensor values. + This is a crucial step in setting up the computational domain for solving differential equations. + Args: - varible_name (str): varible name. - variable_set (Union[List, torch.Tensor]): [start, stop] list for spatial variable or torch.Tensor with points for variable. - n_points (int): number of points in discretization for variable. - dtype (str, optional): dtype of result vector. Defaults to 'float32'. - + variable_name (str): Name of the spatial variable. + variable_set (Union[List, torch.Tensor]): Either a list [start, stop] defining the interval for uniform discretization, + or a torch.Tensor containing the pre-defined variable values. + n_points (Union[None, int]): Number of points to use for uniform discretization. Required if `variable_set` is a list. + dtype (str, optional): Data type of the resulting tensor. Defaults to 'float32'. + + Returns: + None: The method updates the internal `variable_dict` with the created tensor. """ dtype = tensor_dtype(dtype) @@ -69,13 +110,21 @@ def variable( self.variable_dict[variable_name] = variable_tensor def build(self, mode: str) -> torch.Tensor: - """ building the grid for algorithm - + """ + Generates a computational grid based on the specified mode. + + This grid serves as the foundation for representing the problem domain, + enabling the application of various numerical techniques to solve + differential equations. The grid is constructed from the domain's + variables, and its structure depends on the chosen solution mode. + Args: - mode (str): mode for equation solution, *mat, autograd, NN* - + mode (str): Specifies the solution approach ('mat', 'autograd', or 'NN'). + Determines how the grid is generated and used in subsequent calculations. + Returns: - torch.Tensor: resulting grid. + torch.Tensor: A tensor representing the computational grid. Its shape and + values are determined by the domain variables and the selected mode. """ var_lst = list(self.variable_dict.values()) var_lst = [i.cpu() for i in var_lst] @@ -91,9 +140,25 @@ def build(self, mode: str) -> torch.Tensor: class Conditions(): - """class for adding the conditions: initial, boundary, and data. """ + class for adding the conditions: initial, boundary, and data. + """ + def __init__(self): + """ + Initializes a new instance of the Conditions class. + + The Conditions class manages a list of symbolic conditions that are used to filter and select equation candidates during the equation discovery process. This method initializes the list to store these conditions. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + conditions_lst (list): A list to store conditions. + """ self.conditions_lst = [] def dirichlet( @@ -101,14 +166,22 @@ def dirichlet( bnd: Union[torch.Tensor, dict], value: Union[callable, torch.Tensor, float], var: int = 0): - """ determine dirichlet boundary condition. - + """ + Applies a Dirichlet boundary condition to the problem. + + This method specifies fixed values for the solution at the given boundary points. + It's used to constrain the solution space and ensure that the identified equation + satisfies the known boundary behavior. + Args: - bnd (Union[torch.Tensor, dict]): boundary points can be torch.Tensor - or dict with keys as coordinates names and values as coordinates values. - value (Union[callable, torch.Tensor, float]): values at the boundary (bnd) - if callable: value = function(bnd) - var (int, optional): variable for system case, for single equation is 0. Defaults to 0. + bnd (Union[torch.Tensor, dict]): Boundary points where the Dirichlet condition is applied. + Can be a torch.Tensor or a dictionary with coordinate names as keys and their values as values. + value (Union[callable, torch.Tensor, float]): The value(s) of the solution at the boundary points. + If a callable is provided, it will be evaluated at the boundary points to determine the values. + var (int, optional): The variable index for systems of equations. Defaults to 0, representing a single equation. + + Returns: + None: The method updates the internal list of boundary conditions (`self.conditions_lst`) with the specified Dirichlet condition. """ self.conditions_lst.append({'bnd': bnd, @@ -121,14 +194,19 @@ def operator(self, bnd: Union[torch.Tensor, dict], operator: dict, value: Union[callable, torch.Tensor, float]): - """ determine operator boundary condition - - Args: - bnd (Union[torch.Tensor, dict]): boundary points can be torch.Tensor - or dict with keys as coordinates names and values as coordinates values - operator (dict): dictionary with opertor terms: {'operator name':{coeff, term, pow, var}} - value (Union[callable, torch.Tensor, float]): value on the boundary (bnd). - if callable: value = function(bnd) + """ + Adds an operator boundary condition to the list of conditions. This condition constrains the solution based on a differential operator applied at the boundary. + + Args: + bnd (Union[torch.Tensor, dict]): Boundary points where the condition is applied. Can be a tensor or a dictionary with coordinate names as keys and coordinate values as values. + operator (dict): A dictionary defining the differential operator. It specifies the terms, coefficients, powers, and variables involved in the operator. + value (Union[callable, torch.Tensor, float]): The value of the operator at the boundary. Can be a constant, a tensor, or a callable function that takes the boundary points as input. + + Returns: + None: This method adds the boundary condition to an internal list (`self.conditions_lst`) for later use in the equation discovery process. + + Why: + This method is crucial for incorporating boundary conditions into the equation discovery process. By specifying constraints on the solution at the boundaries, we can guide the search towards equations that not only fit the data but also satisfy known physical or mathematical constraints. This helps to refine the search space and improve the accuracy and reliability of the discovered equations. """ try: var = operator[operator.keys()[0]]['var'] @@ -145,17 +223,16 @@ def periodic(self, bnd: Union[List[torch.Tensor], List[dict]], operator: dict = None, var: int = 0): - """Periodic can be: periodic dirichlet (example u(x,t)=u(-x,t)) - if form with bnd and var for system case. - or periodic operator (example du(x,t)/dx=du(-x,t)/dx) - in form with bnd and operator. - Parameter 'bnd' is list: [b_coord1:torch.Tensor, b_coord2:torch.Tensor,..] or - bnd = [{'x': 1, 't': [0,1]},{'x': -1, 't':[0,1]}] - - Args: - bnd (Union[List[torch.Tensor], List[dict]]): list with dicionaries or torch.Tensors - operator (dict, optional): operator dict. Defaults to None. - var (int, optional): variable for system case and periodic dirichlet. Defaults to 0. + """ + Adds a periodic boundary condition to the problem definition. This ensures that the solution exhibits a repeating pattern across the specified boundaries. This is useful when modeling systems where the behavior at one boundary is directly related to the behavior at another, such as in wave propagation or fluid dynamics. + + Args: + bnd (Union[List[torch.Tensor], List[dict]]): A list defining the boundaries for the periodic condition. Each element can be a tensor representing a coordinate or a dictionary specifying coordinate values (e.g., `{'x': 1, 't': [0,1]}`). + operator (dict, optional): A dictionary defining the operator for the periodic condition. This is used when enforcing periodicity on derivatives or other operators. Defaults to None, which implies a Dirichlet-type periodic condition (i.e., the function values are periodic). + var (int, optional): The index of the variable to which the periodic condition applies. This is relevant for systems of equations. Defaults to 0. + + Returns: + None: This method modifies the internal list of conditions (`self.conditions_lst`) by appending a dictionary representing the periodic boundary condition. """ value = torch.tensor([0.]) if operator is None: @@ -182,14 +259,17 @@ def data( operator: Union[dict, None], value: torch.Tensor, var: int = 0): - """ conditions for available solution data - - Args: - bnd (Union[torch.Tensor, dict]): boundary points can be torch.Tensor - or dict with keys as coordinates names and values as coordinates values - operator (Union[dict, None]): dictionary with opertor terms: {'operator name':{coeff, term, pow, var}} - value (Union[torch.Tensor, float]): values at the boundary (bnd) - var (int, optional): variable for system case and periodic dirichlet. Defaults to 0. + """ + Adds data conditions to the list of conditions. These conditions represent known values of the solution at specific locations, potentially influenced by differential operators. This information is crucial for guiding the equation discovery process by providing constraints that candidate equations must satisfy. + + Args: + bnd (Union[torch.Tensor, dict]): Boundary points where the solution's value is known. Can be a tensor or a dictionary mapping coordinate names to values. + operator (Union[dict, None]): Differential operator(s) associated with the data condition. Specifies how the solution's derivatives relate to the known value. Can be None if the value is directly known. + value (torch.Tensor): The known value of the solution (or the result of the operator acting on the solution) at the boundary points. + var (int, optional): Index of the variable if dealing with a system of equations. Defaults to 0. + + Returns: + None """ if operator is not None: operator = EquationMixin.equation_unify(operator) @@ -203,16 +283,30 @@ def _bnd_grid(self, bnd: Union[torch.Tensor, dict], variable_dict: dict, dtype) -> torch.Tensor: - """ build subgrid for every condition. - - Args: - bnd (Union[torch.Tensor, dict]):oundary points can be torch.Tensor - or dict with keys as coordinates names and values as coordinates values - variable_dict (dict): dictionary with torch.Tensors for each domain variable - dtype (dtype): dtype - - Returns: - torch.Tensor: subgrid for boundary cond-s. + """ + Builds a subgrid representing the boundary conditions for the problem. + + This method constructs a grid of points that satisfy the specified boundary conditions. + It handles different types of boundary specifications, including direct tensor inputs, + fixed coordinate values, and interval constraints on coordinate values. The resulting + grid is used to evaluate the solution at the boundaries and enforce the boundary conditions + during the training process. + + Args: + bnd (Union[torch.Tensor, dict]): Boundary conditions. Can be a torch.Tensor + representing the boundary points directly, or a dictionary where keys are + coordinate names and values are either: + - torch.Tensor: Tensor of boundary points for that coordinate. + - float or int: Fixed value for that coordinate. + - list: Interval [lower_bound, upper_bound] specifying the range for that coordinate. + variable_dict (dict): Dictionary containing torch.Tensors for each domain variable, + representing the grid coordinates. + dtype (dtype): The desired data type for the resulting grid. + + Returns: + torch.Tensor: A tensor representing the subgrid of points that satisfy the specified + boundary conditions. The shape of the tensor is (N, D), where N is the number of + boundary points and D is the number of dimensions. """ dtype = variable_dict[list(variable_dict.keys())[0]].dtype @@ -239,13 +333,20 @@ def _bnd_grid(self, def build(self, variable_dict: dict) -> List[dict]: - """ preprocessing of initial boundaries data. - + """ + Processes boundary conditions to prepare them for equation discovery. + + This method prepares boundary conditions by converting them into a usable format, + ensuring compatibility with the data and specified device. It handles different + types of boundary conditions, including periodic ones, and ensures that boundary + values are correctly formatted as tensors with the appropriate data type and device. + This preprocessing step is crucial for the subsequent equation discovery process. + Args: - variable_dict (dict): dictionary with torch.Tensors for each domain variable - + variable_dict (dict): A dictionary containing torch.Tensors representing the domain variables. + Returns: - List[dict]: list with dicts (where is all info obaut bconds) + List[dict]: A list of dictionaries, where each dictionary contains preprocessed information about a boundary condition. """ if self.conditions_lst == []: return None @@ -276,19 +377,51 @@ def build(self, class Equation(): - """class for adding eqution. """ + class for adding eqution. + """ + def __init__(self): + """ + Initializes the EquationList object. + + The EquationList is used to store and manage a collection of equation objects. + This initialization creates an empty list ready to be populated with equations discovered or created during the equation discovery process. + + Args: + self: The object instance. + + Returns: + None + """ self.equation_lst = [] @property def num(self): + """ + Returns the number of equations. + + This property provides a convenient way to access the size of the equation list, + reflecting the complexity of the discovered equation system. + + Args: + self: The Equation object instance. + + Returns: + int: The number of equations currently stored. + """ return len(self.equation_lst) def add(self, eq: dict): - """ add equation - + """ + Adds a new equation to the system of equations. + + This allows the system to represent more complex relationships and dependencies between variables. + Args: - eq (dict): equation in operator form. + eq (dict): A dictionary representing the equation in operator form. + + Returns: + None """ self.equation_lst.append(eq) diff --git a/epde/solver/derivative.py b/epde/solver/derivative.py index a22126fc..528e8930 100644 --- a/epde/solver/derivative.py +++ b/epde/solver/derivative.py @@ -8,10 +8,27 @@ class DerivativeInt(): - """Interface class """ + Interface class + """ + def take_derivative(self, value): - """Method that should be built in every child class""" + """ + Calculates the derivative of the expression with respect to a given variable. + + This method must be implemented by subclasses to provide specific derivative calculation logic + based on the expression's structure. It ensures that all expression types support derivative + calculation, which is a core requirement for equation discovery within the EPDE framework. + + Args: + value (str): The variable with respect to which the derivative is calculated. + + Returns: + NotImplementedError: Always raises an exception, as the method must be implemented in a subclass. + + Raises: + NotImplementedError: If the subclass does not implement this method. + """ raise NotImplementedError @@ -20,21 +37,37 @@ class Derivative_NN(DerivativeInt): Taking numerical derivative for 'NN' method. """ + def __init__(self, model: Any): """ - Args: - model: neural network. + Initializes the Derivative_NN with a neural network model. This model will be used to approximate derivatives, which are then used to construct and evaluate potential differential equations. + + Args: + model: A neural network used for derivative approximation. + + Returns: + None """ self.model = model def take_derivative(self, term: Union[list, int, torch.Tensor], *args) -> torch.Tensor: - """ Auxiliary function serves for single differential operator resulting field - derivation. - - Args: - term (Union[list, int, torch.Tensor]): differential operator in conventional form. - Returns: - torch.Tensor: resulting field, computed on a grid. + """ + Computes the contribution of a single differential operator term to the overall equation. + + This function evaluates a term in the differential equation by applying the specified + derivative scheme to the neural network's output and multiplying by the coefficient. + It effectively calculates one component of the equation's residual, which is then + used to optimize the neural network's parameters to satisfy the differential equation. + + Args: + term (Union[list, int, torch.Tensor]): A dictionary representing a single term + in the differential equation. It contains the coefficient, derivative direction, + variable dependencies, and power of the term. + + Returns: + torch.Tensor: The computed value of the differential operator term, evaluated + on the grid points. This represents the contribution of this specific term + to the overall differential equation at each point in the domain. """ dif_dir = list(term.keys())[1] @@ -87,10 +120,18 @@ class Derivative_autograd(DerivativeInt): Taking numerical derivative for 'autograd' method. """ + def __init__(self, model: torch.nn.Module): """ - Args: - model (torch.nn.Module): model of *autograd* mode. + Initializes the Derivative_autograd class. + + This class prepares a given neural network model for subsequent symbolic differentiation, enabling the identification of potential differential equation terms. + + Args: + model (torch.nn.Module): The neural network model to be differentiated. + + Returns: + None """ self.model = model @@ -99,18 +140,17 @@ def _nn_autograd(model: torch.nn.Module, points: torch.Tensor, var: int, axis: List[int] = [0]): - """ Computes derivative on the grid using autograd method. - - Args: - model (torch.nn.Module): torch neural network. - points (torch.Tensor): points, where numerical derivative is calculated. - var (int): number of dependent variables (for single equation is *0*) - axis (list, optional): term of differentiation, example [0,0]->d2/dx2 - if grid_points(x,y). Defaults to [0]. - - Returns: - gradient_full (torch.Tensor): the result of desired function differentiation - in corresponding axis. + """ + Computes the derivative of a neural network model's output with respect to its input using PyTorch's automatic differentiation. This function is a core component for calculating the residuals required to evaluate candidate differential equations. + + Args: + model (torch.nn.Module): The neural network model. + points (torch.Tensor): The input points at which to compute the derivative. + var (int): The index of the output variable to differentiate (for systems of equations). + axis (List[int], optional): The axes with respect to which to differentiate. Defaults to [0]. + + Returns: + torch.Tensor: The computed derivative at the given points. """ points.requires_grad = True @@ -123,15 +163,30 @@ def _nn_autograd(model: torch.nn.Module, return gradient_full def take_derivative(self, term: dict, grid_points: torch.Tensor) -> torch.Tensor: - """ Auxiliary function serves for single differential operator resulting field - derivation. - + """ + Computes the contribution of a single term in the differential operator. + + This function calculates the value of a single term within a larger differential operator, + effectively evaluating a component of the equation on the given grid points. It handles + various forms of coefficients and derivatives, including those defined by neural networks + or other callable functions. + Args: - term (dict): differential operator in conventional form. - grid_points (torch.Tensor): points, where numerical derivative is calculated. - + term (dict): A dictionary representing a single term in the differential operator. + It contains information about the coefficient, derivative direction, + variables, and powers involved in the term. + grid_points (torch.Tensor): The points on which the derivative is evaluated. + Shape: (N, D), where N is the number of points and D is the + dimensionality of the grid. + Returns: - der_term (torch.Tensor): resulting field, computed on a grid. + torch.Tensor: The value of the term evaluated at the given grid points. + Shape: (N, 1), where N is the number of points. + + Why: + This function is a crucial part of constructing the overall differential operator. + By computing each term individually, the method allows for flexible and modular + representation of complex differential equations. """ dif_dir = list(term.keys())[1] # it is may be int, function of grid or torch.Tensor @@ -174,11 +229,25 @@ class Derivative_mat(DerivativeInt): """ Taking numerical derivative for 'mat' method. """ + def __init__(self, model: torch.Tensor, derivative_points: int): """ + Initializes the Derivative_mat object. + + This class precomputes coefficients used for approximating derivatives + based on a given model and number of derivative points. These coefficients + are then used to efficiently calculate derivatives during the equation + discovery process. + Args: - model (torch.Tensor): model of *mat* mode. - derivative_points (int): points number for derivative calculation. + model (torch.Tensor): The model in *mat* mode, representing the data + from which derivatives will be estimated. + derivative_points (int): The number of points used in the derivative + calculation. A higher number of points can lead to more accurate + derivative estimates, but also increases computational cost. + + Returns: + None """ self.model = model self.backward, self.farward = Derivative_mat._labels(derivative_points) @@ -194,15 +263,20 @@ def __init__(self, model: torch.Tensor, derivative_points: int): @staticmethod def _labels(derivative_points: int) -> Tuple[List, List]: - """ Determine which points are used in derivative calc-n. - If derivative_points = 2, it return ([-1, 0], [0, 1]) - + """ + Determines the indices of neighboring points used to approximate derivatives. + + This function generates index offsets for both backward and forward finite difference schemes, + defining which neighboring points are used in the derivative calculation. These indices are + essential for constructing derivative matrices that approximate differential operators on a discrete grid. + Args: - derivative_points (int): points number for derivative calculation. - + derivative_points (int): The number of points used in the derivative calculation. + Returns: - labels_backward (list): points labels for backward scheme. - labels_forward (list): points labels for forward scheme. + Tuple[List[int], List[int]]: A tuple containing two lists: + - labels_backward: Index offsets for the backward difference scheme. + - labels_forward: Index offsets for the forward difference scheme. """ labels_backward = list(i for i in range(-derivative_points + 1, 1)) labels_farward = list(i for i in range(derivative_points)) @@ -210,15 +284,18 @@ def _labels(derivative_points: int) -> Tuple[List, List]: @staticmethod def _linear_system(labels: list) -> np.ndarray: - """ To caclulate coeeficints in numerical scheme, - we have to solve the linear system of algebraic equations. - A*alpha=b - - Args: - labels (list): points labels for backward/foraward scheme. - - Returns: - alpha (np.ndarray): coefficints for numerical scheme. + """ + Solves a linear system to determine the coefficients for a numerical differentiation scheme. + + The coefficients are calculated by solving a linear system derived from a Vandermonde matrix. + This approach ensures that the resulting differentiation scheme accurately approximates the derivative + based on the provided stencil points. + + Args: + labels (list): The stencil points (x-coordinates) used in the differentiation scheme. + + Returns: + np.ndarray: The coefficients for the numerical differentiation scheme, obtained by solving the linear system. """ points_num = len(labels) # num_points=number of equations labels = np.array(labels) @@ -235,15 +312,15 @@ def _linear_system(labels: list) -> np.ndarray: return alpha def _derivative_1d(self, u_tensor: torch.Tensor, h: torch.Tensor) -> torch.Tensor: - """ Computes derivative in one dimension for matrix method. - - Args: - u_tensor (torch.Tensor): dependenet varible of equation, - some part of model. - h (torch.Tensor): increment of numerical scheme. - - Returns: - du (torch.Tensor): computed derivative along one dimension. + """ + Computes the numerical derivative of a tensor along one dimension using a finite difference scheme. This is a core component for estimating derivatives within the equation discovery process. + + Args: + u_tensor (torch.Tensor): The input tensor for which the derivative is computed. Represents the dependent variable in the equation. + h (torch.Tensor): The step size or increment used in the finite difference approximation. + + Returns: + du (torch.Tensor): The computed derivative of the input tensor along the specified dimension. """ shape = u_tensor.shape @@ -267,14 +344,21 @@ def _derivative_1d(self, u_tensor: torch.Tensor, h: torch.Tensor) -> torch.Tenso def _step_h(self, h_tensor: torch.Tensor) -> List[torch.Tensor]: - """ Calculate increment along each axis of the grid. - + """ + Calculate the grid increments along each axis. + + This function computes the step size (h) along each axis of the grid, + which represents the resolution of the grid in each dimension. + These increments are crucial for calculating derivatives on the grid. + Args: - h_tensor (torch.Tensor): grid of *mat* mode. - + h_tensor (torch.Tensor): Grid of points in *mat* mode. + Shape: (num_axes, num_points_along_axis). + Returns: - h (List[torch.Tensor]): lsit with increment - along each axis of the grid. + List[torch.Tensor]: A list containing the increment (step size) + along each axis of the grid. The length of the list corresponds + to the number of axes. """ h = [] @@ -290,16 +374,16 @@ def _derivative(self, u_tensor: torch.Tensor, h: torch.Tensor, axis: int) -> torch.Tensor: - """ Computing derivative for 'mat' method. - - Args: - u_tensor (torch.Tensor): dependenet varible of equation, - some part of model. - h (torch.Tensor): increment of numerical scheme. - axis (int): axis along which the derivative is calculated. - - Returns: - du (torch.Tensor): computed derivative. + """ + Computes the numerical derivative of a tensor along a specified axis using a finite difference scheme. This is a core component for estimating equation terms from data. + + Args: + u_tensor (torch.Tensor): The tensor for which the derivative is computed. Represents a dependent variable in the equation. + h (torch.Tensor): The step size (increment) used in the finite difference approximation. + axis (int): The axis along which the derivative is calculated. + + Returns: + du (torch.Tensor): The computed derivative of the input tensor. """ if len(u_tensor.shape)==1 or u_tensor.shape[0]==1: @@ -331,15 +415,17 @@ def _derivative(self, return du def take_derivative(self, term: torch.Tensor, grid_points: torch.Tensor) -> torch.Tensor: - """ Auxiliary function serves for single differential operator resulting field - derivation. - - Args: - term (torch.Tensor): differential operator in conventional form. - grid_points (torch.Tensor): grid points. - - Returns: - der_term (torch.Tensor): resulting field, computed on a grid. + """ + Auxiliary function to compute the contribution of a single term in the differential operator. This involves calculating derivatives of the dependent variables based on the specified scheme and grid points, and then combining them according to the term's structure. + + Args: + term (torch.Tensor): A dictionary representing a single term in the differential operator, containing information about the variable, derivative order, and coefficient. + grid_points (torch.Tensor): The coordinates at which the derivatives are evaluated. + + Returns: + der_term (torch.Tensor): The computed value of the term at each grid point. This represents the contribution of this specific term to the overall differential equation. + + WHY: This function is a crucial step in evaluating the differential operator on the grid. By computing each term's contribution individually, the method allows for flexible and modular equation discovery. """ dif_dir = list(term.keys())[1] @@ -362,20 +448,33 @@ def take_derivative(self, term: torch.Tensor, grid_points: torch.Tensor) -> torc class Derivative(): """ - Interface for taking numerical derivative due to chosen calculation mode. + Interface for calculating numerical derivatives. It supports different calculation modes to provide flexibility and accuracy in derivative estimation. + """ - """ def __init__(self, model: Union[torch.nn.Module, torch.Tensor], derivative_points: int): - """_summary_ - + """ + Initializes the Derivative class. + + This class prepares the model for derivative calculations, + setting the stage for subsequent equation discovery. The number of + derivative points influences the accuracy and computational cost of + approximating derivatives, which are crucial for identifying the + underlying differential equations. + Args: - model (Union[torch.nn.Module, torch.Tensor]): neural network or - matrix depending on the selected mode. - derivative_points (int): points number for derivative calculation. - If derivative_points=2, numerical scheme will be ([-1,0],[0,1]), - parameter determine number of poins in each forward and backward scheme. + model (Union[torch.nn.Module, torch.Tensor]): The neural network or + matrix representing the system's behavior. This is the model + from which derivatives will be computed. + derivative_points (int): The number of points to use in the + numerical derivative calculation. A higher number of points + can increase accuracy but also computational complexity. + For example, if derivative_points=2, a simple two-point + numerical scheme like ([-1,0],[0,1]) will be used. + + Returns: + None """ self.model = model @@ -384,11 +483,25 @@ def __init__(self, def set_strategy(self, strategy: str) -> Union[Derivative_NN, Derivative_autograd, Derivative_mat]: """ - Setting the calculation method. + Sets the differentiation strategy. + + This method configures the approach used to calculate derivatives, + allowing the user to select the most appropriate technique based on + the problem and available resources. Different strategies offer trade-offs + between computational cost, accuracy, and the need for analytical + derivatives. The choice of strategy impacts how the derivatives + required for equation discovery are computed. + Args: - strategy: Calculation method. (i.e., "NN", "autograd", "mat"). + strategy (str): The name of the differentiation strategy to use. + Valid options are "NN" (neural network-based), + "autograd" (automatic differentiation), and "mat" + (matrix-based). + Returns: - equation in input form for a given calculation method. + Union[Derivative_NN, Derivative_autograd, Derivative_mat]: An instance of the class + corresponding to the selected differentiation strategy, initialized + with the provided model and derivative points (if applicable). """ if strategy == 'NN': return Derivative_NN(self.model) diff --git a/epde/solver/device.py b/epde/solver/device.py index e9f3db4e..09b0422a 100644 --- a/epde/solver/device.py +++ b/epde/solver/device.py @@ -6,12 +6,25 @@ verbose = False def solver_device(device: str): - """ Corresponding to chosen device, all futher - created tensors will be with the same device - + """ + Sets the default device for subsequent tensor operations. + + This function configures the global default device (CPU or CUDA) + for all newly created tensors. It checks for CUDA availability + if a CUDA device is requested and falls back to CPU if CUDA is + unavailable. + Args: - device (str): device mode, **cuda, gpu, cpu*. - + device (str): The desired device ('cuda', 'gpu', or 'cpu'). + + Returns: + None: This function modifies the global PyTorch state. + + Why: + This ensures consistency in device placement for all tensors + created during the equation discovery process, which is + crucial for efficient computation and compatibility with + available hardware. """ if device in ['cuda','gpu'] and torch.cuda.is_available(): if verbose: @@ -27,14 +40,21 @@ def solver_device(device: str): return torch.set_default_device('cpu') def check_device(data: Any): - """ checking the device of the data. - If the data.device is not same with torch.set_default_device, - change one. + """ + Ensures that the input data (model or tensor) resides on the correct device. + + This function checks if the device of the input data matches the currently + configured default device. If they differ, the data is moved to the + default device to ensure compatibility and proper execution within the + framework. This is crucial for maintaining consistency across different + hardware configurations (CPU/GPU) and preventing device-related errors + during computations. + Args: - data (Any): it could be model or torch.Tensors - + data (Any): The input data, which can be a model or a PyTorch tensor. + Returns: - data (Any): data with correct device + Any: The input data, moved to the default device if necessary. """ device = torch.tensor([0.]).device.type if data.device.type != device: @@ -43,6 +63,16 @@ def check_device(data: Any): return data def device_type(): - """ Return the default device. + """ + Return the default device type used by PyTorch. + + This is useful for ensuring that operations are performed on the correct device, + especially when working with hardware accelerators like GPUs. + + Args: + None + + Returns: + str: A string representing the device type (e.g., 'cpu' or 'cuda'). """ return torch.tensor([0.]).device.type \ No newline at end of file diff --git a/epde/solver/eval.py b/epde/solver/eval.py index 09cd0283..eddbbd85 100644 --- a/epde/solver/eval.py +++ b/epde/solver/eval.py @@ -14,18 +14,29 @@ def integration(func: torch.Tensor, grid: torch.Tensor, power: int = 2) \ -> Union[Tuple[float, float], Tuple[list, torch.Tensor]]: - """ Function realize 1-space integrands, - where func=(L(u)-f)*weak_form subintegrands function and - definite integral parameter is grid. - + """ + Integrates a function over a grid, effectively reducing the dimensionality of the problem. + + This function performs numerical integration of a given function `func` over a specified `grid`. + The integration is performed along one axis of the grid at a time. + If the grid has more than one dimension, the integration result and the reduced grid are returned. + If the grid has only one dimension, the final integration result and 0 are returned. + Args: - func (torch.Tensor): operator multiplied on test function - grid (torch.Tensor): array of a n-D points. - power (int, optional): power of func points. Defults to 2. - + func (torch.Tensor): The function values at each grid point. Represents the operator applied to the test function. + grid (torch.Tensor): The grid points over which to integrate. + power (int, optional): The power to which the function values are raised during integration. Defaults to 2. + Returns: - 'result' is integration result through one grid axis - 'grid' is initial grid without last column or zero (if grid.shape[N,1]) + Union[Tuple[float, float], Tuple[list, torch.Tensor]]: + - If the grid has only one column, returns a tuple containing the final integration result (float) and 0.0 (float). + - If the grid has multiple columns, returns a tuple containing: + - A list of integration results (list) for each segment along the integrated axis. + - The reduced grid (torch.Tensor) with the last column removed. + + Why: + This function is used to reduce the dimensionality of the problem by integrating out one spatial dimension at a time. + This is a key step in solving differential equations using numerical methods. """ if grid.shape[-1] == 1: column = -1 @@ -54,21 +65,34 @@ def integration(func: torch.Tensor, def dict_to_matrix(bval: dict, true_bval: dict)\ -> Tuple[torch.Tensor, torch.Tensor, List, List]: - """ Function for bounaries values matrix creation from dictionary. - + """ + Transforms dictionaries of boundary values into matrix representations for equation discovery. + + This function converts dictionaries containing predicted and true boundary values + for different boundary types into PyTorch tensors. These tensors are structured + as matrices, where each column represents a specific boundary type. The function + also returns lists of boundary types and their corresponding lengths. + Args: - bval (dict): dictionary with predicted boundaries values, - where keys are boundaries types. - true_bval (dict): dictionary with true boundaries values, - where keys are boundaries types. - + bval (dict): Dictionary where keys are boundary types and values are predicted + boundary values (torch.Tensor). + true_bval (dict): Dictionary where keys are boundary types and values are true + boundary values (torch.Tensor). + Returns: - matrix_bval (torch.Tensor): matrix, where each column is predicted - boundary values of one boundary type. - matrix_true_bval (torch.Tensor):matrix, where each column is true - boundary values of one boundary type. - keys (list): boundary types list corresponding matrix_bval columns. - len_list (list): list of length of each boundary type column. + matrix_bval (torch.Tensor): Matrix with predicted boundary values; each column + corresponds to a boundary type. + matrix_true_bval (torch.Tensor): Matrix with true boundary values; each column + corresponds to a boundary type. + keys (list): List of boundary types corresponding to the columns of the matrices. + len_list (list): List of lengths for each boundary type column before padding. + + Why: + This transformation is necessary to prepare the boundary data for efficient + processing and comparison within the equation discovery pipeline. By converting + the dictionaries into matrix form, the framework can leverage vectorized + operations for calculating loss functions and evaluating the fitness of + candidate equations based on their ability to satisfy the given boundary conditions. """ keys = list(bval.keys()) @@ -91,6 +115,7 @@ class Operator(): """ Class for differential equation calculation. """ + def __init__(self, grid: torch.Tensor, prepared_operator: Union[list,dict], @@ -100,15 +125,22 @@ def __init__(self, derivative_points: int, batch_size: int = None): """ - Args: - grid (torch.Tensor): grid (domain discretization). - prepared_operator (Union[list,dict]): prepared (after Equation class) operator. - model (Union[torch.nn.Sequential, torch.Tensor]): *mat or NN or autograd* model. - mode (str): *mat or NN or autograd* - weak_form (List[callable]): list with basis functions (if the form is *weak*). - derivative_points (int): points number for derivative calculation. - For details to Derivative_mat class. - batch_size (int): size of batch. + Initializes the Operator class, preparing it for the application of differential operators to a given model on a specified grid. + + The Operator class facilitates the evaluation of differential operators, + handling different computational modes and data batching for efficient processing. + + Args: + grid (torch.Tensor): The spatial or temporal grid representing the domain of the problem. + prepared_operator (Union[list,dict]): The differential operator, preprocessed into a suitable format for computation. + model (Union[torch.nn.Sequential, torch.Tensor]): The model (e.g., neural network) that approximates the solution of the differential equation. + mode (str): Specifies the computation mode ('mat', 'NN', or 'autograd') for operator evaluation. + weak_form (List[callable]): A list of basis functions used when solving the equation in a weak formulation. + derivative_points (int): The number of points used in the numerical approximation of derivatives. + batch_size (int, optional): The size of mini-batches used for processing the grid. Defaults to None. + + Returns: + None """ self.grid = check_device(grid) self.prepared_operator = prepared_operator @@ -134,8 +166,14 @@ def __init__(self, self.derivative_points).set_strategy(self.mode).take_derivative def init_mini_batches(self): - """ Initialization of batch iterator. - + """ + Initializes the mini-batch iterator for processing data in smaller chunks. This prepares the data loader to provide the next batch of grid points for equation discovery. + + Args: + self: The Operator instance. + + Returns: + None """ self.grid_iter = iter(self.grid_loader) self.grid_batch = next(self.grid_iter) @@ -143,16 +181,17 @@ def init_mini_batches(self): def apply_operator(self, operator: list, grid_points: Union[torch.Tensor, None]) -> torch.Tensor: - """ Deciphers equation in a single grid subset to a field. - + """ + Applies a given operator to a grid subset, effectively evaluating a part of the equation on that subset. + + This process is crucial for breaking down the equation into manageable parts that can be computed and later assembled to represent the entire equation's behavior across the domain. + Args: - operator (list): prepared (after Equation class) operator. See - input_preprocessing.operator_prepare() - grid_points (Union[torch.Tensor, None]): Points, where numerical - derivative is calculated. **Uses only in 'autograd' and 'mat' modes.** - + operator (list): A prepared list of terms representing the operator. This list is structured according to the equation's definition. See input_preprocessing.operator_prepare() for details on the expected format. + grid_points (Union[torch.Tensor, None]): The points on the grid subset where the operator will be evaluated. This is only used in 'autograd' and 'mat' modes, where numerical derivatives are calculated. + Returns: - total (torch.Tensor): Decoded operator on a single grid subset. + torch.Tensor: The result of applying the operator to the grid subset. This represents the decoded operator's contribution on that specific subset. """ for term in operator: @@ -165,10 +204,18 @@ def apply_operator(self, return total def _pde_compute(self) -> torch.Tensor: - """ Computes PDE residual. - + """ + Computes the residual of the partial differential equation (PDE). + + This method calculates the PDE residual by applying the prepared operator to the sorted grid. + The grid can be processed in mini-batches if a batch size is specified. + The method handles cases with single or multiple equations. + + Args: + None + Returns: - torch.Tensor: P/O DE residual. + torch.Tensor: The computed PDE residual. """ if self.batch_size is not None: @@ -194,10 +241,14 @@ def _pde_compute(self) -> torch.Tensor: def _weak_pde_compute(self) -> torch.Tensor: - """ Computes PDE residual in weak form. - - Returns: - torch.Tensor: weak PDE residual. + """ + Computes the weak form of the PDE residual by integrating the product of the PDE operator and test functions over the domain. This process effectively transforms the strong form of the PDE into a weak form, enabling the computation of a solution that minimizes the residual in a Galerkin sense. The method iterates through each component of the PDE operator, applies the weak form functions, and performs integration to obtain the residual. + + Args: + None + + Returns: + torch.Tensor: weak PDE residual. """ device = device_type() @@ -222,10 +273,16 @@ def _weak_pde_compute(self) -> torch.Tensor: return torch.cat(sol_list).reshape(1,-1) def operator_compute(self): - """ Corresponding to form (weak or strong) calculate residual of operator. - + """ + Calculates the residual of the operator, choosing between a direct computation or a weak form computation based on the operator's configuration. + + This selection allows the framework to adapt the residual calculation based on the chosen representation of the differential equation. + + Args: + None + Returns: - torch.Tensor: operator residual. + torch.Tensor: The operator residual. """ if self.weak_form is None or self.weak_form == []: return self._pde_compute() @@ -237,6 +294,7 @@ class Bounds(): """ Class for boundary and initial conditions calculation. """ + def __init__(self, grid: torch.Tensor, prepared_bconds: Union[list, dict], @@ -244,16 +302,26 @@ def __init__(self, mode: str, weak_form: List[callable], derivative_points: int): - """_summary_ - - Args: - grid (torch.Tensor): grid (domain discretization). - prepared_bconds (Union[list,dict]): prepared (after Equation class) baund-y con-s. - model (Union[torch.nn.Sequential, torch.Tensor]): *mat or NN or autograd* model. - mode (str): *mat or NN or autograd* - weak_form (List[callable]): list with basis functions (if the form is *weak*). - derivative_points (int): points number for derivative calculation. - For details to Derivative_mat class. + """ + Initializes the Bounds object, preparing the boundary conditions for solving the differential equation. + + This involves setting up the computational grid, processing boundary conditions, + and configuring the model (neural network, matrix, or autograd) to satisfy these conditions. + The operator is initialized to handle the evaluation of the differential equation + on the grid, considering the specified mode (e.g., matrix-based, neural network-based, or autograd-based) + and weak form (if applicable). This setup ensures that the solution respects the constraints + imposed by the boundary conditions. + + Args: + grid (torch.Tensor): The computational grid representing the domain discretization. + prepared_bconds (Union[list, dict]): Prepared boundary conditions, processed by the Equation class. + model (Union[torch.nn.Sequential, torch.Tensor]): The model (neural network, matrix, or autograd) used to represent the solution. + mode (str): Specifies the mode of operation ('mat', 'NN', or 'autograd'). + weak_form (List[callable]): A list of basis functions for the weak formulation of the equation. + derivative_points (int): The number of points used for derivative calculation (relevant for the Derivative_mat class). + + Returns: + None """ self.grid = check_device(grid) self.prepared_bconds = prepared_bconds @@ -264,15 +332,21 @@ def __init__(self, derivative_points) def _apply_bconds_set(self, operator_set: list) -> torch.Tensor: - """ Method only for *NN* mode. Calculate boundary conditions with derivatives - to use them in _apply_neumann method. - + """ + Calculates the contribution of a set of boundary operators to the solution field. + + This method iterates through a list of pre-processed boundary operators, applies each operator to the grid, + and concatenates the results to form a complete representation of the boundary conditions' influence on the field. + This is a crucial step in incorporating boundary information into the solution obtained by the neural network. + Args: - operator_set (list): list with prepared (after Equation_NN class) boundary operators. - For details to Equation_NN.operator_prepare method. - + operator_set (list): A list of boundary operators, pre-processed to be compatible with the neural network. + These operators define the mathematical constraints imposed by the boundaries. + See Equation_NN.operator_prepare for details on the preparation process. + Returns: - torch.Tensor: Decoded boundary operator on the whole grid. + torch.Tensor: A tensor representing the aggregated effect of all boundary operators on the solution field. + This tensor is used to enforce the specified boundary conditions during the training or inference. """ field_part = [] @@ -282,16 +356,24 @@ def _apply_bconds_set(self, operator_set: list) -> torch.Tensor: return field_part def _apply_dirichlet(self, bnd: torch.Tensor, var: int) -> torch.Tensor: - """ Applies Dirichlet boundary conditions. - + """ + Applies Dirichlet boundary conditions to the model output at specified boundary points. + + This method extracts the model's prediction at the boundary locations and reshapes it + to ensure compatibility with subsequent calculations, effectively enforcing the Dirichlet + boundary condition. The specific implementation depends on the chosen mode of operation + ('NN', 'autograd', or 'mat'), allowing for flexibility in how the model's output is accessed + and processed. + Args: - bnd (torch.Tensor): terms (boundary points) of prepared boundary conditions. - For more deatails to input_preprocessing (bnd_prepare maethos). - var (int): indicates for which dependent variable it is necessary to apply - the boundary condition. For single equation is 0. - + bnd (torch.Tensor): Terms (boundary points) of prepared boundary conditions. + For more details, refer to the input preprocessing (bnd_prepare method). + var (int): Indicates for which dependent variable it is necessary to apply + the boundary condition. For a single equation, this is typically 0. + Returns: - torch.Tensor: calculated boundary condition. + torch.Tensor: The model's output at the boundary points, reshaped for use in + loss calculations or other downstream operations. """ if self.mode == 'NN' or self.mode == 'autograd': @@ -304,14 +386,21 @@ def _apply_dirichlet(self, bnd: torch.Tensor, var: int) -> torch.Tensor: return b_op_val def _apply_neumann(self, bnd: torch.Tensor, bop: list) -> torch.Tensor: - """ Applies boundary conditions with derivative operators. - + """ + Computes the value of the boundary condition based on the specified mode. + + This method calculates the boundary condition value by applying the derivative operator + to the boundary points, using different approaches depending on the configured mode. + The mode determines whether a neural network, automatic differentiation, or a matrix-based + method is used to evaluate the operator. This is a crucial step in enforcing boundary + constraints within the solution domain. + Args: - bnd (torch.Tensor): terms (boundary points) of prepared boundary conditions. - bop (list): terms of prepared boundary derivative operator. - + bnd (torch.Tensor): Terms (boundary points) of prepared boundary conditions. + bop (list): Terms of prepared boundary derivative operator. + Returns: - torch.Tensor: calculated boundary condition. + torch.Tensor: Calculated boundary condition. """ if self.mode == 'NN': @@ -328,16 +417,16 @@ def _apply_neumann(self, bnd: torch.Tensor, bop: list) -> torch.Tensor: return b_op_val def _apply_periodic(self, bnd: torch.Tensor, bop: list, var: int) -> torch.Tensor: - """ Applies periodic boundary conditions. - - Args: - bnd (torch.Tensor): terms (boundary points) of prepared boundary conditions. - bop (list): terms of prepared boundary derivative operator. - var (int): indicates for which dependent variable it is necessary to apply - the boundary condition. For single equation is 0. - - Returns: - torch.Tensor: calculated boundary condition + """ + Applies periodic boundary conditions by combining Dirichlet or Neumann conditions based on the specified mode. This ensures that the solution at one boundary matches the solution at the opposite boundary, which is crucial for modeling systems with repeating spatial or temporal domains. + + Args: + bnd (torch.Tensor): Terms (boundary points) of prepared boundary conditions. + bop (list): Terms of prepared boundary derivative operator. If None, Dirichlet conditions are applied. + var (int): Indicates for which dependent variable it is necessary to apply the boundary condition. For a single equation, it is 0. + + Returns: + torch.Tensor: Calculated boundary condition values, ensuring periodicity. """ if bop is None: @@ -356,16 +445,21 @@ def _apply_periodic(self, bnd: torch.Tensor, bop: list, var: int) -> torch.Tenso return b_op_val def _apply_data(self, bnd: torch.Tensor, bop: list, var: int) -> torch.Tensor: - """ Method for applying known data about solution. - + """ + Applies either Dirichlet or Neumann boundary conditions to the solution. + + This method determines which type of boundary condition to apply based on the provided + derivative operator and then calculates the condition value. This ensures that known + constraints on the solution at the boundaries are enforced, guiding the equation discovery + process towards solutions that respect these constraints. + Args: - bnd (torch.Tensor): terms (data points) of prepared boundary conditions. - bop (list): terms of prepared data derivative operator. - var (int): indicates for which dependent variable it is necessary to apply - the data condition. For single equation is 0. - + bnd (torch.Tensor): Terms (data points) of prepared boundary conditions. + bop (list): Terms of prepared data derivative operator. If None, Dirichlet conditions are applied; otherwise, Neumann conditions are applied using this operator. + var (int): Indicates for which dependent variable it is necessary to apply the data condition. For a single equation, this is 0. + Returns: - torch.Tensor: calculated data condition. + torch.Tensor: Calculated data condition. """ if bop is None: b_op_val = self._apply_dirichlet(bnd, var).reshape(-1, 1) @@ -374,14 +468,24 @@ def _apply_data(self, bnd: torch.Tensor, bop: list, var: int) -> torch.Tensor: return b_op_val def b_op_val_calc(self, bcond: dict) -> torch.Tensor: - """ Auxiliary function. Serves only to choose *type* of the condition and evaluate one. - - Args: - bcond (dict): terms of prepared boundary conditions - (see input_preprocessing module -> bnd_prepare method). - - Returns: - torch.Tensor: calculated operator on the boundary. + """ + Calculates the value of the boundary operator based on the specified boundary condition type. + + This function acts as a dispatcher, selecting the appropriate method + to apply based on the 'type' key within the boundary condition dictionary. + This allows to handle different types of boundary conditions such as Dirichlet, + Neumann, periodic or data-driven in a unified manner. + + Args: + bcond (dict): A dictionary containing the terms of the prepared boundary condition, + as generated by the `bnd_prepare` method in the `input_preprocessing` module. + This dictionary must contain a 'type' key specifying the boundary condition type + (e.g., 'dirichlet', 'operator', 'periodic', 'data') and other keys + relevant to that type (e.g., 'bnd', 'var', 'bop'). + + Returns: + torch.Tensor: The calculated value of the boundary operator, represented as a PyTorch tensor. + The specific calculation depends on the boundary condition type specified in `bcond`. """ if bcond['type'] == 'dirichlet': @@ -397,15 +501,18 @@ def b_op_val_calc(self, bcond: dict) -> torch.Tensor: return b_op_val def apply_bcs(self) -> Tuple[torch.Tensor, torch.Tensor, list, list]: - """ Applies boundary and data conditions for each *type* in prepared_bconds. - - Returns: - bval (torch.Tensor): matrix, where each column is predicted - boundary values of one boundary type. - true_bval (torch.Tensor):matrix, where each column is true - boundary values of one boundary type. - keys (list): boundary types list corresponding matrix_bval columns. - bval_length (list): list of length of each boundary type column. + """ + Applies boundary and data conditions to construct matrices of predicted and true boundary values, organized by boundary type. This is done to prepare boundary conditions for comparison with PDE solutions. + + Args: + self (Bounds): An instance of the Bounds class containing prepared boundary conditions. + + Returns: + Tuple[torch.Tensor, torch.Tensor, list, list]: + - bval (torch.Tensor): A matrix where each column represents the predicted boundary values for a specific boundary type. + - true_bval (torch.Tensor): A matrix where each column represents the true boundary values for a specific boundary type. + - keys (list): A list of boundary types, corresponding to the columns in the `bval` and `true_bval` matrices. + - bval_length (list): A list containing the length of each boundary type column. """ bval_dict = {} diff --git a/epde/solver/finite_diffs.py b/epde/solver/finite_diffs.py index 9cd96036..3c2300d1 100644 --- a/epde/solver/finite_diffs.py +++ b/epde/solver/finite_diffs.py @@ -7,20 +7,32 @@ class First_order_scheme(): - """Class for numerical scheme construction. Central o(h^2) difference scheme - is used for 'central' points, forward ('f') and backward ('b') o(h) schemes - are used for boundary points. 'central', and combination 'f','b' are - corresponding to points_type. - + """ + Class for constructing numerical schemes. It employs a central difference scheme (second-order accurate) for interior points and first-order accurate forward or backward difference schemes for boundary points. The `points_type` attribute determines whether central differences are used, or a combination of forward and backward differences. """ + def __init__(self, term: list, nvars: int, axes_scheme_type: str): """ - Args: - term (list): differentiation direction. Example: [0,0]->d2u/dx2 - if x is first direction in the grid. - nvars (int): task parameters. Example: if grid(x,t) -> nvars = 2. - axes_scheme_type (str): scheme type: 'central' or combination of 'f' and 'b' + Initializes a first-order finite difference scheme for a specific term in a partial differential equation. + + Args: + term (list): Specifies the differentiation order with respect to each independent variable. + For example, [1, 0] represents a first-order derivative with respect to the first variable. + nvars (int): The number of independent variables in the partial differential equation. + For example, if the equation is defined on a grid (x, t), then nvars = 2. + axes_scheme_type (str): Specifies the finite difference scheme to use along each axis. + It can be 'central' for a central difference scheme or a combination of 'f' (forward) and 'b' (backward) + to define a one-sided scheme. + + Returns: + None + + Why: + This initialization sets up the necessary information for approximating derivatives numerically. + The 'term' specifies which derivative is being approximated, 'nvars' defines the dimensionality of the problem, + and 'axes_scheme_type' determines the type of finite difference approximation used along each axis. + This is a crucial step in discretizing the partial differential equation for numerical solution. """ self.term = term @@ -34,15 +46,22 @@ def __init__(self, term: list, nvars: int, axes_scheme_type: str): # [0]->([1]-[-1])/(2h) (in terms of grid nodes position) @staticmethod def _finite_diff_shift(diff: list, axis: int, mode: str) -> list: - """ 1st order points shift for the corresponding finite difference mode. - + """ + Generates shifted coordinate points based on the specified finite difference scheme. + + This function creates two new coordinate points by shifting the given point + along the specified axis, according to the chosen finite difference mode + (forward, backward, or central). These shifted points are used to approximate + derivatives at the original point. + Args: - diff (list): values of finite differences. - axis (int): axis. - mode (str): the finite difference mode (i.e., forward, backward, central). - + diff (list): A list representing the coordinate point. + axis (int): The axis along which the shift is applied. + mode (str): The finite difference mode ('f' for forward, 'b' for backward, 'central' for central difference). + Returns: - list: list with shifted points. + list: A list containing two shifted coordinate points (diff_p, diff_m). + diff_p is shifted in the positive direction, and diff_m in the negative direction. """ diff_p = copy(diff) @@ -57,13 +76,22 @@ def _finite_diff_shift(diff: list, axis: int, mode: str) -> list: return [diff_p, diff_m] def scheme_build(self) -> list: - """ Building first order (in terms of accuracy) finite-difference scheme. - Start from list of zeros where them numbers equal nvars. After that we - move value in that axis which corresponding to term. [0,0]->[[1,0],[-1,0]] - it means that term was [0] (d/dx) and mode (scheme_type) is 'central'. - - Returns: - list: numerical scheme. + """ + Builds a first-order finite-difference scheme to approximate derivatives. + + This method constructs a numerical scheme by iteratively applying finite difference approximations + to each term in the differential equation. It starts with a base scheme (list of zeros) and + perturbs it along the axes corresponding to each term, generating a list of finite difference + approximations. This process effectively discretizes the differential equation, enabling its + numerical solution. + + Args: + self: The instance of the `First_order_scheme` class, containing information about the + differential equation, number of variables, and desired scheme type. + + Returns: + list: A list of lists representing the numerical scheme. Each inner list corresponds to a + finite difference approximation of a term in the equation. """ order = len(self.term) @@ -87,21 +115,19 @@ def scheme_build(self) -> list: return finite_diff def sign_order(self, h: float = 1 / 2) -> list : - """ Determines the sign of the derivative for the corresponding transformation - from Finite_diffs.scheme_build(). - - From transformations above, we always start from +1 (1) - Every +1 changes to ->[+1,-1] when order of differential rises - [0,0] (+1) ->([1,0]-[-1,0]) ([+1,-1]) - Every -1 changes to [-1,+1] - [[1,0],[-1,0]] ([+1,-1])->[[1,1],[1,-1],[-1,1],[-1,-1]] ([+1,-1,-1,+1]) - + """ + Determines the sign of the derivative for each term in the finite difference scheme. + + The method iteratively refines the sign list based on the order of the differential operator. + Each positive sign (+1) is transformed into [+1, -1], while each negative sign (-1) becomes [-1, +1]. + This process ensures that the signs alternate correctly to approximate the derivative. + Args: - h (float, optional): discretizing parameter in finite- - difference method. Defaults to 1/2. - + h (float, optional): Discretization parameter used in the finite difference method. Defaults to 1/2. + Returns: - list: list, with signs for corresponding points. + list: A list containing the signs (+1 or -1) corresponding to each point in the finite difference scheme. + These signs are crucial for correctly weighting the terms in the approximation of the derivative. """ sign_list = [1] @@ -121,16 +147,30 @@ class Second_order_scheme(): """ Crank–Nicolson method. This realization only for boundary points. """ + def __init__(self, term: list, nvars: int, axes_scheme_type: str): """ - Args: - term (list): differentiation direction. Example: [0,0]->d2u/dx2 if x is first - direction in the grid. - nvars (int): task parameters. Example: if grid(x,t) -> nvars = 2. - axes_scheme_type (str): scheme type: 'central' or combination of 'f' and 'b' - - Raises: - ValueError: _description_ + Initializes a second-order finite difference scheme for a specific term in the differential equation. + + This scheme approximates second-order derivatives using finite differences + based on the specified differentiation direction and scheme type. This + initializer prepares the scheme for calculating the contribution of this + term to the overall equation's residual. + + Args: + term (list): Differentiation direction represented as a list. + For example, [0, 0] corresponds to d^2u/dx^2 if x is the + first direction in the grid. + nvars (int): The number of independent variables in the problem. + For example, if the grid is defined by grid(x, t), then nvars = 2. + axes_scheme_type (str): The type of finite difference scheme to use along each axis. + Can be 'central' for a central difference scheme, or a + combination of 'f' and 'b' for forward and backward differences, + respectively. For example, 'fb' would use a forward difference + in the first direction and a backward difference in the second. + + Raises: + ValueError: If an unsupported scheme type is provided. """ self.term = term @@ -144,15 +184,16 @@ def __init__(self, term: list, nvars: int, axes_scheme_type: str): @staticmethod def _second_order_shift(diff, axis, mode) -> list: - """ 2st order points shift for the corresponding finite difference mode. - - Args: - diff (list): values of finite differences. - axis (int): axis. - mode (str): the finite difference mode (i.e., forward, backward). - - Returns: - list: list with shifted points. + """ + Shifts points based on the specified finite difference mode to refine the approximation of derivatives. This is crucial for constructing accurate finite difference schemes used in solving differential equations. + + Args: + diff (list): Values of finite differences. + axis (int): Axis along which the shift is applied. + mode (str): The finite difference mode ('f' for forward, 'b' for backward). + + Returns: + list: A list containing three shifted point configurations, corresponding to the second-order finite difference scheme. """ diff_1 = copy(diff) diff_2 = copy(diff) @@ -168,12 +209,17 @@ def _second_order_shift(diff, axis, mode) -> list: return [diff_3, diff_2, diff_1] def scheme_build(self) -> list: - """Scheme building for Crank-Nicolson variant, it's identical to - 'scheme_build' in first order method, but value is shifted by - 'second_order_shift'. - - Returns: - list: numerical scheme list. + """ + Builds a numerical scheme for the Crank-Nicolson variant, crucial for constructing finite difference approximations of differential operators. This scheme mirrors the structure of the first-order method but incorporates a shift determined by 'second_order_shift' to enhance accuracy. + + Args: + self: The instance of the `Second_order_scheme` class. + + Returns: + list: A list representing the numerical scheme, where each element corresponds to a finite difference approximation. + + Why: + This method constructs the core numerical scheme used to discretize the differential equation. The Crank-Nicolson variant, with its second-order shift, aims to improve the accuracy and stability of the numerical solution, which is essential for reliable equation discovery and modeling. """ order = len(self.term) @@ -197,14 +243,20 @@ def scheme_build(self) -> list: return finite_diff def sign_order(self, h: float = 1/2) -> list: - """ Signs definition for second order schemes. - + """ + Generates the coefficients for the second-order finite difference scheme. + + This method computes the numerical coefficients based on the specified + directions (forward or backward) and discretization parameter `h`. These + coefficients are essential for approximating derivatives in the numerical + solution of differential equations. The signs and magnitudes of these + coefficients determine the accuracy and stability of the approximation. + Args: - h (float, optional): discretizing parameter in finite- - difference method (i.e., grid resolution for scheme). Defaults to 1/2. - + h (float, optional): Discretization parameter (grid resolution). Defaults to 1/2. + Returns: - list: list, with signs for corresponding points. + list: A list of coefficients for the finite difference scheme. """ sign_list = [1] @@ -228,13 +280,26 @@ class Finite_diffs(): Class for numerical scheme choosing. """ + def __init__(self, term: list, nvars: int, axes_scheme_type: str): """ - Args: - term (list): differentiation direction. Example: [0,0]->d2u/dx2 if x is first - direction in the grid. - nvars (int): task parameters. Example: if grid(x,t) -> nvars = 2. - axes_scheme_type (str): scheme type: 'central' or combination of 'f' and 'b' + Initializes a finite difference scheme configuration. + + This configuration defines the differentiation order, the number of independent variables, + and the finite difference scheme type (central, forward, or backward) to be used for a specific term + in the discovered differential equation. This setup is crucial for constructing the operators + that approximate derivatives within the evolutionary search process. + + Args: + term (list): Differentiation order for each independent variable. + For example, [0, 0] represents d²u/dx² if x is the first variable in the grid. + nvars (int): Number of independent variables in the problem. + For example, if the grid is defined by grid(x, t), then nvars = 2. + axes_scheme_type (str): Type of finite difference scheme to use along each axis. + Can be 'central' or a combination of 'f' (forward) and 'b' (backward). + + Returns: + None """ self.term = term @@ -242,16 +307,21 @@ def __init__(self, term: list, nvars: int, axes_scheme_type: str): self.axes_scheme_type = axes_scheme_type def scheme_choose(self, scheme_label: str, h:float = 1 / 2) -> list: - """ Method for numerical scheme choosing via realized above. - + """ + Selects a numerical scheme based on the specified order and discretization parameter. + + This method determines which finite difference scheme (first or second order) to use for approximating derivatives, + based on the user's choice. The selected scheme and its associated sign conventions are then returned. + This choice influences the accuracy and stability of the numerical solution. + Args: - scheme_label (str): '2'- for second order scheme (only boundaries points), - '1' - for first order scheme. - h (float, optional): discretizing parameter in finite- - difference method (i.e., grid resolution for scheme). Defaults to 1/2. - + scheme_label (str): '2' for the second-order scheme (used only at boundary points), + '1' for the first-order scheme. + h (float, optional): Discretization parameter (grid resolution). Defaults to 1/2. + Returns: - list: list where list[0] is numerical scheme and list[1] is signs. + list: A list containing the numerical scheme (as a symbolic expression) at index 0 + and a list of sign conventions for each term in the scheme at index 1. """ if self.term == [None]: diff --git a/epde/solver/input_preprocessing.py b/epde/solver/input_preprocessing.py index 2cd9f9f7..2e353fe2 100644 --- a/epde/solver/input_preprocessing.py +++ b/epde/solver/input_preprocessing.py @@ -13,15 +13,15 @@ def lambda_prepare(val: torch.Tensor, lambda_: Union[int, list, torch.Tensor]) -> torch.Tensor: - """ Prepares lambdas for corresponding equation or bcond type. - - Args: - val (_type_): operator tensor or bval tensor - lambda_ (Union[int, list, torch.Tensor]): regularization parameters values - - Returns: - torch.Tensor: torch.Tensor with lambda_ values, - len(lambdas) = number of columns in val + """ + Prepares regularization parameters to match the dimensions of the operator or boundary condition tensor. This ensures that each term in the equation or boundary condition is regularized appropriately. + + Args: + val (torch.Tensor): Operator tensor or boundary value tensor. The shape determines the number of terms to regularize. + lambda_ (Union[int, list, torch.Tensor]): Regularization parameter(s). Can be a single value (applied to all terms), a list of values (one for each term), or a pre-defined tensor. + + Returns: + torch.Tensor: A tensor containing the regularization parameters, reshaped to have a row for each term in the operator/boundary condition. The number of columns matches the number of terms in `val`. """ if isinstance(lambda_, torch.Tensor): @@ -42,16 +42,17 @@ class EquationMixin: Auxiliary class. This one contains some methods that uses in other classes. """ + @staticmethod def equation_unify(equation: dict) -> dict: - """ Adding 'var' to the 'operator' if it's absent or convert to - list 'pow' and 'var' if it's int or float. - - Args: - equation (dict): operator in input form. - - Returns: - dict: equation with unified for solver parameters. + """ + Unifies the equation format by ensuring the presence of 'var' and converting 'pow' and the differential direction to lists when necessary. This ensures that all operators within the equation have a consistent structure suitable for subsequent processing and solving. + + Args: + equation (dict): A dictionary representing the equation, where keys are operator labels and values are dictionaries containing operator parameters like 'pow', differential direction, and potentially 'var'. + + Returns: + dict: The modified equation dictionary with a unified format for all operators, ensuring 'var' is present and 'pow' and differential direction are lists when appropriate. """ for operator_label in equation.keys(): @@ -76,14 +77,20 @@ def equation_unify(equation: dict) -> dict: @staticmethod def closest_point(grid: torch.Tensor, target_point: float) -> int: - """ Defines the closest boundary point to the grid. - + """ + Finds the grid point closest to a given boundary point. + + This is crucial for accurately applying boundary conditions when solving + differential equations numerically. By identifying the nearest grid + point, we can enforce the boundary condition at a location that is + consistent with the domain discretization. + Args: - grid (torch.Tensor): grid (domain discretization). - target_point (float): boundary point. - + grid (torch.Tensor): The computational grid representing the domain. + target_point (float): The location of the boundary point. + Returns: - int: position of the boundary point on the grid. + int: The index of the grid point closest to the target point. """ min_dist = np.inf @@ -99,14 +106,17 @@ def closest_point(grid: torch.Tensor, target_point: float) -> int: @staticmethod def convert_to_double(bnd: Union[list, np.array]) -> float: - """ Converts points to double type. - - Args: - bnd (Union[list, np.array]): array or list of arrays - points that should be converted - - Returns: - float: bnd with double type. + """ + Converts input data (either a list or a NumPy array) to a double-precision floating-point representation, ensuring compatibility with PyTorch operations for equation discovery. + + Args: + bnd (Union[list, np.array]): The input data, which can be a list of arrays or a NumPy array, representing points or values. + + Returns: + Union[list, torch.Tensor]: The data converted to double precision. If the input is a list, the function recursively converts each element of the list. If the input is a NumPy array, it's converted to a PyTorch tensor with double precision. + + Why: + This conversion is crucial for maintaining numerical stability and precision during the equation discovery process, especially when dealing with complex calculations and optimizations within the PyTorch framework. """ if isinstance(bnd, list): @@ -119,14 +129,22 @@ def convert_to_double(bnd: Union[list, np.array]) -> float: @staticmethod def search_pos(grid: torch.Tensor, bnd) -> list: - """ Method for searching position bnd in grid. - + """ + Identifies the indices of points within a grid, or finds the closest grid points if exact matches are not found. + + This function is crucial for mapping boundary conditions or specific points of interest + onto the discrete grid used for equation solving. By finding the grid positions + corresponding to these points, the framework can accurately apply boundary conditions + and evaluate the solution at specific locations. + Args: - grid (torch.Tensor): array of a n-D points. - bnd (_type_): points that should be converted. - + grid (torch.Tensor): A tensor representing the spatial grid where each row is a point in n-dimensional space. + bnd (torch.Tensor or list of torch.Tensor): Points whose positions on the grid need to be determined. + If a list is provided, the function recursively processes each point in the list. + Returns: - list: list of positions bnd on grid. + list: A list of integer indices representing the positions of the input points on the grid. + If an exact match is not found, the index of the closest grid point is returned. """ if isinstance(bnd, list): @@ -145,15 +163,15 @@ def search_pos(grid: torch.Tensor, bnd) -> list: @staticmethod def bndpos(grid: torch.Tensor, bnd: torch.Tensor) -> Union[list, int]: - """ Returns the position of the boundary points on the grid. - - Args: - grid (torch.Tensor): grid for coefficient in form of - torch.Tensor mapping. - bnd (torch.Tensor):boundary conditions. - - Returns: - Union[list, int]: list of positions of the boundary points on the grid. + """ + Returns the indices of grid points that correspond to boundary conditions. This is crucial for enforcing constraints on the solution space when solving differential equations. + + Args: + grid (torch.Tensor): The spatial grid on which the solution is defined. + bnd (torch.Tensor): The boundary values that the solution must satisfy. + + Returns: + Union[list, int]: A list of indices representing the positions of the boundary points on the grid. """ if grid.shape[0] == 1: @@ -167,9 +185,10 @@ def bndpos(grid: torch.Tensor, bnd: torch.Tensor) -> Union[list, int]: class Equation_NN(EquationMixin, Points_type): """ Class for preprocessing input data: grid, operator, bconds in unified - form. Then it will be used for determine solution by 'NN' method. + form. Then it will be used for determine solution by 'NN' method. """ + def __init__(self, grid: torch.Tensor, operator: Union[dict, list], @@ -177,18 +196,19 @@ def __init__(self, h: float = 0.001, inner_order: str = '1', boundary_order: str = '2'): - """ Prepares equation, boundary conditions for *NN* mode. - - Args: - grid (torch.Tensor): tensor of a n-D points. - operator (Union[dict, list]): equation. - bconds (list): boundary conditions. - h (float, optional): discretizing parameter in finite difference - method(i.e., grid resolution for scheme). Defaults to 0.001. - inner_order (str, optional): accuracy inner order for finite difference. - Defaults to '1'. - boundary_order (str, optional):accuracy boundary order for finite difference. - Defaults to '2'. + """ + Prepares the problem setup for solving differential equations using neural networks. This involves defining the equation itself, specifying boundary conditions, and setting parameters for the numerical scheme. + + Args: + grid (torch.Tensor): Tensor representing the spatial or temporal domain where the solution is sought. + operator (Union[dict, list]): Definition of the differential equation to be solved. + bconds (list): List of boundary conditions that constrain the solution. + h (float, optional): Discretization parameter (grid resolution) for finite difference approximations. Defaults to 0.001. + inner_order (str, optional): Accuracy order for finite difference scheme within the domain. Defaults to '1'. + boundary_order (str, optional): Accuracy order for finite difference scheme at the boundaries. Defaults to '2'. + + Returns: + None """ super().__init__(grid) @@ -203,17 +223,22 @@ def _operator_to_type_op(self, dif_direction: list, nvars: int, axes_scheme_type: str) -> list: - """ Function serves applying different schemes to a different point types - for entire differentiation direction. - - Args: - dif_direction (list): differentiation direction, (example:d2/dx2->[[0,0]]) - nvars (int): dimensionality of the problem. - axes_scheme_type (str): 'central' or combination of 'f' and 'b'. - - Returns: - list: list, where the conventional operator changed to - steps and signs (see scheme_build function description). + """ + Converts a symbolic differentiation operator into a list of finite difference approximations, + tailored to the specified point types and differentiation direction. + + This conversion is essential for numerically evaluating the equation on a grid, + where derivatives are approximated using finite differences. + + Args: + dif_direction (list): Differentiation direction, represented as a list of lists (e.g., `[[0, 0]]` for d2/dx2). + nvars (int): Dimensionality of the problem (number of independent variables). + axes_scheme_type (str): Type of finite difference scheme to use ('central' or a combination of 'f' and 'b' for forward and backward). + + Returns: + list: A list containing two lists: + - The first list contains the finite difference schemes corresponding to each term in the differentiation direction. + - The second list contains the orders of accuracy for each scheme. """ if axes_scheme_type == 'central': @@ -234,17 +259,17 @@ def _operator_to_type_op(self, def _finite_diff_scheme_to_grid_list(self, finite_diff_scheme: list, grid_points: torch.Tensor) -> list: - """ Method that converts integer finite difference steps in term described - in Finite_diffs class to a grids with shifted points, i.e. - from field (x,y) -> (x,y+h). - - Args: - finite_diff_scheme (list): operator_to_type_op one term. - grid_points (torch.Tensor): grid points that will be shifted - corresponding to finite diff scheme. - - Returns: - list: list, where the steps and signs changed to grid and signs. + """ + Converts a finite difference scheme, represented as integer steps, into a list of grid points shifted according to the scheme. + + This transformation is crucial for evaluating differential operators numerically on a grid. By shifting the grid points according to the finite difference scheme, the method effectively approximates derivatives at each point. + + Args: + finite_diff_scheme (list): A list representing a single term in the finite difference scheme. Each element in the list corresponds to the shift along a particular axis. If element is None - that means that no shift is required + grid_points (torch.Tensor): The original grid points that will be shifted based on the finite difference scheme. + + Returns: + list: A list of tensors, where each tensor represents the grid points shifted according to a specific term in the finite difference scheme. """ s_grid_list = [] @@ -261,18 +286,18 @@ def _finite_diff_scheme_to_grid_list(self, def _checking_coeff(self, coeff: Union[int, float, torch.Tensor, callable], grid_points: torch.Tensor) -> torch.Tensor: - """ Checks the coefficient type - - Args: - coeff (Union[int, float, torch.Tensor, callable]): coefficient - in equation operator. - grid_points (torch.Tensor): if coeff is callable or torch.Tensor - - Raises: - NameError: coeff" should be: torch.Tensor or callable or int or float! - - Returns: - torch.Tensor: coefficient + """ + Checks the type of a coefficient used in defining the equation, ensuring it's compatible with the computational graph and data structures. This preprocessing step is crucial for correct equation formation and subsequent analysis. + + Args: + coeff (Union[int, float, torch.Tensor, callable]): The coefficient to be checked. It can be a constant (int or float), a tensor, or a callable function. + grid_points (torch.Tensor): The grid points where the coefficient is evaluated if it's a callable or a tensor. + + Raises: + NameError: If the coefficient is not of the allowed types (int, float, torch.Tensor, or callable). + + Returns: + torch.Tensor: The processed coefficient, ensuring it's in a suitable format (usually a tensor) for further computations within the equation. """ if isinstance(coeff, (int, float)): @@ -290,19 +315,15 @@ def _checking_coeff(self, return coeff1 def _type_op_to_grid_shift_op(self, fin_diff_op: list, grid_points) -> list: - """ Converts operator to a grid_shift form. Includes term coefficient - conversion. - Coeff may be integer, function or array, last two are mapped to a - subgrid that corresponds point type. - - Args: - fin_diff_op (list): operator_to_type_op result. - grid_points (_type_): grid points that will be shifted - corresponding to finite diff scheme. - - Returns: - list: final form of differential operator used in the algorithm for - single grid type. + """ + Converts a finite difference operator for a specific grid type into a grid-shifted operator form, preparing it for numerical evaluation. This involves mapping coefficients (which can be integers, functions, or arrays) to the appropriate subgrid locations based on the point type. This conversion is crucial for applying the finite difference scheme across the grid. + + Args: + fin_diff_op (list): The finite difference operator for a specific grid type, as a result of `operator_to_type_op`. + grid_points: The grid points associated with the finite difference scheme. + + Returns: + list: The grid-shifted form of the differential operator, ready for use in the numerical algorithm for the given grid type. """ shift_grid_op = [] @@ -315,16 +336,16 @@ def _one_operator_prepare(self, operator: dict, grid_points: torch.Tensor, points_type: str) -> dict: - """ Method for operator preparing, there is construct all predefined - methods. - - Args: - operator (dict): operator in input form. - grid_points (torch.Tensor): see type_op_to_grid_shift_op method. - points_type (str): points type of grid_points. - - Returns: - dict: prepared operator + """ + Prepares a single operator term for equation evaluation by converting symbolic representations into numerical operations on the grid. This involves unifying equation formats, checking and converting coefficients, and transforming differential operators into grid-compatible shift operations. + + Args: + operator (dict): A dictionary representing the operator term, containing coefficients and differential operator definitions. + grid_points (torch.Tensor): The coordinates of the grid points where the equation is evaluated. + points_type (str): The type of grid points (e.g., 'uniform', 'chebyshev'). + + Returns: + dict: The prepared operator term with coefficients and differential operators converted into a format suitable for grid-based computation. The operator is prepared to be evaluated on the grid by converting symbolic representations into numerical operations. """ nvars = self.grid.shape[-1] @@ -340,12 +361,19 @@ def _one_operator_prepare(self, return operator def operator_prepare(self) -> list: - """ Method for all operators preparing. If system case is, it will call - 'one_operator_prepare' method for number of equations times. - - Returns: - list: list of dictionaries, where every dictionary is the result of - 'one_operator_prepare' + """ + Prepares the operator(s) for equation discovery by associating them with grid points. + + This method ensures that the operators are correctly formatted and linked to the spatial grid, + which is a crucial step for evaluating the equation's fitness against the data. + If multiple equations are present, it prepares each operator individually. + + Args: + None + + Returns: + list: A list of dictionaries, where each dictionary represents a prepared operator + associated with the central grid points. """ grid_points = self.grid_sort()['central'] @@ -364,15 +392,17 @@ def operator_prepare(self) -> list: return prepared_operator def _apply_bnd_operators(self, bnd_operator: dict, bnd_dict: dict) -> list: - """ Method for applying boundary operator for all points type in bnd_dict. - - Args: - bnd_operator (dict): boundary operator in input form. - bnd_dict (dict): dictionary (keys is points type, values is boundary points). - - Returns: - list: final form of differential operator used in the algorithm for - subset grid types. + """ + Applies the boundary operator to each specified type of boundary point. + + This function iterates through different types of boundary points and prepares the corresponding differential operator for each type. This is a crucial step in constructing the complete equation system, ensuring that boundary conditions are correctly incorporated into the discovered equations. + + Args: + bnd_operator (dict): A dictionary containing the boundary operator in its initial form. + bnd_dict (dict): A dictionary where keys represent the type of boundary points and values are the corresponding boundary points themselves. + + Returns: + list: A list of prepared differential operators, one for each type of boundary point, ready for use in the equation discovery process. """ operator_list = [] @@ -383,10 +413,14 @@ def _apply_bnd_operators(self, bnd_operator: dict, bnd_dict: dict) -> list: return operator_list def bnd_prepare(self) -> list: - """ Method for boundary conditions preparing to final form. - - Returns: - list: list of dictionaries where every dict is one boundary condition + """ + Prepares boundary conditions for use in the equation discovery process. It sorts and applies boundary operators to each condition based on the grid and boundary type. + + Args: + self: The Equation_NN object containing the grid, boundary conditions, and operators. + + Returns: + list: A list of dictionaries, where each dictionary represents a boundary condition with sorted grid locations and applied boundary operators. The boundary operators are applied to enforce specific constraints or conditions at the boundaries of the domain, ensuring that the discovered equations are physically meaningful and consistent with the observed data. """ grid_dict = self.grid_sort() @@ -408,17 +442,23 @@ class Equation_autograd(EquationMixin): Prepares equation for autograd method (i.e., from conventional form to input form). """ + def __init__(self, grid: torch.Tensor, operator: Union[dict, list], bconds: list): - """ Prepares equation for autograd method - (i.e., from conventional form to input form). - - Args: - grid (torch.Tensor): tensor of a n-D points. - operator (Union[dict, list]): equation. - bconds (list): boundary conditions in input form. + """ + Prepares the problem for solving by storing the grid, equation, and boundary conditions. + + This setup is crucial for the subsequent application of the evolutionary algorithm to find the optimal equation representation. + + Args: + grid (torch.Tensor): Tensor of a n-D points where the solution is evaluated. + operator (Union[dict, list]): Equation to be solved, represented in a suitable format. + bconds (list): Boundary conditions that constrain the solution space. + + Returns: + None """ self.grid = grid @@ -427,16 +467,23 @@ def __init__(self, def _checking_coeff(self, coeff: Union[int, float, torch.Tensor]) -> Union[int, float, torch.Tensor]: - """ Checks the coefficient type - + """ + Validates and prepares the coefficient for use in equation operations. + + This method ensures that the provided coefficient is of a supported type + (int, float, torch.Tensor, or callable) and converts it into a suitable + format for subsequent calculations within the equation. This ensures + compatibility with the framework's computational graph. + Args: - coeff (Union[int, float, torch.Tensor]): coefficient in equation operator. - + coeff (Union[int, float, torch.Tensor]): The coefficient to be checked and prepared. + Raises: - NameError: "coeff" should be: torch.Tensor or callable or int or float! - + NameError: If the provided "coeff" is not one of the supported types + (torch.Tensor, callable, int, or float). + Returns: - Union[int, float, torch.Tensor]: coefficient + Union[int, float, torch.Tensor]: The validated and potentially reshaped coefficient. """ if isinstance(coeff, (int, float)): @@ -453,14 +500,17 @@ def _checking_coeff(self, return coeff1 def _one_operator_prepare(self, operator: dict) -> dict: - """ Method for all operators preparing. If system case is, it will call - 'one_operator_prepare' method for number of equations times. - + """ + Prepares a single operator by unifying its format and checking the coefficients of its terms. + + This ensures that the operator is in a consistent format and that the coefficients + are valid before further processing, which is crucial for the equation discovery process. + Args: - operator (dict): operator in input form. - + operator (dict): The operator in its initial input form. + Returns: - dict: dict, where coeff is checked. + dict: The processed operator with unified format and checked coefficients. """ operator = self.equation_unify(operator) @@ -470,12 +520,14 @@ def _one_operator_prepare(self, operator: dict) -> dict: return operator def operator_prepare(self) -> list: - """ Method for all operators preparing. If system case is, it will call - 'one_operator_prepare' method for number of equations times. - - Returns: - list: list of dictionaries, where every dictionary is the result of - 'one_operator_prepare' + """ + Prepares the operators for symbolic differentiation. It handles both single equations and systems of equations by unifying and preparing each operator. + + Args: + self: The instance of the Equation_autograd class. + + Returns: + list: A list of dictionaries, where each dictionary represents a prepared operator ready for symbolic differentiation. The length of the list corresponds to the number of equations in the system, or 1 if it's a single equation. """ if isinstance(self.operator, list) and isinstance(self.operator[0], dict): @@ -491,10 +543,24 @@ def operator_prepare(self) -> list: return prepared_operator def bnd_prepare(self) -> list: - """ Method for boundary conditions preparing to final form - + """ + Prepares boundary conditions for use in the equation discovery process. + + This method ensures that the boundary conditions are in the correct format + (a list of dictionaries) for subsequent calculations within the EPDE framework. + If no boundary conditions are provided, it returns None. + + Args: + self: The Equation_autograd object. + Returns: - list: list of dictionaries where every dict is one boundary condition + list: A list of dictionaries, where each dictionary represents a boundary condition. + Returns None if no boundary conditions are specified. + + Why: + This preparation step is crucial for ensuring compatibility between the user-defined + boundary conditions and the equation discovery algorithms, allowing the framework + to correctly incorporate these constraints into the equation search and validation process. """ if self.bconds is None: @@ -506,20 +572,23 @@ def bnd_prepare(self) -> list: class Equation_mat(EquationMixin): """ Class realizes input data preprocessing (operator and boundary conditions - preparing) for 'mat' method. + preparing) for 'mat' method. """ + def __init__(self, grid: torch.Tensor, operator: Union[list, dict], bconds: list): - """ Prepares equation for autograd method - (i.e., from conventional form to input form). - - Args: - grid (torch.Tensor): grid, result of meshgrid. - operator (Union[list, dict]): operator in input form. - bconds (list): boundary conditions in input form. + """ + Prepares the problem setup by storing the computational grid, differential operator, and boundary conditions. + + This initialization is crucial for subsequent calculations, ensuring that all necessary components of the problem are readily accessible. + + Args: + grid (torch.Tensor): The computational grid where the solution is approximated. + operator (Union[list, dict]): Definition of the differential operator. + bconds (list): Boundary conditions applied to constrain the solution. """ self.grid = grid @@ -527,10 +596,19 @@ def __init__(self, self.bconds = bconds def operator_prepare(self) -> list: - """ Method realizes operator preparing for 'mat' method - using only 'equation_unify' method. + """ + Transforms the input operator into a standardized format suitable for subsequent matrix construction. + + This method ensures that the operator, whether a single equation or a list of equations, + is converted into a unified representation, facilitating the creation of the matrix + required for the equation discovery process. + + Args: + self: Instance of the Equation_mat class, containing the operator to be prepared. + Returns: - list: final form of differential operator used in the algorithm. + list: A list of unified equations, ready for matrix construction. Each element + in the list represents a single equation in a standardized dictionary format. """ if isinstance(self.operator, list) and isinstance(self.operator[0], dict): @@ -546,13 +624,22 @@ def operator_prepare(self) -> list: return prepared_operator def _point_position(self, bnd: torch.Tensor) -> list: - """ Define position of boundary points on the grid. - + """ + Locates the grid indices corresponding to boundary points. + + This method is crucial for mapping boundary conditions onto the computational grid, + ensuring accurate problem representation. By identifying the precise grid locations + of the boundary points, the method facilitates the application of boundary conditions + during the equation solving process. + Args: - bnd (torch.Tensor): boundary subgrid. - + bnd (torch.Tensor): A tensor representing the boundary subgrid. Each element in the tensor + corresponds to a point on the boundary. + Returns: - list: list of positions, where boundary points intersects on the grid. + list: A list of tuples, where each tuple contains the indices of the grid point + closest to the corresponding boundary point. The indices represent the + position of the boundary point within the computational grid. """ bpos = [] @@ -570,10 +657,22 @@ def _point_position(self, bnd: torch.Tensor) -> list: return bpos def bnd_prepare(self) -> list: - """ Method for boundary conditions preparing to final form. - - Returns: - list: list of dictionaries where every dict is one boundary condition. + """ + Prepares boundary conditions for equation discovery by unifying and positioning them. + + This method processes the boundary conditions, converting boundary definitions + into numerical positions within the domain and unifying the boundary operators + to ensure compatibility with the equation discovery process. This ensures + that boundary conditions are in a standardized format suitable for use + in subsequent equation solving and analysis. + + Args: + self: The Equation_mat object. + + Returns: + list: A list of dictionaries, where each dictionary represents a boundary + condition with updated 'bnd' (boundary position) and 'bop' + (boundary operator) fields. """ for bcond in self.bconds: @@ -593,6 +692,7 @@ class Operator_bcond_preproc(): """ Interface for preparing equations due to chosen calculation method. """ + def __init__(self, grid: torch.Tensor, operator: Union[dict, list], @@ -600,16 +700,22 @@ def __init__(self, h: float = 0.001, inner_order: str ='1', boundary_order: str ='2'): - """_summary_ - - Args: - grid (torch.Tensor): grid from cartesian_prod or meshgrid result. - operator (Union[dict, list]): equation. - bconds (list): boundary conditions. - h (float, optional): discretizing parameter in finite- - difference method (i.e., grid resolution for scheme). Defaults to 0.001. - inner_order (str, optional): accuracy inner order for finite difference. Defaults to '1'. - boundary_order (str, optional): accuracy boundary order for finite difference. Defaults to '2'. + """ + Initializes the Operator_bcond_preproc class, preparing the problem setup for numerical solution. + + This involves storing the grid, equation, and boundary conditions, + along with discretization parameters, to facilitate the finite difference approximation. + + Args: + grid (torch.Tensor): Grid representing the domain where the equation is solved. Typically obtained from cartesian_prod or meshgrid. + operator (Union[dict, list]): Definition of the differential equation to be solved. + bconds (list): List of boundary conditions that constrain the solution. + h (float, optional): Discretization parameter (grid resolution) for the finite difference method. Defaults to 0.001. + inner_order (str, optional): Accuracy order for the finite difference scheme in the interior of the domain. Defaults to '1'. + boundary_order (str, optional): Accuracy order for the finite difference scheme at the boundaries. Defaults to '2'. + + Returns: + None """ self.grid = check_device(grid) @@ -620,11 +726,16 @@ def __init__(self, self.boundary_order = boundary_order def set_strategy(self, strategy: str) -> Union[Equation_NN, Equation_mat, Equation_autograd]: - """ Setting the calculation method. - + """ + Specifies the numerical approach for solving the problem. + + This choice influences how the equation is represented and solved, + affecting both accuracy and computational cost. Different strategies + are suitable for different problem types and computational resources. + Args: strategy (str): Calculation method. (i.e., "NN", "autograd", "mat"). - + Returns: Union[Equation_NN, Equation_mat, Equation_autograd]: A given calculation method. """ diff --git a/epde/solver/losses.py b/epde/solver/losses.py index 4c617489..c7873ca9 100644 --- a/epde/solver/losses.py +++ b/epde/solver/losses.py @@ -11,6 +11,7 @@ class Losses(): """ Class which contains all losses. """ + def __init__(self, mode: str, weak_form: Union[None, list], @@ -18,12 +19,19 @@ def __init__(self, tol: Union[int, float], n_t_operation: callable = None): """ - Args: - mode (str): calculation mode, *NN, autograd, mat*. - weak_form (Union[None, list]): list of basis functions if form is weak. - n_t (int): number of unique points in time dimension. - tol (Union[int, float])): penalty in *casual loss*. - n_t_operation (callable): function to calculate n_t for each batch + Initializes the Losses class with parameters defining the loss calculation. + + This setup configures how the loss will be computed based on the chosen mode, + basis functions (if applicable), temporal discretization, and tolerance levels. + These parameters are essential for tailoring the loss function to the specific + characteristics of the differential equation being discovered and the data used for training. + + Args: + mode (str): Calculation mode (*NN*, *autograd*, or *mat*), determining the method for loss computation. + weak_form (Union[None, list]): List of basis functions if using a weak formulation of the loss. + n_t (int): Number of unique points in the time dimension, influencing the temporal discretization. + tol (Union[int, float]): Tolerance value used as a penalty in the causal loss calculation. + n_t_operation (callable): Function to calculate `n_t` for each batch, allowing dynamic adjustment of temporal discretization. """ self.mode = mode @@ -37,17 +45,26 @@ def __init__(self, def _loss_op(self, operator: torch.Tensor, lambda_op: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """ Operator term in loss calc-n. - + """ + Computes the operator loss term, penalizing deviations from the governing equation. + + This loss encourages the discovered equation to accurately represent the + relationships present in the data. It measures how well the learned + operator satisfies the equation across the domain. + Args: - operator (torch.Tensor): operator calc-n result. - For more details to eval module -> operator_compute(). - - lambda_op (torch.Tensor): regularization parameter for operator term in loss. - + operator (torch.Tensor): The result of applying the discovered operator. + This represents the equation's residual at each point. + See `eval` module -> `operator_compute()` for details. + lambda_op (torch.Tensor): Regularization parameter controlling the + strength of the operator loss. This balances the trade-off + between equation fit and complexity. + Returns: - loss_operator (torch.Tensor): operator term in loss. - op (torch.Tensor): MSE of operator on the whole grid. + Tuple[torch.Tensor, torch.Tensor]: + - loss_operator (torch.Tensor): The operator loss term, a scalar value. + - op (torch.Tensor): The mean squared error of the operator on the grid, + providing a measure of equation satisfaction. """ if self.weak_form is not None and self.weak_form != []: op = operator @@ -62,16 +79,17 @@ def _loss_bcs(self, bval: torch.Tensor, true_bval: torch.Tensor, lambda_bound: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """ Computes boundary loss for corresponding type. - + """ + Computes the loss associated with boundary conditions, penalizing deviations from the true boundary values. This ensures that the discovered equation accurately reflects the system's behavior at its boundaries. + Args: - bval (torch.Tensor): calculated values of boundary conditions. - true_bval (torch.Tensor): true values of boundary conditions. - lambda_bound (torch.Tensor): regularization parameter for boundary term in loss. - + bval (torch.Tensor): Calculated values of boundary conditions. + true_bval (torch.Tensor): True values of boundary conditions. + lambda_bound (torch.Tensor): Regularization parameter for the boundary term in the loss. + Returns: - loss_bnd (torch.Tensor): boundary term in loss. - bval_diff (torch.Tensor): MSE of all boundary con-s. + loss_bnd (torch.Tensor): Boundary term in the loss. + bval_diff (torch.Tensor): Mean squared error of all boundary conditions. """ bval_diff = torch.mean((bval - true_bval)**2, 0) @@ -87,20 +105,20 @@ def _default_loss(self, lambda_op: torch.Tensor, lambda_bound: torch.Tensor, save_graph: bool = True) -> Tuple[torch.Tensor, torch.Tensor]: - """ Compute l2 loss. - - Args: - operator (torch.Tensor): operator calc-n result. - For more details to eval module -> operator_compute(). - bval (torch.Tensor): calculated values of boundary conditions. - true_bval (torch.Tensor): true values of boundary conditions. - lambda_op (torch.Tensor): regularization parameter for operator term in loss. - lambda_bound (torch.Tensor): regularization parameter for boundary term in loss. - save_graph (bool, optional): saving computational graph. Defaults to True. - - Returns: - loss (torch.Tensor): loss. - loss_normalized (torch.Tensor): loss, where regularization parameters are 1. + """ + Compute the overall loss based on the discrepancy between the predicted and true values, considering both the governing equation and boundary conditions. This loss function guides the optimization process to find the equation that best fits the data. + + Args: + operator (torch.Tensor): The result of applying the discovered differential operator to the input data. + bval (torch.Tensor): The predicted values at the boundaries of the domain. + true_bval (torch.Tensor): The true (observed) values at the boundaries of the domain. + lambda_op (torch.Tensor): A weighting factor to balance the importance of the operator loss. + lambda_bound (torch.Tensor): A weighting factor to balance the importance of the boundary condition loss. + save_graph (bool, optional): Whether to save the computational graph for backpropagation. Defaults to True. + + Returns: + loss (torch.Tensor): The total loss, combining the operator and boundary condition losses. + loss_normalized (torch.Tensor): The total loss with regularization parameters set to 1, providing a baseline for comparison. """ if bval is None: @@ -133,22 +151,25 @@ def _causal_loss(self, true_bval: torch.Tensor, lambda_op: torch.Tensor, lambda_bound: torch.Tensor)-> Tuple[torch.Tensor, torch.Tensor]: - """ Computes causal loss, which is calculated with weights matrix: - W = exp(-tol*(Loss_i)) where Loss_i is sum of the L2 loss from 0 - to t_i moment of time. This loss function should be used when one - of the DE independent parameter is time. - - Args: - operator (torch.Tensor): operator calc-n result. - For more details to eval module -> operator_compute(). - bval (torch.Tensor): calculated values of boundary conditions. - true_bval (torch.Tensor): true values of boundary conditions. - lambda_op (torch.Tensor): regularization parameter for operator term in loss. - lambda_bound (torch.Tensor): regularization parameter for boundary term in loss. - - Returns: - loss (torch.Tensor): loss. - loss_normalized (torch.Tensor): loss, where regularization parameters are 1. + """ + Computes a weighted loss, emphasizing earlier time points in the data. + + This loss function is designed to address the challenges of time-dependent + differential equations, where the influence of initial states on the solution + decreases over time. By weighting the loss at each time step, the method + prioritizes accurate modeling of the initial dynamics. + + Args: + operator (torch.Tensor): The result of applying the differential operator. + See `eval module -> operator_compute()` for details. + bval (torch.Tensor): Calculated values of boundary conditions. + true_bval (torch.Tensor): True values of boundary conditions. + lambda_op (torch.Tensor): Regularization parameter for the operator term in the loss. + lambda_bound (torch.Tensor): Regularization parameter for the boundary term in the loss. + + Returns: + loss (torch.Tensor): The total loss, combining operator and boundary losses. + loss_normalized (torch.Tensor): The total loss with regularization parameters set to 1. """ if self.n_t_operation is not None: # calculate if batch mod self.n_t = self.n_t_operation(operator) @@ -180,19 +201,21 @@ def _weak_loss(self, true_bval: torch.Tensor, lambda_op: torch.Tensor, lambda_bound: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: - """ Weak solution of O/PDE problem. - - Args: - operator (torch.Tensor): operator calc-n result. - For more details to eval module -> operator_compute(). - bval (torch.Tensor): calculated values of boundary conditions. - true_bval (torch.Tensor): true values of boundary conditions. - lambda_op (torch.Tensor): regularization parameter for operator term in loss. - lambda_bound (torch.Tensor): regularization parameter for boundary term in loss. - - Returns: - loss (torch.Tensor): loss. - loss_normalized (torch.Tensor): loss, where regularization parameters are 1. + """ + Computes the loss function for a weak solution of a differential equation, considering both the equation itself and boundary conditions. + + Args: + operator (torch.Tensor): The result of applying the differential operator to the predicted solution. See `eval` module -> `operator_compute()` for details. + bval (torch.Tensor): The predicted values at the boundaries of the domain. + true_bval (torch.Tensor): The true (target) values at the boundaries of the domain. + lambda_op (torch.Tensor): Regularization parameter to weight the importance of satisfying the differential equation. + lambda_bound (torch.Tensor): Regularization parameter to weight the importance of satisfying the boundary conditions. + + Returns: + loss (torch.Tensor): The total loss, combining the equation and boundary condition losses. + loss_normalized (torch.Tensor): The total loss with regularization parameters set to 1, providing a baseline loss value. + + Why: This function calculates the loss, guiding the optimization process to find a solution that minimizes the error in both satisfying the differential equation and adhering to the specified boundary conditions. The regularization parameters allow weighting the relative importance of these two aspects. """ if bval is None: @@ -219,19 +242,28 @@ def compute(self, lambda_op: torch.Tensor, lambda_bound: torch.Tensor, save_graph: bool = True) -> Union[_default_loss, _weak_loss, _causal_loss]: - """ Setting the required loss calculation method. - + """ + Selects and applies the appropriate loss calculation method based on the specified mode and tolerances. + + This method acts as a dispatcher, choosing between different loss calculation strategies + depending on whether a weak form is specified or a tolerance level is set. This allows the framework + to adapt the loss calculation to different problem settings and solution requirements. + Args: - operator (torch.Tensor): operator calc-n result. - For more details to eval module -> operator_compute(). - bval (torch.Tensor): calculated values of boundary conditions. - true_bval (torch.Tensor): true values of boundary conditions. - lambda_op (torch.Tensor): regularization parameter for operator term in loss. - lambda_bound (torch.Tensor): regularization parameter for boundary term in loss. - save_graph (bool, optional): saving computational graph. Defaults to True. - + operator (torch.Tensor): The result of the operator calculation. See `eval module -> operator_compute()` for details. + bval (torch.Tensor): Calculated values of boundary conditions. + true_bval (torch.Tensor): True values of boundary conditions. + lambda_op (torch.Tensor): Regularization parameter for the operator term in the loss. + lambda_bound (torch.Tensor): Regularization parameter for the boundary term in the loss. + save_graph (bool, optional): Whether to save the computational graph. Defaults to True. + Returns: - Union[default_loss, weak_loss, causal_loss]: A given calculation method. + Union[_default_loss, _weak_loss, _causal_loss]: The calculated loss based on the chosen method. + + Why: + This method centralizes the selection of the appropriate loss calculation strategy, + allowing the framework to handle different problem formulations (e.g., with or without + weak forms, with different tolerance requirements) in a modular and adaptable way. """ if self.mode in ('mat', 'autograd'): diff --git a/epde/solver/model.py b/epde/solver/model.py index 18deff94..0c75a98c 100644 --- a/epde/solver/model.py +++ b/epde/solver/model.py @@ -15,7 +15,10 @@ class Model(): - """class for preprocessing""" + """ + class for preprocessing + """ + def __init__( self, net: Union[torch.nn.Module, torch.Tensor], @@ -24,12 +27,16 @@ def __init__( conditions: Conditions, batch_size: int = None): """ - Args: - net (Union[torch.nn.Module, torch.Tensor]): neural network or torch.Tensor for mode *mat* - grid (Domain): object of class Domain - equation (Equation): object of class Equation - conditions (Conditions): object of class Conditions - batch_size (int): size of batch + Initializes the Model class, setting up the neural network, domain, equation, and boundary conditions. + + This setup is crucial for defining the problem to be solved, ensuring that the neural network is properly configured to learn the solution within the specified domain and satisfying the given equation and conditions. + + Args: + net (Union[torch.nn.Module, torch.Tensor]): The neural network to be trained, or a torch.Tensor for matrix-based approaches. + domain (Domain): The spatial or temporal domain over which the equation is defined. + equation (Equation): The differential equation to be solved. + conditions (Conditions): The boundary or initial conditions that constrain the solution. + batch_size (int, optional): The size of the batches used during training. Defaults to None. """ self.net = net self.domain = domain @@ -58,24 +65,30 @@ def compile( derivative_points: int = 2, weak_form: List[callable] = None, tol: float = 0): - """ Compile model for training process. - - Args: - mode (str): *mat, NN, autograd* - lambda_operator (Union[List[float], float]): weight for operator term. - It can be float for single equation or list of float for system. - lambda_bound (Union[List[float], float]): weight for boundary term. - It can be float for all types of boundary cond-ns or list of float for every condition type. - normalized_loss_stop (bool, optional): loss with lambdas=1. Defaults to False. - h (float, optional): increment for finite-difference scheme only for *NN*. Defaults to 0.001. - inner_order (str, optional): order of finite-difference scheme *'1', '2'* for inner points. - Only for *NN*. Defaults to '1'. - boundary_order (str, optional): order of finite-difference scheme *'1', '2'* for boundary points. - Only for *NN*. Defaults to '2'. - derivative_points (int, optional): number of points for finite-difference scheme in *mat* mode. - if derivative_points=2 the central scheme are used. Defaults to 2. - weak_form (List[callable], optional): basis function for weak loss. Defaults to None. - tol (float, optional): tolerance for causual loss. Defaults to 0. + """ + Compiles the model by setting up the computational domain, equation, and boundary conditions based on the specified mode. + + This process prepares the model for the training loop by constructing the necessary components for loss calculation and optimization. + The compilation configures the model to solve the problem using either matrix-based methods, neural networks, or automatic differentiation, + depending on the chosen mode. + + Args: + mode (str): Specifies the computational approach (*mat*, *NN*, or *autograd*). + lambda_operator (Union[List[float], float]): Weight(s) for the operator term in the loss function. + Can be a single float for a single equation or a list of floats for a system of equations. + lambda_bound (Union[List[float], float]): Weight(s) for the boundary term in the loss function. + Can be a single float for uniform weighting or a list of floats for individual boundary conditions. + normalized_loss_stop (bool, optional): If True, the loss is normalized with lambdas set to 1. Defaults to False. + h (float, optional): Increment for finite-difference schemes (only for *NN* mode). Defaults to 0.001. + inner_order (str, optional): Order of the finite-difference scheme for inner points (*'1'* or *'2'*, only for *NN* mode). Defaults to '1'. + boundary_order (str, optional): Order of the finite-difference scheme for boundary points (*'1'* or *'2'*, only for *NN* mode). Defaults to '2'. + derivative_points (int, optional): Number of points for finite-difference derivative calculation (*mat* mode). + If set to 2, a central difference scheme is used. Defaults to 2. + weak_form (List[callable], optional): Basis functions for the weak formulation of the loss. Defaults to None. + tol (float, optional): Tolerance for the casual loss term. Defaults to 0. + + Returns: + None """ self.mode = mode self.lambda_bound = lambda_bound @@ -108,11 +121,19 @@ def _model_save( self, save_model: bool, model_name: str): - """ Model saving. - + """ + Saves the trained model to disk. + + This function persists the model, allowing for later reuse without retraining. + The saving format depends on the operational mode (MAT or NN), using either a + MAT-file format or a neural network-specific format. + Args: - save_model (bool): save model or not. - model_name (str): model name. + save_model (bool): A flag indicating whether to save the model. + model_name (str): The name to use for the saved model file. + + Returns: + None. The function saves the model to a file in the specified directory. """ if save_model: if self.mode == 'mat': @@ -131,16 +152,22 @@ def train(self, save_model: bool = False, model_name: Union[str, None] = None, callbacks: Union[List, None] = None): - """ train model. - - Args: - optimizer (Optimizer): the object of Optimizer class - epochs (int): number of epoch for training. - info_string_every (Union[int, None], optional): print loss state after *info_string_every* epoch. Defaults to None. - mixed_precision (bool, optional): apply mixed precision for calculation. Defaults to False. - save_model (bool, optional): save resulting model in cache. Defaults to False. - model_name (Union[str, None], optional): model name. Defaults to None. - callbacks (Union[List, None], optional): callbacks for training process. Defaults to None. + """ + Trains the model to discover the underlying differential equation. + + The training process involves optimizing the model's parameters using the provided optimizer and training data over a specified number of epochs. Callbacks are used to monitor and control the training process, allowing for customization and early stopping. The goal is to minimize the loss function, which represents the difference between the model's predictions and the observed data. This optimization helps to refine the model's structure and parameters, ultimately leading to a more accurate representation of the underlying differential equation. + + Args: + optimizer (Optimizer): The optimizer object used for updating the model's parameters during training. + epochs (int): The number of training epochs to perform. + info_string_every (Union[int, None], optional): Prints the loss value every *info_string_every* epochs. Defaults to None. + mixed_precision (bool, optional): Enables mixed precision training for faster computation and reduced memory usage. Defaults to False. + save_model (bool, optional): Saves the trained model to the cache. Defaults to False. + model_name (Union[str, None], optional): The name to use when saving the model. Defaults to None. + callbacks (Union[List, None], optional): A list of callbacks to execute during training, providing hooks for monitoring and control. Defaults to None. + + Returns: + torch.Tensor: The final loss value achieved during training. """ self.t = 1 diff --git a/epde/solver/models.py b/epde/solver/models.py index 6c0ce0d0..0dd8e8a0 100644 --- a/epde/solver/models.py +++ b/epde/solver/models.py @@ -8,20 +8,34 @@ class Fourier_embedding(nn.Module): """ - Class for Fourier features generation. - - Examples: - u(t,x) if user wants to create 5 Fourier features in 'x' direction with L=5: - L=[None, 5], M=[None, 5]. + Transforms input data into a Fourier feature representation. + + + Examples: + u(t,x) if user wants to create 5 Fourier features in 'x' direction with L=5: + L=[None, 5], M=[None, 5]. """ + def __init__(self, L=[1], M=[1], ones=False, device='cpu'): """ - Args: - L (list, optional): (sin(w*x), cos(w*X)) frequencie parameter, - w = 2*pi/L. Defaults to [1]. - M (list, optional): number of (sin, cos) pairs in result embedding. Defaults to [1]. - ones (bool, optional): enter or not ones vector in result embedding. Defaults to False. + Initializes the Fourier embedding layer. + + This layer transforms input features into a higher-dimensional space using Fourier basis functions. + The transformation enriches the representation of the input, allowing the model to capture complex relationships + and periodic patterns in the data. The frequencies and number of components used in the Fourier embedding + are configurable, providing flexibility in adapting to different data characteristics. + + Args: + L (list, optional): Characteristic length scales for the Fourier basis functions. + Determines the frequencies (w = 2*pi/L) used in the sine and cosine components. Defaults to [1]. + M (list, optional): Number of (sin, cos) pairs to use for each length scale specified in `L`. + If an element is None, it will not be used. Defaults to [1]. + ones (bool, optional): Whether to include a vector of ones in the output embedding. Defaults to False. + device (str, optional): Device to store tensors. Defaults to 'cpu'. + + Returns: + None """ self._device = device @@ -41,13 +55,27 @@ def __init__(self, L=[1], M=[1], ones=False, device='cpu'): self.out_features += 1 def forward(self, grid: torch.Tensor) -> torch.Tensor: - """ Forward method for Fourier features generation. - + """ + Generates Fourier feature embeddings for a given grid. + + This method transforms the input grid into a higher-dimensional space + using Fourier features. This transformation allows the model to + capture complex relationships in the data by representing it in terms + of sinusoidal functions with varying frequencies. This is particularly + useful when the underlying relationships in the data are periodic or + oscillatory, which is common in many physical systems governed by + differential equations. + Args: - grid (torch.Tensor): calculation domain. - + grid (torch.Tensor): The input grid representing the domain of the + problem. Shape should be (N, D) where N is the number of points + and D is the dimensionality of the grid. + Returns: - torch.Tensor: embedding with Fourier features. + torch.Tensor: The embedding of the grid with Fourier features. The + output tensor has shape (N, M), where M is the dimensionality + of the embedded space, determined by the number of Fourier + features added. """ if self.idx == []: @@ -76,20 +104,26 @@ def forward(self, grid: torch.Tensor) -> torch.Tensor: class FourierNN(nn.Module): """ Class for realizing neural network with Fourier features - and skip connection. + and skip connection. """ + def __init__(self, layers=[100, 100, 100, 1], L=[1], M=[1], activation=nn.Tanh(), ones=False): """ - - Args: - layers (list, optional): neurons quantity in each layer (exclusion input layer), - the number of neurons in the hidden layers must match. Defaults to [100, 100, 100, 1]. - L (list, optional): (sin(w*x),cos(w*x)) frequency parameter, w=2*pi/L. Defaults to [1]. - M (list, optional): number of (sin, cos) pairs in result embedding. Defaults to [1]. - activation (_type_, optional): nn.Module object, activ-n function. Defaults to nn.Tanh(). - ones (bool, optional): enter or not ones vector in result embedding. Defaults to False. + Initializes the Fourier Neural Network (FNN) with specified layer configurations and Fourier embedding parameters. + + This initialization prepares the network architecture, incorporating a Fourier embedding layer to map the input data into a higher-dimensional space using sinusoidal functions. This approach enhances the network's ability to capture complex patterns and relationships within the data, which is particularly useful when modeling solutions to differential equations. The Fourier embedding enables the network to learn the underlying frequencies and modes present in the data, leading to more accurate and efficient equation discovery. + + Args: + layers (list, optional): Number of neurons in each layer (excluding the input layer). The number of neurons in the hidden layers must match. Defaults to [100, 100, 100, 1]. + L (list, optional): Frequency parameter for the Fourier embedding, where w = 2*pi/L. Defaults to [1]. + M (list, optional): Number of (sin, cos) pairs in the Fourier embedding. Defaults to [1]. + activation (nn.Module, optional): Activation function to be used in the hidden layers. Defaults to nn.Tanh(). + ones (bool, optional): Whether to include a vector of ones in the Fourier embedding. Defaults to False. + + Returns: + None """ super(FourierNN, self).__init__() @@ -108,13 +142,20 @@ def __init__(self, layers=[100, 100, 100, 1], L=[1], M=[1], self.model.append(nn.Linear(layers[i], layers[i + 1])) def forward(self, grid: torch.Tensor) -> torch.Tensor: - """ Forward pass for neural network. - + """ + Performs a forward pass through the Fourier Neural Network. + + The forward pass propagates the input grid through a series of linear and non-linear transformations, + modulated by learned functions 'u' and 'v'. These functions control the contribution of each layer's + output to the subsequent layer's input, effectively shaping the network's response to capture complex relationships + within the input data. This architecture allows the network to approximate solutions to differential equations + by learning the underlying functional relationships. + Args: - grid (torch.Tensor): calculation domain. - + grid (torch.Tensor): The input grid representing the domain of the differential equation. + Returns: - torch.Tensor: predicted values. + torch.Tensor: The predicted values at each point on the grid, representing the approximate solution to the differential equation. """ input_ = self.model[0](grid) @@ -130,20 +171,36 @@ def forward(self, grid: torch.Tensor) -> torch.Tensor: class FeedForward(nn.Module): - """Simple MLP neural network""" + """ + Simple MLP neural network + """ + def __init__(self, layers: List = [2, 100, 100, 100, 1], activation: nn.Module = nn.Tanh(), parameters: dict = None): """ - Args: - layers (List, optional): neurons quantity in each layer. - Defaults to [2, 100, 100, 100, 1]. - activation (nn.Module, optional): nn.Module object, activ-n function. - Defaults to nn.Tanh(). - parameters (dict, optional): parameters initial values (for inverse task). - Defaults to None. + Initializes the FeedForward neural network. + + This network architecture is designed to approximate solutions + to differential equations by learning the underlying relationships + within the data. The structure and activation functions are + configured to effectively capture the complex dynamics inherent + in these equations. + + Args: + layers (List, optional): A list defining the number of neurons in each layer of the network. + The first element represents the input layer size, and the last element represents the output layer size. + Defaults to [2, 100, 100, 100, 1]. + activation (nn.Module, optional): The activation function to be applied after each linear layer (except the last). + Defaults to nn.Tanh(). + parameters (dict, optional): Initial parameter values for the network's weights and biases. + This can be useful for initializing the network with prior knowledge or for inverse problems. + Defaults to None. + + Returns: + None """ super().__init__() @@ -158,23 +215,35 @@ def __init__(self, self.reg_param(parameters) def forward(self, x: torch.Tensor) -> torch.Tensor: - """ forward run - + """ + Performs a forward pass through the feedforward network. + + This method takes input data and propagates it through the network's layers + to generate predictions. It leverages the defined network architecture + to transform the input into a corresponding output, effectively mapping + the input data to a solution space. + Args: - x (torch.Tensor): neural network inputs - + x (torch.Tensor): The input tensor to the network. + Returns: - torch.Tensor: outputs + torch.Tensor: The output tensor produced by the network. """ return self.net(x) def reg_param(self, parameters: dict): - """ Parameters registration as neural network parameters. - Should be used in inverse coefficients tasks. - - Args: - parameters (dict): dict with initial values. + """ + Registers provided parameters as trainable parameters of the neural network. + + This is crucial for tasks where parameters need to be optimized alongside the network's weights, + allowing the network to learn the optimal values for these parameters during training. + + Args: + parameters (dict): A dictionary where keys are parameter names and values are initial parameter values. + + Returns: + None """ for key, value in parameters.items(): parameters[key] = torch.nn.Parameter(torch.tensor([value], @@ -184,12 +253,18 @@ def reg_param(self, def parameter_registr(model: torch.nn.Module, parameters: dict) -> None: - """Parameters registration as neural network (mpdel) parameters. - Should be used in inverse coefficients tasks. - + """ + Registers given parameters as trainable parameters of the neural network. + + This is essential for incorporating prior knowledge or initial guesses into the model, + allowing the optimization process to refine these values alongside the network's weights. + Args: - model (torch.nn.Module): neural network. - parameters (dict): dict with initial values. + model (torch.nn.Module): The neural network model to register parameters with. + parameters (dict): A dictionary where keys are parameter names (strings) and values are initial numerical values. + + Returns: + None """ for key, value in parameters.items(): parameters[key] = torch.nn.Parameter(torch.tensor([value], @@ -200,16 +275,21 @@ def parameter_registr(model: torch.nn.Module, def mat_model(domain: Any, equation: Any, nn_model: torch.nn.Module = None) -> torch.Tensor: - """ Model creation for *mat* mode. - + """ + Generates a matrix-based representation of the model based on the domain and equation. + + This function constructs a model by evaluating the equation on a grid defined by the domain. + If a neural network is provided, it uses the network's output to shape the model; otherwise, + it initializes a matrix of ones. This matrix-based representation is useful for further + analysis or processing within the equation discovery workflow. + Args: - domain (Any): object of Domian class. - equation (Any): Equation class object (see data module). - nn_model (torch.nn.Module, optional): neural network which outputs will be *mat* model. - Defaults to None. - + domain (Any): Object representing the domain over which the equation is defined. + equation (Any): Object containing the equation to be modeled. + nn_model (torch.nn.Module, optional): A neural network to be used as a part of model. Defaults to None. + Returns: - torch.nn.Module: model for *mat* mode. + torch.Tensor: A tensor representing the model in matrix form. """ grid = domain.build('mat') diff --git a/epde/solver/optimizers/closure.py b/epde/solver/optimizers/closure.py index c354f149..21eed35b 100644 --- a/epde/solver/optimizers/closure.py +++ b/epde/solver/optimizers/closure.py @@ -3,9 +3,39 @@ from epde.solver.device import device_type class Closure(): + """ + A class that manages different closure types for optimization. + + Class Methods: + - __init__: + """ + def __init__(self, mixed_precision: bool, model): + """ + Initializes the Trainer, preparing it for equation discovery. + + This setup configures the training environment based on the provided model and mixed precision settings. + It determines the appropriate device (CPU or CUDA), data type, and initializes the optimizer. + The trainer is initialized with the necessary components to search for the best equation structure. + + Args: + mixed_precision (bool): Whether to use mixed precision training for faster computations. + model: The model to train, containing the equation structure and parameters. + + Returns: + None + + Fields: + mixed_precision (bool): Whether mixed precision training is enabled. + model (torch.nn.Module): The model representing the equation to be discovered. + optimizer (torch.optim.Optimizer): The optimizer used for training the model parameters. + normalized_loss_stop (float): The normalized loss value at which training should stop. + device (str): The device ('cuda' or 'cpu') used for training. + cuda_flag (bool): A flag indicating whether CUDA is used. + dtype (torch.dtype): The data type (torch.float16 or torch.bfloat16) used for training. + """ self.mixed_precision = mixed_precision self.set_model(model) @@ -19,25 +49,53 @@ def __init__(self, def set_model(self, model): + """ + Sets the internal equation model. + + This model represents the discovered differential equation. Setting it allows the system to evaluate and utilize the identified equation for further analysis or simulation. + + Args: + model: The equation model to be set. This model encapsulates the structure and parameters of the discovered differential equation. + + Returns: + None. + """ self._model = model @property def model(self): + """ + Gets the symbolic representation of the discovered equation. + + This representation allows for further analysis, manipulation, + and integration with other symbolic computation tools. + + Args: + None + + Returns: + str: The symbolic representation of the discovered equation. + """ return self._model def _amp_mixed(self): - """ Preparation for mixed precsion operations. - + """ + Configures mixed precision training using torch.cuda.amp. + + This setup is crucial for leveraging mixed precision, which can significantly accelerate training on CUDA-enabled devices. + It initializes a GradScaler if mixed precision is enabled, which is essential for preventing underflow issues during backpropagation with lower precision data types. + It also performs a check to ensure compatibility with the LBFGS optimizer, as it is known to be incompatible with mixed precision training. + Args: - mixed_precision (bool): use or not torch.amp. - + mixed_precision (bool): A flag indicating whether to use mixed precision training. + Raises: - NotImplementedError: AMP and the LBFGS optimizer are not compatible. - + NotImplementedError: If the LBFGS optimizer is used in conjunction with mixed precision. + Returns: - scaler: GradScaler for CUDA. - cuda_flag (bool): True, if CUDA is activated and mixed_precision=True. - dtype (dtype): operations dtype. + scaler (torch.cuda.amp.GradScaler): A GradScaler instance for managing gradients during mixed precision training. + cuda_flag (bool): True if CUDA is active and mixed_precision is enabled, False otherwise. + dtype (torch.dtype): The data type to be used for operations (torch.float16 or torch.float32). """ self.scaler = torch.cuda.amp.GradScaler(enabled=self.mixed_precision) @@ -48,6 +106,19 @@ def _amp_mixed(self): def _closure(self): + """ + Performs a closure step for optimization. This step is crucial for refining the equation discovery process by iteratively adjusting model parameters based on the loss. + + This method performs the core optimization step, including zeroing gradients, + evaluating the loss, performing backpropagation, and updating the optimizer. + It also handles mixed precision training if enabled. The loss is calculated and backpropagated to refine the equation's coefficients, guiding the search towards a better fit with the observed data. + + Args: + self: The object instance. + + Returns: + torch.Tensor: The computed loss value, representing the discrepancy between the model's predictions and the data. + """ self.optimizer.zero_grad() with torch.autocast(device_type=self.device, dtype=self.dtype, @@ -65,6 +136,27 @@ def _closure(self): return loss def _closure_nncg(self): + """ + Compute the loss and gradients to refine the equation discovery process. + + This method evaluates the loss based on how well the current equation + fits the data, computes the gradients of the loss with respect to the + equation's parameters, and updates the model's current loss. It leverages + mixed precision training when enabled for faster and more efficient + computation. This is a crucial step in guiding the search for the + optimal equation structure by quantifying the error and providing + direction for improvement. + + Args: + self: The object instance. + + Returns: + tuple: A tuple containing the loss and the gradients. + - loss: The computed loss, representing the error between the + equation's predictions and the observed data. + - grads: The computed gradients, indicating the direction to + adjust the equation's parameters to reduce the loss. + """ self.optimizer.zero_grad() with torch.autocast(device_type=self.device, dtype=self.dtype, @@ -86,6 +178,24 @@ def _closure_nncg(self): return loss, grads def _closure_pso(self): + """ + Performs a Particle Swarm Optimization (PSO) iteration to refine equation candidates. + + This method evaluates the loss and gradients for each particle (equation candidate) in the swarm, + updates the swarm's losses and gradients based on how well each candidate fits the data, + and sets the current loss of the model to the best performing candidate's loss. This process + iteratively improves the swarm's ability to represent the underlying differential equation. + + Args: + self: The instance of the class containing the PSO optimizer and model. + + Returns: + tuple: A tuple containing: + - losses (torch.Tensor): A tensor of losses for each particle in the swarm, + representing the error between the equation candidate's predictions and the data. + - gradients (torch.Tensor): A tensor of gradients for each particle in the swarm, + indicating the direction to adjust the equation candidate to reduce the error. + """ def loss_grads(): self.optimizer.zero_grad() with torch.autocast(device_type=self.device, @@ -117,6 +227,24 @@ def loss_grads(): return losses, gradients def _closure_ngd(self): + """ + Performs a single optimization step using the NGD optimizer to refine the solution. + + This method computes the loss, performs backpropagation to adjust model parameters, and updates these parameters using the optimizer. + It also calculates interior residuals and boundary values to assess how well the current solution satisfies the differential equation and boundary conditions. + This is crucial for guiding the search towards a solution that accurately represents the underlying physical system described by the data. + + Args: + self: The object instance. + + Returns: + tuple: A tuple containing: + - int_res: Interior residuals computed by the PDE operator, indicating the equation's satisfaction within the domain. + - bval: Boundary values obtained after applying boundary conditions, representing the solution's behavior at the domain's edges. + - true_bval: True boundary values, the target values for the boundary conditions. + - loss: The computed loss value, quantifying the discrepancy between the model's predictions and the observed data and boundary conditions. + - self.model.solution_cls.evaluate: The evaluation function of the solution class, used for subsequent evaluations. + """ self.optimizer.zero_grad() with torch.autocast(device_type=self.device, dtype=self.dtype, @@ -137,6 +265,20 @@ def _closure_ngd(self): return int_res, bval, true_bval, loss, self.model.solution_cls.evaluate def get_closure(self, _type: str): + """ + Retrieves a specific optimization closure based on the provided type. + + This method allows to switch between different optimization strategies + during the equation discovery process. Different closures encapsulate different + optimization algorithms, enabling the framework to explore various search + directions in the equation space. + + Args: + _type (str): The type of closure to retrieve ('PSO', 'NGD', 'NNCG', or default). + + Returns: + callable: The requested closure function. Returns a default closure if the type is not recognized. + """ if _type == 'PSO': return self._closure_pso elif _type == 'NGD': diff --git a/epde/solver/optimizers/ngd.py b/epde/solver/optimizers/ngd.py index c4774a51..1be4bed7 100644 --- a/epde/solver/optimizers/ngd.py +++ b/epde/solver/optimizers/ngd.py @@ -6,16 +6,26 @@ class NGD(torch.optim.Optimizer): + """ + NGD implementation (https://arxiv.org/abs/2302.13163). + """ """NGD implementation (https://arxiv.org/abs/2302.13163). """ def __init__(self, params, grid_steps_number: int = 30): - """The Natural Gradient Descent class. - + """ + Initializes the Natural Gradient Descent optimizer. + + This class sets up the optimizer with necessary parameters, + including the grid resolution for exploration of potential step sizes. + The grid is constructed in a log scale. + Args: - grid_steps_number (int, optional): Grid steps number. Defaults to 30. + params (iterable): Iterable of parameters to optimize. + grid_steps_number (int, optional): Number of steps in the grid for step size selection. + A finer grid allows for a more precise step size selection. Defaults to 30. """ defaults = {'grid_steps_number': grid_steps_number} super(NGD, self).__init__(params, defaults) @@ -27,13 +37,17 @@ def __init__(self, params, self.cuda_empty_once_for_test=True def grid_line_search_update(self, loss_function: callable, f_nat_grad: torch.Tensor) -> None: - """ Update models paramters by natural gradient. - - Args: - loss (callable): function to calculate loss. - - Returns: - None. + """ + Update model parameters using a grid line search along the natural gradient direction to minimize the loss. + + This method explores different step sizes along the natural gradient to find the one that results in the lowest loss. This ensures a more stable and efficient update compared to using a fixed step size, which is crucial for discovering accurate differential equation models. + + Args: + loss_function (callable): A callable that computes the loss value. It should return a tuple containing the loss tensor and any auxiliary information. + f_nat_grad (torch.Tensor): The natural gradient, a tensor representing the direction of steepest descent in the parameter space, preconditioned by the Fisher information matrix. + + Returns: + None. The model parameters are updated in place. """ # function to update models paramters at each step def loss_at_step(step, loss_function: callable, f_nat_grad: torch.Tensor) -> torch.Tensor: @@ -55,13 +69,18 @@ def loss_at_step(step, loss_function: callable, f_nat_grad: torch.Tensor) -> tor vector_to_parameters(new_params, self.params) def gram_factory(self, residuals: torch.Tensor) -> torch.Tensor: - """ Make Gram matrice. - + """ + Computes the Gram matrix of the Jacobian of the residuals with respect to the model parameters. + + This matrix is used to estimate the inverse of the Fisher Information Matrix, + which is crucial for preconditioning the gradient during optimization. + The preconditioning enhances the convergence and stability of the training process. + Args: - residuals (callable): PDE residual. - + residuals (torch.Tensor): The PDE residuals evaluated at different points. + Returns: - torch.Tensor: Gram matrice. + torch.Tensor: The Gram matrix, a symmetric positive semi-definite matrix. """ # Make Gram matrice. def jacobian() -> torch.Tensor: @@ -78,13 +97,18 @@ def jacobian() -> torch.Tensor: def gram_factory_cpu(self, residuals: torch.Tensor) -> torch.Tensor: - """ Make Gram matrice. - + """ + Computes the Gram matrix of the Jacobian of the residuals with respect to the model parameters. + + This matrix is used to quantify the linear dependencies between the gradients of the residuals, + providing insights into the sensitivity of the model to parameter changes and the identifiability + of the parameters themselves. It is calculated on the CPU. + Args: - residuals (callable): PDE residual. - + residuals (torch.Tensor): A tensor containing the PDE residuals. + Returns: - torch.Tensor: Gram matrice. + torch.Tensor: The Gram matrix, a measure of the linear dependence between parameter gradients. """ # Make Gram matrice. def jacobian() -> torch.Tensor: @@ -102,15 +126,16 @@ def jacobian() -> torch.Tensor: def torch_cuda_lstsq(self, A: torch.Tensor, B: torch.Tensor, tol: float = None) -> torch.Tensor: - """ Find lstsq (least-squares solution) for torch.tensor cuda. - - Args: - A (torch.Tensor): lhs tensor of shape (*, m, n) where * is zero or more batch dimensions. - B (torch.Tensor): rhs tensor of shape (*, m, k) where * is zero or more batch dimensions. - tol (float): used to determine the effective rank of A. By default set to the machine precision of the dtype of A. - - Returns: - torch.Tensor: solution for A and B. + """ + Finds the least-squares solution for a system of linear equations represented by CUDA tensors. This is a crucial step in identifying the underlying differential equations from data by minimizing the error between the model's predictions and the observed data. + + Args: + A (torch.Tensor): The left-hand side tensor of shape (*, m, n), where * represents zero or more batch dimensions. + B (torch.Tensor): The right-hand side tensor of shape (*, m, k), where * represents zero or more batch dimensions. + tol (float, optional): Tolerance value used to determine the effective rank of A. Defaults to the machine precision of the dtype of A. + + Returns: + torch.Tensor: The least-squares solution for A and B. """ tol = torch.finfo(A.dtype).eps if tol is None else tol U, S, Vh = torch.linalg.svd(A, full_matrices=False) @@ -125,6 +150,30 @@ def torch_cuda_lstsq(self, A: torch.Tensor, B: torch.Tensor, tol: float = None) def numpy_lstsq(self, A: torch.Tensor, B: torch.Tensor, rcond: float = None) -> torch.Tensor: + """ + Solves a linear least squares problem using NumPy to estimate natural gradients. + + This method leverages NumPy's least squares solver to efficiently compute + the solution to a linear system, which is then used to approximate the + natural gradient. By solving this system, the method aims to find the + optimal update direction for parameters, facilitating efficient learning. + The input tensors `A` and `B` are detached from the computation graph + and moved to the CPU as NumPy arrays for compatibility with the solver. + The solution is then converted back to a PyTorch tensor and placed on + the appropriate device. + + Args: + A: The "coefficient" matrix (left-hand side of the equation). + B: The "dependent variable" matrix (right-hand side of the equation). + rcond: Cutoff ratio for small singular values of a. + For the purposes of rank determination, singular values are treated + as zero if they are smaller than rcond times the largest singular + value of a. + + Returns: + torch.Tensor: The least squares solution, as a PyTorch tensor on the + correct device. + """ A = A.detach().cpu().numpy() B = B.detach().cpu().numpy() @@ -139,10 +188,19 @@ def numpy_lstsq(self, A: torch.Tensor, B: torch.Tensor, rcond: float = None) -> def step(self, closure=None) -> torch.Tensor: - """ It runs ONE step on the natural gradient descent. - - Returns: - torch.Tensor: loss value for NGD step. + """ + Performs a single natural gradient descent update step, adjusting model parameters based on the computed natural gradient. This involves assembling the Gramian matrix, solving a least squares problem to obtain the natural gradient, and updating the parameters using a line search to ensure stable convergence. + + Args: + closure (callable, optional): A closure that reevaluates the model and returns a tuple containing: + - int_res (torch.Tensor): Interior residual values. + - bval (torch.Tensor): Boundary values. + - true_bval (torch.Tensor): True boundary values. + - loss (torch.Tensor): The loss value. + - loss_function (callable): The loss function itself. + + Returns: + torch.Tensor: The loss value after the NGD step. """ int_res, bval, true_bval, loss, loss_function = closure() diff --git a/epde/solver/optimizers/optimizer.py b/epde/solver/optimizers/optimizer.py index 418f1e76..528ad203 100644 --- a/epde/solver/optimizers/optimizer.py +++ b/epde/solver/optimizers/optimizer.py @@ -7,12 +7,43 @@ class Optimizer(): + """ + A base class for defining optimization algorithms. + + This class serves as a foundation for implementing various optimization techniques. + + Class Methods: + - __init__: Initializes the optimizer with specified parameters. + - step: Performs a single optimization step. + - zero_grad: Resets the gradients of the optimized parameters. + """ + def __init__( self, optimizer: str, params: dict, gamma: Union[float, None]=None, decay_every: Union[int, None]=None): + """ + Initializes the optimizer configuration for equation discovery. + + Configures the optimizer type and its parameters, including optional learning rate decay settings, + to fine-tune the search process for identifying the best equation structure. + + Args: + optimizer: The name of the optimizer to use (e.g., 'Adam', 'SGD'). + params: A dictionary containing the optimizer's parameters (e.g., learning rate, momentum). + gamma: The learning rate decay factor (optional). + decay_every: The frequency (in steps) at which to decay the learning rate (optional). + + Returns: + None. + + Why: + This configuration is crucial for effectively training the equation discovery process. + By allowing flexible optimizer choices and decay schedules, the search for the optimal + equation structure can be more precisely controlled and potentially accelerated. + """ self.optimizer = optimizer self.params = params self.gamma = gamma @@ -23,16 +54,23 @@ def optimizer_choice( mode, model) -> \ Union[torch.optim.Adam, torch.optim.SGD, torch.optim.LBFGS, PSO]: - """ Setting optimizer. If optimizer is string type, it will get default settings, - or it may be custom optimizer defined by user. - - Args: - optimizer: optimizer choice (Adam, SGD, LBFGS, PSO). - learning_rate: determines the step size at each iteration - while moving toward a minimum of a loss function. - - Returns: - optimzer: ready optimizer. + """ + Selects and configures the optimization algorithm for training the model. + + This method allows the user to choose from several optimization algorithms, + including Adam, SGD, LBFGS, PSO, and NGD, and configures them based on + user-specified parameters. The choice of optimizer and its configuration + are crucial for effectively training the model to accurately represent + the underlying differential equation. + + Args: + mode (str): Specifies the mode of operation ('NN', 'autograd', or 'mat'), + which determines how the model parameters are passed to the optimizer. + model (torch.nn.Module): The model whose parameters need to be optimized. + + Returns: + Union[torch.optim.Adam, torch.optim.SGD, torch.optim.LBFGS, PSO]: + The configured optimizer instance. """ if self.optimizer == 'Adam': diff --git a/epde/solver/optimizers/pso.py b/epde/solver/optimizers/pso.py index b0300e43..312472c9 100644 --- a/epde/solver/optimizers/pso.py +++ b/epde/solver/optimizers/pso.py @@ -7,6 +7,9 @@ class PSO(torch.optim.Optimizer): + """ + Custom PSO optimizer. + """ """Custom PSO optimizer. """ @@ -23,21 +26,32 @@ def __init__(self, variance: float = 1, epsilon: float = 1e-8, n_iter: int = 2000): - """The Particle Swarm Optimizer class. - + """ + Initializes the Particle Swarm Optimizer (PSO). + + This method sets up the PSO algorithm with specified hyperparameters, + preparing it to explore the parameter space of a given model to find optimal configurations. + The initialization includes setting the swarm size, inertia, cognitive and social coefficients, + learning rate for gradient descent, and other parameters that control the optimization process. + The swarm is initialized based on the model parameters, and initial velocities are assigned to each particle. + The method prepares the optimizer to iteratively update the swarm's positions and velocities + to minimize a given loss function, effectively searching for the best-fitting differential equation. + Args: - pop_size (int, optional): Population of the PSO swarm. Defaults to 30. - b (float, optional): Inertia of the particles. Defaults to 0.99. - c1 (float, optional): The *p-best* coeficient. Defaults to 0.08. - c2 (float, optional): The *g-best* coeficient. Defaults to 0.5. - lr (float, optional): Learning rate for gradient descent. Defaults to 0.00, - so there will not be any gradient-based optimization. - betas (tuple(float, float), optional): same coeff in Adam algorithm. Defaults to (0.99, 0.999). - c_decrease (bool, optional): Flag for update_pso_params method. Defautls to False. - variance (float, optional): Variance parameter for swarm creation - based on model. Defaults to 1. - epsilon (float, optional): some add to gradient descent like in Adam optimizer. - Defaults to 1e-8. + params (iterable): Iterable of parameters to optimize (typically model parameters). + pop_size (int, optional): Population size of the PSO swarm. Defaults to 30. + b (float, optional): Inertia of the particles, controlling their tendency to continue in their current direction. Defaults to 0.9. + c1 (float, optional): The *p-best* coefficient, influencing the particle's attraction to its best historical position. Defaults to 0.08. + c2 (float, optional): The *g-best* coefficient, influencing the particle's attraction to the swarm's best historical position. Defaults to 0.5. + lr (float, optional): Learning rate for gradient descent, used to refine particle positions based on gradient information. Defaults to 1e-3. + betas (tuple(float, float), optional): Coefficients used for computing running averages of gradient and its square (similar to Adam). Defaults to (0.99, 0.999). + c_decrease (bool, optional): Flag indicating whether to decrease c1 and c2 over iterations. Defaults to False. + variance (float, optional): Variance parameter for initializing the swarm based on the model parameters. Defaults to 1. + epsilon (float, optional): Small value added to the denominator for numerical stability in gradient-based updates. Defaults to 1e-8. + n_iter (int, optional): Number of iterations. Defaults to 2000. + + Returns: + None """ defaults = {'pop_size': pop_size, 'b': b, 'c1': c1, 'c2': c2, @@ -74,11 +88,21 @@ def __init__(self, self.indicator = True def params_to_vec(self) -> torch.Tensor: - """ Method for converting model parameters *NN and autograd* - or model values *mat* to vector. - + """ + Converts the model's parameters or values into a single vector. + + This is a utility function to represent the model's state in a flattened format, + which is useful for optimization algorithms that require vector-based inputs. + + Args: + None + Returns: - torch.Tensor: model parameters/model values vector. + torch.Tensor: A 1D tensor containing the model's parameters or values. + + Why: + This method facilitates the application of vector-based optimization techniques + by providing a unified representation of the model's parameters or values. """ if not isinstance(self.params, torch.Tensor): vec = parameters_to_vector(self.params) @@ -89,11 +113,19 @@ def params_to_vec(self) -> torch.Tensor: return vec def vec_to_params(self, vec: torch.Tensor) -> None: - """Method for converting vector to model parameters (NN, autograd) - or model values (mat) - + """ + Distributes a vector's values into the model's parameters. + + This method takes a vector, typically representing a particle in the optimization process, + and maps its values onto the parameters of the model. This is a crucial step in evaluating + the fitness of a candidate solution (particle) by updating the model with the particle's + parameter values before assessing its performance. + Args: - vec (torch.Tensor): The particle of swarm. + vec (torch.Tensor): A tensor containing the parameter values for the model. + + Returns: + None """ if not isinstance(self.params, torch.Tensor): vector_to_parameters(vec, self.params) @@ -101,11 +133,23 @@ def vec_to_params(self, vec: torch.Tensor) -> None: self.params.data = vec.reshape(self.params).data def build_swarm(self): - """Creates the swarm based on solution class model. - + """ + Initializes the swarm population by perturbing a base solution. + + The swarm is created by adding random variations to a vectorized + representation of the initial solution. This ensures diversity in the + search space, allowing the algorithm to explore different potential + equation structures. The first particle in the swarm is set to the + original solution to preserve a baseline. + + Args: + None + Returns: - torch.Tensor: The PSO swarm population. - Each particle represents a neural network (NN, autograd) or model values (mat). + torch.Tensor: The initialized swarm population. Each row represents a + particle (potential equation), and each column represents a parameter + within the equation. The tensor is detached from the computation graph + and requires gradients for optimization. """ vector = self.params_to_vec() matrix = [] @@ -119,28 +163,48 @@ def build_swarm(self): return swarm.clone().detach().requires_grad_(True) def update_pso_params(self) -> None: - """Method for updating pso parameters if c_decrease=True. + """ + Updates the cognitive and social parameters (c1, c2) of the PSO algorithm. + + This adjustment refines the balance between individual exploration and social influence + as the optimization progresses, potentially leading to improved convergence. + + Args: + None + + Returns: + None """ self.c1 -= 2 * self.c1 / self.n_iter self.c2 += self.c2 / self.n_iter def start_velocities(self) -> torch.Tensor: - """Start the velocities of each particle in the population (swarm) as `0`. - - Returns: - torch.Tensor: The starting velocities. + """ + Initializes particle velocities to zero. + + This ensures a neutral starting point for the swarm's exploration + of the search space, preventing any initial bias towards specific regions. + + Returns: + torch.Tensor: A tensor of zeros representing the initial velocities + for each particle in the swarm. The shape is (population size, vector shape). """ return torch.zeros((self.pop_size, self.vec_shape)) def gradient(self, loss: torch.Tensor) -> torch.Tensor: - """ Calculation of loss gradient by model parameters (NN, autograd) - or model values (mat). - + """ + Computes the gradient of the loss with respect to the model parameters. + + This gradient is used to guide the optimization process, indicating the direction + in which to adjust the parameters to reduce the loss. The calculation leverages + automatic differentiation to efficiently compute the derivatives. + Args: - loss (torch.Tensor): result of loss calculation. - + loss (torch.Tensor): The scalar loss value to differentiate. + Returns: - torch.Tensor: calculated gradient vector. + torch.Tensor: A flattened vector representing the gradient of the loss with + respect to all model parameters. """ dl_dparam = torch.autograd.grad(loss, self.params) @@ -149,15 +213,29 @@ def gradient(self, loss: torch.Tensor) -> torch.Tensor: return grads def get_randoms(self) -> torch.Tensor: - """Generate random values to update the particles' positions. - + """ + Generate random values for exploration during the search process. + + Args: + None + Returns: - torch.Tensor: random tensor + torch.Tensor: A tensor of random values used to introduce diversity and explore the search space when updating particle positions. The shape of the tensor is (2, 1, self.vec_shape). """ return torch.rand((2, 1, self.vec_shape)) def update_p_best(self) -> None: - """Updates the *p-best* positions.""" + """ + Updates the personal best positions of particles in the swarm. + + This method compares the current loss of each particle with its personal best loss so far. If a particle has found a new position with a lower loss, its personal best position and corresponding loss are updated. This ensures that each particle remembers its best-performing location, which is crucial for guiding the swarm's search for the global optimum. + + Args: + None + + Returns: + None + """ idx = torch.where(self.loss_swarm < self.f_p) @@ -165,14 +243,31 @@ def update_p_best(self) -> None: self.f_p[idx] = self.loss_swarm[idx].detach() def update_g_best(self) -> None: - """Update the *g-best* position.""" + """ + Updates the global best position (*g-best*) of the particle swarm. + + The *g-best* represents the best solution found by any particle in the swarm so far. This method identifies the particle with the lowest objective function value among all particles and sets its position as the new *g-best*. This ensures that the swarm converges towards the most promising region of the search space, effectively guiding the equation discovery process towards better-fitting models. + + Args: + None + + Returns: + None + """ self.g_best = self.p[torch.argmin(self.f_p)] def gradient_descent(self) -> torch.Tensor: - """ Gradiend descent based on Adam algorithm. - + """ + Updates velocities of particles based on calculated gradients using Adam algorithm. + + The method refines the search trajectory of particles by incorporating gradient information + into their velocities, facilitating efficient exploration of the solution space. + + Args: + None + Returns: - torch.Tensor: gradient term in velocities vector. + torch.Tensor: Updated velocities for each particle, representing a refined search direction. """ self.m1 = self.beta1 * self.m1 + (1 - self.beta1) * self.grads_swarm self.m2 = self.beta2 * self.m2 + (1 - self.beta2) * torch.square( @@ -183,10 +278,14 @@ def gradient_descent(self) -> torch.Tensor: return update def step(self, closure=None) -> torch.Tensor: - """ It runs ONE step on the particle swarm optimization. - - Returns: - torch.Tensor: loss value for best particle of thw swarm. + """ + Runs a single iteration of the Particle Swarm Optimization algorithm to refine the search for the optimal equation. It evaluates the swarm's fitness, updates particle velocities and positions based on personal and global best solutions, and adjusts PSO parameters if specified. The method ensures the swarm explores the search space effectively by adapting its movement based on individual and collective experiences, ultimately aiming to identify the equation that best describes the underlying data. + + Args: + closure (callable, optional): A function that evaluates the loss and gradients for each particle in the swarm. Defaults to None. + + Returns: + torch.Tensor: The minimum loss value achieved by the best particle in the swarm after the iteration. """ self.loss_swarm, self.grads_swarm = closure() diff --git a/epde/solver/points_type.py b/epde/solver/points_type.py index 06846776..22eba5a8 100644 --- a/epde/solver/points_type.py +++ b/epde/solver/points_type.py @@ -10,26 +10,38 @@ class Points_type(): """ Discretizing the grid and allocating subsets for Finite Difference method. """ + def __init__(self, grid: torch.Tensor): """ - Args: - grid (torch.Tensor): discretization points of comp-l domain. + Initializes the `Points_type` object with a grid of discretization points. + + This grid represents the spatial or temporal domain over which the differential equation is defined. + It's crucial for evaluating equation terms and calculating residuals, which are then used to assess the fitness of candidate equations. + + Args: + grid (torch.Tensor): Discretization points of the domain. + + Returns: + None """ self.grid = grid @staticmethod def shift_points(grid: torch.Tensor, axis: int, shift: float) -> torch.Tensor: - """ Shifts all values of an array 'grid' on a value 'shift' in a direction of - axis 'axis', somewhat is equivalent to a np.roll. - - Args: - grid (torch.Tensor): discretization of comp-l domain. - axis (int): axis to which the shift is applied. - shift (float): shift value. - - Returns: - torch.Tensor: shifted array of a n-D points. + """ + Shifts the grid points along a specified axis by a given amount. + + This function modifies the grid to explore alternative configurations in the search space. + By shifting points, the algorithm can evaluate different arrangements and identify solutions that better fit the observed data. + + Args: + grid (torch.Tensor): Discretization of the computational domain. + axis (int): The axis along which to apply the shift. + shift (float): The amount by which to shift the points. + + Returns: + torch.Tensor: A new tensor with the shifted grid points. """ grid_shift = grid.clone() @@ -38,17 +50,22 @@ def shift_points(grid: torch.Tensor, axis: int, shift: float) -> torch.Tensor: @staticmethod def _in_hull(p: torch.Tensor, hull: torch.Tensor) -> np.ndarray: - """ Test if points in `p` are in `hull` - `p` should be a `NxK` coordinates of `N` points in `K` dimensions - `hull` is either a scipy.spatial.Delaunay object or the `MxK` array of the - coordinates of `M` points in `K`dimensions for which Delaunay triangulation - will be computed. + """ + Test if points in `p` are within the convex hull defined by `hull`. + + This function determines whether a given set of points lies within the + convex hull formed by another set of points. It leverages Delaunay + triangulation for efficient spatial partitioning and point location + in multi-dimensional spaces. This check is crucial for validating + the physical plausibility of discovered equations by ensuring that + solutions remain within the bounds defined by the observed data. + Args: - p (torch.Tensor): shifted array of a n-D points. - hull (torch.Tensor): initial array of a n-D points. + p (torch.Tensor): A `NxK` tensor representing the coordinates of `N` points in `K` dimensions. These are the points to be tested for inclusion in the hull. + hull (torch.Tensor): A `MxK` tensor representing the coordinates of `M` points in `K` dimensions. These points define the convex hull. Can also be a precomputed `scipy.spatial.Delaunay` object. + Returns: - np.ndarray: array of a n-D boolean type points. - True - if 'p' in 'hull', False - otherwise. + np.ndarray: A boolean array of length `N`. Each element indicates whether the corresponding point in `p` is inside the convex hull defined by `hull`. `True` indicates that the point is inside, `False` otherwise. """ if p.shape[1] > 1: @@ -64,13 +81,22 @@ def _in_hull(p: torch.Tensor, hull: torch.Tensor) -> np.ndarray: return np.array(((p.cpu() <= upbound) & (p.cpu() >= lowbound)).reshape(-1)) def point_typization(self) -> dict: - """ Allocating subsets for FD (i.e., 'f', 'b', 'central'). - - Returns: - dict: type with a points in a 'grid' above. Type may be 'central' - inner point - and string of 'f' and 'b', where the length of the string is a dimension n. 'f' means that if we add - small number to a position of corresponding coordinate we stay in the 'hull'. 'b' means that if we - subtract small number from o a position of corresponding coordinate we stay in the 'hull'. + """ + Identifies the type of each point in the grid based on its proximity to the boundary. + + This method classifies each point in the grid as either 'central' (lying strictly inside the domain) or + assigns it a string of 'f' and 'b' characters representing the directions in which small perturbations + would keep the point within the domain. This information is crucial for understanding the point's + location relative to the domain's boundaries and is used in subsequent calculations. + + Args: + self: An instance of the Points_type class containing the grid of points. + + Returns: + dict: A dictionary where keys are the points in the grid (represented as NumPy arrays) and values + are either 'central' or a string of 'f' and 'b' characters. 'f' indicates that moving the + point slightly in the positive direction along the corresponding axis keeps it inside the domain, + while 'b' indicates the same for the negative direction. """ direction_list = [] @@ -106,10 +132,19 @@ def point_typization(self) -> dict: return point_type def grid_sort(self) -> dict: - """ Sorting grid points for each subset from result Points_type.point_typization. - + """ + Sorts grid points into subsets based on their typization, preparing them for equation discovery. + + The method groups points based on a prior typization, organizing them into subsets + associated with distinct types. This organization facilitates subsequent processing + steps in identifying governing differential equations. + + Args: + self: An instance of the Points_type class containing the grid points and typization information. + Returns: - dict: sorted grid in each subset (see Points_type.point_typization). + dict: A dictionary where keys are point types and values are tensors containing the + corresponding grid points, enabling type-specific analysis. """ point_type = self.point_typization() @@ -125,16 +160,18 @@ def grid_sort(self) -> dict: return grid_dict def bnd_sort(self, grid_dict: dict, b_coord: Union[torch.Tensor, list]) -> list: - """ Sorting boundary points - - Args: - grid_dict (dict): _description_ - b_coord (Union[torch.Tensor, list]): boundary points of grid. - It will be list if periodic condition is. + """ + Sorts boundary points into dictionaries based on their correspondence to grid partitions. - Returns: - list: bnd_dict is similar to grid_dict but with b_coord values. It - will be list of 'bnd_dict's if 'b_coord' is list too. + This function organizes boundary points by associating them with specific grid partitions. + This is useful for applying boundary conditions or constraints specific to each partition when solving differential equations. + + Args: + grid_dict (dict): A dictionary where keys represent grid partition identifiers and values are tensors representing the coordinates of points within that partition. + b_coord (Union[torch.Tensor, list]): A tensor or list of tensors containing the coordinates of boundary points. If a list is provided, it implies a periodic boundary condition with multiple boundary segments. + + Returns: + list: A list of dictionaries. Each dictionary corresponds to a boundary segment (if `b_coord` is a list) and contains keys matching `grid_dict`. The values are tensors of boundary points that fall within the corresponding grid partition. If `b_coord` is a single tensor, a single dictionary is returned instead of a list. """ def bnd_to_dict(grid_dict, b_coord): diff --git a/epde/solver/solution.py b/epde/solver/solution.py index 50d27010..33570252 100644 --- a/epde/solver/solution.py +++ b/epde/solver/solution.py @@ -20,6 +20,7 @@ class Solution(): """ class for different loss functions calculation. """ + def __init__( self, grid: torch.Tensor, @@ -33,18 +34,26 @@ def __init__( derivative_points: int = 2, batch_size: int = None): """ - Args: - grid (torch.Tensor): discretization of comp-l domain. - equal_cls (Union[Equation_NN, Equation_mat, Equation_autograd]): Equation_{NN, mat, autograd} object. - model (Union[torch.nn.Sequential, torch.Tensor]): model of *mat or NN or autograd* mode. - mode (str): *mat or NN or autograd* - weak_form (Union[None, List[callable]]): list with basis functions, if the form is *weak*. - lambda_operator (_type_): regularization parameter for operator term in loss. - lambda_bound (_type_): regularization parameter for boundary term in loss. - tol (float, optional): penalty in *casual loss*. Defaults to 0. - derivative_points (int, optional): points number for derivative calculation. - batch_size (int): size of batch. - For details to Derivative_mat class.. Defaults to 2. + Initializes the Solution object, setting up the problem domain, equation, model, and loss functions. + + This method prepares the necessary components for solving a differential equation, + including the domain discretization, equation definition, model setup, and boundary conditions. + It also initializes the loss functions used to train the model. + + Args: + grid (torch.Tensor): Discretization of the computational domain. + equal_cls (Union[Equation_NN, Equation_mat, Equation_autograd]): Equation object defining the differential equation. + model (Union[torch.nn.Sequential, torch.Tensor]): Model representing the solution to the differential equation. + mode (str): Specifies the calculation mode (*mat*, *NN*, or *autograd*). + weak_form (Union[None, List[callable]]): List of basis functions for the weak formulation, if applicable. + lambda_operator: Regularization parameter for the operator term in the loss function. + lambda_bound: Regularization parameter for the boundary term in the loss function. + tol (float, optional): Penalty value used in the *casual loss* calculation. Defaults to 0. + derivative_points (int, optional): Number of points used for derivative calculation. Defaults to 2. + batch_size (int, optional): Size of the batch used for training. Defaults to None. + + Returns: + None """ self.grid = check_device(grid) @@ -90,11 +99,20 @@ def __init__( @staticmethod def _operator_coeff(equal_cls: Any, operator: list): - """ Coefficient checking in operator. - - Args: - equal_cls (Any): Equation_{NN, mat, autograd} object. - operator (list): prepared operator (result of operator_prepare()) + """ + Updates the operator coefficients, transferring them to the appropriate device. + + This ensures that the coefficients within the operator are compatible + with the computational device (CPU or GPU) being used for the equation + solving process. This is crucial for efficient and accurate calculations + during the equation discovery and modeling process. + + Args: + equal_cls (Any): Equation object containing the equation definition and coefficients. + operator (list): Prepared operator (result of operator_prepare()) to be updated. + + Returns: + None. The operator list is modified in place. """ for i, _ in enumerate(equal_cls.operator): eq = equal_cls.operator[i] @@ -108,11 +126,17 @@ def _operator_coeff(equal_cls: Any, operator: list): eq[key]['coeff'] = eq[key]['coeff'].to(device_type()) def _model_change(self, new_model: torch.nn.Module) -> None: - """Change self.model for class and *operator, boundary* object. - It should be used in cache_lookup and cache_retrain method. - - Args: - new_model (torch.nn.Module): new self model. + """ + Updates the internal model and related components with a new model. This ensures consistency across the solution components when a new model is selected, either from the cache or after retraining. + + Args: + new_model (torch.nn.Module): The new PyTorch model to be used. + + Returns: + None + + Why: + This method is crucial for updating the model within the solution and ensuring that all dependent components, such as the operator (PDE definition) and boundary conditions, use the same model. This synchronization is essential for the correct evaluation and optimization of the solution. """ self.model = new_model self.operator.model = new_model @@ -129,19 +153,18 @@ def _model_change(self, new_model: torch.nn.Module) -> None: def evaluate(self, save_graph: bool = True) -> Tuple[torch.Tensor, torch.Tensor]: - """ Computes loss. - - Args: - second_order_interactions (bool, optional): optimizer iteration - (serves only for computing adaptive lambdas). Defaults to True. - sampling_N (int, optional): parameter for accumulation of - solutions (op, bcs). The more sampling_N, the more accurate the - estimation of the variance (only for computing adaptive lambdas). Defaults to 1. - lambda_update (bool, optional): update lambda or not. Defaults to False. - save_graph (bool, optional): responsible for saving the computational graph. Defaults to True. - - Returns: - Tuple[torch.Tensor, torch.Tensor]: loss + """ + Computes the loss function based on the current state of the operator and boundary conditions. + + This method orchestrates the computation of the loss, incorporating adaptive lambda parameters for regularization. + It prepares the operator and boundary conditions, applies boundary conditions, and then calculates the loss using the configured loss class. + The method also handles batching of the operator output, accumulating results across batches when applicable. + + Args: + save_graph (bool, optional): Determines whether to save the computational graph for visualization or debugging. Defaults to True. + + Returns: + Tuple[torch.Tensor, torch.Tensor]: A tuple containing the computed loss and the normalized loss. """ self.op = self.operator.operator_compute() diff --git a/epde/solver/solver.py b/epde/solver/solver.py index 9c3e0d31..4edf04d9 100644 --- a/epde/solver/solver.py +++ b/epde/solver/solver.py @@ -16,14 +16,14 @@ def grid_format_prepare(coord_list, mode='NN') -> torch.Tensor: """ - Prepares grid to a general form. Further, formatted grid can be processed - by Points_type.point_typization for 'NN' and 'autograd' methods. - Args: - coord_list: list with coordinates. - mode: Calculation method. (i.e., "NN", "autograd", "mat"). - - Returns: - grid in a general form. + Prepares the coordinate grid into a standardized tensor format suitable for downstream processing within the equation discovery pipeline. This ensures compatibility with various computational methods used for equation fitting. + + Args: + coord_list (list or torch.Tensor): A list of coordinate arrays or a pre-existing coordinate tensor. If a list, each element represents the coordinates along a single dimension. + mode (str, optional): Specifies the method for grid creation. Options include 'NN', 'autograd', and 'mat', each influencing how the grid is constructed. Defaults to 'NN'. + + Returns: + torch.Tensor: A tensor representing the formatted grid. The shape of the tensor depends on the input `coord_list` and the specified `mode`. """ device = device_type() if type(coord_list) == torch.Tensor: @@ -49,7 +49,28 @@ def grid_format_prepare(coord_list, mode='NN') -> torch.Tensor: class Plots(): + """ + A class for plotting solutions obtained from different methods. + + Class Methods: + - __init__: + """ + def __init__(self, model, grid, mode, tol = 0): + """ + Initializes the Plots class, configuring it for visualization and analysis. + + The initialization process involves setting up the model, grid, mode, and tolerance parameters, which are essential for generating meaningful plots and visualizations that reflect the underlying dynamics of the discovered differential equations. + + Args: + model: The discovered equation model to be visualized. + grid: The spatial or temporal grid over which the model is defined. + mode: The plotting mode (e.g., '2D', '3D', 'time_series'). + tol: The tolerance value used for filtering or smoothing the data (default is 0). + + Returns: + None + """ self.model = model self.grid = grid self.mode = mode @@ -57,10 +78,18 @@ def __init__(self, model, grid, mode, tol = 0): def print_nn(self, title: str): """ - Solution plot for NN method. - - Args: - title: title + Solution plot for the learned differential equation by the neural network. + + This method visualizes the solution obtained from the neural network model, + allowing for inspection of the learned behavior across the input grid. + It helps to understand how well the model approximates the true solution + of the differential equation. + + Args: + title (str): The title of the plot. + + Returns: + None: The plot is displayed using matplotlib. """ try: nvars_model = self.model[-1].out_features @@ -97,10 +126,15 @@ def print_nn(self, title: str): def print_mat(self, title): """ - Solution plot for mat method. - - Args: - title: title + Generates a visualization of the discovered equation's solution compared to the provided data. It creates either a 2D scatter plot or a 3D surface plot, depending on the dimensionality of the input data. + + Args: + title (str, optional): The title for the plot. Defaults to None. + + Returns: + None. The function displays the plot using matplotlib.pyplot.show(). + + Why: This method helps to visually assess how well the discovered equation matches the data. By plotting the equation's solution against the data, we can quickly identify areas where the model performs well or poorly, guiding further refinement of the equation discovery process. """ nparams = self.grid.shape[0] @@ -127,10 +161,16 @@ def print_mat(self, title): def dir_path(self, save_dir: str): """ - Path for save figures. - + Generates a file path for saving a plot, creating the necessary directories if they don't exist. + + This method ensures that plots are saved in a structured manner, either in a default 'img' directory or a user-specified directory. + It generates a unique file name based on the current timestamp to prevent overwriting previous plots. + Args: - save_dir: directory where saves in + save_dir (str, optional): The directory where the plot should be saved. If None, the plot is saved to a default 'img' directory. + + Returns: + str: The absolute path to the generated plot file. """ if save_dir == None: try: @@ -152,6 +192,23 @@ def dir_path(self, save_dir: str): def solution_print(self, title=None, solution_print=False, solution_save=False, save_dir=None): + """ + Prints and saves the discovered equation's solution. + + Depending on the representation (`mode`), the solution is printed either as a matrix or a neural network. + This function also handles saving the visualization of the solution to a file and/or displaying it, + allowing for inspection and preservation of the identified equation's behavior. The plot is closed after + display or save. + + Args: + title: The title of the plot (optional). + solution_print: Whether to display the plot (optional, default: False). + solution_save: Whether to save the plot to a file (optional, default: False). + save_dir: The directory to save the plot to (optional). + + Returns: + None. + """ directory = self.dir_path(save_dir) @@ -171,17 +228,23 @@ class Solver(): High-level interface for solving equations. """ + def __init__(self, grid: torch.Tensor, equal_cls, model: Any, mode: str, weak_form: Union[None, list] = None): """ - High-level interface for solving equations. - + Initializes the Solver for discovering differential equations from data. + + This class provides a high-level interface for setting up the problem. + It takes the computational grid, equation definition, neural network model, + calculation mode, and weak form (if applicable) as inputs. + The Solver then uses these components to find the solution of the problem. + Args: - grid: array of a n-D points. - equal_cls: object from input_preprocessing (see input_preprocessing.Equation). - model: neural network. - mode: calculation method. (i.e., "NN", "autograd", "mat"). - weak_form: list of basis functions. + grid (torch.Tensor): Array of n-D points representing the computational domain. + equal_cls: Object defining the equation to be solved. + model: Neural network model used to approximate the solution. + mode (str): Calculation method (e.g., "NN", "autograd", "mat"). + weak_form (list, optional): List of basis functions for weak formulation. Defaults to None. """ self.grid = check_device(grid) self.equal_cls = equal_cls @@ -192,13 +255,14 @@ def __init__(self, grid: torch.Tensor, equal_cls, def optimizer_choice(self, optimizer: str, learning_rate: float) -> \ Union[torch.optim.Adam, torch.optim.SGD, torch.optim.LBFGS]: """ - Setting optimizer. - - Args: - optimizer: optimizer choice (Adam, SGD, LBFGS). - learning_rate: determines the step size at each iteration while moving toward a minimum of a loss function. - Returns: - torch.optimizer object as is. + Sets the optimizer for training the model. The choice of optimizer impacts how the model learns from data and converges to a solution. Different optimizers are suitable for different types of problems and model architectures. + + Args: + optimizer: The name of the optimizer to use ('Adam', 'SGD', or 'LBFGS'). + learning_rate: The learning rate for the optimizer, controlling the step size during optimization. + + Returns: + A torch.optim object (Adam, SGD, or LBFGS) configured with the model parameters and specified learning rate. This optimizer will be used to update the model's weights during the training process, guiding it towards a solution that best fits the data. """ if optimizer == 'Adam': torch_optim = torch.optim.Adam @@ -218,6 +282,21 @@ def optimizer_choice(self, optimizer: str, learning_rate: float) -> \ return optimizer def str_param(self, inverse_param: dict): + """ + Generates a string representation of model parameters that are part of the equation search space. + + This method is used to create a readable representation of the parameters + currently being explored by the evolutionary algorithm. This helps in + tracking the progress of the search and understanding the parameter values + associated with different equation candidates. + + Args: + inverse_param (dict): A dictionary whose keys are parameter names to include in the string. + + Returns: + str: A string containing the name and value of each specified parameter, + formatted as "name=value ", or an empty string if no parameters are found. + """ param = list(inverse_param.keys()) for name, p in self.model.named_parameters(): if name in param: @@ -242,39 +321,50 @@ def solve(self,lambda_operator: Union[float, list] = 1,lambda_bound: Union[float step_plot_save: Union[bool, int] = False, image_save_dir: Union[str, None] = None, tol: float = 0, clear_cache: bool =False, normalized_loss_stop: bool = False, inverse_parameters: dict = None) -> Any: """ - High-level interface for solving equations. - - Args: - lambda_operator: coeff for operator part in loss. - lambda_bound: coeff for boundary part in loss. - lambda_update: enable lambda update. - verbose: detailed info about training process. - learning_rate: determines the step size at each iteration while moving toward a minimum of a loss function. - gamma: multiplicative factor of learning rate decay. - lr_decay: decays the learning rate of each parameter group by gamma every epoch. - eps: arbitrarily small number that uses for loss comparison criterion. - tmax: maximum execution time. - nmodels: number cached models - name: model name if saved. - abs_loss: absolute loss. - use_cache: as is. - cache_dir: directory where saved cache in. - cache_verbose: detailed info about models in cache. - save_always: saves trained model even if the cache is False. - print_every: prints the state of each given iteration to the command line. - cache_model: model that uses in cache - patience:if the loss is less than a certain value, then the counter increases when it reaches the given patience, the calculation stops. - loss_oscillation_window: smth - no_improvement_patience: smth - model_randomize_parameter: creates a random model parameters (weights, biases) multiplied with a given randomize parameter. - optimizer_mode: optimizer choice (Adam, SGD, LBFGS). - step_plot_print: draws a figure through each given step. - step_plot_save: saves a figure through each given step. - image_save_dir: a directory where saved figure in. - tol: float constant, influences on error penalty in casual_loss algorithm. - - Returns: - model. + High-level interface for solving equations, aiming to find the best model representation of the underlying system. + + This method iteratively refines a model by minimizing the loss function, which balances the equation's residual and boundary conditions. + It employs optimization techniques and caching mechanisms to efficiently explore the solution space and identify the optimal model parameters. + The training process can be customized with various parameters, including learning rate, regularization coefficients, and stopping criteria. + + Args: + lambda_operator: Coefficient for the operator part of the loss function, controlling the weight of the equation's residual. Can be a single value or a list for adaptive weighting. + lambda_bound: Coefficient for the boundary part of the loss function, controlling the weight of satisfying boundary conditions. Can be a single value or a list for adaptive weighting. + derivative_points: Number of points to use for derivative calculations. + lambda_update: Enable adaptive update of lambda coefficients during training. + second_order_interactions: Consider second-order interactions between variables in the loss calculation. + sampling_N: Number of sampling points to use during training. + verbose: Level of detail to print during the training process (0 for no output, higher values for more detailed information). + learning_rate: Determines the step size at each iteration while moving toward a minimum of a loss function. + gamma: Multiplicative factor of learning rate decay. + lr_decay: Decays the learning rate of each parameter group by gamma every epoch. + eps: Arbitrarily small number that uses for loss comparison criterion. + tmin: Minimum execution time. + tmax: Maximum execution time. + nmodels: Number of cached models to store. If None, caching is disabled. + name: Model name for saving and loading from cache. + abs_loss: Absolute loss threshold for stopping the training process. + use_cache: Whether to use cached models to initialize the training process. + cache_dir: Directory where cached models are stored. + cache_verbose: Whether to print detailed information about the models in the cache. + save_always: Whether to save the trained model even if caching is disabled. + print_every: Frequency (in iterations) at which to print the training progress. If None, no printing occurs. + cache_model: A pre-trained model to use as the initial model in the cache. + patience: Number of iterations to wait for improvement in loss before stopping the training. + loss_oscillation_window: Window size for detecting loss oscillations. + no_improvement_patience: Number of iterations with no improvement in loss before triggering a model randomization. + model_randomize_parameter: Creates a random model parameters (weights, biases) multiplied with a given randomize parameter. + optimizer_mode: Optimizer choice (Adam, SGD, LBFGS). + step_plot_print: Whether to print a plot of the solution at each given step. Can be a boolean or an integer representing the frequency of plotting. + step_plot_save: Whether to save a plot of the solution at each given step. Can be a boolean or an integer representing the frequency of plotting. + image_save_dir: Directory where to save the plots. + tol: Float constant, influences on error penalty in casual_loss algorithm. + clear_cache: Whether to clear the cache directory before starting the training process. + normalized_loss_stop: Whether to use normalized loss for stopping criterion. + inverse_parameters: Dictionary of parameters to inverse during training. + + Returns: + The trained model. """ print('before Model_prepare:', self.grid.dtype) Cache_class = Model_prepare(self.grid, self.equal_cls, diff --git a/epde/solver/utils.py b/epde/solver/utils.py index d90f34d2..7cfa9b62 100644 --- a/epde/solver/utils.py +++ b/epde/solver/utils.py @@ -10,13 +10,18 @@ from epde.solver.device import check_device def create_random_fn(eps: float) -> callable: - """ Create random tensors to add some variance to torch neural network. - + """ + Creates a function to randomly perturb the weights and biases of linear and convolutional layers in a neural network. + + This randomization helps explore the model space during the evolutionary search for differential equations, + introducing diversity and preventing premature convergence. + Args: - eps (float): randomize parameter. - + eps (float): The magnitude of the random perturbation applied to the weights and biases. + Returns: - callable: creating random params function. + callable: A function that, when applied to a PyTorch module, adds random noise to the weights and biases + of its linear and convolutional layers. """ def randomize_params(m): if (isinstance(m, torch.nn.Linear) or isinstance(m, torch.nn.Conv2d)) and m.bias is not None: @@ -31,17 +36,28 @@ def samples_count(second_order_interactions: bool, sampling_N: int, op_length: list, bval_length: list) -> Tuple[int, int]: - """ Count samples for variance based sensitivity analysis. - + """ + Estimate the number of samples required for sensitivity analysis within the equation discovery process. + + This function calculates the sampling requirements based on the complexity of the search space, + specifically the number of operators and boundary values, and whether second-order interactions + are considered. This is crucial for efficiently exploring the space of possible differential + equations and identifying those that best fit the data. + Args: - second_order_interactions (bool): Calculate second-order sensitivities. - sampling_N (int): essentially determines how often the lambda will be re-evaluated. - op_length (list): operator values length. - bval_length (list): boundary value length. - + second_order_interactions (bool): Flag indicating whether to calculate second-order sensitivities, + which increases the sampling requirements. + sampling_N (int): A scaling factor that determines the base number of samples. It influences + how thoroughly the search space is explored. + op_length (list): List containing the lengths of different operator sets used in the equation + discovery process. + bval_length (list): List containing the lengths of different boundary value sets. + Returns: - sampling_amount (int): overall sampling value. - sampling_D (int): sum of length of grid and boundaries. + Tuple[int, int]: A tuple containing: + - sampling_amount (int): The total number of samples required for the sensitivity analysis. + - sampling_D (int): The sum of the lengths of the operator and boundary value sets, + representing the dimensionality of the search space. """ grid_len = sum(op_length) @@ -57,11 +73,20 @@ def samples_count(second_order_interactions: bool, def lambda_print(lam: torch.Tensor, keys: List) -> None: - """ Print lambda value. - + """ + Prints the values of identified equation coefficients. + + This function displays the optimized coefficients (lambdas) + along with their corresponding equation terms, providing a + readable output of the discovered equation. + Args: - lam (torch.Tensor): lambdas values. - keys (List): types of lambdas. + lam (torch.Tensor): A tensor containing the optimized coefficient values. + keys (List): A list of strings, where each string represents the + corresponding term in the identified equation. + + Returns: + None """ lam = lam.reshape(-1) @@ -73,17 +98,16 @@ def bcs_reshape( bval: torch.Tensor, true_bval: torch.Tensor, bval_length: List) -> Tuple[dict, dict, dict, dict]: - """ Preprocessing for lambda evaluating. - - Args: - bval (torch.Tensor): matrix, where each column is predicted - boundary values of one boundary type. - true_bval (torch.Tensor): matrix, where each column is true - boundary values of one boundary type. - bval_length (list): list of length of each boundary type column. - - Returns: - torch.Tensor: vector of difference between bval and true_bval. + """ + Reshapes the predicted and true boundary values into a format suitable for calculating boundary condition losses. This function prepares the boundary conditions for use in the equation discovery process. + + Args: + bval (torch.Tensor): Predicted boundary values, where each column represents a different boundary type. + true_bval (torch.Tensor): True (observed) boundary values, structured similarly to `bval`. + bval_length (List): A list containing the length of each boundary type column in `bval` and `true_bval`. + + Returns: + torch.Tensor: A concatenated vector representing the difference between predicted and true boundary values for all boundary types. This flattened vector is used to compute the loss associated with boundary condition satisfaction. """ bval_diff = bval - true_bval @@ -95,10 +119,17 @@ def bcs_reshape( def remove_all_files(folder: str) -> None: - """ Remove all files from folder. - + """ + Remove all files and subdirectories from the specified folder. + + This function is used to clean up working directories before or after + equation discovery runs, ensuring a clean environment for each experiment. + Args: - folder (str): folder name. + folder (str): The path to the folder to be cleaned. + + Returns: + None """ for filename in os.listdir(folder): file_path = os.path.join(folder, filename) @@ -112,14 +143,18 @@ def remove_all_files(folder: str) -> None: def mat_op_coeff(equation: Any) -> Any: - """ Preparation of coefficients in the operator of the *mat* method - to suit methods *NN, autograd*. - + """ + Reshapes coefficient tensors within the equation to ensure compatibility with downstream numerical methods and automatic differentiation. + + This function iterates through the terms of the equation and reshapes coefficient tensors to a column vector format (shape (-1, 1)). + This reshaping is crucial for correct matrix operations and gradient calculations within the numerical solvers and automatic differentiation routines used by the EPDE framework. + Callable coefficients are flagged with a warning, as they may interfere with caching mechanisms. + Args: - operator (dict): operator (equation dict). - + equation (Any): The equation object containing a list of operators and their terms. + Returns: - operator (dict): operator (equation dict) with suitable coefficients. + Any: The modified equation object with reshaped coefficient tensors. """ for op in equation.equation_lst: @@ -136,17 +171,18 @@ def mat_op_coeff(equation: Any) -> Any: def model_mat(model: torch.Tensor, domain: Any, cache_model: torch.nn.Module=None) -> Tuple[torch.Tensor, torch.nn.Module]: - """ Create model for *NN or autograd* modes from grid - and model of *mat* mode. - + """ + Creates a neural network model that approximates a pre-computed solution on a grid. + + This function takes a solution (computed using a matrix-based method) and a corresponding grid, and trains a neural network to approximate this solution. This allows for efficient evaluation of the solution at arbitrary points within the domain, as opposed to being limited to the grid points. + Args: - model (torch.Tensor): model from *mat* method. - grid (torch.Tensor): grid from *mat* method. - cache_model (torch.nn.Module, optional): neural network that will - approximate *mat* model. Defaults to None. - + model (torch.Tensor): The pre-computed solution (e.g., from a matrix method). + domain (Domain): The domain on which the solution is defined. Used to build the grid. + cache_model (torch.nn.Module, optional): An existing neural network to refine. If None, a new network is created. Defaults to None. + Returns: - cache_model (torch.nn.Module): model satisfying the *NN, autograd* methods. + torch.nn.Module: A neural network that approximates the provided solution. """ grid = domain.build('mat') input_model = grid.shape[0] @@ -171,12 +207,15 @@ def save_model_nn( model: torch.nn.Module, name: Union[str, None] = None) -> None: """ - Saves model in a cache (uses for 'NN' and 'autograd' methods). - Args: - cache_dir (str): path to cache folder. - model (torch.nn.Module): model to save. - (uses only with mixed precision and device=cuda). Defaults to None. - name (str, optional): name for a model. Defaults to None. + Saves a trained neural network model to the specified cache directory. This allows for later reuse of the trained model, avoiding the need to retrain it from scratch. + + Args: + cache_dir (str): The path to the directory where the model will be saved. + model (torch.nn.Module): The trained neural network model to be saved. + name (str, optional): A custom name for the saved model file. If None, a timestamp-based name will be generated. Defaults to None. + + Returns: + None """ if name is None: @@ -203,14 +242,21 @@ def save_model_mat(cache_dir: str, domain: Any, cache_model: Union[torch.nn.Module, None] = None, name: Union[str, None] = None) -> None: - """ Saves model in a cache (uses for 'mat' method). - + """ + Refines a coarse-grained solution by training a neural network to approximate it, and saves the trained network. + + This allows for efficient evaluation of the solution at arbitrary points within the domain, + overcoming the limitations of the original coarse-grained representation. + Args: - cache_dir (str): path to cache folder. - model (torch.Tensor): *mat* model - grid (torch.Tensor): grid from *mat* mode - cache_model (Union[torch.nn.Module, None], optional): model to save. Defaults to None. - name (Union[str, None], optional): name for a model. Defaults to None. + cache_dir (str): Path to the directory where the trained neural network will be saved. + model (torch.Tensor): The coarse-grained solution (e.g., from a numerical solver). + domain (Any): The domain over which the solution is defined. + cache_model (Union[torch.nn.Module, None], optional): An existing neural network to initialize the training from. Defaults to None. + name (Union[str, None], optional): A name to use when saving the trained neural network. Defaults to None. + + Returns: + None """ net_autograd = model_mat(model, domain, cache_model) @@ -235,10 +281,18 @@ def closure(): save_model_nn(cache_dir, net_autograd, name=name) def replace_none_by_zero(tuple_data: tuple) -> torch.Tensor: - """ Make tensor from tuple (or None element) ad replace None elements to zero. - + """ + Convert a tuple of data, replacing any None elements with zero tensors. + + This ensures that the data structure is compatible with downstream processing + steps that require numerical tensors, particularly when constructing + equation structures. + Args: - tuple_data (tuple): path to cache folder. + tuple_data (tuple | torch.Tensor | None): A tuple, tensor, or None representing the data. + + Returns: + torch.Tensor | tuple: A tensor or tuple with None elements replaced by zero tensors. """ if isinstance(tuple_data, torch.Tensor): tuple_data[tuple_data == None] = 0 @@ -250,32 +304,49 @@ def replace_none_by_zero(tuple_data: tuple) -> torch.Tensor: return tuple_data class PadTransform(Module): - """Pad tensor to a fixed length with given padding value. + """ + Pad tensor to a fixed length with given padding value. - src: https://pytorch.org/text/stable/transforms.html#torchtext.transforms.PadTransform + src: https://pytorch.org/text/stable/transforms.html#torchtext.transforms.PadTransform - Done to avoid torchtext dependency (we need only this function). + Done to avoid torchtext dependency (we need only this function). """ - def __init__(self, max_length: int, pad_value: int) -> None: - """_summary_ - Args: - max_length (int): Maximum length to pad to. - pad_value (int): Value to pad the tensor with. + def __init__(self, max_length: int, pad_value: int) -> None: + """ + Pads sequences to a specified maximum length. + + This ensures consistent input sizes for subsequent processing steps, + which is crucial for algorithms that require fixed-size inputs. + + Args: + max_length (int): Maximum length to pad to. + pad_value (int): Value to pad the tensor with. + + Returns: + None """ super().__init__() self.max_length = max_length self.pad_value = float(pad_value) def forward(self, x: torch.Tensor) -> torch.Tensor: - """ Tensor padding - - Args: - x (torch.Tensor): tensor for padding. - - Returns: - torch.Tensor: filled tensor with pad value. + """ + Pads a tensor to a specified maximum length. + + This ensures that all input tensors have the same length, which is crucial + for consistent processing, especially when dealing with variable-length + sequences or signals. + + Args: + x (torch.Tensor): The input tensor to pad. The padding is applied along the last dimension. + + Returns: + torch.Tensor: The padded tensor. If the input tensor's length along the last dimension + is already equal to or greater than the maximum length, the original tensor is returned. + Otherwise, the tensor is padded with the specified `pad_value` until it reaches the + maximum length. """ max_encoded_length = x.size(-1) diff --git a/epde/structure/Tokens.py b/epde/structure/Tokens.py index d9383816..ca2a7f21 100644 --- a/epde/structure/Tokens.py +++ b/epde/structure/Tokens.py @@ -17,22 +17,44 @@ class Token(ABC): """ A token is an entity that has some meaning in the context - of a given task, and encapsulates information that is sufficient to work with it. + of a given task, and encapsulates information that is sufficient to work with it. """ + @abstractmethod def value(self, grid): """ - Return value of the token in the context of the task. - - Parameters - ---------- - grid: - The grid on which the value is calculated. + Return the token's contribution to the overall equation's value, based on the provided grid data. + + This value represents how well the token fits the observed data within the equation. + + Args: + grid (ndarray): The grid data used to calculate the token's value. + + Returns: + float: The calculated value of the token. """ pass def name(self, with_params=False): + """ + Returns a descriptive name for the token, incorporating parameter information if requested. + + This method aims to provide a user-friendly representation of the token, + which is crucial for interpreting the discovered equation structures. + The name is constructed using available parameters to enhance readability + and understanding of the token's role within the equation. + + Args: + with_params (bool): If True, include parameter information in the name. + + Returns: + str: A string representing the token's name. It prioritizes a + parameter-based name if available, falling back to the type name + (with or without parameters) if necessary. This ensures a + meaningful representation even when parameter information is + incomplete. + """ try: return str(self.params[0]) + self.name_ except: @@ -44,55 +66,63 @@ def name(self, with_params=False): return type(self).__name__ def copy(self): + """ + Creates a deep copy of the token. + + This ensures that modifications to the copied token do not affect the original, + which is crucial for maintaining the integrity of the equation as it evolves + during the equation discovery process. + + Args: + self: The token to be copied. + + Returns: + A new token object that is a deep copy of the original. + """ return deepcopy(self) class TerminalToken(Token): """ - TerminalToken is the token that returns a value as a vector whose evaluating - requaires only numeric parameters. - + TerminalToken represents a fundamental building block in constructing mathematical expressions. It encapsulates a vector of numerical values, serving as a basic operand within a larger equation. Its evaluation relies solely on these pre-defined numeric parameters. """ + def __init__(self, number_params: int = 0, params_description: dict = None, params: np.ndarray = None, cache_val: bool = True, fix_val: bool = False, fix: bool = False, val: np.ndarray = None, type_: str = 'TerminalToken', optimizer: str = None, name_: str = None, mandatory: float = 0, optimize_id: int = None): """ - - Parameters - ---------- - number_params: int - Number of numeric parameters describing the behavior of the token. - params_description: dict - The dictionary of dictionaries for describing numeric parameters of the token. - Must have the form like: - { - parameter_index: dict(name='name', bounds=(min_value, max_value)[, ...]), - ... - } - params: numpy.ndarray - Numeric parameters of the token for calculating its value. - cache_val: bool - If true, token value will be calculated only when its params are changed. Calculated value - is written to the token property 'self.val'. - fix_val: bool - Defined by parameter 'cache_val'. If true, token value returns 'self.val'. - fix: bool - If true, numeric parameters will not be changed by optimization procedures. - val: np.ndarray - Value of the token. - type_: str - Type of the token. - optimizer: str - Optimizer - name_: str - The name of the token that will be used for visualisation results and some comparison operations. - mandatory: float - Unique id for the token. If not zero, the token must be present in the result construct. - optimize_id: int - Used for identifications by optimizers which token to optimize. + Initializes a TerminalToken instance, representing a basic building block for constructing mathematical expressions. + + This token holds numerical parameters and a value, and it can be configured for caching, fixing, and optimization. + The parameters define the token's behavior, and the value represents the result of applying the token's function. + The token can be fixed to prevent changes during optimization or its value can be cached to improve performance. + The token is a fundamental element in the expression tree, contributing to the overall equation being discovered. + + Args: + number_params (int): Number of numeric parameters describing the behavior of the token. + params_description (dict): The dictionary of dictionaries for describing numeric parameters of the token. + Must have the form like: + { + parameter_index: dict(name='name', bounds=(min_value, max_value)[, ...]), + ... + } + params (numpy.ndarray): Numeric parameters of the token for calculating its value. + cache_val (bool): If true, token value will be calculated only when its params are changed. Calculated value + is written to the token property 'self.val'. + fix_val (bool): Defined by parameter 'cache_val'. If true, token value returns 'self.val'. + fix (bool): If true, numeric parameters will not be changed by optimization procedures. + val (np.ndarray): Value of the token. + type_ (str): Type of the token. + optimizer (str): Optimizer + name_ (str): The name of the token that will be used for visualisation results and some comparison operations. + mandatory (float): Unique id for the token. If not zero, the token must be present in the result construct. + optimize_id (int): Used for identifications by optimizers which token to optimize. + + Returns: + None """ self._number_params = number_params if params_description is None: @@ -118,6 +148,19 @@ def __init__(self, number_params: int = 0, params_description: dict = None, para self.optimize_id = optimize_id def copy(self): + """ + Creates a copy of the token. + + This method creates a new token with the same properties as the original, + ensuring that any modifications to the new token's parameters do not affect the original token. + It performs a shallow copy of the token and then deep copies the 'params' attribute. + + Args: + self: The TerminalToken instance to copy. + + Returns: + A new TerminalToken instance that is a copy of the original. + """ new_copy = copy(self) new_copy.params = deepcopy(new_copy.params) return new_copy @@ -125,31 +168,59 @@ def copy(self): # Methods for work with params and its descriptions @property def params_description(self): + """ + Returns the description of the parameters. + This information is essential for constructing valid equation candidates. + + Args: + None + + Returns: + str: The description of the parameters. + """ return self._params_description @params_description.setter def params_description(self, params_description: dict): """ - Params_description is dictionary of dictionaries for describing numeric parameters of the token. - Must have the form like: - { - parameter_index=0: dict(name='name', bounds=(min_value, max_value)[, ...]), - ... - } - Params_description must contain all fields for work in current tokens that will be checked by - method 'self.check_params_description()'. - - Parameters - ---------- - params_description: dict - Dictionary with description for each parameter + Sets the description of the token's numeric parameters. + + This description is crucial for defining the search space of the evolutionary algorithm. + It specifies the name and allowed bounds for each parameter, guiding the optimization process + towards meaningful and physically plausible solutions. + + Args: + params_description (dict): A dictionary where keys are parameter indices and values are dictionaries + describing each parameter. Each parameter dictionary must have a 'name' key and a 'bounds' key, + specifying the parameter's name and its minimum and maximum allowed values, respectively. + Example: + { + 0: {'name': 'diffusion_coefficient', 'bounds': (0.0, 1.0)}, + 1: {'name': 'reaction_rate', 'bounds': (-10.0, 10.0)} + } + + Returns: + None """ assert isinstance(params_description, dict) self._params_description = params_description def check_params_description(self): """ - Check params_description for requirements for current token. + Validates the structure and content of the parameters description dictionary. + + This method ensures that the `params_description` attribute, which defines the characteristics of each parameter + associated with the current token, adheres to the expected format. It verifies that the description is a dictionary + containing nested dictionaries for each parameter, and that each nested dictionary includes the required 'name' and + 'bounds' keys with valid values. This validation is crucial for the proper functioning of the equation discovery + process, as it guarantees that the parameters are well-defined and can be effectively used in the evolutionary + search for the best-fitting equation. + + Args: + self: The instance of the TerminalToken class. + + Returns: + None. Raises an AssertionError if the `params_description` is invalid. """ recomendations = "\nUse methods 'params_description.setter' or 'set_descriptor' to change params_descriptions" assert isinstance(self._params_description, dict), "Invalid params_description structure," \ @@ -170,18 +241,71 @@ def check_params_description(self): " boundaries MIN <= MAX." + recomendations def set_descriptor(self, key: int, descriptor_name: str, descriptor_value): + """ + Sets a specific descriptor for a parameter at the given index. + + This allows modification of parameter properties, which is crucial for + fine-tuning the equation discovery process by providing additional + information about the parameters. If the parameter or descriptor + does not exist, an error message is printed. + + Args: + key (int): The index of the parameter to modify. + descriptor_name (str): The name of the descriptor to set (e.g., 'type', 'value'). + descriptor_value: The value to assign to the descriptor. + + Returns: + None: This method modifies the internal state of the TerminalToken object. + """ try: self._params_description[key][descriptor_name] = descriptor_value except KeyError: print('There is no parameter with such index/descriptor') def get_key_use_params_description(self, descriptor_name: str, descriptor_value): + """ + Retrieves a key from the `_params_description` dictionary based on a descriptor name and value. + + This method is used to locate the appropriate parameter key within the token's + description based on specific descriptor criteria. This allows the system to dynamically + access and utilize token parameters based on their associated descriptors, + facilitating the automated equation discovery process. + + Args: + descriptor_name: The name of the descriptor to search for within the parameter descriptions. + descriptor_value: The value that the specified descriptor should match. + + Returns: + The key associated with the matching descriptor in the `_params_description` dictionary. + + Raises: + KeyError: If no key is found in the `_params_description` dictionary with the specified descriptor name and value. + """ for key, value in self._params_description.items(): if value[descriptor_name] == descriptor_value: return key raise KeyError() def get_descriptor_foreach_param(self, descriptor_name: str) -> list: + """ + Extract descriptor values for each parameter associated with this terminal token. + + This method retrieves specific descriptor values for each parameter + defined within the token's parameter descriptions. It's used to access + parameter properties, which are later utilized in equation construction + and evaluation. + + Args: + descriptor_name (str): The name of the descriptor to retrieve. + This specifies which property of each parameter + is to be extracted (e.g., 'type', 'range'). + + Returns: + list: A list containing the descriptor value for each parameter. + The list maintains the parameter order, ensuring that each + element corresponds to the descriptor value of the + corresponding parameter. + """ ret = [None for _ in range(self._number_params)] for key, value in self._params_description.items(): ret[key] = value[descriptor_name] @@ -189,15 +313,66 @@ def get_descriptor_foreach_param(self, descriptor_name: str) -> list: @property def params(self): + """ + Returns the parameters associated with this terminal token. + + These parameters define the specific characteristics of the token, + allowing it to be adapted and optimized during the equation discovery process. + + Returns: + dict: The parameters of the terminal token. + """ return self._params @params.setter def params(self, params): + """ + Sets the parameter vector for the terminal token. + + This method updates the internal parameter vector of the terminal token, + allowing its behavior to be adjusted during the equation discovery process. + The parameters are essential for the token to accurately represent a specific + mathematical function or value within the discovered equation. + + Args: + params (list or array): A list or array of parameter values. The length + of the input array must match the expected number of parameters + for this terminal token. + + Returns: + None. + + Raises: + AssertionError: If the length of the input `params` array does not + match the expected number of parameters for this token. + + Class Fields Initialized/Modified: + _params (np.ndarray): The parameter vector, initialized or updated + from the input `params`. + _fix_val (bool): A flag indicating whether the parameter values are + fixed, set to False, indicating that the parameters can be + modified during the equation discovery process. + """ assert len(params) == self._number_params, "Input array has incorrect size" self._params = np.array(params, dtype=float) self._fix_val = False def check_params(self): + """ + Checks and adjusts the parameter values to be within their defined bounds, ensuring the token's configuration remains valid. + + This method verifies that the number of provided parameters matches the + expected number and then iterates through each parameter, ensuring that + its value falls within the specified lower and upper bounds defined in + the `_params_description` dictionary. If a parameter exceeds its bounds, + it is clipped to the corresponding boundary value. This is crucial for maintaining the integrity of the equation discovery process by preventing tokens from operating with out-of-range parameters, which could lead to invalid or nonsensical equation structures. + + Args: + self: The instance of the class. + + Returns: + None. The method modifies the `_params` attribute in place. + """ recomendations = "\nUse methods 'params.setter' or 'set_param' to change params" assert len( self._params) == self._number_params, "The number of parameters does not match the length of params array" + recomendations @@ -208,6 +383,22 @@ def check_params(self): self._params[key] = value['bounds'][0] def param(self, name=None, idx=None): + """ + Accesses a parameter of the terminal token either by its name or index within the token's parameter list. + + This is useful for retrieving specific components of a terminal token, + allowing for manipulation or evaluation within the evolutionary process + of discovering differential equations. It prioritizes access by name, + falling back to index-based access if no name is provided. + + Args: + name (str, optional): The name of the parameter to retrieve. Defaults to None. + idx (int, optional): The index of the parameter to retrieve. Defaults to None. + + Returns: + The parameter at the specified index or with the specified name. + Returns None if the parameter is not found, and prints an error message. + """ try: idx = idx if name == None else self.get_key_use_params_description('name', name) except KeyError: @@ -218,6 +409,21 @@ def param(self, name=None, idx=None): print('There is no parameter with this index') def set_param(self, param, name=None, idx=None): + """ + Sets the value of a parameter, updating the token's internal state. This is crucial for modifying the equation being built and evaluated during the evolutionary search process. By changing parameter values, the algorithm explores different equation configurations to find the best fit to the data. + + Args: + param: The new value for the parameter. + name: The name of the parameter to set (optional). + idx: The index of the parameter to set (optional). + + Returns: + None. + + Raises: + KeyError: If a parameter with the given name does not exist. + IndexError: If a parameter with the given index does not exist. + """ try: idx = idx if name is None else self.get_key_use_params_description('name', name) except KeyError: @@ -229,6 +435,26 @@ def set_param(self, param, name=None, idx=None): raise IndexError('"{}" have no parameter with index "{}"'.format(self, idx)) def init_params(self): + """ + Initializes the token's parameters with random values based on their defined bounds. + + This ensures each token starts with a diverse set of configurations, + facilitating a broader exploration of the search space during the + equation discovery process. By randomly initializing parameters within + their allowed ranges, the algorithm can effectively explore different + equation forms and parameter combinations. + + Args: + self: The object instance. + + Raises: + OverflowError: If the parameter bounds defined in `_params_description` + are invalid (e.g., infinite or lead to numerical overflow) during + random number generation. + + Returns: + None. + """ try: for key, value in self._params_description.items(): self.set_param(np.random.uniform(value['bounds'][0], value['bounds'][1]), idx=key) @@ -237,21 +463,34 @@ def init_params(self): raise OverflowError('Bounds have incorrect/infinite values') def set_val(self, val): + """ + Sets the terminal token's value. + + This value is used during the equation discovery process to represent a specific constant or variable. + + Args: + val: The new value to assign to the token. This could represent a numerical constant or a symbolic variable. + + Returns: + None. The method updates the internal state of the TerminalToken instance. + """ self.val = val def value(self, grid): """ - Returns value of the token on the grid. - Returns either cache result in self.val or calculated value in self.val by method self.evaluate(). - - Parameters - ---------- - grid: np.ndarray - Grid for evaluation. - - Returns - ------- - Value of the token. + Returns the value of this terminal token, computed based on the input grid. + + The value is either retrieved from the cache (if available and caching is enabled) or computed + by evaluating the token's parameters against the grid. This ensures that each token contributes + meaningfully to the overall equation being discovered. + + Args: + grid (np.ndarray): The grid on which to evaluate the token. This grid represents the independent + variable space over which the equation is defined. + + Returns: + np.ndarray: The value of the token, with the same shape as the input grid. This value represents + the token's contribution to the equation's solution at each point on the grid. """ if not self._fix_val or self.val is None: # self.check_params() @@ -265,20 +504,20 @@ def value(self, grid): @staticmethod def evaluate(params, grid): """ - Calculating token value on the grid depending on parameters. - Must be override/implement in each TerminalToken. - May be not staticmethod if it is necessary. - - Parameters - ---------- - params: numpy.ndarray - Numeric token parameters. - grid: numpy.ndarray - Grid for evaluation. - - Returns - ------- - numpy.ndarray + Calculates the contribution of this token to the overall equation's value on the given grid. + + This method determines the token's effect based on its parameters and the grid's values. + It is designed to be overridden in subclasses representing specific terminal tokens + to implement their unique evaluation logic. The result represents a component of the + overall equation's solution, enabling the evolutionary algorithm to explore different + equation structures. + + Args: + params (numpy.ndarray): Numeric token parameters that influence its value. + grid (numpy.ndarray): The grid of independent variable values over which to evaluate the token. + + Returns: + numpy.ndarray: A numerical array representing the token's evaluated value at each point on the grid. """ return np.zeros(grid.shape) @@ -286,23 +525,39 @@ def evaluate(params, grid): class ComplexToken(TerminalToken): """ ComplexToken is the Token which consists other tokens (as subtokens in property self.subtokens) - in addition to the numeric parameters. - Example: Product of TerminalTokens. + in addition to the numeric parameters. + Example: Product of TerminalTokens. """ + def __init__(self, number_params: int = 0, params_description: dict = None, params: np.ndarray = None, cache_val: bool = True, fix_val: bool = False, fix: bool = False, val: np.ndarray = None, type_: str = 'TerminalToken', optimizer: str = None, name_: str = None, mandatory: float = 0, optimize_id: int = None, subtokens: list = None): """ - - Parameters - ---------- - See documentation TerminalToken.__init__.__doc__ - - subtokens: list - List of other tokens which current token uses for calculating its value. + Initializes a ComplexToken, which represents a token composed of other tokens. + + This class extends TerminalToken and incorporates the concept of subtokens, enabling the construction of more complex expressions. + The ComplexToken relies on its subtokens to compute its value, allowing for a hierarchical representation of mathematical expressions. + + Args: + number_params (int, optional): The number of parameters associated with this token. Defaults to 0. + params_description (dict, optional): A dictionary describing the parameters. Defaults to None. + params (np.ndarray, optional): An array of parameter values. Defaults to None. + cache_val (bool, optional): Whether to cache the computed value. Defaults to True. + fix_val (bool, optional): Whether the value should be fixed. Defaults to False. + fix (bool, optional): Whether the token is fixed during optimization. Defaults to False. + val (np.ndarray, optional): The initial value of the token. Defaults to None. + type_ (str, optional): The type of the token. Defaults to 'TerminalToken'. + optimizer (str, optional): The optimizer to use for this token. Defaults to None. + name_ (str, optional): The name of the token. Defaults to None. + mandatory (float, optional): A mandatory value associated with the token. Defaults to 0. + optimize_id (int, optional): An ID used during optimization. Defaults to None. + subtokens (list, optional): A list of tokens that this token depends on. Defaults to None. + + Returns: + None """ super().__init__(number_params=number_params, params_description=params_description, params=params, cache_val=cache_val, fix_val=fix_val, fix=fix, @@ -314,10 +569,38 @@ def __init__(self, number_params: int = 0, params_description: dict = None, para @property def subtokens(self): + """ + Returns the subtokens of the complex token. These subtokens represent the building blocks + from which the complex token is constructed, enabling a hierarchical representation + of mathematical expressions. + + Args: + None + + Returns: + list: The subtokens of the token. + + Why: Accessing subtokens allows for the analysis and manipulation of the token's internal structure, + which is crucial for tasks such as equation simplification, term rewriting, and symbolic computation + within the equation discovery process. + """ return self._subtokens @subtokens.setter def subtokens(self, subtokens: list): + """ + Sets and configures the subtokens, preparing them for equation discovery. + + This method initializes the 'Amplitude' parameter for each subtoken, which is crucial for the evolutionary search process. + By setting this parameter, the framework ensures that each subtoken is properly weighted and considered during the equation discovery process. + The method also disables a flag and performs a mandatory check to maintain the internal state and consistency of the object. + + Args: + subtokens (list): A list of subtokens to be processed. These subtokens represent the building blocks of potential equation terms. + + Returns: + None: This method modifies the internal state of the object. + """ for token in subtokens: token.set_param(1, name='Amplitude') self._fix_val = False @@ -325,27 +608,72 @@ def subtokens(self, subtokens: list): self._check_mandatory() def add_subtoken(self, token): + """ + Adds a subtoken to the list, marking the parent token as unfixed. + + This is done to incorporate the new subtoken into the equation discovery process, + allowing the evolutionary algorithm to further refine the equation structure. + The subtoken's 'Amplitude' parameter is also initialized. + + Args: + token: The subtoken to add. + + Returns: + None. + """ token.set_param(1, name='Amplitude') self._fix_val = False self.subtokens.append(token) self._check_mandatory() def set_subtoken(self, token, idx): + """ + Sets a subtoken at a specific index, enabling equation discovery. + + This method sets the 'Amplitude' parameter for the given token, marks the value as + non-fixed to allow for optimization during the equation search, and assigns the token to the specified index in the `subtokens` list. + Finally, it checks if all mandatory subtokens are present to ensure the equation structure is complete enough for evaluation. + + Args: + token: The token to be set as a subtoken. + idx: The index at which to set the subtoken. + + Returns: + None. + + Why: + This method is crucial for constructing candidate equation structures. By setting subtokens at specific indices, it allows the evolutionary algorithm to explore different combinations of terms and operators, ultimately leading to the discovery of the underlying differential equation. + """ token.set_param(1, name='Amplitude') self._fix_val = False self.subtokens[idx] = token self._check_mandatory() def del_subtoken(self, token): + """ + Removes a subtoken from the token's list of subtokens. + + This operation is essential for refining the token structure during the evolutionary search for the optimal equation. By removing less relevant subtokens, the algorithm can explore simpler and more accurate equation candidates. + + Args: + token: The subtoken to remove. + + Returns: + None. + """ self.subtokens.remove(token) def _check_mandatory(self): """ - If some subtoken in ComplexToken is mandatory then ComplexToken is mandatory too. - - Returns - ------- - + Marks the complex token as mandatory if any of its sub-tokens are mandatory. + + This ensures that the evolutionary algorithm prioritizes tokens containing essential components. + + Args: + self: The ComplexToken instance. + + Returns: + None. Modifies the `mandatory` attribute of the ComplexToken instance. """ for subtoken in self.subtokens: if subtoken.mandatory != 0: diff --git a/epde/structure/encoding.py b/epde/structure/encoding.py index 02e9e43a..f514ef95 100644 --- a/epde/structure/encoding.py +++ b/epde/structure/encoding.py @@ -14,7 +14,64 @@ class Gene(object): + """ + Represents a gene with a key, value, and value type. + + Class Methods: + - __init__ + - value + - value + - __eq__ + - set_metaparam + - __deepcopy__ + + Attributes: + key: The key or name of the gene. + val_type: The expected type of the gene's value. + value: The current value of the gene. + + Class Methods: + - __init__: + Initializes a new Gene object. + + Args: + key: The key or name of the gene. + value: The initial value of the gene (optional). + value_type: The expected type of the gene's value (optional). + + Raises: + ValueError: If both value and value_type are None. + ValueError: If the type of value does not match value_type. + + Returns: + None. + + Class Fields: + key (str): The key or name of the gene. + val_type (type): The expected type of the gene's value. + value (any): The current value of the gene. + """ + def __init__(self, key, value=None, value_type=None): + """ + Initializes a new Gene object, representing a parameter within an equation. + + This method sets up a gene with a specific key (name), an initial value, and a data type. + The value and value_type are crucial for defining the search space and constraints during the evolutionary process. + The type checking ensures that the gene's value adheres to the expected data type, maintaining consistency and validity within the equation. + + Args: + key (str): The key or name of the gene. + value (any, optional): The initial value of the gene. Defaults to None. + value_type (type, optional): The expected type of the gene's value. Defaults to None. + + Raises: + ValueError: If both value and value_type are None. + ValueError: If the type of value does not match value_type. + + Returns: + None + """ if value is None and value_type is None: raise ValueError( 'Both value and value type can not be None during gene initialization') @@ -28,12 +85,40 @@ def __init__(self, key, value=None, value_type=None): @property def value(self): + """ + Gets the value of the gene. + + This value represents a specific parameter or component within the equation being evolved. Accessing it allows the evolutionary algorithm to evaluate the fitness and contribution of this gene to the overall equation. + + Raises: + ValueError: If the value has not been set, indicating an uninitialized gene. + + Returns: + The value of the gene. + """ if self._value is None: raise ValueError('The value of gene was not set before its call') return self._value @value.setter def value(self, val): + """ + Sets the value of the gene, ensuring it conforms to the expected data type. + + This is crucial for maintaining the integrity of the equation being evolved, + as each gene represents a specific parameter or component within the equation. + Setting the correct value type ensures that the evolutionary process explores + valid equation structures. + + Args: + val: The new value to set for the gene. + + Raises: + TypeError: If the provided value is not of the expected type. + + Returns: + None. + """ if isinstance(val, self.val_type): self._value = val else: @@ -41,16 +126,60 @@ def value(self, val): f'Value of the incorrect type {type(val)} was passed into a gene of {self.val_type}') def __eq__(self, other): + """ + Compares this gene with another to determine if they represent the same element. + + This comparison is crucial for evolutionary operations like crossover and mutation, + ensuring that genetic material is combined and modified in a meaningful way + by checking if the key-value pairs are identical. + + Args: + other: The other gene to compare against. + + Returns: + bool: True if both the key and value of the genes are equal, False otherwise. + """ try: return self.key == other.key and self.value == other.value except AttributeError: warnings.warn(f'The equality method is not implemented for object of type {type(self)} or {type(other)}') def set_metaparam(self, key: str, value: Union[float, int]): + """ + Sets the value of a specified metaparameter for the gene. + + This allows fine-grained control over the gene's behavior during the evolutionary process, influencing how it adapts to the data. + + Args: + key: The name of the metaparameter to set. + value: The new value for the metaparameter. + + Returns: + None. + """ assert key in self._value.metaparameters, f'Incorrect parameter key {key} passed into the gene, containing {self.key}' self._value.metaparameters[key]['value'] = value def __deepcopy__(self, memo): + """ + Creates a deep copy of the Gene object. + + This method is essential for maintaining the integrity of the population + during the evolutionary process. By creating a new instance with copied + attributes, we ensure that modifications to a Gene in one generation do + not inadvertently affect other Genes or previous generations. This is + crucial for the evolutionary algorithm to explore the search space + effectively and avoid unintended consequences due to shared object + references. + + Args: + self: The Gene object to be copied. + memo: A dictionary used by `deepcopy` to prevent infinite recursion + when copying objects with circular references. + + Returns: + A deep copy of the Gene object. + """ cls = self.__class__ result = cls.__new__(cls) memo[id(self)] = result @@ -60,22 +189,50 @@ def __deepcopy__(self, memo): class Chromosome(object): + """ + Represents a chromosome in an evolutionary algorithm. + + The chromosome stores a collection of equations and provides methods for + manipulating and comparing them. + + Class Methods: + - __init__ + - replace_gene + - pass_parametric_gene + - __eq__ + - __getitem__ + - __len__ + - text_form + - hash_descr + - __iter__ + - same_encoding + - __deepcopy__ + + Attributes: + equations (dict of epde.strucutre.main_structures.Equation objects): List of equation, that form the chromosome. + """ - def __init__(self, equations, params): - ''' - - Parameters - ---------- - equations : dict of epde.strucutre.main_structures.Equation objects - List of equation, that form the chromosome. - **kwargs : TYPE - DESCRIPTION. - - Returns - ------- - None. - ''' + def __init__(self, equations, params): + """ + Initializes a Chromosome object with a set of equations and parameters. + + The chromosome is a collection of genes, where each gene represents either an equation or a parameter. + This structure facilitates the evolutionary process by allowing for the manipulation and optimization of both + the equation structure and its associated parameters. + + Args: + equations (dict of epde.strucutre.main_structures.Equation objects): + A dictionary of equations, where keys are equation identifiers and values are Equation objects. + These equations form the basis of the chromosome and represent potential solutions. + params (dict): + A dictionary of parameters, where keys are parameter identifiers and values are dictionaries + containing parameter information (e.g., 'value'). These parameters are also incorporated into the + chromosome as genes, allowing for their optimization during the evolutionary process. + + Returns: + None + """ # print('equations', equations) self.equation_type = type(next(iter(equations))) @@ -89,24 +246,29 @@ def __init__(self, equations, params): self.chromosome[key] = Gene(key=key, value=arg['value']) def replace_gene(self, gene_key, value): + """ + Replaces the value of a specific gene in the chromosome. This is a crucial step in the evolutionary process, allowing the algorithm to explore different equation structures by modifying the parameters and operators within the chromosome. + + Args: + gene_key: The key of the gene to replace. + value: The new value for the gene. + + Returns: + None + """ self.chromosome[gene_key].value = value def pass_parametric_gene(self, key, value): - ''' - - Parameters - ---------- - key : tuple of format (parameter_name, variable_name) of 'str', or 'str' for parameter_name, - The key, encoding with a tuple, dedicated to the altered metaparametric gene of the chromosome. - First element of the key is the label of the parameter, while the second is the name of the parameter - value : float or integer, - The value, which is replacing the previous gene value. - - Returns - ------- - None. - - ''' + """ + Updates a specific parameter within the chromosome's genes. This method facilitates the modification of equation parameters during the evolutionary search process, allowing the algorithm to explore different equation configurations. + + Args: + key (tuple of str or str): Specifies the parameter to be updated. If a tuple, it should be in the format (parameter_name, variable_name). If a string, it represents the parameter name. + value (float or int): The new value for the specified parameter. + + Returns: + None + """ if isinstance(key, str) and (key in self.chromosome[np.random.choice(self.equation_keys)]): for eq_name in self.equation_keys: self.chromosome[eq_name].set_metaparam(key=key, value=value) @@ -117,22 +279,86 @@ def pass_parametric_gene(self, key, value): 'Incorrect value passed into genes parameters setting.') def __eq__(self, other): + """ + Compares two `Chromosome` objects for equality. + + This comparison is crucial for the evolutionary process, ensuring that identical chromosomes are recognized and redundant solutions are avoided, thus maintaining diversity within the population. + + Args: + other (Chromosome): The `Chromosome` object to compare with. + + Returns: + bool: True if the two `Chromosome` objects are equal, False otherwise. + """ if set(self.chromosome.keys()) != set(self.chromosome.keys()): return False return all([self.chromosome[key] == other.chromosome[key] for key in self.chromosome.keys()]) def __getitem__(self, key): + """ + Retrieves the value of a gene at a specific index within the chromosome's genetic sequence. + + This enables access to individual components of a candidate equation, + allowing for evaluation and manipulation during the evolutionary process. + + Args: + key: The index of the gene to retrieve. + + Returns: + The value of the gene (token) at the specified index. + """ return self.chromosome[key].value def __len__(self): + """ + Returns the number of equation keys in the chromosome. + + This represents the complexity of the equation encoded by this chromosome. + A chromosome with more equation keys potentially represents a more complex equation. + + Args: + self: The Chromosome instance. + + Returns: + int: The number of equation keys. + """ return len(self.equation_keys) @property def text_form(self): + """ + Presents the chromosome's genetic information as a list of key-value pairs. + + This representation is useful for inspecting the chromosome's composition and + facilitates its interpretation within the evolutionary process. Each key represents + a specific gene location, and the corresponding value reflects the gene's current state. + + Args: + None + + Returns: + list: A list of tuples, where each tuple contains a key (gene identifier) + from the chromosome and its corresponding gene value. + """ return [(key, gene.value) for key, gene in self.chromosome.items()] @property def hash_descr(self): + """ + Returns a hashable description of the chromosome. + + This method generates a tuple of hashable values representing the genes within the chromosome, + allowing for efficient comparison and caching of chromosome states during the evolutionary process. + It attempts to use the `hash_descr` method of gene values if available, otherwise it returns the gene's + value directly if it's a simple type (int, float, str, tuple). This ensures that even complex gene + representations can be efficiently compared. + + Args: + self: The object instance. + + Returns: + tuple: A tuple containing the hashable descriptions of the genes in the chromosome. Returns None for a gene if its value does not have a `hash_descr` attribute and is not a simple type. + """ def get_gene_hash(gene): if isinstance(gene.val_type, (int, float, str, tuple)): return gene.value @@ -145,9 +371,32 @@ def get_gene_hash(gene): return tuple(map(get_gene_hash, self.chromosome.values())) def __iter__(self): + """ + Returns an iterator for comparing chromosomes by equality. + + This iterator facilitates the comparison of chromosomes, enabling the evolutionary algorithm to assess the diversity and convergence of the population during the equation discovery process. + + Returns: + ChromosomeEqIterator: An iterator that allows comparing chromosomes for equality. + """ return ChromosomeEqIterator(self) def same_encoding(self, other): + """ + Checks if two chromosomes are structurally compatible for genetic operations. + + This method determines if two chromosomes can undergo crossover or mutation + by ensuring they possess the same set of genes (keys). This is crucial for + maintaining the validity and consistency of the population during the + evolutionary process, as it ensures that genetic material is exchanged + between compatible individuals. + + Args: + other: The other chromosome to compare with. + + Returns: + bool: True if both chromosomes have the same keys, False otherwise. + """ cond_1 = all([key in other.chromosome.keys() for key in self.chromosome.keys()]) cond_2 = all([key in self.chromosome.keys() @@ -159,17 +408,78 @@ def __deepcopy__(self, memo): # TODO: overload result = cls.__new__(cls) memo[id(self)] = result for k, v in self.__dict__.items(): + """ + Creates a deep copy of the chromosome. + + This method is essential for evolutionary algorithms, where chromosomes + (representing potential solutions) need to be duplicated without + affecting the original. It creates a new chromosome instance and + copies all attributes from the original chromosome to the new one + using deepcopy. This ensures that all nested objects within the + chromosome are also copied, preventing unintended modifications to + the original chromosome during evolutionary operations like crossover + or mutation. The `memo` dictionary is crucial to prevent infinite + recursion when copying complex, potentially self-referential chromosome + structures. + + Args: + self: The chromosome object to be copied. + memo: A dictionary used to keep track of objects that have + already been copied during the deep copy process, to prevent + infinite recursion. + + Returns: + The deep copy of the chromosome. + """ setattr(result, k, deepcopy(v, memo)) return result class ChromosomeEqIterator(object): + """ + An iterator for traversing equation keys within a chromosome. + + Class Methods: + - __init__: + """ + def __init__(self, chromosome): + """ + Initializes the EquationKeysIterator for traversing equation keys. + + This iterator facilitates sequential access to the equation keys within a chromosome, + enabling the evolutionary algorithm to explore different equation structures. + + Args: + chromosome (Chromosome): The chromosome whose equation keys are to be iterated. + + Returns: + None + + Class Fields: + _chromosome (Chromosome): The chromosome being iterated over. + _idx (int): The current index of the iterator. + _chromosome_equation_labels (list): A list of equation keys from the chromosome. + """ self._chromosome = chromosome self._idx = 0 self._chromosome_equation_labels = list(self._chromosome.equation_keys) def __next__(self): + """ + Returns the next gene in the chromosome. + + Iterates through the chromosome based on the order of equation labels to ensure genes are accessed in a consistent and meaningful way during the evolutionary process. This ordered access is crucial for evaluating the fitness of the chromosome and guiding the search for optimal equation structures. + + Args: + self: The object instance. + + Returns: + The next gene in the chromosome. + + Raises: + StopIteration: If the end of the chromosome is reached. + """ if self._idx < len(self._chromosome_equation_labels): res = self._chromosome[self._chromosome_equation_labels[self._idx]] self._idx += 1 diff --git a/epde/structure/factor.py b/epde/structure/factor.py index 1b90232a..0b3fb357 100644 --- a/epde/structure/factor.py +++ b/epde/structure/factor.py @@ -22,19 +22,21 @@ class EvaluatorContained(object): """ - Class for evaluator of token (factor of the term in the sought equation) values with arbitrary function - - Attributes: - _evaluator (`callable`): a function, which returns the vector of token values, evaluated on the studied area; - params (`dict`): dictionary, containing parameters of the evaluator (like grid, on which the function is evaluated or matrices of pre-calculated function) - - Methods: - set_params(**params) - set the parameters of the evaluator, using keyword arguments - apply(token, token_params) - apply the defined evaluator to evaluate the token with specific parameters + Class for evaluating the values of tokens (factors within terms of a candidate equation). This evaluator supports the use of arbitrary functions in the equation. + + + Attributes: + _evaluator (`callable`): a function, which returns the vector of token values, evaluated on the studied area; + params (`dict`): dictionary, containing parameters of the evaluator (like grid, on which the function is evaluated or matrices of pre-calculated function) + + Methods: + set_params(**params) + set the parameters of the evaluator, using keyword arguments + apply(token, token_params) + apply the defined evaluator to evaluate the token with specific parameters """ + def __init__(self, eval_function): # , eval_kwargs_keys={} self._evaluator = eval_function # self.eval_kwargs_keys = eval_kwargs_keys @@ -44,6 +46,23 @@ def apply(self, token, structural=False, func_args=None, torch_mode=False): # , Apply the defined evaluator to evaluate the token with specific parameters. Args: + """ + Apply the evaluator to a given symbolic token. + + This method serves as an interface to the underlying evaluator, allowing the application of specific functions or operations represented by the token. + + Args: + token (`epde.main_structures.factor.Factor`): The symbolic representation of the token to be evaluated. + structural (`bool`): A flag indicating whether the evaluation is part of a structural analysis. Defaults to False. + func_args (`object`): Additional arguments to be passed to the evaluator function. Defaults to None. + torch_mode (`bool`): A flag indicating whether to use torch mode. Defaults to False. + + Returns: + `Any`: The result of applying the evaluator to the token. + + Raises: + `TypeError`: If the evaluator cannot be applied to the provided token. + """ token (`epde.main_structures.factor.Factor`): symbolic label of the specific token, e.g. 'cos'; token_params (`dict`): dictionary with keys, naming the token parameters (such as frequency, axis and power for trigonometric function) and values - specific values of corresponding parameters. @@ -57,6 +76,38 @@ def apply(self, token, structural=False, func_args=None, torch_mode=False): # , class Factor(TerminalToken): + """ + Represents a factor in an equation. + + The Factor class encapsulates the properties and behavior of a factor within an equation, + including its label, family type, associated variable, status, and evaluation logic. + + Class Methods: + - __init__ + - variable + - manual_reconst + - ann_representation + - predict_with_ann + - reset_saved_state + - status + - set_parameters + - __eq__ + - partial_equlaity + - evaluator + - evaluate + - cache_label + - name + - latex_name + - hash_descr + - grids + - use_grids_cache + - __deepcopy__ + - use_cache + + Class Attributes: + - __slots__ + """ + __slots__ = ['_params', '_params_description', '_hash_val', '_latex_constructor', 'label', 'ftype', '_variable', '_all_vars', 'grid_set', 'grid_idx', 'is_deriv', 'deriv_code', 'cache_linked', '_status', 'equality_ranges', '_evaluator', 'saved'] @@ -64,6 +115,48 @@ class Factor(TerminalToken): def __init__(self, token_name: str, status: dict, family_type: str, latex_constructor: Callable, variable: str = None, all_vars: list = None, randomize: bool = False, params_description=None, deriv_code=None, equality_ranges = None): + """ + Initializes a `Factor` object, representing a term within a differential equation. + + This constructor sets up the `Factor` with its symbolic representation (`token_name`), + family type, and any associated variables. It prepares the `Factor` for use in the + equation discovery process, including potential randomization of parameters and caching + of computed values to improve efficiency. The initialization also determines if the factor + represents a derivative, storing the derivative code if applicable. + + Args: + token_name: The symbolic name of this factor (e.g., 'u', 'du/dx'). + status: A dictionary containing status information about the factor, such as whether it requires a grid. + family_type: The type of family this factor belongs to (e.g., polynomial, trigonometric). + latex_constructor: A callable that converts the factor into a LaTeX representation for display. + variable: An optional variable associated with the factor (e.g., 'x' in 'du/dx'). Defaults to None. + all_vars: An optional list of all variables in the equation. Defaults to None. + randomize: A boolean indicating whether to randomize the factor's parameters. Defaults to False. + params_description: Description of parameters for randomization, including ranges and distributions. + deriv_code: Code for calculating the derivative of this factor, if it represents a derivative. + equality_ranges: Ranges for equality constraints on the parameters. + + Returns: + None + + Class Fields: + label: The name of the token (str). + ftype: The type of family (str). + _variable: An optional variable associated with the object (str). + _all_vars: An optional list of all variables (list). + status: A dictionary containing status information (dict). + grid_set: A boolean indicating whether the grid is set (bool). Initialized to False. + _hash_val: A random integer used for hashing (int). + _latex_constructor: A callable used to construct LaTeX representations (Callable). + is_deriv: A boolean indicating whether the object represents a derivative (bool). + deriv_code: Code for derivative calculation (any). + cache_linked: A boolean indicating whether the cache is linked (bool). + + The Factor class represents a building block of a differential equation. The initialization process prepares + it for symbolic manipulation, numerical evaluation, and integration within the equation discovery workflow. + Caching and parameter randomization are key aspects of this initialization, enabling efficient exploration + of the solution space. + """ self.label = token_name self.ftype = family_type self._variable = variable @@ -93,12 +186,39 @@ def __init__(self, token_name: str, status: dict, family_type: str, latex_constr @property def variable(self): + """ + Return the `Variable` associated with this `Factor`. + + If a specific `Variable` has been assigned, it is returned. Otherwise, the default feature type (`ftype`) is returned. This ensures that a valid feature representation is always available for use in equation construction and evaluation. + + Args: + self: The `Factor` instance. + + Returns: + The `Variable` or `ftype` associated with this `Factor`. + """ if self._variable is None: return self.ftype else: return self._variable def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Reconstructs a specific attribute of the `Factor` object, enabling targeted adjustments to the equation discovery process. + + This method allows for the manual modification of a `Factor` object's attributes, providing a way to fine-tune the equation search based on prior knowledge or specific requirements. It is particularly useful when certain aspects of a factor need to be adjusted without re-running the entire discovery process. + + Args: + attribute: The name of the attribute to reconstruct. + value: The new value to assign to the attribute. + except_attrs: A dictionary of attributes to exclude during the reconstruction process. + + Raises: + ValueError: If the specified attribute is not supported for manual reconstruction. + + Returns: + None. + """ from epde.loader import obj_to_pickle, attrs_from_dict supported_attrs = [] if attribute not in supported_attrs: @@ -106,6 +226,19 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): @property def ann_representation(self) -> torch.nn.modules.container.Sequential: + """ + Return the ANN representation of the factor. + + This representation is crucial for efficiently evaluating the factor + across different grid configurations. By caching the ANN representation, + subsequent evaluations become significantly faster. + + Args: + self: The instance of the Factor class. + + Returns: + torch.nn.modules.container.Sequential: The ANN representation of the factor. + """ try: return self._ann_repr except AttributeError: @@ -114,41 +247,89 @@ def ann_representation(self) -> torch.nn.modules.container.Sequential: return self._ann_repr def predict_with_ann(self, grids: list): + """ + Uses the ANN representation, learned during the equation discovery process, to predict values for given grids. + + This method leverages the pre-trained ANN model to generate predictions + based on the provided input grids. It reshapes the grids, converts them + to tensors, and feeds them into the ANN model. The output is then reshaped + to match the original grid dimensions. This allows to evaluate discovered equation on unseen data. + + Args: + grids (list): A list of grids for which predictions are to be made. + + Returns: + np.ndarray: The predicted values, reshaped to match the dimensions of the input grids. + """ return use_ann_to_predict(self.ann_representation, grids) def reset_saved_state(self): + """ + Resets the tracking of saved states. + + This method resets the 'saved' dictionary, marking both 'base' and + 'structural' states as unsaved. This ensures that the system accurately + reflects whether modifications have occurred since the last save, + allowing the evolutionary process to correctly identify changes and + potentially trigger necessary actions like re-evaluation or storage. + + Args: + self: The object instance. + + Returns: + None. + """ self.saved = {'base': False, 'structural': False} @property def status(self): + """ + Returns the current status of the symbolic factor. + + This status reflects the stage of the factor within the equation discovery process, + such as whether it's being actively considered, has been selected, or has been discarded. + + Returns: + str: The current status of the factor. + """ return self._status @status.setter def status(self, status_dict): - ''' - Parameters - ---------- - status_dict : dict - Description of token behaviour during the equation construction and processsing. - Keys: - 'mandatory' - if True, a token from the family must be present in every term; - - 'unique_token_type' - if True, only one token of the family can be present in the term; - - 'unique_specific_token' - if True, a specific token can be present only once per term; - - 'requires_grid' - if True, the token requires grid for evaluation, if False, the tokens will be - loaded from cache. - ''' + """ + Configures the token's behavior within the equation discovery process. + + This method sets the internal status dictionary, which governs how the token interacts with other tokens during equation construction and simplification. This configuration is crucial for guiding the search process towards valid and meaningful equation structures. + + Args: + status_dict (dict): A dictionary defining the token's constraints and requirements. + Keys: + 'mandatory' (bool): If True, a token from this family must be present in every term. + 'unique_token_type' (bool): If True, only one token of this type can be present in a term. + 'unique_specific_token' (bool): If True, this specific token can only appear once per term. + 'requires_grid' (bool): If True, the token requires grid data for evaluation; otherwise, it will be loaded from cache. + + Returns: + None + """ self._status = status_dict def set_parameters(self, params_description: dict, equality_ranges: dict, random=True, **kwargs): - ''' - - Avoid periodic parameters (e.g. phase shift) - - ''' + """ + Sets the parameters for the factor, either randomly within specified bounds or using provided values. + + This method initializes the factor's parameters, ensuring they are appropriately set for subsequent calculations and avoids issues with periodic parameters or other constraints. + + Args: + params_description (dict): A dictionary describing the parameters, including their names and bounds. + equality_ranges (dict): A dictionary defining ranges for equality constraints on the parameters. + random (bool, optional): Whether to randomly initialize the parameters within the specified bounds. Defaults to True. If False, parameters are taken from kwargs. + **kwargs: Keyword arguments providing specific values for the parameters when random is False. + + Returns: + None + """ _params_description = {} if not random: _params = np.empty(len(kwargs)) @@ -177,6 +358,26 @@ def set_parameters(self, params_description: dict, equality_ranges: dict, self.use_grids_cache() def __eq__(self, other): + """ + Tests the equality of two factors. + + This method determines if two factors are equivalent by comparing their types, + labels, and parameters. It checks if the parameters of the two factors + are within the acceptable equality ranges defined for each parameter. + + Args: + other (Factor): The factor to compare with this factor. + + Returns: + bool: True if the factors are considered equal based on their type, + label, and parameter values; False otherwise. + + Why: + This ensures that factors representing the same underlying equation + are treated as identical, which is crucial for the evolutionary + search process to avoid redundant computations and to properly + evaluate the fitness of different equation candidates. + """ if type(self) != type(other): return False elif self.label != other.label: @@ -188,6 +389,19 @@ def __eq__(self, other): return True def partial_equlaity(self, other): + """ + Checks for similarity between two factors, focusing on key characteristics relevant to equation discovery. + + This method assesses whether two factors are similar enough to be considered equivalent within the context of equation search. + It compares the 'label' (mathematical operation) and 'params' (parameters), ensuring they align within acceptable tolerances. + The 'power' parameter is excluded from the comparison as it represents scaling and does not affect equation structure. + + Args: + other: The factor to compare with. + + Returns: + bool: True if the factors are similar, False otherwise. + """ for param_idx, param_info in self.params_description.items(): if param_info['name'] == 'power': power_idx = param_idx @@ -205,10 +419,39 @@ def partial_equlaity(self, other): @property def evaluator(self): + """ + Returns the evaluator object. + This object is responsible for assessing the quality of the generated equations. + + Args: + None + + Returns: + The evaluator object used to score equation fitness. + """ return self._evaluator @evaluator.setter def evaluator(self, evaluator): + """ + Sets the evaluator for the factor. + + The evaluator is responsible for calculating the loss and gradients + associated with the factor during the equation discovery process. + If the provided evaluator is self-contained (EvaluatorContained), it is used directly. + Otherwise, the appropriate evaluator is extracted from the factor families + within the provided evaluator, ensuring compatibility with the factor's type. + + Args: + evaluator: The evaluator to set. It can be either an EvaluatorContained instance + or an evaluator containing factor families. + + Returns: + None. + + Class Fields: + _evaluator (EvaluatorBase): The evaluator for the factor. + """ if isinstance(evaluator, EvaluatorContained): self._evaluator = evaluator else: @@ -216,6 +459,17 @@ def evaluator(self, evaluator): self._evaluator = factor_family._evaluator # TODO: fix calling private attribute def evaluate(self, structural=False, grids=None, torch_mode: bool = False): + """ + Evaluates the token's value, leveraging cached results when available or applying the appropriate evaluator function. This process is central to constructing equation candidates and assessing their fitness against the observed data. + + Args: + structural (bool): A flag indicating whether to evaluate the structural form of the token. Structural evaluation might involve different simplifications or representations. + grids (optional): Grids to use for evaluation, typically representing the domain over which the equation is defined. Defaults to None. + torch_mode (bool): A flag indicating whether to use torch tensors for evaluation. + + Returns: + The evaluated value of the token, which can be a numerical value, a symbolic expression, or a tensor, depending on the evaluation context. + """ assert self.cache_linked, 'Missing linked cache.' if self.is_deriv and grids is not None: raise Exception( @@ -270,11 +524,33 @@ def evaluate(self, structural=False, grids=None, torch_mode: bool = False): @property def cache_label(self): + """ + Caches a unique string representation of the factor's parameters. + + This representation is used to efficiently identify and reuse previously computed results for factors with the same parameter values. This avoids redundant calculations when exploring the search space of possible equation structures. + + Args: + self: The factor object. + + Returns: + str: A string that uniquely identifies the factor based on its label and parameters. + """ cache_label = factor_params_to_str(self) return cache_label @property def name(self): + """ + Generates a string representation of the factor, including its label and parameter values. + + This provides a human-readable identifier for the factor, useful for tracking its configuration during the equation discovery process. The string includes the factor's label and a dictionary-like representation of its parameters. + + Args: + self: The object instance. + + Returns: + str: A formatted string containing the label and parameter values. + """ form = self.label + '{' for param_idx, param_info in self.params_description.items(): form += param_info['name'] + ': ' + str(self.params[param_idx]) @@ -285,6 +561,20 @@ def name(self): @property def latex_name(self): + """ + Returns the LaTeX representation of the factor. + + This representation is crucial for displaying the discovered equation in a human-readable format. + If a LaTeX constructor is defined, it uses the constructor and parameter descriptions to generate the LaTeX string, + formatting the parameters with appropriate scientific notation. + Otherwise, it returns the factor's name. + + Args: + self: The Factor instance. + + Returns: + str: The LaTeX representation of the factor. + """ if self._latex_constructor is not None: params_dict = {} for param_idx, param_info in self.params_description.items(): @@ -298,14 +588,54 @@ def latex_name(self): @property def hash_descr(self) -> int: + """ + Returns the precomputed hash value of the factor. + + This method returns the precomputed hash value, which is calculated + during object initialization. This hash is used for efficient comparison + and caching of factors within the equation discovery process. By precomputing + the hash, we avoid redundant calculations when evaluating the fitness + of different equation candidates. + + Args: + self: The object instance. + + Returns: + int: The precomputed hash value of the factor. + """ return self._hash_val @property def grids(self): + """ + Retrieves all grid configurations currently stored. + + This provides access to the explored search space, + allowing inspection of different equation structures + evaluated during the discovery process. + + Returns: + A list of grid configurations representing the search space. + """ _, grids = global_var.grid_cache.get_all() return grids def use_grids_cache(self): + """ + Uses a precomputed grid based on the 'dim' parameter to speed up calculations. + + This method searches for a 'dim' parameter within the defined parameters. If found, it uses the corresponding value to select a pre-calculated grid, avoiding redundant grid generation. If 'dim' is absent, a default grid (index 0) is used. This optimization is crucial for efficient equation discovery. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields Initialized: + grid_idx (int): The index of the grid to use, derived from the 'dim' parameter or 0 if 'dim' is not found. + grid_set (bool): A flag indicating whether the grid index has been set (True after this method is called). + """ dim_param_idx = np.inf dim_set = False for param_idx, param_descr in self.params_description.items(): @@ -316,6 +646,22 @@ def use_grids_cache(self): self.grid_set = True def __deepcopy__(self, memo=None): + """ + Creates a deep copy of the Factor object. + + This method ensures that when a Factor object is copied, all its attributes, + including those defined in `__slots__`, are also copied independently. This is + crucial for maintaining the integrity of equation structures during evolutionary + operations, preventing unintended modifications to shared components. + + Args: + memo (dict, optional): A dictionary used by `copy.deepcopy` to keep track of + objects that have already been copied, preventing infinite recursion + when dealing with circular references. Defaults to None. + + Returns: + Factor: A new Factor object that is a deep copy of the original. + """ clss = self.__class__ new_struct = clss.__new__(clss) memo[id(self)] = new_struct @@ -342,4 +688,19 @@ def __deepcopy__(self, memo=None): return new_struct def use_cache(self): + """ + Enables the caching mechanism for storing and reusing previously computed factor values. + + This optimization can significantly reduce computational cost when the same factor is encountered multiple times during the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. + + Class Fields: + cache_linked (bool): A boolean indicating whether the cache is linked. + Initialized to True when this method is called. + """ self.cache_linked = True diff --git a/epde/structure/main_structures.py b/epde/structure/main_structures.py index 1125d26e..4b5b45f4 100644 --- a/epde/structure/main_structures.py +++ b/epde/structure/main_structures.py @@ -38,25 +38,62 @@ class Term(ComplexStructure): """ - Class for describing the term of differential equation - - Attributes: - _descr_variable_marker - - pool - max_factors_in_term: - cache_linked: - structure: - occupied_tokens_labels: - descr_variable_marker: - prev_normalized + Represents a term within a differential equation, encapsulating its structure and properties. + + + Attributes: + _descr_variable_marker + + pool + max_factors_in_term: + cache_linked: + structure: + occupied_tokens_labels: + descr_variable_marker: + prev_normalized """ + __slots__ = ['_history', 'structure', 'interelement_operator', 'saved', 'saved_as', 'pool', 'max_factors_in_term', 'cache_linked', 'occupied_tokens_labels', '_descr_variable_marker'] def __init__(self, pool, passed_term=None, mandatory_family=None, max_factors_in_term=1, create_derivs: bool = False, interelement_operator=np.multiply, collapse_powers = True): + """ + Initializes a Term object, representing a candidate term in the equation search space. + + This method constructs a Term object, either by randomly selecting factors + from a pool or by using a provided term as a starting point. The initialization + process prepares the term for evaluation and optimization within the + equation discovery process. + + Args: + pool: The pool of factors to use for term creation. + passed_term: An existing term to use as a basis (optional). + mandatory_family: A family that must be present in the term (optional). + max_factors_in_term: The maximum number of factors allowed in the term. + create_derivs: A boolean indicating whether to create derivatives. + interelement_operator: The operator to use between elements (default: np.multiply). + collapse_powers: A boolean indicating whether to collapse powers (default: True). + + Returns: + None + + Class Fields: + pool: The pool of factors used for term creation. + max_factors_in_term: The maximum number of factors allowed in the term. + term: The underlying term representation (list of factors). + tensor: The tensor representation of the term. + is_normalized: A boolean indicating whether the term is normalized. + saved_state: A dictionary tracking the saved state of normalization. + + Why: + The Term object represents a potential building block of a differential equation. + This initialization is a crucial step in creating and manipulating these terms + during the equation discovery process. The choice between random initialization + and using a passed term allows for both exploration of the search space and + refinement of existing candidate solutions. + """ super().__init__(interelement_operator) self.pool = pool self.max_factors_in_term = max_factors_in_term @@ -73,6 +110,22 @@ def __init__(self, pool, passed_term=None, mandatory_family=None, max_factors_in self.reset_saved_state() def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Manually reconstructs a specific attribute of the `Term` object, primarily to define its structure. + + This method is used to bypass the standard term creation process, allowing for direct specification of the `Term`'s internal components. This is particularly useful when loading a previously discovered equation or when fine-tuning the equation's structure. + + Args: + attribute (str): The name of the attribute to reconstruct. Currently, only 'structure' is supported. + value (list): The new value to assign to the 'structure' attribute. This should be a list of dictionaries, where each dictionary represents a `Factor` object. + except_attrs (dict): A dictionary of attributes to exclude during the reconstruction of `Factor` objects. This allows for selective overriding of attributes during reconstruction. + + Returns: + None. The method modifies the object's state directly. + + Fields: + structure (list): A list of `Factor` objects representing the structure of the equation term. + """ from epde.loader import attrs_from_dict, get_typespec_attrs supported_attrs = ['structure'] if attribute not in supported_attrs: @@ -90,6 +143,22 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): @property def cache_label(self): + """ + Caches and returns a label representing the sorted structure of the `Term`. + + This property ensures that the order of elements within the `Term`'s structure + is consistently represented in the cache label. This is crucial for + equivalence checks, as it guarantees that terms with the same elements, + regardless of their original order, are treated as identical. + + Args: + self: The instance of the `Term` class. + + Returns: + tuple or any: A tuple of cache labels if the structure contains multiple + elements (sorted by their cache labels), or the cache label of the + single element if the structure has only one element. + """ if len(self.structure) > 1: structure_sorted = sorted(self.structure, key=lambda x: x.cache_label) cache_label = tuple([elem.cache_label for elem in structure_sorted]) @@ -98,6 +167,25 @@ def cache_label(self): return cache_label def use_cache(self): + """ + Enables caching for the current term and propagates the setting to its children. + + This method sets the `cache_linked` flag to True for the current term, + indicating that caching should be used. It then iterates through the + term's structure (children) and recursively calls the `use_cache` method + on any child term that does not already have caching enabled. This optimization + is crucial for improving the efficiency of the equation discovery process, + as it avoids redundant computations of term values during the evolutionary search. + + Args: + self: The instance of the Term class. + + Returns: + None. + + Initializes: + cache_linked (bool): A boolean flag indicating whether caching is enabled for this term. + """ self.cache_linked = True for idx, _ in enumerate(self.structure): if not self.structure[idx].cache_linked: @@ -107,11 +195,40 @@ def use_cache(self): @singledispatchmethod def defined(self, passed_term): + """ + Abstract method to handle term definitions. It serves as a base for specialized implementations that process different types of terms within the equation discovery process. + + Args: + passed_term: The term to be defined. This term will be processed to construct a symbolic representation of a component within a differential equation. + + Raises: + NotImplementedError: Always raised, as this is the base implementation. It indicates that a specific implementation for the given term type is required. + + Returns: + None: This method always raises an error, signaling the need for a concrete implementation in a subclass. + """ raise NotImplementedError( f'passed term should have string or list/dict types, not {type(passed_term)}') @defined.register def _(self, passed_term: list, collapse_powers = True): + """ + Initializes a Term object, building its structure from a list of factors. + + This method processes a list of factors, which can be strings (factor labels) or existing Factor objects, + to define the term's structure. It also simplifies the term by collapsing powers of identical factors if specified. + This ensures that the term is represented in its most compact form, which is crucial for efficient equation discovery. + + Args: + passed_term (list): A list of strings (factor labels) or Factor objects representing the term's structure. + collapse_powers (bool, optional): A boolean indicating whether to combine identical factors into powers (e.g., x*x becomes x^2). Defaults to True. + + Returns: + None + + Class Fields: + structure (list): A list of Factor objects representing the term's structure after processing the input passed_term. + """ self.structure = [] for _, factor in enumerate(passed_term): if isinstance(factor, str): @@ -126,6 +243,22 @@ def _(self, passed_term: list, collapse_powers = True): @defined.register def _(self, passed_term: str, collapse_powers = True): + """ + Initializes the term structure by parsing a string representation or directly using a `Factor` object. This prepares the term for symbolic manipulation within the equation discovery process. + + Args: + passed_term (str | Factor): The term to be added, either as a string to be parsed into a `Factor`, or a `Factor` object directly. + collapse_powers (bool): A boolean indicating whether to collapse powers (default is True). + + Returns: + None + + Raises: + ValueError: If `passed_term` is not a string or a `Factor` object. + + Why: + This method ensures that each term within an equation is represented as a structured list of `Factor` objects, facilitating subsequent operations such as simplification, differentiation, and evaluation. + """ self.structure = [] if isinstance(passed_term, str): _, temp_f = self.pool.create(label=passed_term) @@ -137,6 +270,41 @@ def _(self, passed_term: str, collapse_powers = True): def randomize(self, mandatory_family=None, forbidden_factors=None, create_derivs=False, **kwargs): + """ + Generates a randomized structure (a list of factors) representing a potential equation term. + + This method constructs a term by iteratively creating factors, ensuring + that the generated structure adheres to token availability and constraints + defined within the pool. The process can be seeded with a mandatory token + family to guide the search or initialized randomly. The method ensures + that the generated term is valid by tracking token usage and filtering + the final structure. This approach allows exploring diverse equation + structures while respecting predefined constraints. + + Args: + mandatory_family: A specific token family that must be included in the + initial factor. If None, the initial factor is created randomly. + forbidden_factors: A dictionary specifying tokens that are forbidden + and their current status. If None, it is initialized based on + `self.max_factors_in_term`. + create_derivs: A boolean indicating whether derivatives should be + created during factor creation. + **kwargs: Additional keyword arguments passed to the `pool.create` + method. + + Returns: + list: The randomized structure, which is a list of factors. + + Initializes: + self.occupied_tokens_labels (dict): A dictionary tracking the status of occupied tokens, + initialized based on `forbidden_factors`. Each key is a token label, and the value + is a list containing the current count, maximum count, and a boolean indicating + whether the token is fully occupied. + self.descr_variable_marker: Stores the value of `mandatory_family` if it's provided, otherwise False. + Used to mark if the initial factor was created with a mandatory family. + self.structure (list): A list of factors representing the generated structure. + Initialized with the first factor and subsequently appended to in the loop. + """ if np.sum(self.pool.families_cardinality(meaningful_only=True)) == 0: raise ValueError('No token families are declared as meaningful for the process of the system search') @@ -194,16 +362,57 @@ def update_token_status(token_status, changes): @property def descr_variable_marker(self): + """ + Gets the description variable marker. This marker is used to identify the description variable within the equation's symbolic representation, enabling the framework to correctly interpret and process the equation during the equation discovery process. + + Returns: + str: The description variable marker. + """ return self._descr_variable_marker @descr_variable_marker.setter def descr_variable_marker(self, marker: False): + """ + Sets the marker used to identify described variables within a symbolic expression. + + This marker is used to distinguish variables that have been explicitly described or defined, + allowing the system to handle them differently during the equation discovery process. + It ensures that the framework can correctly interpret and manipulate these variables + when constructing and evaluating candidate equations. + + Args: + marker: The marker to set. Must be `False` or a string representing a family label (e.g., "u"). + + Returns: + None. + + Raises: + ValueError: If the provided marker is not `False` or a string. + + Class Fields: + _descr_variable_marker (str or False): The marker used for described variables. + """ if not marker or isinstance(marker, str): self._descr_variable_marker = marker else: raise ValueError('Described variable marker shall be a family label (i.e. "u") of "False"') def evaluate(self, structural, grids=None): + """ + Evaluates the term, potentially normalizing its structure, and leverages a tensor cache for efficiency. + + This method checks if the term's value is already computed and stored in a global tensor cache. + If so, it retrieves the value directly. Otherwise, it computes the value, normalizes it based on the `structural` flag, + and stores it in the cache for future use. Normalization ensures consistent scaling of terms, + which is crucial when combining them to form more complex equation structures. + + Args: + structural (bool): A flag indicating whether to normalize the structure. Normalization helps in maintaining consistent scales across different terms. + grids (object, optional): Optional grids data (currently unused). + + Returns: + np.ndarray: The evaluated value as a flattened NumPy array. The shape is reshaped to 1-D array + """ assert global_var.tensor_cache is not None, 'Currently working only with connected cache' normalize = structural if self.saved[structural] or (self.cache_label, normalize) in global_var.tensor_cache: @@ -243,6 +452,19 @@ def evaluate(self, structural, grids=None): return value def filter_tokens_by_right_part(self, reference_target, equation, equation_position): + """ + Filters the tokens of a term to ensure its uniqueness within the equation. + + This method refines the term's structure by iteratively modifying tokens that might cause conflicts with other parts of the equation. It aims to generate a unique term by replacing problematic tokens until a suitable solution is found or a maximum number of attempts is reached. This process is crucial for maintaining the integrity and identifiability of terms within the equation discovery process. + + Args: + reference_target: The reference target object providing context for token uniqueness. + equation: The equation object to which the term belongs. + equation_position: The position of the term within the equation's structure. + + Returns: + None. The method modifies the term's structure in place to ensure uniqueness. + """ warnings.warn(message='Tokens can no longer be set as right-part-unique', category=DeprecationWarning) taken_tokens = [factor.label for factor in reference_target.structure @@ -274,6 +496,20 @@ def filter_tokens_by_right_part(self, reference_target, equation, equation_posit print('proposed', new_term.name, 'for ', equation.text_form, 'with respect to', reference_target.name) def reset_occupied_tokens(self): + """ + Resets the list of occupied token labels based on the current structure and token pool. + + This method iterates through the factors in the current equation structure and the available token families. + It identifies tokens that represent unique elements within the equation or unique instances of specific tokens + and updates the list of occupied tokens accordingly. This ensures that the evolutionary process considers + only valid and meaningful combinations of tokens when constructing new equation structures. + + Args: + self: The instance of the Term class. + + Returns: + None. The method updates the `occupied_tokens_labels` attribute of the object, reflecting the tokens currently in use. + """ occupied_tokens_new = [] for factor in self.structure: for token_family in self.pool.families: @@ -286,6 +522,23 @@ def reset_occupied_tokens(self): @property def available_tokens(self): + """ + Identify and return token families that have at least one token available for use. + + This method iterates through the token families in the pool and checks if any of their tokens are not currently occupied. + If a token family has available tokens, a copy of that family is created, containing only the available tokens, and added to the list of available token families. + This ensures that only usable tokens are considered during the equation discovery process. + + Args: + None + + Returns: + list: A list of token families with at least one available token. + + Class Fields: + pool (Pool): The pool of token families. + occupied_tokens_labels (set): A set of labels that are currently occupied. + """ available_tokens = [] for token in self.pool.families: if not all([label in self.occupied_tokens_labels for label in token.tokens]): @@ -297,10 +550,36 @@ def available_tokens(self): @property def total_params(self): + """ + Calculates the total number of trainable parameters within the term's structure. + + This property computes the sum of parameters across all elements in the term's structure, + representing the complexity of the term. It ensures that the returned value is at least 1, + preventing issues in subsequent calculations or interpretations where a non-positive number of parameters might be problematic. + + Args: + None + + Returns: + int: The total number of trainable parameters in the term's structure, with a minimum value of 1. + """ return max(sum([len(element.params) - 1 for element in self.structure]), 1) @property def name(self): + """ + Returns the formatted name of the term. + + The name is constructed by concatenating the names of the tokens + in the term's structure, separated by ' * '. This provides a human-readable representation of the term, + making it easier to interpret and analyze the discovered equation. + + Args: + self: The term instance. + + Returns: + str: The formatted name of the term. + """ form = '' for token_idx in range(len(self.structure)): form += self.structure[token_idx].name @@ -310,11 +589,31 @@ def name(self): @property def latex_form(self): + """ + Returns the latex form of the term's structure. + + This representation is crucial for displaying and interpreting the discovered equation. + It ensures that the equation structure is easily understandable in mathematical notation. + + Returns: + str: The latex form of the term. + """ form = reduce(lambda x, y: x + r' \cdot ' + y, [factor.latex_name for factor in self.structure]) return form def contains_deriv(self, variable=None): + """ + Checks if the term contains a derivative factor suitable for symbolic manipulation. + + This check is crucial for determining if the term can be further simplified or processed within the equation discovery workflow. It specifically looks for derivative factors that are compatible with the symbolic evaluation methods used in the project. + + Args: + variable: The variable to check for derivatives with respect to. If None, checks for any derivative factor. + + Returns: + bool: True if the term contains a derivative factor (with respect to the specified variable, if provided) and is compatible with symbolic evaluation, False otherwise. + """ if variable is None: return any([factor.is_deriv and factor.deriv_code != [None,] and factor.evaluator._evaluator == simple_function_evaluator @@ -325,15 +624,56 @@ def contains_deriv(self, variable=None): for factor in self.structure]) def contains_variable(self, variable): + """ + Checks if any factor within the term's structure involves a specific variable. + + This is crucial for determining the term's dependence on that variable, + which is essential for constructing the overall differential equation. + + Args: + variable: The variable to check for within the term's factors. + + Returns: + bool: True if the variable is found in any of the term's factors, False otherwise. + """ return any([factor.variable == variable for factor in self.structure]) def __eq__(self, other): + """ + Compares this term with another object to determine structural equality. + + This method verifies if the other object represents the same mathematical term, + irrespective of the order of its components. This is crucial for identifying + equivalent expressions during the equation discovery process. + + Args: + other: The object to compare with. + + Returns: + bool: True if the objects have the same structure and length, False otherwise. + """ return (all([any([other_elem == self_elem for other_elem in other.structure]) for self_elem in self.structure]) and all([any([other_elem == self_elem for self_elem in self.structure]) for other_elem in other.structure]) and len(other.structure) == len(self.structure)) @HistoryExtender('\n -> was copied by deepcopy(self)', 'n') def __deepcopy__(self, memo=None): + """ + Creates a deep copy of the Term object. + + This method ensures that when a Term object is copied, all its attributes + are also copied independently, preventing modifications to the copy from + affecting the original. This is crucial for maintaining the integrity of + equation structures during evolutionary operations. + + Args: + memo (dict, optional): A dictionary used by `copy.deepcopy` to keep track + of already copied objects, preventing infinite recursion in case of + circular references. Defaults to None. + + Returns: + Term: A new Term object that is a deep copy of the original. + """ clss = self.__class__ new_struct = clss.__new__(clss) memo[id(self)] = new_struct @@ -359,6 +699,28 @@ def __deepcopy__(self, memo=None): class Equation(ComplexStructure): + """ + Class for the single equation for the dynamic system. + + Attributes: + structure : list of Term objects + List, containing all terms of the equation; first 2 terms are reserved for constant value and the input function; + + target_idx : int + Index of the target term, selected in the Split phase; + + target : 1-d array of float + values of the Term object, reshaped into 1-d array, designated as target for application in sparse regression; + + features : matrix of float + matrix, composed of terms, not included in target, value columns, designated as features for application in sparse regression; + + fitness_value : float + Inverse value of squared error for the selected target 2function and features and discovered weights; + + estimator : sklearn estimator of selected type + """ + __slots__ = ['_history', 'structure', 'interelement_operator', 'n_immutable', 'pool', # '_target', '_features', 'saved', 'saved_as','max_factors_in_term', 'operator', 'target_idx', 'right_part_selected', '_weights_final', 'weights_final_evald', 'simplified', @@ -372,38 +734,21 @@ def __init__(self, pool: TFPool, basic_structure: Union[list, tuple, set], var_t 'max_factors_in_term': {'optimizable': False, 'value': 1.}}, interelement_operator: Callable = np.add): """ - - Class for the single equation for the dynamic system. - - attributes: - structure : list of Term objects \r\n - List, containing all terms of the equation; first 2 terms are reserved for constant value and the input function; - - target_idx : int \r\n - Index of the target term, selected in the Split phase; - - target : 1-d array of float \r\n - values of the Term object, reshaped into 1-d array, designated as target for application in sparse regression; - - features : matrix of float \r\n - matrix, composed of terms, not included in target, value columns, designated as features for application in sparse regression; - - fitness_value : float \r\n - Inverse value of squared error for the selected target 2function and features and discovered weights; - - estimator : sklearn estimator of selected type \r\n - - parameters: - - Matrix of derivatives: first axis through various orders/coordinates in order: ['1', 'f', all derivatives by one coordinate axis - in increasing order, ...]; second axis: time, further - spatial coordinates; - - tokens : list of strings \r\n - Symbolic forms of functions, including derivatives; - - max_factors_in_term : int, base value of 2\r\n - Maximum number of factors, that can form a term (e.g. with 2: df/dx_1 * df/dx_2) - + Initializes an Equation object, representing a single equation within a dynamic system. + + This class encapsulates the structure and parameters of an equation, facilitating its evolution and optimization within the EPDE framework. + The initialization process involves setting up the basic terms of the equation and generating additional terms to explore the solution space. + The goal is to create a flexible representation that can be refined through evolutionary algorithms to accurately model the underlying dynamics of the system. + + Args: + pool (TFPool): A pool of symbolic tokens (functions, derivatives) used to construct the equation terms. + basic_structure (Union[list, tuple, set]): A collection of initial terms (either Term objects or symbolic strings) that form the basis of the equation. These terms are considered immutable during the evolutionary process. + var_to_explain (str, optional): A variable that the equation should primarily explain. Defaults to None. + metaparameters (dict, optional): A dictionary containing metaparameters that control the equation's complexity and optimization. Includes 'sparsity', 'terms_number', and 'max_factors_in_term'. Defaults to a predefined dictionary. + interelement_operator (Callable, optional): The operator used to combine the terms of the equation (e.g., np.add). Defaults to np.add. + + Returns: + None """ super().__init__(interelement_operator) self.reset_state() @@ -445,6 +790,22 @@ def __init__(self, pool: TFPool, basic_structure: Union[list, tuple, set], var_t # self.coefficients_stability = np.inf def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Manually reconstructs a specific attribute of the equation. + + This method allows for direct assignment of a new value to the specified attribute, + bypassing the typical attribute setting process. It is used to rebuild equation's structure + from the scratch, for example, after crossover or mutation operations during evolutionary search. + + Args: + attribute: The name of the attribute to reconstruct. Currently, only 'structure' is supported. + value: The new value to assign to the specified attribute. For 'structure', this should be a list of dictionaries, + where each dictionary represents a term. + except_attrs: A dictionary of attributes to exclude during the reconstruction process. + + Returns: + None. + """ from epde.loader import attrs_from_dict, get_typespec_attrs supported_attrs = ['structure'] if attribute not in supported_attrs: @@ -461,6 +822,28 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): self.structure.append(term) def reset_explaining_term(self, term_idx=0): + """ + Resets the term designated to explain the target variable within the equation. + + This method iterates through the equation's terms, assigning the target variable as the descriptor + to the specified term while ensuring that only terms containing the target variable can be assigned. + All other terms are marked as not contributing to the explanation of the target variable. + + Args: + term_idx (int): The index of the term to designate as the primary descriptor of the target variable. Defaults to 0. + + Returns: + None + + Raises: + AssertionError: If the selected term does not contain the target variable, + preventing it from being a valid descriptor. + + Why: + This ensures that the equation structure correctly reflects the relationships + between terms and the variable being explained, which is crucial for the + equation discovery process. + """ for idx, term in enumerate(self.structure): if idx == term_idx: assert term.contains_variable( @@ -471,6 +854,26 @@ def reset_explaining_term(self, term_idx=0): term.descr_variable_marker = False def __eq__(self, other): + """ + Compares two `Equation` objects for equality. + + This method checks if two `Equation` objects are structurally equivalent and, + if both have their final weights evaluated, compares their final weights as well. + Structural equivalence is determined by ensuring that both equations contain + the same terms, regardless of their order. Comparing final weights ensures + that the equations not only have the same structure but also represent the + same quantitative relationship. This is crucial for determining if two independently + evolved equations represent the same underlying model. + + Args: + other: The `Equation` object to compare with. + + Returns: + bool: True if the `Equation` objects are equal, False otherwise. + Equality is determined by comparing the structure and, if + both objects have final weights evaluated, by comparing the + final weights as well. + """ if self.weights_final_evald and other.weights_final_evald: return (all([any([other_elem == self_elem for other_elem in other.structure]) for self_elem in self.structure]) and all([any([other_elem == self_elem for self_elem in self.structure]) for other_elem in other.structure]) @@ -482,13 +885,57 @@ def __eq__(self, other): and len(other.structure) == len(self.structure)) def contains_deriv(self, variable=None): + """ + Checks if the equation contains any derivative terms. + + This is crucial for determining the equation's complexity and + suitability for fitting observed data, as equations with derivatives + require specific numerical methods for evaluation and solution. + + Args: + variable: The variable to check for in the derivative. + If None, checks for any derivative. + + Returns: + bool: True if any term contains a derivative (optionally of the + specified variable), False otherwise. + """ return any([term.contains_deriv(variable) for term in self.structure]) def contains_variable(self, variable): + """ + Checks if any term within the equation's structure involves a specific variable. + + This is crucial for determining the equation's dependence on particular variables, + which is a key step in the equation discovery process. By identifying which variables + are present, the search space for potential equation structures can be effectively narrowed. + + Args: + variable: The variable to check for within the equation's terms. + + Returns: + bool: True if the variable is found in any of the equation's terms, False otherwise. + """ return any([term.contains_variable(variable) for term in self.structure]) @property def forbidden_token_labels(self): + """ + Return the set of token labels that cannot be used in the equation's target expression. + + This method identifies and returns a set of token labels that are + considered "forbidden" based on the structure and status of token + families within the pool. It issues a deprecation warning as tokens + can no longer be set as right-part-unique. This is done to prevent + the evolutionary search from generating equations that are structurally + invalid or lead to poor model performance. + + Args: + self: The instance of the Equation class. + + Returns: + set: A set containing the forbidden token labels. + """ warnings.warn(message='Tokens can no longer be set as right-part-unique', category=DeprecationWarning) target_symbolic = [ @@ -502,6 +949,24 @@ def forbidden_token_labels(self): return forbidden_tokens def restore_property(self, deriv: bool = False, mandatory_family: bool = False): + """ + Restores a property within the equation's structure to maintain validity. + + This method attempts to restore either a derivative or a mandatory family + property within the equation's structure. It replaces a randomly selected + element in the structure with a new term that satisfies the specified + property requirements. This is crucial for maintaining the equation's + integrity during operations like mutation or crossover, ensuring that + the generated equations remain physically meaningful and well-defined. + + Args: + deriv: A boolean indicating whether to restore a derivative property. + mandatory_family: A boolean indicating whether to restore a mandatory + family property. + + Returns: + None. The method modifies the object's structure in place. + """ # TODO: non-urgent, rewrite for an arbitrary equation property check if not (deriv or mandatory_family): raise ValueError('No property passed for restoration.') @@ -526,6 +991,21 @@ def restore_property(self, deriv: bool = False, mandatory_family: bool = False): self.main_var_to_explain) def reconstruct_by_right_part(self, right_part_idx): + """ + Reconstructs the equation to emphasize a specific term, enhancing equation discovery. + + This method creates a modified copy of the equation, focusing on the term + specified by `right_part_idx`. It filters tokens in other terms based on + the factors present in the focused term. This process helps to isolate + and refine the relationships captured within that specific term, + facilitating the identification of relevant equation structures. This is done to simplify equation discovery by focusing search on particular equation terms. + + Args: + right_part_idx (int): The index of the term to reconstruct around. + + Returns: + Equation: A new equation object reconstructed to emphasize the specified term. + """ warnings.warn(message='Tokens can no longer be set as right-part-unique', category=DeprecationWarning) new_eq = copy.deepcopy(self) @@ -541,6 +1021,22 @@ def reconstruct_by_right_part(self, right_part_idx): return new_eq def evaluate(self, normalize=True, return_val=False, grids=None): + """ + Evaluates the equation model against the data and returns the predicted values, target, and features. + + This method is central to assessing how well the discovered equation represents the underlying relationships in the data. It calculates the model's output based on the learned structure and weights, allowing for comparison with the actual target values. + + Args: + normalize: A boolean indicating whether to normalize the features before evaluation. Normalization can improve the stability and accuracy of the evaluation, especially when dealing with features on different scales. + return_val: A boolean indicating whether to return the predicted values. If True, the method returns the model's predictions; otherwise, it returns None for the predicted values. + grids: Grids used for evaluation. + + Returns: + A tuple containing: + - value: The predicted values if `return_val` is True, otherwise None. These values represent the model's output based on the input features and learned equation structure. + - target: The target values from the data, representing the actual values the model is trying to predict. + - features: The feature values used as input to the model. These are the independent variables used to predict the target variable. + """ target = self.structure[self.target_idx].evaluate(normalize, grids=grids) # Place for improvent: introduce shifted_idx where necessary @@ -596,6 +1092,32 @@ def shifted_idx(idx): return None, target, features def reset_state(self, reset_right_part: bool = True): + """ + Resets the internal state of the equation. + + This method is crucial for ensuring a clean slate when re-evaluating or + re-fitting the equation, particularly after modifications or during + the evolutionary search process. It resets flags associated with + different stages of the equation's processing, such as weight evaluation, + fitness calculation, and simplification. + + Args: + reset_right_part: Whether to also reset the flag indicating if the + right-hand side of the equation has been selected. Defaults to True. + + Returns: + None + + Class fields (object properties) that are initialized: + right_part_selected (bool): Indicates whether the right part is selected. + weights_internal_evald (bool): Indicates whether internal weights have been evaluated. + weights_final_evald (bool): Indicates whether final weights have been evaluated. + fitness_calculated (bool): Indicates whether fitness has been calculated. + stability_calculated (bool): Indicates whether stability has been calculated. + aic_calculated (bool): Indicates whether AIC has been calculated. + simplified (bool): Indicates whether the object has been simplified. + solver_form_defined (bool): Indicates whether the solver form has been defined. + """ if reset_right_part: self.right_part_selected = False self.weights_internal_evald = False @@ -608,6 +1130,26 @@ def reset_state(self, reset_right_part: bool = True): @HistoryExtender('\n -> was copied by deepcopy(self)', 'n') def __deepcopy__(self, memo=None): + """ + Creates a deep copy of the `Equation` object. + + This method ensures that when an equation is duplicated, all its components, + including tokens and parameters, are independently copied. This prevents + unintended modifications to the original equation during the evolutionary + process of equation discovery. By creating a new instance of the class and + copying all attributes from the original object to the new object using + `copy.deepcopy`, the method guarantees that the copied equation is a + distinct entity. + + Args: + memo (dict, optional): A dictionary used by `copy.deepcopy` to keep track + of objects that have already been copied during the deep copy process. + This prevents infinite recursion when copying objects that contain + circular references. Defaults to None. + + Returns: + Equation: A deep copy of the `Equation` object. + """ clss = self.__class__ new_struct = clss.__new__(clss) memo[id(self)] = new_struct @@ -631,6 +1173,25 @@ def __deepcopy__(self, memo=None): return new_struct def copy_properties_to(self, new_equation): + """ + Copies essential equation attributes to a new equation instance. + + This method transfers flags related to equation evaluation status, + selection, simplification and calculated values, ensuring the new equation + inherits the relevant state of the original. This is crucial when + generating populations of equations and evolving them, as it preserves + the progress made in evaluating and refining individual equations. + The solver form is reset to `False` to ensure that the new equation + will be properly processed during evolution. + It handles potential AttributeError exceptions when copying + specific properties that might not exist in either equation. + + Args: + new_equation: The equation object to copy properties to. + + Returns: + None + """ new_equation.weights_internal_evald = self.weights_internal_evald new_equation.weights_final_evald = self.weights_final_evald new_equation.right_part_selected = self.right_part_selected @@ -656,42 +1217,164 @@ def copy_properties_to(self, new_equation): pass def add_history(self, add): + """ + Adds a step to the equation's derivation history. + + This function records each transformation applied to the equation, + allowing for later inspection and reconstruction of the solution path. + + Args: + add (str): The string representing the transformation step to add to the history. + + Returns: + None + + Why: + Tracking the history of transformations is crucial for understanding + how the equation was solved and for verifying the correctness of the solution. + It enables debugging and allows users to retrace the steps taken during the derivation process. + """ # print(add) self._history += add @property def history(self): + """ + Returns the history of the equation's evolution. + + This history tracks the transformations and refinements + applied to the equation during the discovery process, + providing insights into how the final form was achieved. + + Returns: + list: A list representing the history of the equation. + """ return self._history @property def fitness_value(self): + """ + Gets the fitness value of the equation. + This value represents how well the equation fits the observed data, + guiding the search for the best equation structure. + + Returns: + float: The fitness value. + """ return self._fitness_value @fitness_value.setter def fitness_value(self, val): + """ + Sets the fitness value of the equation. + + This value reflects how well the equation fits the observed data, + guiding the search for the best equation structure. + + Args: + val: The fitness value to set. + + Returns: + None. + """ self._fitness_value = val def penalize_fitness(self, coeff=1.): + """ + Penalizes the fitness value, influencing equation selection during the evolutionary process. + + This adjustment, controlled by the coefficient, guides the search towards simpler and more accurate equation representations. + + Args: + coeff (float): The coefficient to penalize the fitness value. A value less than 1.0 will improve the fitness, while a value greater than 1.0 will worsen it. Defaults to 1.0 (no penalty). + + Returns: + None. Modifies the `_fitness_value` attribute of the Equation object directly. + + Why: + Penalizing fitness allows the evolutionary algorithm to prioritize certain equation characteristics, such as simplicity or specific structural properties, during the equation discovery process. + + Class Fields: + _fitness_value (float): The fitness value of the individual. + """ self._fitness_value = self._fitness_value*coeff @property def coefficients_stability(self): + """ + Return the stability of the equation's coefficients. + + This property provides a measure of how much the coefficients of the equation vary during the evolutionary process. + It helps assess the robustness and reliability of the identified equation, as stable coefficients indicate a more consistent and trustworthy model. + + Returns: + np.ndarray: The coefficients stability. + """ return self._coefficients_stability @coefficients_stability.setter def coefficients_stability(self, val): + """ + Sets the stability threshold for equation coefficients. + + This value influences the evolutionary search process by guiding the algorithm + towards solutions with more robust and reliable coefficients. Setting an appropriate + stability threshold can help prevent overfitting and improve the generalization + performance of the discovered equations. + + Args: + val (Any): The desired stability value for the coefficients. + + Returns: + None + """ self._coefficients_stability = val @property def aic(self): + """ + Calculates the Akaike Information Criterion (AIC). + + The AIC is used to compare different equation structures and select the one that best balances model fit and complexity. + It estimates the quality of each equation, relative to each of the other candidates, penalizing models with more free parameters. + + Args: + self: The instance of the Equation class. + + Returns: + float: The AIC value, used for model selection. + """ return self._aic @aic.setter def aic(self, val): + """ + Sets the Akaike Information Criterion (AIC) value. + + This value is used to evaluate the trade-off between the goodness of fit and the complexity of the equation. + A lower AIC indicates a better model. + + Args: + val (float): The AIC value to set. + + Returns: + None + """ self._aic = val @property def weights_internal(self): + """ + Return the weights used internally within the equation. + + These weights are crucial for evaluating the equation's terms and determining its overall behavior. Accessing them allows for inspection of the learned equation structure. + + Raises: + AttributeError: If internal weights are accessed before the equation has been initialized and its weights have been determined. + + Returns: + The internal weights assigned to each term in the equation. + """ if self.weights_internal_evald: return self._weights_internal else: @@ -700,12 +1383,36 @@ def weights_internal(self): @weights_internal.setter def weights_internal(self, weights): + """ + Sets the internal equation weights and resets evaluation flags. + + This ensures that the equation is re-evaluated with the new weights, + reflecting their impact on the equation's fitness. + + Args: + weights (object): The weights to be set internally. + + Returns: + None + """ self._weights_internal = weights self.weights_internal_evald = True self.weights_final_evald = False @property def weights_final(self): + """ + Return the final optimized weights of the equation. These weights represent the coefficients in the discovered differential equation that minimize the error between the equation's predictions and the observed data. + + Args: + None + + Raises: + AttributeError: If final weights are accessed before the equation has been optimized (i.e., before the evolutionary search has been completed). + + Returns: + np.ndarray: A NumPy array containing the final optimized weights for each term in the equation. + """ if self.weights_final_evald: return self._weights_final else: @@ -714,11 +1421,34 @@ def weights_final(self): @weights_final.setter def weights_final(self, weights): + """ + Assigns the optimized weights to the equation and flags it as fully evaluated. + + This ensures that the equation is ready for use in subsequent calculations or analysis, + such as prediction or sensitivity analysis, by marking the weights as finalized. + + Args: + weights (np.ndarray): The optimized weights to be assigned to the equation's terms. + + Returns: + None + """ self._weights_final = weights self.weights_final_evald = True @property def text_form(self): + """ + Generates a textual representation of the equation. + + This representation is crucial for interpreting the discovered equation. + It allows users to view the equation in a human-readable format, + either with the optimized weights or with symbolic coefficients, + facilitating understanding of the relationships between terms. + + Returns: + str: A string representing the equation, either with evaluated weights or symbolic coefficients. + """ form = '' if self.weights_final_evald: for term_idx in range(len(self.structure)): @@ -737,6 +1467,17 @@ def text_form(self): @property def latex_form(self): + """ + Returns the equation in LaTeX format, combining the identified structure and optimized coefficients. + + The equation is constructed by summing the LaTeX representations of each term in the identified structure, + weighted by their corresponding optimized coefficients. This provides a human-readable representation + of the discovered equation, facilitating analysis and interpretation of the identified relationships + within the data. + + Returns: + str: A LaTeX formatted string representing the equation. + """ form = self.structure[self.target_idx].latex_form + r' = ' digits_rounding_max = 3 for idx, term in enumerate(self.structure): @@ -756,10 +1497,43 @@ def latex_form(self): @property def state(self): + """ + Returns the equation in a human-readable format. + + This representation is used to display the equation and can be + useful for debugging or understanding the discovered model. + + Returns: + str: The equation represented as a string. + """ return self.text_form @property def described_variables(self): + """ + Identifies variable types effectively captured by the equation. + + This method determines which variable types are well-represented + within the equation, considering both the equation's structure + and the learned coefficients. It assesses each term in the + equation to see if it contributes meaningfully to describing + variables. The target term invariably contributes. Other terms + contribute if their corresponding coefficient has a significant + magnitude. A term's contribution involves adding the family + types of its factors to the set of described variables, but + only if the factor is a derivative and its derivative code is + not None. + + Args: + self: The Equation instance. + + Returns: + frozenset: A frozen set containing the variable types + (family types of factors) that are considered described + by the equation. The equation uses this information to + understand which variables are most relevant to its + solution. + """ eps = 1e-7 described = set() for term_idx, term in enumerate(self.structure): @@ -775,6 +1549,22 @@ def described_variables(self): return described def max_deriv_orders(self): + """ + Computes the maximum derivative orders for each axis in the solver form. + + This method analyzes the solver form of the equation to determine the + highest derivative order present for each spatial dimension. It iterates + through the terms in the solver form, counts the derivative order for each + axis, and updates the maximum orders accordingly. This is crucial for + determining the complexity of the equation and ensuring compatibility + with the numerical solver. + + Args: + self: The object instance. + + Returns: + np.ndarray: An array containing the maximum derivative order for each axis. + """ solver_form = self.solver_form() max_orders = np.zeros(global_var.grid_cache.get('0').ndim) @@ -800,6 +1590,34 @@ def count_order(obj, deriv_ax): def boundary_conditions(self, max_deriv_orders=(1,), main_var_key=('u', (1.0,)), full_domain: bool = False, grids : list = None): + """ + Generates boundary conditions for the problem domain. + + This method constructs boundary conditions necessary for solving + differential equations. It determines the locations and values + where the solution is known or constrained, using grid and tensor + data to define these conditions. These constraints are essential + for obtaining a unique and physically relevant solution to the + equation. + + Args: + max_deriv_orders: A tuple indicating the maximum derivative orders + for each axis. This determines how many boundary conditions + are needed along each dimension. + main_var_key: A tuple containing the key for the main variable + and its associated scaling factor. Specifies the variable + for which boundary conditions are being defined. + full_domain: A boolean flag indicating whether to use the full + domain for boundary conditions. If True, uses initial data + instead of the current grid state. + grids: A list of grids. + + Returns: + list: A list of boundary conditions, where each boundary condition + is a list containing coordinates, values, and the type of boundary + condition (currently only 'dirichlet'). These conditions constrain + the solution space of the differential equation. + """ required_bc_ord = max_deriv_orders # We assume, that the maximum order of the equation here is 2 if global_var.grid_cache is None: raise NameError('Grid cache has not been initialized yet.') @@ -838,20 +1656,88 @@ def get_boundary_ind(tensor_shape, axis, rel_loc): return bconds def clear_after_solver(self): + """ + Clears the solver and model from memory after solving. + + After a solution is found, this method releases the memory occupied by the model and solver. + This ensures efficient resource management, especially when exploring multiple equation candidates. + It also resets the flag indicating solver form definition. + + Args: + self: The Equation object instance. + + Returns: + None. + """ del self.model del self._solver_form self.solver_form_defined = False gc.collect() def __iter__(self): + """ + Returns an iterator for the equation. + + This allows to traverse the equation's components sequentially, which is essential for + various operations like evaluation, simplification, or transformation during the equation + discovery process. + + Args: + None + + Returns: + EquationIterator: An iterator object for traversing the equation. + """ return EquationIterator(self) class EquationIterator(object): + """ + An iterator class for traversing an equation's terms. + + Class Methods: + - __init__: Initializes the EquationIterator. + - __next__: Returns the next coefficient and term in the equation structure. + + Class Fields: + _internal_idx (int): An internal index used to track the current position in the equation's terms. + _equation (Equation): The equation being iterated over. + """ + def __init__(self, equation: Equation): + """ + Initializes the EquationIterator for traversing terms within an equation. + + The iterator prepares to step through the equation's terms, facilitating the extraction and manipulation of individual components during the equation discovery process. This is a crucial step for algorithms that analyze and modify equations to fit observed data. + + Args: + equation (Equation): The equation to iterate over. + + Returns: + None + """ self._internal_idx = 0 self._equation = equation def __next__(self) -> Tuple[Union[None, float], Term]: + """ + Returns the next coefficient and term in the equation. + + This iterator is used to traverse the equation's structure, yielding coefficient-term pairs. + If the equation has been evaluated (weights are available), it returns the corresponding weight as the coefficient for each term. + Terms with zero weight are skipped to optimize the equation's representation, except for the target term, which always has a coefficient of -1. + If the equation hasn't been evaluated, it returns `None` as the coefficient, indicating that the weights are not yet determined. + This is done to provide an efficient way to access the equation's components during the evolutionary process, + allowing for manipulation and evaluation of the equation's structure. + + Args: + self: The instance of the EquationIterator class. + + Returns: + Tuple[Union[None, float], Term]: A tuple containing the coefficient (a float if weights are evaluated, None otherwise, or -1.0 for the target term) and the Term object. + + Raises: + StopIteration: If the end of the equation structure is reached. + """ if self._internal_idx < len(self._equation.structure): if self._equation.weights_final_evald: while True: @@ -878,6 +1764,27 @@ def __next__(self) -> Tuple[Union[None, float], Term]: raise StopIteration def solver_formed_grid(training_grid=None): + """ + Solves a formed grid (deprecated). + + This method is intended to be deprecated as the approach of forming a grid + before equation discovery is no longer optimal within the EPDE framework. + It processes a training grid, potentially retrieving it from a global cache + if not provided. It then reshapes the grid and converts it into a PyTorch + FloatTensor. This was initially used to prepare data for the equation + discovery process, but newer methods directly operate on raw data or + use more flexible data structures. + + Args: + training_grid: The training grid to process. If None, the grid is + retrieved from the global cache. + + Returns: + torch.Tensor: A PyTorch FloatTensor representing the processed training grid. + + Raises: + NotImplementedError: Always raised, as this function is deprecated. + """ raise NotImplementedError('solver_formed_grid function is to be depricated') if training_grid is None: keys, training_grid = global_var.grid_cache.get_all() @@ -890,26 +1797,77 @@ def solver_formed_grid(training_grid=None): return torch.from_numpy(training_grid).T.type(torch.FloatTensor) def check_metaparameters(metaparameters: dict): + """ + Validates essential metaparameters required for equation discovery. + + This function ensures that the necessary configuration parameters + for the equation search process are present and structurally sound, + allowing the system to proceed with the equation discovery. + It checks for the existence of key labels within the provided + metaparameters dictionary. + + Args: + metaparameters (dict): A dictionary containing metaparameters, + expected to include keys like 'terms_number', + 'max_factors_in_term', and 'sparsity'. + + Returns: + bool: True, indicating that the metaparameters dictionary + contains the expected keys. Always returns True, but serves + as a placeholder for potential future validation logic. + """ metaparam_labels = ['terms_number', 'max_factors_in_term', 'sparsity'] return True class SoEq(moeadd.MOEADDSolution): + """ + Represents a system of equations (SoEq) for symbolic regression. + + The class encapsulates a collection of equations and provides + functionality for their manipulation, evaluation, and optimization + within an evolutionary algorithm. + + Class Methods: + - __init__ + - manual_reconst + - use_default_multiobjective_function + - use_legacy_multiobjective_function + - use_pic_multiobjective_function + - use_default_singleobjective_function + - set_objective_functions + - matches_complexitiy + - create + - equation_opt_iteration + - obj_fun + - __call__ + - text_form + - __eq__ + - latex_form + - __hash__ + - __deepcopy__ + - reset_state + - copy_properties_to + - solver_params + - __iter__ + - fitness_calculated + """ + def __init__(self, pool: TFPool, metaparameters: dict): ''' - - Parameters - ---------- - pool : epde.interface.token_familiy.TFPool - Pool, containing token families for the equation search algorithm. - metaparameters : dict - Metaparameters dictionary for the search. Key - label of the parameter (e.g. 'sparsity'), - value - tuple, containing flag for metaoptimization and initial value. - - Returns - ------- - None. - + Initializes the equation search object with token families and metaparameters. + + This setup prepares the search space by separating tokens intended for equation construction + from those used for supporting roles, and initializes the metaparameters that guide the search process. + It ensures that the provided metaparameters are valid and prepares the token pools for equation discovery. + + Args: + pool (epde.interface.token_familiy.TFPool): Pool containing token families for the equation search. + metaparameters (dict): Dictionary of metaparameters, where each key is a parameter label (e.g., 'sparsity') + and each value is a tuple containing a flag for metaoptimization and an initial value. + + Returns: + None ''' check_metaparameters(metaparameters) @@ -923,6 +1881,39 @@ def __init__(self, pool: TFPool, metaparameters: dict): self.vars_to_describe = [token_family.variable for token_family in self.tokens_for_eq.families] def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Manually reconstructs a chromosome attribute, ensuring the solution space remains valid after direct manipulation. + + This method allows for the targeted modification of a chromosome's attribute, + specifically the 'vals' attribute which represents the equation terms. It + validates the provided term definition and updates the chromosome's + internal state accordingly, maintaining the integrity of the equation discovery process. + + Args: + attribute: The attribute to reconstruct. Currently, only 'vals' is supported. + value: The new value for the specified attribute. For 'vals', this should be + a list of equation elements, each defining a term in the equation. + except_attrs: A dictionary of attributes to exclude during the reconstruction + process. This allows for selective updates while preserving other aspects + of the equation definition. + + Returns: + None. + + Raises: + ValueError: If the specified attribute is not supported. + + Class Fields: + vals (Chromosome): The chromosome representing the solution, containing + equations and optimizable metaparameters. Initialized with a new + Chromosome object based on the provided value and metaparameters. + + Why: + This method is crucial for directly influencing the equation search process. + It enables the user to inject prior knowledge or correct specific parts of + the equation structure, guiding the evolutionary algorithm towards more + promising solutions. + """ from epde.loader import attrs_from_dict, get_typespec_attrs supported_attrs = ['vals'] if attribute not in supported_attrs: @@ -939,12 +1930,44 @@ def manual_reconst(self, attribute:str, value, except_attrs:dict): if val['optimizable']}) def use_default_multiobjective_function(self, use_pic: bool = False): + """ + Selects and applies a default multi-objective function to guide the evolutionary search. + + This method determines which multi-objective function to use during the equation discovery process. + The choice between the PIC-based and legacy functions is made based on the `use_pic` flag, + allowing the system to adapt its optimization strategy. This choice influences how the algorithm + balances different objectives when searching for the best equation structure. + + Args: + use_pic: A boolean flag indicating whether to use the PIC-based + multi-objective function. + + Returns: + None. + """ if use_pic: self.use_pic_multiobjective_function() else: self.use_legacy_multiobjective_function() def use_legacy_multiobjective_function(self): + """ + Configures the evolutionary process to use a legacy multi-objective function. + + This method sets the objective functions for the evolutionary process + using a combination of equation fitness and equation complexity, + calculated for each variable to be described. It leverages legacy + functions for calculating fitness and complexity. This is done to + evaluate candidate equations based on both their accuracy in fitting + the data and their simplicity, guiding the search towards parsimonious + models. + + Args: + self: The object instance. + + Returns: + None. + """ from epde.eq_mo_objectives import generate_partial, equation_fitness, equation_complexity_by_factors complexity_objectives = [generate_partial(equation_complexity_by_factors, eq_key) for eq_key in self.vars_to_describe] @@ -954,6 +1977,20 @@ def use_legacy_multiobjective_function(self): quality_objectives + complexity_objectives) def use_pic_multiobjective_function(self): + """ + Sets the objective functions for multi-objective optimization. + + This method configures the objective functions used in the multi-objective optimization process. + It generates partial functions for equation fitness and equation term stability for each variable to be described. + Finally, it sets the objective functions to be a combination of quality (fitness) and stability objectives. + This configuration is crucial for guiding the search towards solutions that not only accurately describe the data but also exhibit stable and reliable behavior. + + Args: + self: The instance of the class. + + Returns: + None + """ from epde.eq_mo_objectives import generate_partial, equation_fitness, equation_complexity_by_factors, equation_terms_stability, equation_aic complexity_objectives = [generate_partial(equation_complexity_by_factors, eq_key) for eq_key in self.vars_to_describe] @@ -969,27 +2006,56 @@ def use_pic_multiobjective_function(self): quality_objectives + stability_objectives) def use_default_singleobjective_function(self): + """ + Sets the objective functions to default single-objective functions. + + It generates partial functions based on equation fitness for each variable + to describe and sets them as the objective functions. This is done to + evaluate the quality of candidate equations by focusing on individual + variables, enabling a more granular assessment of equation fitness + during the equation discovery process. + + Args: + self: The object instance. + + Returns: + None. + + Initializes: + objective_functions (list): A list of objective functions, where each function + evaluates the fitness of an equation with respect to a specific variable + being described. + """ from epde.eq_mo_objectives import generate_partial, equation_fitness quality_objectives = [generate_partial(equation_fitness, eq_key) for eq_key in self.vars_to_describe]#range(len(self.tokens_for_eq))] self.set_objective_functions(quality_objectives) def set_objective_functions(self, obj_funs): - ''' - Method to set the objective functions to evaluate the "quality" of the system of equations. - - Parameters: - ----------- - obj_funs - callable or list of callables; - function/functions to evaluate quality metrics of system of equations. Can return a single - metric (for example, quality of the process modelling with specific system), or - a list of metrics (for example, number of terms for each equation in the system). - The function results will be flattened after their application. - - ''' + """ + Method to define the functions used to assess the suitability of a generated system of equations. These functions quantify how well the equations capture the underlying dynamics of the system. + + Args: + obj_funs (callable or list of callables): A function or list of functions. Each function should evaluate the system of equations and return a numerical score (or a list of scores) representing its quality. For example, a function might measure the error between the model's predictions and the observed data. The results from all functions are combined to provide a comprehensive evaluation. + + Returns: + None + + Why: + Objective functions are set to evaluate generated equations and guide the search towards those that best represent the underlying system dynamics. + """ assert callable(obj_funs) or all([callable(fun) for fun in obj_funs]) self.obj_funs = obj_funs def matches_complexitiy(self, complexity : Union[int, list]): + """ + Checks if the provided complexity values align with the tail end of the objective function. This ensures that the evolutionary search is guided towards solutions that respect pre-defined complexity constraints or default to the objective function's inherent complexity. + + Args: + complexity (Union[int, list]): The complexity to check. If an integer or float, it's treated as a uniform complexity for all variables. If a list, it specifies the complexity for each variable being described. + + Returns: + bool: True if the last elements of the objective function match the (possibly adjusted) complexity, indicating a match; False otherwise. + """ if isinstance(complexity, (int, float)): complexity = [complexity,] @@ -1003,6 +2069,35 @@ def matches_complexitiy(self, complexity : Union[int, list]): return list(self.obj_fun[-len(adj_complexity):]) == adj_complexity def create(self, passed_equations: list = None): + """ + Creates the equation structure that defines the search space for potential solutions. + + This method sets up the equation structure, either by generating a default one + based on available tokens and variables or by using a pre-defined list of + equations. This structure is then used to initialize the chromosome, which + represents a candidate solution in the evolutionary search process. + + Args: + passed_equations: A list of Equation objects to use for the structure. + If None, a default structure is generated based on available tokens. + + Returns: + None + + Class Fields Initialized: + vals (Chromosome): A Chromosome object representing the solution, + initialized with the created equation structure and optimizable + metaparameters. + moeadd_set (bool): A boolean flag set to True, indicating that the + MOEADDSolution has been initialized. + + Why: + This method is crucial for defining the space of possible differential + equations that the evolutionary algorithm will explore. By creating an + appropriate equation structure, the search can be guided towards + discovering equations that accurately model the underlying dynamics of + the system. + """ if passed_equations is None: structure = {} @@ -1025,6 +2120,22 @@ def create(self, passed_equations: list = None): @staticmethod def equation_opt_iteration(population, evol_operator, population_size, iter_index, unexplained_vars, strict_restrictions=True): + """ + Performs one iteration of the equation optimization process. + + This method refines the population of equation candidates by penalizing those that include unexplained variables, sorting them based on their fitness, and truncating the population to maintain a manageable size. The evolutionary operator is then applied to generate a new, potentially improved population of equation candidates. This iterative process aims to evolve a population of equations that accurately describe the underlying dynamics of the system. + + Args: + population: The current population of equations. + evol_operator: The evolutionary operator to apply. + population_size: The desired size of the population. + iter_index: The index of the current iteration (unused). + unexplained_vars: A list of variables that are currently unexplained. + strict_restrictions: A boolean indicating whether to apply strict restrictions (default: True). (unused) + + Returns: + The new population after applying the evolutionary operator. + """ for equation in population: if equation.described_variables in unexplained_vars: equation.penalize_fitness(coeff=0.) @@ -1036,14 +2147,61 @@ def equation_opt_iteration(population, evol_operator, population_size, iter_inde @property def obj_fun(self): + """ + Calculates the objective function values based on the defined objective functions. + + This property evaluates each objective function defined within the model + and returns a flattened array of the resulting values. This is crucial + for the multi-objective optimization process, where the algorithm seeks + to minimize all objective functions simultaneously to find the best equation + representation. + + Returns: + np.ndarray: An array containing the flattened objective function values. + """ return np.array(flatten([func(self) for func in self.obj_funs])) def __call__(self): + """ + Call the object to retrieve the objective function. + + This method ensures that the equation's structure, represented by the moeadd_set, has been defined. + This is crucial because the objective function relies on this structure to evaluate the equation's fitness + against the data. + + Args: + self: The object instance. + + Returns: + The objective function. + """ assert self.moeadd_set, 'The structure of the equation is not defined, therefore no moeadd operations can be called' return self.obj_fun @property def text_form(self): + """ + Generates a text-based representation of the equation or system of equations. + + This method constructs a string representation of the object, + primarily based on the 'vals' (equations) and 'metaparameters' attributes. + If 'vals' contains multiple equations, it formats them with separators to + represent a system of equations. The metaparameters are also included + in the final string representation. + + Args: + self: The instance of the class (SoEq). + + Returns: + str: A string representation of the equation or system of equations, + including metaparameters. + + The text form is essential for displaying the discovered equation or + system of equations in a human-readable format, allowing users to + easily inspect the discovered relationships and their associated + metaparameters. This is crucial for understanding the model discovered + by the EPDE framework. + """ form = '' if len(self.vals) > 1: for eq_idx, equation in enumerate(self.vals): @@ -1059,6 +2217,22 @@ def text_form(self): return form def __eq__(self, other): + """ + Compares two equation objects for structural equality. + + This method determines if two equation objects are structurally equivalent by comparing their 'vals' attributes, + ensuring that they represent the same equation structure. Structural equality is crucial for + identifying redundant or equivalent equation forms during the evolutionary search process. + It verifies that each element in the 'vals' of both objects has a corresponding equal element in the 'vals' + of the other object, and that the lengths of their 'vals' are the same. It also asserts that the moeadd_set + is not empty, ensuring that the equation structure is properly defined before comparison. + + Args: + other: The equation object to compare with. + + Returns: + bool: True if the equation objects are structurally equal, False otherwise. + """ assert self.moeadd_set, 'The structure of the equation is not defined, therefore no moeadd operations can be called' return (all([any([other_elem == self_elem for other_elem in other.vals]) for self_elem in self.vals]) and all([any([other_elem == self_elem for self_elem in self.vals]) for other_elem in other.vals]) and @@ -1066,6 +2240,22 @@ def __eq__(self, other): @property def latex_form(self): + """ + Returns the LaTeX form of the equations. + + Generates a LaTeX representation of the stored equations, + formatted within an `eqnarray*` environment. This representation + facilitates the symbolic manipulation and interpretation of the + discovered equations. + + Args: + self: The object instance. + + Returns: + str: A LaTeX string representing the equations. + + Why: To provide a human-readable and easily integrable representation of the discovered equations for further analysis or use in other symbolic computation tools. + """ form = r"\begin{eqnarray*} " for idx, equation in enumerate(self.vals): postfix = '' if idx == len(self.vals) - 1 else r", \\ " @@ -1074,9 +2264,37 @@ def latex_form(self): return form def __hash__(self): + """ + Returns the hash value of the object. + + This hash value is derived from the internal 'vals' object's hash descriptor, ensuring that structurally equivalent objects are treated as identical for hashing purposes. This is crucial for efficient storage and retrieval of equations within sets and dictionaries, as it allows the framework to quickly identify and avoid redundant equation representations during the evolutionary search process. + + Args: + self: The object instance. + + Returns: + int: The hash value of the object. + """ return hash(self.vals.hash_descr) def __deepcopy__(self, memo=None): + """ + Creates a deep copy of the object, ensuring that all components are duplicated independently. + + This method is essential for preserving the integrity of equation structures + during evolutionary processes. It creates a new instance of the class and + recursively copies all attributes from the original object to the new object + using `copy.deepcopy`. This prevents unintended modifications to shared + components when exploring different equation candidates. Handles both + `__dict__` and `__slots__` attributes to ensure a complete and independent copy. + + Args: + memo (dict, optional): A dictionary used to keep track of objects that have + already been copied to prevent infinite recursion. Defaults to None. + + Returns: + SoEq: The deep copy of the object. + """ clss = self.__class__ new_struct = clss.__new__(clss) memo[id(self)] = new_struct @@ -1098,17 +2316,54 @@ def __deepcopy__(self, memo=None): return new_struct def reset_state(self, reset_right_part: bool = True): + """ + Resets the state of all equations stored in the object. + + This method iterates through each equation and resets its internal state, + preparing it for a new optimization cycle. This ensures that previous + computations do not influence subsequent evolutionary steps in discovering + the underlying differential equation. + + Args: + reset_right_part: Whether to reset the right-hand side of the equation. + + Returns: + None + """ for equation in self.vals: equation.reset_state(reset_right_part) def copy_properties_to(self, objective): + """ + Copies the equation properties to another object. + + This ensures that the objective object has the same equation + properties as the current object, maintaining consistency in + equation structure during the evolutionary process. + + Args: + objective: The objective object to copy properties to. + + Returns: + None. + """ for eq_label in self.vals.equation_keys: # Not the best code possible here self.vals[eq_label].copy_properties_to(objective.vals[eq_label]) def solver_params(self, full_domain, grids=None): - ''' - Returns solver form, grid and boundary conditions - ''' + """ + Returns the equation transformed into a solver-compatible form, the grid on which the solution is computed, and the boundary conditions applied to the equations. This is a crucial step in preparing the problem for numerical solution, ensuring that the discovered equations can be accurately solved within the specified domain and constraints. + + Args: + full_domain: The full domain of the problem. + grids: Optional grid to use for the solver. If None, a default grid is used. + + Returns: + A tuple containing: + - equation_forms: A list of equations in a solver-compatible form. + - solver_formed_grid: The grid used for the solver. + - bconds: A list of boundary conditions for each equation. + """ equation_forms = [] bconds = [] @@ -1120,20 +2375,77 @@ def solver_params(self, full_domain, grids=None): return equation_forms, solver_formed_grid(grids), bconds def __iter__(self): + """ + Returns an iterator for the equation. + + This allows to traverse through the equation's terms, enabling operations like evaluation and simplification. + + Returns: + SoEqIterator: An iterator object for traversing the elements of the SoEq object. + """ return SoEqIterator(self) @property def fitness_calculated(self): + """ + Indicates whether all equations in the system have their fitness values computed. This is crucial for assessing the overall quality of the equation system during the evolutionary search process. + + Args: + self: The SoEq instance. + + Returns: + bool: True if all equations have calculated fitness, False otherwise. + """ return all([equation.fitness_calculated for equation in self.vals]) class SoEqIterator(object): + """ + An iterator for stepping through solutions of a system of equations. + + Class Methods: + - __init__: + """ + def __init__(self, system: SoEq): + """ + Initializes the iterator with a system of equations. + + This prepares the iterator to traverse the solution space of the given equation system. + The iterator extracts the variables to be described and sets up the internal state for exploration. + + Args: + system: The system of equations to solve. + + Class Fields: + _idx: An index, initialized to 0, used for tracking the current position within the solution space. + system: The system of equations to be solved. + keys: A list of the variables to be described, extracted from the input system. + + Returns: + None + """ self._idx = 0 self.system = system self.keys = list(system.vars_to_describe) def __next__(self): + """ + Returns the next value associated with a key in the system. + + This method advances the iterator to the next key and retrieves the corresponding value + from the system's value store. It's used to sequentially access values during the + equation discovery process. + + Args: + self: The object instance. + + Returns: + The next value in the iteration. + + Raises: + StopIteration: If all keys have been iterated over, indicating the end of the sequence. + """ if self._idx < len(self.keys): res = self.system.vals[self.keys[self._idx]] self._idx += 1 diff --git a/epde/structure/structure_template.py b/epde/structure/structure_template.py index fd49f6ed..b215cef3 100644 --- a/epde/structure/structure_template.py +++ b/epde/structure/structure_template.py @@ -14,22 +14,98 @@ from collections import Iterable def check_uniqueness(obj, background): + """ + Checks if a given equation structure is unique within a population of candidate solutions. + + This prevents redundant computations and ensures diversity within the evolutionary search process. + + Args: + obj: The equation structure to check for uniqueness. + background: A list of existing equation structures in the population. + + Returns: + bool: True if the equation structure is not already present in the population, False otherwise. + """ return not any([elem == obj for elem in background]) class ComplexStructure(object): + """ + Represents a complex structure with a name and a dictionary of components. + + Methods: + - __init__ + - add_component + - get_component + - remove_component + - get_all_components + - set_name + - get_name + + Attributes: + - name (str): The name of the complex structure. + - components (dict): A dictionary storing the components of the structure. + """ + def __init__(self, interelement_operator=np.add, *params): + """ + Initializes the ComplexStructure object with an interelement operator. + + This method sets up the ComplexStructure by storing the provided operator, + which will be used to combine individual elements within the structure during + the equation discovery process. An empty history and a placeholder for the + structure are also initialized. The interelement operator is crucial for + defining how the discovered equation terms are combined to form a complete + equation. + + Args: + interelement_operator: The operator (e.g., np.add, np.multiply) to use when combining elements. Defaults to np.add. + *params: Variable length argument list (currently unused). + + Returns: + None. + """ self._history = '' self.structure = None self.interelement_operator = interelement_operator def manual_reconst(self, attribute:str, value, except_attrs:dict): + """ + Reconstructs a specific attribute of the object, bypassing the standard reconstruction process. + + This method allows for direct manipulation of the object's internal state, which is necessary when the standard reconstruction via evolutionary search or data loading fails to properly restore the object's attributes. It is used to ensure that critical components of the object are correctly initialized or updated, especially when dealing with complex dependencies or custom data structures. + + Args: + attribute (str): The name of the attribute to reconstruct. + value: The new value to assign to the attribute. + except_attrs (dict): A dictionary of attributes to exclude during the reconstruction process. + + Returns: + None. + + Raises: + ValueError: If the specified attribute is not supported for manual reconstruction. + """ from epde.loader import obj_to_pickle, attrs_from_dict supported_attrs = [] if attribute not in supported_attrs: raise ValueError(f'Attribute {attribute} is not supported by manual_reconst method.') def __eq__(self, other): + """ + Compares two `ComplexStructure` objects for structural equality, regardless of element order. + + This method determines if two `ComplexStructure` instances represent the same underlying structure, even if the elements within their respective structures are arranged differently. This is crucial for identifying equivalent equation structures during the equation discovery process, where the order of terms might vary without affecting the equation's meaning. + + Args: + other: The `ComplexStructure` object to compare with. + + Returns: + bool: True if the structures are equivalent, False otherwise. + + Raises: + ValueError: If 'other' is not a `ComplexStructure` instance. + """ if type(other) != type(self): raise ValueError('Type of self and other are different') return (all([any([other_elem == self_elem for other_elem in other.structure]) for self_elem in self.structure]) and @@ -37,16 +113,93 @@ def __eq__(self, other): len(other.structure) == len(self.structure)) def __iter__(self): + """ + Returns an iterator for the columns within this structure. + + This method enables iteration over the structure's columns, which is essential + for traversing and processing the data organized within. It returns a + `CSIterator` object, facilitating access to individual columns for + analysis or manipulation. + + Args: + self: The ComplexStructure instance. + + Returns: + CSIterator: An iterator object for the ComplexStructure. + + Why: + Iterating through columns allows the evolutionary algorithm to + explore different combinations and transformations of features, + which is crucial for discovering the underlying differential + equations. + """ return CSIterator(self) def hash_descr(self): + """ + Returns a hashable description of the complex structure. + + This method creates a tuple of hashable descriptions, one for each term + within the structure. This allows for efficient comparison and + identification of similar complex structures based on their constituent + terms. + + Args: + self: The ComplexStructure instance. + + Returns: + tuple: A tuple containing the hashable descriptions of the terms in the structure. + """ return tuple([term.hash_descr for term in self.structure]) def set_evaluator(self, evaluator): + """ + Sets the evaluator for the evolutionary algorithm. + + This method is no longer used directly. The evaluator is now configured + within the evolutionary operator to ensure that the equation discovery + process is tightly integrated with the search strategy. This change + promotes a more modular and flexible design, allowing for easier + experimentation with different evolutionary approaches. + + Args: + evaluator: The evaluator to be set (not used). + + Returns: + None. + + Raises: + NotImplementedError: Always raised, as the functionality has been moved. + """ raise NotImplementedError( 'Functionality of this method has been moved to the evolutionary operator declaration') def evaluate(self, structural=False): + """ + Evaluates the complex structure by recursively applying the interelement operator. + + This method orchestrates the evaluation of the entire complex structure. + If the structure is composed of a single element, that element is evaluated directly. + For multi-element structures, it iteratively applies the `interelement_operator` + to the evaluated results of each element, effectively reducing the entire structure + to a single representative value. This process mirrors how differential equations + are constructed from simpler terms and operations. + + Args: + structural (bool): A flag indicating whether structural evaluation should be performed + on the individual elements within the complex structure. + + Returns: + The result of evaluating the complex structure, representing the overall value + obtained after applying the interelement operator across all elements. + + Raises: + ValueError: If the operands within the structure cannot be combined due to + incompatible shapes or types, indicating a potential issue with the + structure's composition. + AssertionError: If the complex structure is empty, as there are no elements + to evaluate or combine. + """ assert len(self.structure) > 0, 'Attempt to evaluate an empty complex structure' if len(self.structure) == 1: return self.structure[0].evaluate(structural) @@ -59,6 +212,28 @@ def evaluate(self, structural=False): raise ValueError('operands could not be broadcast together with shapes') def reset_saved_state(self): + """ + Resets the saved state of the object and its constituent elements. + + This method ensures that the object and its internal structure are marked as not yet saved. + It achieves this by resetting the `saved` and `saved_as` attributes to their initial, unsaved states. + The method recursively calls itself on each element within the object's structure to propagate + this reset throughout the entire data representation. This is crucial to ensure consistency when + re-evaluating or re-fitting the equation discovery process, preventing the use of potentially outdated + saved states. + + Args: + self: The instance of the class. + + Returns: + None. + + Class Fields (initialized or modified): + saved (dict): A dictionary tracking whether the object has been saved + in different states (True/False). Initialized to {True: False, False: False}. + saved_as (dict): A dictionary storing the file paths where the object + has been saved in different states (True/False). Initialized to {True: None, False: None}. + """ self.saved = {True: False, False: False} self.saved_as = {True: None, False: None} for elem in self.structure: @@ -66,15 +241,75 @@ def reset_saved_state(self): @property def name(self): + """ + Gets the name of the complex structure. + + This name serves as an identifier for the structure within the equation discovery process, + allowing for easy referencing and organization of different structural components. + + Returns: + str: The name of the complex structure. + """ pass class CSIterator(object): + """ + An iterator for a case-insensitive sequence. + + This class wraps an existing sequence and provides an iterator that + returns each element of the sequence in lowercase. + + Class Methods: + - __init__: + """ + def __init__(self, complex_structure: ComplexStructure): + """ + Initializes the iterator with a complex structure. + + This constructor prepares the iterator to traverse the given complex structure, + starting from the initial element. The index is set to zero to begin iteration + from the beginning of the structure. + + Args: + complex_structure (ComplexStructure): The complex structure to be iterated over. + + Fields: + _idx (int): The current index of the iterator, initialized to 0. + _complex_structure (ComplexStructure): The complex structure being iterated. + + Returns: + None. + + Why: + The iterator needs to be initialized with the data structure it will traverse + and a starting index to keep track of the current position during iteration, + which is essential for systematically accessing elements within the structure. + """ self._idx = 0 self._complex_structure = complex_structure def __next__(self): + """ + Returns the next part of the equation's structure. + + This method enables sequential access to the components + of the complex equation structure, which is essential for + iterating through and processing the equation's elements + during the evolutionary search. + + Args: + self: The object instance. + + Returns: + The next element in the equation's structure. + + Raises: + StopIteration: If the end of the structure is reached, + indicating that all parts of the equation + have been processed. + """ if self._idx < len(self._complex_structure.structure): res = self._complex_structure.structure[self._idx] self._idx += 1 diff --git a/epde/supplementary.py b/epde/supplementary.py index 12a1097d..0b04a572 100644 --- a/epde/supplementary.py +++ b/epde/supplementary.py @@ -22,19 +22,93 @@ class BasicDeriv(ABC): + """ + Abstract base class for defining custom derivative implementations. + + This class serves as a template for creating new derivative methods. + It enforces the implementation of the `take_derivative` method. + + Methods: + - take_derivative: Abstract method for computing the derivative. + """ + def __init__(self, *args, **kwargs): + """ + Initializes the BasicDeriv object. + + This method is designed to prevent direct instantiation of the abstract `BasicDeriv` class. + Since `BasicDeriv` serves as a blueprint for specific differentiation strategies used in equation discovery, + it should not be instantiated directly. + + Args: + *args: Variable length argument list. Not used directly. + **kwargs: Arbitrary keyword arguments. Not used directly. + + Raises: + NotImplementedError: Always raised, indicating that direct instantiation of `BasicDeriv` is prohibited. + + Returns: + None. + """ raise NotImplementedError('Trying to create abstract differentiation method') def take_derivative(self, u: torch.Tensor, args: torch.Tensor, axes: list): + """ + Takes the derivative of a tensor with respect to specified variables. + + This method serves as a placeholder in the abstract `BasicDeriv` class and must be implemented by subclasses to provide concrete differentiation logic. Since `BasicDeriv` defines the interface for all differentiation methods, this abstract method ensures that any concrete implementation provides a `take_derivative` method. + + Args: + u (torch.Tensor): The input tensor to differentiate. + args (torch.Tensor): The tensor representing the variables with respect to which the derivative is taken. + axes (list): A list of axes along which to compute the derivative. + + Returns: + None: This method is abstract and should raise an error. + + Raises: + NotImplementedError: Always raised, as this is an abstract method that must be implemented by a subclass. + """ raise NotImplementedError('Trying to differentiate with abstract differentiation method') class AutogradDeriv(BasicDeriv): + """ + A class for computing derivatives using PyTorch's autograd functionality. + + Class Methods: + - __init__: + """ + def __init__(self): + """ + Initializes an AutogradDeriv object. + + This class facilitates automatic differentiation, enabling the computation of derivatives of mathematical expressions. + The initialization prepares the object for tracking operations and calculating gradients. + + Args: + self: The AutogradDeriv instance. + + Returns: + None. + """ pass def take_derivative(self, u: Union[torch.nn.Sequential, torch.Tensor], args: torch.Tensor, axes: list = [], component: int = 0): + """ + Computes the derivative of a function `u` with respect to input `args` along specified axes. This is a crucial step in identifying the underlying differential equations, as it allows us to estimate the rates of change of the system's variables. + + Args: + u: The function to differentiate. It can be a `torch.nn.Sequential` model or a `torch.Tensor`. Represents the system or model being analyzed. + args: The input tensor with respect to which the derivative is computed. Represents the independent variables of the system. + axes: A list of axes along which to compute the derivative. Defaults to an empty list. Specifies the dimensions along which the rate of change is calculated. + component: The component of the output to consider for differentiation. Defaults to 0. Allows focusing on specific parts of a multi-dimensional output. + + Returns: + torch.Tensor: The computed derivative. This derivative is then used to construct and evaluate candidate differential equations. + """ if not args.requires_grad: args.requires_grad = True if axes == [None,]: @@ -53,11 +127,54 @@ def take_derivative(self, u: Union[torch.nn.Sequential, torch.Tensor], args: tor return output_vals class FDDeriv(BasicDeriv): + """ + Calculates numerical derivatives of multi-dimensional arrays using finite difference methods. + + This class provides a method to compute the derivative of a function represented by a multi-dimensional array. + + Class Methods: + - take_derivative: + """ + def __init__(self): + """ + Initializes a new instance of the FDDeriv class. + + This method serves as the constructor for the FDDeriv class, preparing it + for subsequent operations involving finite difference approximations. + It currently performs no actions but initializes the object. + + Args: + self: The object instance. + + Returns: + None. + + Why: + The initialization prepares the FDDeriv object for calculating derivatives + using finite difference methods, a core component in discovering + differential equations from data by estimating derivatives from provided + data points. + """ pass def take_derivative(self, u: np.ndarray, args: np.ndarray, axes: list = [], component: int = 0): + """ + Calculates the numerical derivative of a function. + + This method computes the derivative of a function represented by a multi-dimensional array `u` with respect to specified axes. + It uses `np.gradient` to approximate the derivative. This is a crucial step in discovering the underlying differential equations, as it allows us to estimate the rates of change of the function, which are fundamental to formulating the equations. + + Args: + u (np.ndarray): The input array representing the function values. + args (np.ndarray): The coordinates at which the function is evaluated. + axes (list, optional): The axes along which to compute the derivative. Defaults to an empty list. + component (int, optional): The component of the input array to differentiate. Defaults to 0. + + Returns: + np.ndarray: The derivative of the input array along the specified axes. + """ if not isinstance(args, torch.Tensor): args = args.detach().cpu().numpy() @@ -71,9 +188,23 @@ def take_derivative(self, u: np.ndarray, args: np.ndarray, def create_solution_net(equations_num: int, domain_dim: int, use_fourier = True, # mode: str, domain: Domain fourier_params: dict = None, device = 'cpu'): - ''' - fft_params have to be passed as dict with entries like: {'L' : [4,], 'M' : [3,]} - ''' + """ + Creates a neural network architecture suitable for solving differential equations, optionally incorporating a Fourier embedding layer. + + The Fourier embedding enhances the network's ability to represent complex functions, particularly those arising from differential equations. + The network architecture is designed to map input coordinates to the solution of the differential equation. + + Args: + equations_num (int): The number of equations in the system. Determines the output dimension of the network. + domain_dim (int): The dimensionality of the input domain. + use_fourier (bool, optional): Whether to use a Fourier embedding layer. Defaults to True. + fourier_params (dict, optional): Parameters for the Fourier embedding layer. If None, default parameters are used. + Should be a dict with entries like: {'L' : [4,], 'M' : [3,]}. Defaults to None. + device (str, optional): The device to use for the network (e.g., 'cpu', 'cuda'). Defaults to 'cpu'. + + Returns: + torch.nn.Sequential: A sequential neural network model. + """ L_default, M_default = 4, 10 if use_fourier: if fourier_params is None: @@ -105,6 +236,24 @@ def create_solution_net(equations_num: int, domain_dim: int, use_fourier = True, return torch.nn.Sequential(*operators) def exp_form(a, sign_num: int = 4): + """ + Expresses a number in exponential form for equation simplification. + + This method decomposes a number into its normalized form and exponent. + The normalized form is the number divided by 10 raised to the power of the exponent, + and is rounded to a specified number of significant digits. This is useful for + representing coefficients and terms within differential equations in a consistent + and comparable manner, aiding in the discovery process. + + Args: + a: The number to express in exponential form. + sign_num: The number of significant digits to round the normalized form to. Defaults to 4. + + Returns: + A tuple containing: + - The normalized form of the number, rounded to `sign_num` significant digits. + - The exponent of the number (base 10). + """ if np.isclose(a, 0): return 0.0, 0 exp = np.floor(np.log10(np.abs(a))) @@ -113,7 +262,18 @@ def exp_form(a, sign_num: int = 4): def rts(value, sign_num: int = 5): """ - Round to a ``sign_num`` of significant digits. + Round the input value to a specified number of significant digits. + + This ensures that numerical values are represented with a consistent level of precision, + facilitating comparison and reducing the impact of insignificant variations when + identifying underlying equation structures. + + Args: + value (float): The numerical value to be rounded. + sign_num (int, optional): The number of significant digits to retain. Defaults to 5. + + Returns: + float: The rounded numerical value. """ if value == 0: return 0 @@ -126,6 +286,27 @@ def rts(value, sign_num: int = 5): def train_ann(args: list, data: np.ndarray, epochs_max: int = 500, batch_frac = 0.5, dim = None, model = None, device = 'cpu'): + """ + Trains an artificial neural network (ANN) model to approximate a given dataset. + + This method refines the ANN model to accurately represent the underlying patterns + within the data. By adjusting model architecture, training parameters, and device + usage, it optimizes the model's ability to capture the relationships present in the data. + This is a crucial step in creating a surrogate model that accurately reflects the + behavior of the system being studied. + + Args: + args: A list of arguments representing the grid coordinates of the data. + data: A NumPy array containing the data to be approximated. + epochs_max: The maximum number of training epochs. + batch_frac: The fraction of the data to use for each batch. + dim: The dimensionality of the data. If None, it is inferred from the data shape. + model: A PyTorch model to be trained. If None, a default model is created. + device: The device to use for training (e.g., 'cpu' or 'cuda'). + + Returns: + The best trained PyTorch model based on the minimum loss achieved during training. + """ if dim is None: dim = 1 if np.any([s == 1 for s in data.shape]) and data.ndim == 2 else data.ndim # assert len(args) == dim, 'Dimensionality of data does not match with passed grids.' @@ -193,6 +374,26 @@ def train_ann(args: list, data: np.ndarray, epochs_max: int = 500, batch_frac = return best_model def use_ann_to_predict(model, recalc_grids: list): + """ + Uses a pre-trained ANN model to predict values on given spatial grids. + + This method leverages a trained artificial neural network (ANN) to estimate values + across spatial grids. It prepares the grid data by reshaping and converting it + into a suitable format for the ANN model, performs the prediction, and then + restores the output to the original grid dimensions. This is a crucial step in + approximating the solution space of the discovered differential equations. + + Args: + model: The pre-trained ANN model to use for prediction. + recalc_grids: A list of NumPy arrays representing the spatial grids for which + predictions are to be made. These grids define the domain over which the + solution is approximated. + + Returns: + np.ndarray: A NumPy array containing the predicted values, reshaped to + match the shape of the input grids. These values represent the ANN's + approximation of the solution on the given spatial domain. + """ data_grid = np.stack([grid.reshape(-1) for grid in recalc_grids]) recalc_grid_tensor = torch.from_numpy(data_grid).float().T recalc_grid_tensor = recalc_grid_tensor #.to(device) @@ -200,9 +401,22 @@ def use_ann_to_predict(model, recalc_grids: list): return model(recalc_grid_tensor).detach().numpy().reshape(recalc_grids[0].shape) def flatten(obj): - ''' - Method to flatten list, passed as ``obj`` - the function parameter. - ''' + """ + Transforms a list of elements into a flat list of lists. + + This function ensures that each element within the input list is itself a list, + converting non-list elements into single-element lists before concatenating + all sublists into a single, flattened list. This is a preliminary step for + further processing, ensuring data compatibility for subsequent equation discovery. + + Args: + obj (list): The list to be flattened. Each element should ideally be a list + or convertible to a list. + + Returns: + list: A flattened list containing all elements from the original list, + with each original element now residing within its own sublist. + """ assert type(obj) == list for idx, elem in enumerate(obj): @@ -211,12 +425,46 @@ def flatten(obj): return reduce(lambda x, y: x+y, obj) def factor_params_to_str(factor, set_default_power=False, power_idx=0): + """ + Converts factor parameters to a string representation for equation building. + + This method prepares the parameters of a factor within a potential differential equation + for representation as a string. It retrieves the factor's parameters and label, + optionally setting a specific parameter to a default value of 1. This is useful + when exploring different equation structures where certain terms might be temporarily + disabled or set to a neutral value during the evolutionary search process. + + Args: + factor: The factor object whose parameters are to be converted. + set_default_power: A boolean indicating whether to set a default + value for a specific parameter. Defaults to False. + power_idx: The index of the parameter to set to the default value + if `set_default_power` is True. Defaults to 0. + + Returns: + A tuple containing the factor's label and a tuple of its parameters. + """ param_label = np.copy(factor.params) if set_default_power: param_label[power_idx] = 1. return (factor.label, tuple(param_label)) def form_label(x, y): + """ + Forms a descriptive label by combining a base string with a component's identifier. + + This function constructs a label that represents a combination of terms within a differential equation. + It's used to create human-readable representations of equation components during the equation discovery process. + The label is formed by concatenating a base string `x` with the `cache_label` of a component `y`, + inserting " * " if the base string is not empty. + + Args: + x (str): The base string, potentially representing a combination of terms. + y: An object with a 'cache_label' attribute (string) representing a component of the equation. + + Returns: + str: The combined label string. + """ print(type(x), type(y.cache_label)) return x + ' * ' + y.cache_label if len(x) > 0 else x + y.cache_label @@ -232,6 +480,26 @@ def detect_similar_terms(base_equation_1, base_equation_2): # Передела different_terms_from_eq1 = [] different_terms_from_eq2 = [] for eq1_term in base_equation_1.structure: + """ + Detects and categorizes corresponding terms between two base equations. + + This method aligns terms from two base equations, classifying them as 'same', + 'similar', or 'different' based on their structural and label similarities. + This alignment is crucial for identifying shared components and variations + between different equation representations of the same underlying phenomenon. + + Args: + base_equation_1: The first base equation to compare, represented as a structured object. + base_equation_2: The second base equation to compare, represented as a structured object. + + Returns: + tuple: A tuple containing two lists. The first list represents terms from + `base_equation_1` and the second represents terms from `base_equation_2`. + Each list contains three sub-lists: + - same_terms: Terms that are identical in both structure and labels. + - similar_terms: Terms that share the same labels but may differ in structure. + - different_terms: Terms that are unique to the respective equation. + """ found_similar = False for idx, eq2_term in enumerate(base_equation_2.structure): if eq1_term == eq2_term and not eq2_processed[idx]: @@ -262,6 +530,28 @@ def detect_similar_terms(base_equation_1, base_equation_2): # Передела def filter_powers(gene): + """ + Filters a gene to refine the representation of equation terms. + + This method aggregates the 'power' parameter of tokens within a gene that + exhibit partial equality. By summing the powers of similar tokens and + capping the result at a maximum value, it ensures that the gene + representation remains concise and avoids over-emphasizing redundant terms + in the equation. This process helps to simplify the equation structure + and improve the overall interpretability of the discovered model. + + Args: + gene: A list of tokens representing a gene. Each token is expected + to have a `partial_equlaity` method and a `params` attribute, + where `params` is a list of parameter values and + `params_description` is a dictionary describing the parameters. + Each parameter description should have 'name' and 'bounds' keys. + + Returns: + A list of tokens representing the filtered gene, where each token's + 'power' parameter has been updated based on the total power of + partially equal tokens in the original gene. + """ gene_filtered = [] for token_idx in range(len(gene)): @@ -283,16 +573,22 @@ def filter_powers(gene): def define_derivatives(var_name='u', dimensionality=1, max_order=2): """ - Method for generating derivative keys - + Generates derivative keys and corresponding derivative orders up to a specified order for each dimension. + + This function is crucial for constructing the search space of potential differential equations. + By systematically generating derivative keys, the algorithm can explore various combinations of derivatives + to identify the equation that best describes the underlying dynamics of the system. + Args: - var_name (`str`): name of input data dependent variable - dimensionality (`int`): dimensionallity of data - max_order (`int`|`list`): max order of delivative + var_name (`str`): Name of the dependent variable. Defaults to 'u'. + dimensionality (`int`): Dimensionality of the data. Defaults to 1. + max_order (`int` | `list`): Maximum order of derivative. If an integer, the same maximum order is applied to all dimensions. + If a list, each element specifies the maximum order for the corresponding dimension. Defaults to 2. Returns: - deriv_names (`list` with `str` values): keys for epde - var_deriv_orders (`list` with `int` values): keys for enter to solver + `tuple`: A tuple containing two lists: + - `deriv_names` (`list` of `str`): Keys representing the derivative terms (e.g., 'du/dx0', 'd^2u/dx1^2'). + - `var_deriv_orders` (`list` of `list` of `int`): Keys for accessing the derivatives in numerical solvers. Each sublist indicates the variable index and the order of differentiation with respect to that variable (e.g., `[[0], [0, 0]]` for du/dx0 and d^2u/dx0^2). """ deriv_names = [] var_deriv_orders = [] @@ -311,6 +607,22 @@ def define_derivatives(var_name='u', dimensionality=1, max_order=2): def population_sort(input_population): + """ + Sorts the population to prioritize well-performing individuals. + + This function arranges the population based on the fitness of each + individual, ensuring that those with higher fitness values are placed + earlier in the sorted list. This is a crucial step in the evolutionary + process, as it allows the algorithm to focus on the most promising + candidates for equation discovery. + + Args: + input_population: A list of individuals representing the population. + + Returns: + A new list containing the individuals from the input population, + sorted in descending order of fitness value. + """ individ_fitvals = [ individual.fitness_value if individual.fitness_calculated else 0 for individual in input_population] pop_sorted = [x for x, _ in sorted( @@ -319,6 +631,23 @@ def population_sort(input_population): def normalize_ts(Input): + """ + Normalizes a time series matrix by subtracting the mean and dividing by the standard deviation. + + This normalization ensures that each time series has a zero mean and unit variance, + which is crucial for algorithms that are sensitive to the scale of the input data. + If a time series has a standard deviation of zero, it is set to a constant value of 1 to avoid division by zero and to represent a stable, unchanging signal. + + Args: + Input (np.ndarray): The input time series data. It can be a 1D or 2D numpy array. + + Returns: + np.ndarray: The normalized time series data. If the input is a 1D array, it returns the same array. + If the input is a 2D array, it returns a normalized 2D array. + + Raises: + ValueError: If the input data has 0 dimensions. + """ matrix = np.copy(Input) if np.ndim(matrix) == 0: raise ValueError( @@ -337,9 +666,13 @@ def normalize_ts(Input): def minmax_normalize(matrix): """ - Apply min-max normalization to a matrix. - For 1D arrays: returns as-is - For 2D+ arrays: normalizes each row to [0, 1] range + Apply min-max normalization to the input matrix to ensure consistent scaling of data ranges, which is crucial for the equation discovery process. Normalization helps to prevent features with larger values from dominating the search for governing equations. + + Args: + matrix (numpy.ndarray): The input matrix to be normalized. + + Returns: + numpy.ndarray: The normalized matrix. For 1D arrays, the original array is returned. For 2D+ arrays, each row is normalized to the [0, 1] range. """ matrix = np.copy(matrix) diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/osa_docs/epde/cache/cache.md b/osa_docs/epde/cache/cache.md new file mode 100644 index 00000000..38a28cca --- /dev/null +++ b/osa_docs/epde/cache/cache.md @@ -0,0 +1,5 @@ +# Cache + + + +::: epde.cache.cache \ No newline at end of file diff --git a/osa_docs/epde/cache/ctrl_cache.md b/osa_docs/epde/cache/ctrl_cache.md new file mode 100644 index 00000000..584a8e07 --- /dev/null +++ b/osa_docs/epde/cache/ctrl_cache.md @@ -0,0 +1,5 @@ +# Ctrl Cache + + + +::: epde.cache.ctrl_cache \ No newline at end of file diff --git a/osa_docs/epde/cache/index.md b/osa_docs/epde/cache/index.md new file mode 100644 index 00000000..d5c91017 --- /dev/null +++ b/osa_docs/epde/cache/index.md @@ -0,0 +1,9 @@ +# Cache Module + +## Overview + +The Cache module provides functionalities for managing and storing intermediate data, such as tensors and grids, during the equation discovery process. It includes tools for uploading, transforming, and converting data between different formats (NumPy arrays and PyTorch tensors). The module also incorporates neural networks for control purposes, encapsulating them within a dedicated container. + +## Purpose + +The primary purpose of the Cache module is to optimize the performance of the equation discovery process by caching and reusing intermediate computations. This involves storing precomputed tensors, managing data transformations, and providing a mechanism for integrating neural networks into the equation discovery workflow. The module facilitates the efficient evaluation of equation terms and factors, contributing to faster and more accurate identification of governing differential equations. diff --git a/osa_docs/epde/control/constr.md b/osa_docs/epde/control/constr.md new file mode 100644 index 00000000..3b82a898 --- /dev/null +++ b/osa_docs/epde/control/constr.md @@ -0,0 +1,5 @@ +# Constr + + + +::: epde.control.constr \ No newline at end of file diff --git a/osa_docs/epde/control/control.md b/osa_docs/epde/control/control.md new file mode 100644 index 00000000..6cd5e241 --- /dev/null +++ b/osa_docs/epde/control/control.md @@ -0,0 +1,5 @@ +# Control + + + +::: epde.control.control \ No newline at end of file diff --git a/osa_docs/epde/control/index.md b/osa_docs/epde/control/index.md new file mode 100644 index 00000000..264f089b --- /dev/null +++ b/osa_docs/epde/control/index.md @@ -0,0 +1,15 @@ +# Control Module + +## Overview + +The Control module within the EPDE project focuses on providing tools for incorporating control strategies into the equation discovery and modeling process. It offers classes and functions for defining control constraints, implementing optimization algorithms, and conducting control experiments. This module enables the user to optimize system behavior by manipulating control parameters within the discovered differential equation models. + +## Purpose + +The primary purpose of the Control module is to facilitate the integration of control mechanisms into the EPDE framework. This allows users to not only discover the underlying differential equations governing a system but also to actively influence the system's behavior through optimized control inputs. The module provides functionalities for: + +* Defining and applying constraints on control parameters. +* Implementing various optimization algorithms to find optimal control strategies. +* Conducting control experiments to evaluate the effectiveness of different control approaches. +* Calculating finite-difference approximations of gradients for parameter optimization. +* Preparing control input tensors from solutions of controlled equations. diff --git a/osa_docs/epde/control/optim.md b/osa_docs/epde/control/optim.md new file mode 100644 index 00000000..2bb1243c --- /dev/null +++ b/osa_docs/epde/control/optim.md @@ -0,0 +1,5 @@ +# Optim + + + +::: epde.control.optim \ No newline at end of file diff --git a/osa_docs/epde/control/utils.md b/osa_docs/epde/control/utils.md new file mode 100644 index 00000000..a0f8ecfe --- /dev/null +++ b/osa_docs/epde/control/utils.md @@ -0,0 +1,5 @@ +# Utils + + + +::: epde.control.utils \ No newline at end of file diff --git a/osa_docs/epde/decorators.md b/osa_docs/epde/decorators.md new file mode 100644 index 00000000..c12595ed --- /dev/null +++ b/osa_docs/epde/decorators.md @@ -0,0 +1,5 @@ +# Decorators + + + +::: epde.decorators \ No newline at end of file diff --git a/osa_docs/epde/eq_mo_objectives.md b/osa_docs/epde/eq_mo_objectives.md new file mode 100644 index 00000000..1c515e86 --- /dev/null +++ b/osa_docs/epde/eq_mo_objectives.md @@ -0,0 +1,5 @@ +# Eq Mo Objectives + + + +::: epde.eq_mo_objectives \ No newline at end of file diff --git a/osa_docs/epde/evaluators.md b/osa_docs/epde/evaluators.md new file mode 100644 index 00000000..0d5bd5d7 --- /dev/null +++ b/osa_docs/epde/evaluators.md @@ -0,0 +1,5 @@ +# Evaluators + + + +::: epde.evaluators \ No newline at end of file diff --git a/osa_docs/epde/globals.md b/osa_docs/epde/globals.md new file mode 100644 index 00000000..112b9ecf --- /dev/null +++ b/osa_docs/epde/globals.md @@ -0,0 +1,5 @@ +# Globals + + + +::: epde.globals \ No newline at end of file diff --git a/osa_docs/epde/index.md b/osa_docs/epde/index.md new file mode 100644 index 00000000..0dacbd13 --- /dev/null +++ b/osa_docs/epde/index.md @@ -0,0 +1,24 @@ +```markdown +# EPDE Module + +## Overview + +The EPDE module provides a comprehensive framework for discovering and modeling differential equations from data. It encompasses a wide range of functionalities, including data preprocessing, equation representation, evolutionary optimization, and numerical solution techniques. The module is designed to facilitate the automated identification of governing equations from experimental or observational data. + +## Purpose + +The primary purpose of the EPDE module is to automate the process of discovering differential equations from data. This involves: + +* **Data Handling:** Providing tools for loading, caching, transforming, and normalizing data, including time series data. +* **Equation Representation:** Defining data structures for representing equations, terms, and factors, and providing mechanisms for parsing and translating equations from text-based formats. +* **Derivative Calculation:** Implementing various methods for calculating numerical derivatives, including finite differences, spectral methods, and automatic differentiation. +* **Evolutionary Optimization:** Employing evolutionary algorithms and multi-objective optimization techniques to search for equation structures that best fit the observed data. +* **Fitness Evaluation:** Defining fitness functions to evaluate the quality of candidate equations based on their ability to reproduce the observed data. +* **Sparsity Application:** Simplifying equations by removing insignificant terms and applying sparsity constraints. +* **Numerical Solution:** Providing tools for numerically solving differential equations using various methods, including spectral solvers, physics-informed neural networks (PINNs), and traditional numerical integration techniques. +* **Control Integration:** Incorporating control strategies into the equation discovery and modeling process, allowing users to optimize system behavior through control parameters. +* **Experiment Management:** Combining results from multiple experiments to identify the best solutions and providing tools for analyzing the equation discovery process. +* **Model Training and Prediction:** Training artificial neural networks (ANNs) to approximate data and using them to predict values for given grids. +* **Data Preprocessing:** Smoothing data, pruning the domain, and interpolating/oversampling data to prepare it for equation discovery. +* **Parametric Equation Optimization:** Representing and optimizing parametric equations by managing optimizable parameters within factors. +``` \ No newline at end of file diff --git a/osa_docs/epde/integrate/bop.md b/osa_docs/epde/integrate/bop.md new file mode 100644 index 00000000..9d754c3b --- /dev/null +++ b/osa_docs/epde/integrate/bop.md @@ -0,0 +1,5 @@ +# Bo + + + +::: epde.integrate.bop \ No newline at end of file diff --git a/osa_docs/epde/integrate/index.md b/osa_docs/epde/integrate/index.md new file mode 100644 index 00000000..273a965f --- /dev/null +++ b/osa_docs/epde/integrate/index.md @@ -0,0 +1,11 @@ +```markdown +# Integration Module + +## Overview + +The Integration Module within the EPDE project focuses on numerically solving differential equations and handling boundary conditions. It provides tools for setting up and solving both ordinary and partial differential equations, including implicit equation systems. The module incorporates functionalities for spectral solvers, PINN (Physics-Informed Neural Networks) solvers, and traditional numerical integration methods. It also includes utilities for pre-generating boundary conditions and adapting various solvers to a unified interface. + +## Purpose + +The primary purpose of the Integration Module is to provide a set of tools and classes that facilitate the numerical solution of differential equations identified or modeled within the EPDE framework. This includes adapting different solver types (spectral, PINN, odeint), handling boundary conditions, and managing the setup of implicit equation systems for numerical computation. The module aims to bridge the gap between symbolic equation representation and numerical solution, enabling users to simulate and analyze the behavior of systems described by differential equations. +``` \ No newline at end of file diff --git a/osa_docs/epde/integrate/numeric_integration.md b/osa_docs/epde/integrate/numeric_integration.md new file mode 100644 index 00000000..f96de20d --- /dev/null +++ b/osa_docs/epde/integrate/numeric_integration.md @@ -0,0 +1,5 @@ +# Numeric Integration + + + +::: epde.integrate.numeric_integration \ No newline at end of file diff --git a/osa_docs/epde/integrate/pinn_integration.md b/osa_docs/epde/integrate/pinn_integration.md new file mode 100644 index 00000000..d8c0800b --- /dev/null +++ b/osa_docs/epde/integrate/pinn_integration.md @@ -0,0 +1,5 @@ +# Inn Integration + + + +::: epde.integrate.pinn_integration \ No newline at end of file diff --git a/osa_docs/epde/interface/equation_translator.md b/osa_docs/epde/interface/equation_translator.md new file mode 100644 index 00000000..573b143f --- /dev/null +++ b/osa_docs/epde/interface/equation_translator.md @@ -0,0 +1,5 @@ +# Equation Translator + + + +::: epde.interface.equation_translator \ No newline at end of file diff --git a/osa_docs/epde/interface/index.md b/osa_docs/epde/interface/index.md new file mode 100644 index 00000000..72f82251 --- /dev/null +++ b/osa_docs/epde/interface/index.md @@ -0,0 +1,15 @@ +# Interface + +## Overview + +The `interface` module within the EPDE project serves as a crucial bridge between raw data, equation representations, and the evolutionary search algorithms that drive the equation discovery process. It provides tools for translating equations from text-based formats, defining custom tokens representing mathematical functions and data features, and managing the overall search strategy. This module encapsulates the core logic for parsing equations, preparing data, and configuring the evolutionary search, enabling users to effectively explore the space of possible equation structures. + +## Purpose + +The primary purpose of the `interface` module is to facilitate the automated discovery of differential equations from data by providing the necessary components for: + +* **Equation Translation:** Converting equations from human-readable text formats into a structured representation suitable for manipulation by the EPDE framework. +* **Token Definition:** Defining custom tokens that represent mathematical functions, data features (e.g., polynomials, trigonometric functions), or control variables, allowing the user to tailor the search space to the specific problem. +* **Search Configuration:** Setting up the evolutionary search strategy, including parameters for optimization algorithms, multi-objective optimization, and data preprocessing. +* **Experiment Management:** Combining results from multiple experiments to identify the best solutions and providing tools for analyzing the equation discovery process. +* **Multi-Sample Handling:** Extending the EPDE framework to handle multiple datasets, enabling equation discovery across different experimental conditions or scenarios. diff --git a/osa_docs/epde/interface/interface.md b/osa_docs/epde/interface/interface.md new file mode 100644 index 00000000..72fa9736 --- /dev/null +++ b/osa_docs/epde/interface/interface.md @@ -0,0 +1,5 @@ +# Interface + + + +::: epde.interface.interface \ No newline at end of file diff --git a/osa_docs/epde/interface/logger.md b/osa_docs/epde/interface/logger.md new file mode 100644 index 00000000..ce5cce02 --- /dev/null +++ b/osa_docs/epde/interface/logger.md @@ -0,0 +1,5 @@ +# Logger + + + +::: epde.interface.logger \ No newline at end of file diff --git a/osa_docs/epde/interface/prepared_tokens.md b/osa_docs/epde/interface/prepared_tokens.md new file mode 100644 index 00000000..a9dba2fc --- /dev/null +++ b/osa_docs/epde/interface/prepared_tokens.md @@ -0,0 +1,5 @@ +# Repared Tokens + + + +::: epde.interface.prepared_tokens \ No newline at end of file diff --git a/osa_docs/epde/interface/token_family.md b/osa_docs/epde/interface/token_family.md new file mode 100644 index 00000000..df8a74f8 --- /dev/null +++ b/osa_docs/epde/interface/token_family.md @@ -0,0 +1,5 @@ +# Token Famil + + + +::: epde.interface.token_family \ No newline at end of file diff --git a/osa_docs/epde/interface/type_checks.md b/osa_docs/epde/interface/type_checks.md new file mode 100644 index 00000000..e5bf5f5e --- /dev/null +++ b/osa_docs/epde/interface/type_checks.md @@ -0,0 +1,5 @@ +# Type Checks + + + +::: epde.interface.type_checks \ No newline at end of file diff --git a/osa_docs/epde/loader.md b/osa_docs/epde/loader.md new file mode 100644 index 00000000..6235c138 --- /dev/null +++ b/osa_docs/epde/loader.md @@ -0,0 +1,5 @@ +# Loader + + + +::: epde.loader \ No newline at end of file diff --git a/osa_docs/epde/operators/common/coeff_calculation.md b/osa_docs/epde/operators/common/coeff_calculation.md new file mode 100644 index 00000000..111ae607 --- /dev/null +++ b/osa_docs/epde/operators/common/coeff_calculation.md @@ -0,0 +1,5 @@ +# Coeff Calculation + + + +::: epde.operators.common.coeff_calculation \ No newline at end of file diff --git a/osa_docs/epde/operators/common/fitness.md b/osa_docs/epde/operators/common/fitness.md new file mode 100644 index 00000000..ab3ea53c --- /dev/null +++ b/osa_docs/epde/operators/common/fitness.md @@ -0,0 +1,5 @@ +# Fitness + + + +::: epde.operators.common.fitness \ No newline at end of file diff --git a/osa_docs/epde/operators/common/index.md b/osa_docs/epde/operators/common/index.md new file mode 100644 index 00000000..6e821079 --- /dev/null +++ b/osa_docs/epde/operators/common/index.md @@ -0,0 +1,11 @@ +```markdown +# Common + +## Overview + +The `common` module provides a set of tools and operators essential for the equation discovery process within the EPDE framework. It includes functionalities for calculating equation coefficients, evaluating fitness functions, selecting the right-hand side of equations, and applying sparsity techniques. This module is designed to work with data and equation objects, facilitating the search for the best-fitting equation structures. + +## Purpose + +The primary purpose of the `common` module is to provide the building blocks for constructing and evaluating candidate differential equations. It offers a range of fitness functions to quantify how well an equation matches the observed data, methods for selecting the most appropriate terms for the equation, and techniques for simplifying equations by identifying and removing insignificant terms. These components are crucial for automating the process of identifying governing differential equations from data. +``` \ No newline at end of file diff --git a/osa_docs/epde/operators/common/right_part_selection.md b/osa_docs/epde/operators/common/right_part_selection.md new file mode 100644 index 00000000..f4749311 --- /dev/null +++ b/osa_docs/epde/operators/common/right_part_selection.md @@ -0,0 +1,5 @@ +# Right Part Selection + + + +::: epde.operators.common.right_part_selection \ No newline at end of file diff --git a/osa_docs/epde/operators/common/sparsity.md b/osa_docs/epde/operators/common/sparsity.md new file mode 100644 index 00000000..76252645 --- /dev/null +++ b/osa_docs/epde/operators/common/sparsity.md @@ -0,0 +1,5 @@ +# Sparsit + + + +::: epde.operators.common.sparsity \ No newline at end of file diff --git a/osa_docs/epde/operators/index.md b/osa_docs/epde/operators/index.md new file mode 100644 index 00000000..f8cfd1c2 --- /dev/null +++ b/osa_docs/epde/operators/index.md @@ -0,0 +1,11 @@ +```markdown +# Operators + +## Overview + +The `operators` module within the EPDE framework provides a suite of tools and techniques for discovering and optimizing differential equations from data. It encompasses functionalities for single-objective and multi-objective optimization, equation coefficient calculation, fitness evaluation, and sparsity application. The module is designed to facilitate the construction, evaluation, and simplification of candidate differential equations, enabling the identification of governing equations from data. + +## Purpose + +The primary purpose of the `operators` module is to supply the essential building blocks for constructing and refining candidate differential equations. It offers a range of optimization algorithms, fitness functions, selection methods, and utility functions to quantify how well an equation matches the observed data, select the most appropriate terms for the equation, and simplify equations by removing insignificant terms. This module automates the process of identifying governing differential equations from data by providing tools for parameter management, operator mapping, and handling compound operations. +``` \ No newline at end of file diff --git a/osa_docs/epde/operators/multiobjective/index.md b/osa_docs/epde/operators/multiobjective/index.md new file mode 100644 index 00000000..74607305 --- /dev/null +++ b/osa_docs/epde/operators/multiobjective/index.md @@ -0,0 +1,9 @@ +# Multi-Objective Optimization Module + +## Overview + +This module focuses on multi-objective optimization techniques used within the EPDE framework. It provides tools for handling populations of candidate solutions, evaluating their performance across multiple objectives, and driving the evolutionary search process. Key functionalities include distributing solutions into sectors defined by weight vectors, selecting solutions based on Pareto dominance and penalty-based intersection, and updating populations by removing less desirable candidates. The module also incorporates mechanisms for constraint handling, ensuring that solutions adhere to specified constraints. + +## Purpose + +The primary purpose of this module is to implement the multi-objective optimization algorithms used to discover differential equations from data. It provides the necessary components for evolving a population of equation candidates, considering multiple objectives such as model accuracy and complexity. The module facilitates the search for Pareto-optimal solutions, representing the best trade-offs between competing objectives, and incorporates variation and selection operators to drive the evolutionary process. diff --git a/osa_docs/epde/operators/multiobjective/moeadd_specific.md b/osa_docs/epde/operators/multiobjective/moeadd_specific.md new file mode 100644 index 00000000..447736fc --- /dev/null +++ b/osa_docs/epde/operators/multiobjective/moeadd_specific.md @@ -0,0 +1,5 @@ +# Moeadd Specific + + + +::: epde.operators.multiobjective.moeadd_specific \ No newline at end of file diff --git a/osa_docs/epde/operators/multiobjective/mutations.md b/osa_docs/epde/operators/multiobjective/mutations.md new file mode 100644 index 00000000..1879b074 --- /dev/null +++ b/osa_docs/epde/operators/multiobjective/mutations.md @@ -0,0 +1,5 @@ +# Mutations + + + +::: epde.operators.multiobjective.mutations \ No newline at end of file diff --git a/osa_docs/epde/operators/multiobjective/selections.md b/osa_docs/epde/operators/multiobjective/selections.md new file mode 100644 index 00000000..2ef92dd4 --- /dev/null +++ b/osa_docs/epde/operators/multiobjective/selections.md @@ -0,0 +1,5 @@ +# Selections + + + +::: epde.operators.multiobjective.selections \ No newline at end of file diff --git a/osa_docs/epde/operators/multiobjective/variation.md b/osa_docs/epde/operators/multiobjective/variation.md new file mode 100644 index 00000000..7629a989 --- /dev/null +++ b/osa_docs/epde/operators/multiobjective/variation.md @@ -0,0 +1,5 @@ +# Variation + + + +::: epde.operators.multiobjective.variation \ No newline at end of file diff --git a/osa_docs/epde/operators/singleobjective/ea_stop_criteria.md b/osa_docs/epde/operators/singleobjective/ea_stop_criteria.md new file mode 100644 index 00000000..c8f51126 --- /dev/null +++ b/osa_docs/epde/operators/singleobjective/ea_stop_criteria.md @@ -0,0 +1,5 @@ +# Ea Stop Criteria + + + +::: epde.operators.singleobjective.ea_stop_criteria \ No newline at end of file diff --git a/osa_docs/epde/operators/singleobjective/index.md b/osa_docs/epde/operators/singleobjective/index.md new file mode 100644 index 00000000..7c765608 --- /dev/null +++ b/osa_docs/epde/operators/singleobjective/index.md @@ -0,0 +1,11 @@ +```markdown +# Singleobjective + +## Overview + +The `singleobjective` module provides components for performing single-objective evolutionary optimization within the EPDE framework. It includes tools for defining stopping criteria, implementing mutation and crossover operators, and applying selection strategies. This module focuses on evolving equation structures to fit observed data by using specific genetic operators and selection mechanisms tailored for single-objective optimization. + +## Purpose + +The primary purpose of the `singleobjective` module is to facilitate the discovery of differential equations from data by using evolutionary algorithms that optimize a single objective function. This involves defining how candidate equations are mutated and crossed over, determining which individuals are selected for reproduction, and establishing criteria for when the evolutionary process should terminate. The module provides specific implementations for these steps, allowing users to effectively search for equation structures that best represent the underlying dynamics of a system. +``` \ No newline at end of file diff --git a/osa_docs/epde/operators/singleobjective/mutations.md b/osa_docs/epde/operators/singleobjective/mutations.md new file mode 100644 index 00000000..de3f5df9 --- /dev/null +++ b/osa_docs/epde/operators/singleobjective/mutations.md @@ -0,0 +1,5 @@ +# Mutations + + + +::: epde.operators.singleobjective.mutations \ No newline at end of file diff --git a/osa_docs/epde/operators/singleobjective/selections.md b/osa_docs/epde/operators/singleobjective/selections.md new file mode 100644 index 00000000..3b71d530 --- /dev/null +++ b/osa_docs/epde/operators/singleobjective/selections.md @@ -0,0 +1,5 @@ +# Selections + + + +::: epde.operators.singleobjective.selections \ No newline at end of file diff --git a/osa_docs/epde/operators/singleobjective/so_specific.md b/osa_docs/epde/operators/singleobjective/so_specific.md new file mode 100644 index 00000000..f15bd5f2 --- /dev/null +++ b/osa_docs/epde/operators/singleobjective/so_specific.md @@ -0,0 +1,5 @@ +# So Specific + + + +::: epde.operators.singleobjective.so_specific \ No newline at end of file diff --git a/osa_docs/epde/operators/singleobjective/variation.md b/osa_docs/epde/operators/singleobjective/variation.md new file mode 100644 index 00000000..9d0915e6 --- /dev/null +++ b/osa_docs/epde/operators/singleobjective/variation.md @@ -0,0 +1,5 @@ +# Variation + + + +::: epde.operators.singleobjective.variation \ No newline at end of file diff --git a/osa_docs/epde/operators/utils/default_parameter_loader.md b/osa_docs/epde/operators/utils/default_parameter_loader.md new file mode 100644 index 00000000..929c5515 --- /dev/null +++ b/osa_docs/epde/operators/utils/default_parameter_loader.md @@ -0,0 +1,5 @@ +# Default Parameter Loader + + + +::: epde.operators.utils.default_parameter_loader \ No newline at end of file diff --git a/osa_docs/epde/operators/utils/index.md b/osa_docs/epde/operators/utils/index.md new file mode 100644 index 00000000..b56d54e5 --- /dev/null +++ b/osa_docs/epde/operators/utils/index.md @@ -0,0 +1,9 @@ +# Utils + +## Overview + +The `Utils` module provides a set of utility classes and functions for managing parameters, mapping operators, and handling compound operations within the EPDE framework. It includes functionalities for loading default parameters, applying operators based on specified conditions, and structuring complex operators from simpler ones. + +## Purpose + +The primary purpose of this module is to facilitate the configuration and customization of the equation discovery process. It centralizes parameter management, enables conditional application of operators, and provides tools for building complex operations from simpler components. This allows users to fine-tune the evolutionary algorithms, optimization techniques, and other processes involved in discovering differential equations from data. diff --git a/osa_docs/epde/operators/utils/operator_mappers.md b/osa_docs/epde/operators/utils/operator_mappers.md new file mode 100644 index 00000000..7bef1b24 --- /dev/null +++ b/osa_docs/epde/operators/utils/operator_mappers.md @@ -0,0 +1,5 @@ +# Operator Mappers + + + +::: epde.operators.utils.operator_mappers \ No newline at end of file diff --git a/osa_docs/epde/operators/utils/parameters/index.md b/osa_docs/epde/operators/utils/parameters/index.md new file mode 100644 index 00000000..cc8e1282 --- /dev/null +++ b/osa_docs/epde/operators/utils/parameters/index.md @@ -0,0 +1,9 @@ +# Parameters + +## Overview + +This module is designed to manage and handle parameters within the EPDE framework. It likely provides functionalities for defining, storing, and manipulating parameters used in various stages of the equation discovery process. + +## Purpose + +The purpose of this module is to centralize parameter management, ensuring consistency and ease of access to configuration settings throughout the EPDE framework. This facilitates the customization and control of the evolutionary algorithms, optimization techniques, and other processes involved in discovering differential equations from data. diff --git a/osa_docs/epde/operators/utils/template.md b/osa_docs/epde/operators/utils/template.md new file mode 100644 index 00000000..434d29f0 --- /dev/null +++ b/osa_docs/epde/operators/utils/template.md @@ -0,0 +1,5 @@ +# Template + + + +::: epde.operators.utils.template \ No newline at end of file diff --git a/osa_docs/epde/optimizers/blocks.md b/osa_docs/epde/optimizers/blocks.md new file mode 100644 index 00000000..053d82bb --- /dev/null +++ b/osa_docs/epde/optimizers/blocks.md @@ -0,0 +1,5 @@ +# Blocks + + + +::: epde.optimizers.blocks \ No newline at end of file diff --git a/osa_docs/epde/optimizers/builder.md b/osa_docs/epde/optimizers/builder.md new file mode 100644 index 00000000..d4c161e7 --- /dev/null +++ b/osa_docs/epde/optimizers/builder.md @@ -0,0 +1,5 @@ +# Builder + + + +::: epde.optimizers.builder \ No newline at end of file diff --git a/osa_docs/epde/optimizers/index.md b/osa_docs/epde/optimizers/index.md new file mode 100644 index 00000000..f20bf68c --- /dev/null +++ b/osa_docs/epde/optimizers/index.md @@ -0,0 +1,15 @@ +# Optimizers + +## Overview + +The Optimizers module provides a framework for constructing and applying evolutionary strategies to optimize equation discovery within the EPDE project. It includes tools for building customizable sequences of evolutionary operators, managing populations of candidate solutions, and implementing both single-criterion and multi-objective optimization algorithms. The module facilitates the creation of complex evolutionary search strategies through a modular block-based approach. + +## Purpose + +The primary purpose of the Optimizers module is to enable the automated discovery of governing differential equations from data by evolving equation structures that best fit the observed data. It offers functionalities for: + +* Constructing and linking evolutionary operators into flexible, modular strategies. +* Managing populations of candidate solutions, including systems of equations. +* Implementing single-criterion optimization using evolutionary algorithms, guided by a single objective function. +* Performing multi-objective optimization using a Decomposition-based Evolutionary Algorithm (MOEA/D) to find Pareto-optimal solutions. +* Defining custom stopping conditions for the evolutionary algorithm. diff --git a/osa_docs/epde/optimizers/moeadd/index.md b/osa_docs/epde/optimizers/moeadd/index.md new file mode 100644 index 00000000..94cbbb8d --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/index.md @@ -0,0 +1,11 @@ +```markdown +# MOEADD + +## Overview + +The MOEADD module implements a Multiobjective Evolutionary Algorithm based on Decomposition (MOEA/D) for solving multiobjective optimization problems. It provides tools for managing Pareto levels, constructing populations, defining solution templates, and implementing evolutionary strategies. The module includes functionalities for non-dominated sorting, constraint handling, and visualization of Pareto frontiers. + +## Purpose + +The primary purpose of the MOEADD module is to provide an efficient and flexible framework for finding a set of Pareto-optimal solutions to multiobjective optimization problems. It decomposes the multiobjective problem into several single-objective subproblems, which are then optimized using an evolutionary approach. This allows users to approximate the Pareto front, representing the trade-offs between different objectives. The module is designed to be customizable, allowing users to define their own solution representations, evolutionary operators, and constraint handling methods. +``` \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/moeadd.md b/osa_docs/epde/optimizers/moeadd/moeadd.md new file mode 100644 index 00000000..bfd399ac --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/moeadd.md @@ -0,0 +1,5 @@ +# Moeadd + + + +::: epde.optimizers.moeadd.moeadd \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/population_constr.md b/osa_docs/epde/optimizers/moeadd/population_constr.md new file mode 100644 index 00000000..70c0d3a8 --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/population_constr.md @@ -0,0 +1,5 @@ +# Opulation Constr + + + +::: epde.optimizers.moeadd.population_constr \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/solution_template.md b/osa_docs/epde/optimizers/moeadd/solution_template.md new file mode 100644 index 00000000..ea384843 --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/solution_template.md @@ -0,0 +1,5 @@ +# Solution Template + + + +::: epde.optimizers.moeadd.solution_template \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/strategy.md b/osa_docs/epde/optimizers/moeadd/strategy.md new file mode 100644 index 00000000..3cd0ab48 --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/strategy.md @@ -0,0 +1,5 @@ +# Strateg + + + +::: epde.optimizers.moeadd.strategy \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/strategy_elems.md b/osa_docs/epde/optimizers/moeadd/strategy_elems.md new file mode 100644 index 00000000..f2158ca2 --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/strategy_elems.md @@ -0,0 +1,5 @@ +# Strategy Elems + + + +::: epde.optimizers.moeadd.strategy_elems \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/supplementary.md b/osa_docs/epde/optimizers/moeadd/supplementary.md new file mode 100644 index 00000000..5161fab0 --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/supplementary.md @@ -0,0 +1,5 @@ +# Supplementar + + + +::: epde.optimizers.moeadd.supplementary \ No newline at end of file diff --git a/osa_docs/epde/optimizers/moeadd/vis.md b/osa_docs/epde/optimizers/moeadd/vis.md new file mode 100644 index 00000000..46f54feb --- /dev/null +++ b/osa_docs/epde/optimizers/moeadd/vis.md @@ -0,0 +1,5 @@ +# Vis + + + +::: epde.optimizers.moeadd.vis \ No newline at end of file diff --git a/osa_docs/epde/optimizers/single_criterion/ea_stop_conds.md b/osa_docs/epde/optimizers/single_criterion/ea_stop_conds.md new file mode 100644 index 00000000..cdcf00d4 --- /dev/null +++ b/osa_docs/epde/optimizers/single_criterion/ea_stop_conds.md @@ -0,0 +1,5 @@ +# Ea Stop Conds + + + +::: epde.optimizers.single_criterion.ea_stop_conds \ No newline at end of file diff --git a/osa_docs/epde/optimizers/single_criterion/index.md b/osa_docs/epde/optimizers/single_criterion/index.md new file mode 100644 index 00000000..85b648ca --- /dev/null +++ b/osa_docs/epde/optimizers/single_criterion/index.md @@ -0,0 +1,9 @@ +# Single Criterion Optimization Module + +## Overview + +This module focuses on evolutionary strategies for single-criterion optimization within the EPDE framework. It provides tools for constructing and managing populations of candidate solutions, defining stopping conditions for the evolutionary algorithm, and implementing the core optimization loop. The module includes classes for defining evolutionary strategies, managing populations, and constructing populations of systems of equations. It also offers a baseline evolutionary strategy director for assembling a basic evolutionary algorithm pipeline. + +## Purpose + +The primary purpose of this module is to provide a set of tools and classes for performing single-criterion optimization using evolutionary algorithms within the EPDE framework. This involves evolving populations of candidate solutions to find the best fit to the observed data, guided by a single objective function. The module supports defining custom stopping conditions, constructing populations of systems of equations with specific properties, and implementing the core optimization loop. It aims to automate the process of identifying governing differential equations from data by evolving equation structures that minimize a single error metric. diff --git a/osa_docs/epde/optimizers/single_criterion/optimizer.md b/osa_docs/epde/optimizers/single_criterion/optimizer.md new file mode 100644 index 00000000..35f0475d --- /dev/null +++ b/osa_docs/epde/optimizers/single_criterion/optimizer.md @@ -0,0 +1,5 @@ +# Optimizer + + + +::: epde.optimizers.single_criterion.optimizer \ No newline at end of file diff --git a/osa_docs/epde/optimizers/single_criterion/population_constr.md b/osa_docs/epde/optimizers/single_criterion/population_constr.md new file mode 100644 index 00000000..b86672f7 --- /dev/null +++ b/osa_docs/epde/optimizers/single_criterion/population_constr.md @@ -0,0 +1,5 @@ +# Opulation Constr + + + +::: epde.optimizers.single_criterion.population_constr \ No newline at end of file diff --git a/osa_docs/epde/optimizers/single_criterion/strategy.md b/osa_docs/epde/optimizers/single_criterion/strategy.md new file mode 100644 index 00000000..3073ff97 --- /dev/null +++ b/osa_docs/epde/optimizers/single_criterion/strategy.md @@ -0,0 +1,5 @@ +# Strateg + + + +::: epde.optimizers.single_criterion.strategy \ No newline at end of file diff --git a/osa_docs/epde/optimizers/single_criterion/supplementary.md b/osa_docs/epde/optimizers/single_criterion/supplementary.md new file mode 100644 index 00000000..4be5e1a5 --- /dev/null +++ b/osa_docs/epde/optimizers/single_criterion/supplementary.md @@ -0,0 +1,5 @@ +# Supplementar + + + +::: epde.optimizers.single_criterion.supplementary \ No newline at end of file diff --git a/osa_docs/epde/optimizers/strategy.md b/osa_docs/epde/optimizers/strategy.md new file mode 100644 index 00000000..a8d11581 --- /dev/null +++ b/osa_docs/epde/optimizers/strategy.md @@ -0,0 +1,5 @@ +# Strateg + + + +::: epde.optimizers.strategy \ No newline at end of file diff --git a/osa_docs/epde/parametric/index.md b/osa_docs/epde/parametric/index.md new file mode 100644 index 00000000..8521d7d0 --- /dev/null +++ b/osa_docs/epde/parametric/index.md @@ -0,0 +1,11 @@ +```markdown +# Parametric Equations + +## Overview + +This module provides tools for representing and optimizing parametric equations. It includes classes and functions for parsing equations, managing optimizable parameters within factors, and evaluating equations and their gradients. The module is designed to work with both parametric and defined factors, allowing for the construction and optimization of complex equation structures. + +## Purpose + +The primary purpose of this module is to facilitate the discovery of differential equations by enabling the optimization of equation parameters to fit observed data. It provides the necessary components for representing equations with optimizable terms, parsing parametric factors, and constructing factor objects. This allows users to define and optimize equation structures within the EPDE framework. +``` \ No newline at end of file diff --git a/osa_docs/epde/parametric/parametric_eq_translator.md b/osa_docs/epde/parametric/parametric_eq_translator.md new file mode 100644 index 00000000..734bd942 --- /dev/null +++ b/osa_docs/epde/parametric/parametric_eq_translator.md @@ -0,0 +1,5 @@ +# Arametric Eq Translator + + + +::: epde.parametric.parametric_eq_translator \ No newline at end of file diff --git a/osa_docs/epde/parametric/parametric_equation.md b/osa_docs/epde/parametric/parametric_equation.md new file mode 100644 index 00000000..03f364dd --- /dev/null +++ b/osa_docs/epde/parametric/parametric_equation.md @@ -0,0 +1,5 @@ +# Arametric Equation + + + +::: epde.parametric.parametric_equation \ No newline at end of file diff --git a/osa_docs/epde/parametric/parametric_factor.md b/osa_docs/epde/parametric/parametric_factor.md new file mode 100644 index 00000000..95386dfd --- /dev/null +++ b/osa_docs/epde/parametric/parametric_factor.md @@ -0,0 +1,5 @@ +# Arametric Factor + + + +::: epde.parametric.parametric_factor \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/cheb.md b/osa_docs/epde/preprocessing/cheb.md new file mode 100644 index 00000000..a06b26bc --- /dev/null +++ b/osa_docs/epde/preprocessing/cheb.md @@ -0,0 +1,5 @@ +# Cheb + + + +::: epde.preprocessing.cheb \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/deriv_calculators.md b/osa_docs/epde/preprocessing/deriv_calculators.md new file mode 100644 index 00000000..caf5cab5 --- /dev/null +++ b/osa_docs/epde/preprocessing/deriv_calculators.md @@ -0,0 +1,5 @@ +# Deriv Calculators + + + +::: epde.preprocessing.deriv_calculators \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/derivatives.md b/osa_docs/epde/preprocessing/derivatives.md new file mode 100644 index 00000000..8358d9c2 --- /dev/null +++ b/osa_docs/epde/preprocessing/derivatives.md @@ -0,0 +1,5 @@ +# Derivatives + + + +::: epde.preprocessing.derivatives \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/domain_pruning.md b/osa_docs/epde/preprocessing/domain_pruning.md new file mode 100644 index 00000000..a51bee33 --- /dev/null +++ b/osa_docs/epde/preprocessing/domain_pruning.md @@ -0,0 +1,5 @@ +# Domain Pruning + + + +::: epde.preprocessing.domain_pruning \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/fin_der.md b/osa_docs/epde/preprocessing/fin_der.md new file mode 100644 index 00000000..e5f15c21 --- /dev/null +++ b/osa_docs/epde/preprocessing/fin_der.md @@ -0,0 +1,5 @@ +# Fin Der + + + +::: epde.preprocessing.fin_der \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/index.md b/osa_docs/epde/preprocessing/index.md new file mode 100644 index 00000000..42849caf --- /dev/null +++ b/osa_docs/epde/preprocessing/index.md @@ -0,0 +1,11 @@ +```markdown +# Preprocessing + +## Overview + +The `preprocessing` module provides a suite of tools for preparing data for differential equation discovery within the EPDE framework. It includes functionalities for smoothing, derivative calculation, domain pruning, and data interpolation/oversampling. The module offers a flexible pipeline for transforming raw data into a suitable format for subsequent equation learning algorithms. + +## Purpose + +The primary purpose of the `preprocessing` module is to facilitate the accurate and efficient estimation of derivatives from data, which is a crucial step in identifying underlying differential equations. It offers various methods for derivative calculation, including finite differences, Chebyshev polynomials, spectral methods, and neural networks. Additionally, it provides tools for data smoothing to reduce noise and domain pruning to focus on relevant regions of the data. The module aims to enhance the quality and reliability of the data used for equation discovery, ultimately improving the accuracy and interpretability of the identified models. +``` \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/interpolation_oversampling.md b/osa_docs/epde/preprocessing/interpolation_oversampling.md new file mode 100644 index 00000000..d0d62b5f --- /dev/null +++ b/osa_docs/epde/preprocessing/interpolation_oversampling.md @@ -0,0 +1,5 @@ +# Interpolation Oversampling + + + +::: epde.preprocessing.interpolation_oversampling \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/preprocessor.md b/osa_docs/epde/preprocessing/preprocessor.md new file mode 100644 index 00000000..e8cc60a1 --- /dev/null +++ b/osa_docs/epde/preprocessing/preprocessor.md @@ -0,0 +1,5 @@ +# Reprocessor + + + +::: epde.preprocessing.preprocessor \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/preprocessor_setups.md b/osa_docs/epde/preprocessing/preprocessor_setups.md new file mode 100644 index 00000000..622445fa --- /dev/null +++ b/osa_docs/epde/preprocessing/preprocessor_setups.md @@ -0,0 +1,5 @@ +# Reprocessor Setups + + + +::: epde.preprocessing.preprocessor_setups \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/smoothers.md b/osa_docs/epde/preprocessing/smoothers.md new file mode 100644 index 00000000..2580de6b --- /dev/null +++ b/osa_docs/epde/preprocessing/smoothers.md @@ -0,0 +1,5 @@ +# Smoothers + + + +::: epde.preprocessing.smoothers \ No newline at end of file diff --git a/osa_docs/epde/preprocessing/smoothing.md b/osa_docs/epde/preprocessing/smoothing.md new file mode 100644 index 00000000..7c0576d9 --- /dev/null +++ b/osa_docs/epde/preprocessing/smoothing.md @@ -0,0 +1,5 @@ +# Smoothing + + + +::: epde.preprocessing.smoothing \ No newline at end of file diff --git a/osa_docs/epde/solver/cache.md b/osa_docs/epde/solver/cache.md new file mode 100644 index 00000000..9ac53854 --- /dev/null +++ b/osa_docs/epde/solver/cache.md @@ -0,0 +1,5 @@ +# Cache + + + +::: epde.solver.cache \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/adaptive_lambda.md b/osa_docs/epde/solver/callbacks/adaptive_lambda.md new file mode 100644 index 00000000..8d35fef9 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/adaptive_lambda.md @@ -0,0 +1,5 @@ +# Adaptive Lambda + + + +::: epde.solver.callbacks.adaptive_lambda \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/cache.md b/osa_docs/epde/solver/callbacks/cache.md new file mode 100644 index 00000000..ab93f9b6 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/cache.md @@ -0,0 +1,5 @@ +# Cache + + + +::: epde.solver.callbacks.cache \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/callback.md b/osa_docs/epde/solver/callbacks/callback.md new file mode 100644 index 00000000..050543f7 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/callback.md @@ -0,0 +1,5 @@ +# Callback + + + +::: epde.solver.callbacks.callback \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/callback_list.md b/osa_docs/epde/solver/callbacks/callback_list.md new file mode 100644 index 00000000..e5b25b3e --- /dev/null +++ b/osa_docs/epde/solver/callbacks/callback_list.md @@ -0,0 +1,5 @@ +# Callback List + + + +::: epde.solver.callbacks.callback_list \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/early_stopping.md b/osa_docs/epde/solver/callbacks/early_stopping.md new file mode 100644 index 00000000..f82a1132 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/early_stopping.md @@ -0,0 +1,5 @@ +# Early Stopping + + + +::: epde.solver.callbacks.early_stopping \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/index.md b/osa_docs/epde/solver/callbacks/index.md new file mode 100644 index 00000000..d3e8db92 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/index.md @@ -0,0 +1,18 @@ +```markdown +# Callbacks + +## Overview + +The `callbacks` module within the EPDE project provides a set of tools for customizing and extending the training process of equation discovery models. It includes functionalities for adaptive lambda computation, caching preprocessed data and trained models, defining custom callbacks, managing lists of callbacks, implementing early stopping criteria, printing parameters during inverse task solutions, and plotting solutions. + +## Purpose + +The primary purpose of the `callbacks` module is to enhance the flexibility and efficiency of the equation discovery process. It allows users to: + +- Adaptively adjust lambda parameters during training. +- Cache preprocessed data and trained models to accelerate subsequent training runs. +- Define custom actions to be performed at various stages of the training process. +- Implement early stopping to prevent overfitting and reduce training time. +- Monitor and visualize the progress of inverse task solutions. +- Plot solutions to gain insights into the discovered equations. +``` \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/inverse_task.md b/osa_docs/epde/solver/callbacks/inverse_task.md new file mode 100644 index 00000000..7d637418 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/inverse_task.md @@ -0,0 +1,5 @@ +# Inverse Task + + + +::: epde.solver.callbacks.inverse_task \ No newline at end of file diff --git a/osa_docs/epde/solver/callbacks/plot.md b/osa_docs/epde/solver/callbacks/plot.md new file mode 100644 index 00000000..6e8ed2c3 --- /dev/null +++ b/osa_docs/epde/solver/callbacks/plot.md @@ -0,0 +1,5 @@ +# Lot + + + +::: epde.solver.callbacks.plot \ No newline at end of file diff --git a/osa_docs/epde/solver/config.md b/osa_docs/epde/solver/config.md new file mode 100644 index 00000000..e085d85c --- /dev/null +++ b/osa_docs/epde/solver/config.md @@ -0,0 +1,5 @@ +# Config + + + +::: epde.solver.config \ No newline at end of file diff --git a/osa_docs/epde/solver/data.md b/osa_docs/epde/solver/data.md new file mode 100644 index 00000000..115e21fb --- /dev/null +++ b/osa_docs/epde/solver/data.md @@ -0,0 +1,5 @@ +# Data + + + +::: epde.solver.data \ No newline at end of file diff --git a/osa_docs/epde/solver/derivative.md b/osa_docs/epde/solver/derivative.md new file mode 100644 index 00000000..92b4c19a --- /dev/null +++ b/osa_docs/epde/solver/derivative.md @@ -0,0 +1,5 @@ +# Derivative + + + +::: epde.solver.derivative \ No newline at end of file diff --git a/osa_docs/epde/solver/device.md b/osa_docs/epde/solver/device.md new file mode 100644 index 00000000..3d4c90b9 --- /dev/null +++ b/osa_docs/epde/solver/device.md @@ -0,0 +1,5 @@ +# Device + + + +::: epde.solver.device \ No newline at end of file diff --git a/osa_docs/epde/solver/eval.md b/osa_docs/epde/solver/eval.md new file mode 100644 index 00000000..befac41b --- /dev/null +++ b/osa_docs/epde/solver/eval.md @@ -0,0 +1,5 @@ +# Eval + + + +::: epde.solver.eval \ No newline at end of file diff --git a/osa_docs/epde/solver/finite_diffs.md b/osa_docs/epde/solver/finite_diffs.md new file mode 100644 index 00000000..ee486b90 --- /dev/null +++ b/osa_docs/epde/solver/finite_diffs.md @@ -0,0 +1,5 @@ +# Finite Diffs + + + +::: epde.solver.finite_diffs \ No newline at end of file diff --git a/osa_docs/epde/solver/index.md b/osa_docs/epde/solver/index.md new file mode 100644 index 00000000..6b221c3d --- /dev/null +++ b/osa_docs/epde/solver/index.md @@ -0,0 +1,11 @@ +```markdown +# Solver + +## Overview + +The `solver` module within the EPDE project provides the core functionality for solving differential equations using various numerical and analytical methods. It encompasses data preprocessing, model definition, numerical derivative calculation, loss computation, and solution evaluation. The module is designed to be flexible, supporting different computational devices and offering tools for caching and accelerating the solution process. + +## Purpose + +The primary purpose of the `solver` module is to provide a high-level interface for solving differential equations within the EPDE framework. It handles the complexities of data preparation, model construction, and numerical computation, allowing users to focus on defining the problem and interpreting the results. The module supports various solution methods, including neural networks, finite differences, and analytical techniques, making it suitable for a wide range of applications. +``` \ No newline at end of file diff --git a/osa_docs/epde/solver/input_preprocessing.md b/osa_docs/epde/solver/input_preprocessing.md new file mode 100644 index 00000000..e626f944 --- /dev/null +++ b/osa_docs/epde/solver/input_preprocessing.md @@ -0,0 +1,5 @@ +# Input Preprocessing + + + +::: epde.solver.input_preprocessing \ No newline at end of file diff --git a/osa_docs/epde/solver/losses.md b/osa_docs/epde/solver/losses.md new file mode 100644 index 00000000..07608466 --- /dev/null +++ b/osa_docs/epde/solver/losses.md @@ -0,0 +1,5 @@ +# Losses + + + +::: epde.solver.losses \ No newline at end of file diff --git a/osa_docs/epde/solver/model.md b/osa_docs/epde/solver/model.md new file mode 100644 index 00000000..bc6048b9 --- /dev/null +++ b/osa_docs/epde/solver/model.md @@ -0,0 +1,5 @@ +# Model + + + +::: epde.solver.model \ No newline at end of file diff --git a/osa_docs/epde/solver/models.md b/osa_docs/epde/solver/models.md new file mode 100644 index 00000000..55de2ea0 --- /dev/null +++ b/osa_docs/epde/solver/models.md @@ -0,0 +1,5 @@ +# Models + + + +::: epde.solver.models \ No newline at end of file diff --git a/osa_docs/epde/solver/optimizers/closure.md b/osa_docs/epde/solver/optimizers/closure.md new file mode 100644 index 00000000..0857e634 --- /dev/null +++ b/osa_docs/epde/solver/optimizers/closure.md @@ -0,0 +1,5 @@ +# Closure + + + +::: epde.solver.optimizers.closure \ No newline at end of file diff --git a/osa_docs/epde/solver/optimizers/index.md b/osa_docs/epde/solver/optimizers/index.md new file mode 100644 index 00000000..24c9f8db --- /dev/null +++ b/osa_docs/epde/solver/optimizers/index.md @@ -0,0 +1,11 @@ +```markdown +# Optimizers + +## Overview + +The `optimizers` module provides a set of optimization algorithms tailored for use within the EPDE framework. It includes base classes for defining custom optimizers and implementations of specific optimization techniques like NGD and PSO. The module is designed to facilitate the training and parameter tuning of equation discovery models. + +## Purpose + +The primary purpose of the `optimizers` module is to offer a flexible and extensible collection of optimization methods suitable for training models that identify differential equations from data. It allows users to select and customize optimization algorithms to improve the accuracy and efficiency of equation discovery within the EPDE project. +``` \ No newline at end of file diff --git a/osa_docs/epde/solver/optimizers/ngd.md b/osa_docs/epde/solver/optimizers/ngd.md new file mode 100644 index 00000000..59fc21c6 --- /dev/null +++ b/osa_docs/epde/solver/optimizers/ngd.md @@ -0,0 +1,5 @@ +# Ngd + + + +::: epde.solver.optimizers.ngd \ No newline at end of file diff --git a/osa_docs/epde/solver/optimizers/optimizer.md b/osa_docs/epde/solver/optimizers/optimizer.md new file mode 100644 index 00000000..4e0f01c4 --- /dev/null +++ b/osa_docs/epde/solver/optimizers/optimizer.md @@ -0,0 +1,5 @@ +# Optimizer + + + +::: epde.solver.optimizers.optimizer \ No newline at end of file diff --git a/osa_docs/epde/solver/optimizers/pso.md b/osa_docs/epde/solver/optimizers/pso.md new file mode 100644 index 00000000..4612d73e --- /dev/null +++ b/osa_docs/epde/solver/optimizers/pso.md @@ -0,0 +1,5 @@ +# So + + + +::: epde.solver.optimizers.pso \ No newline at end of file diff --git a/osa_docs/epde/solver/points_type.md b/osa_docs/epde/solver/points_type.md new file mode 100644 index 00000000..1f68822d --- /dev/null +++ b/osa_docs/epde/solver/points_type.md @@ -0,0 +1,5 @@ +# Oints Type + + + +::: epde.solver.points_type \ No newline at end of file diff --git a/osa_docs/epde/solver/solution.md b/osa_docs/epde/solver/solution.md new file mode 100644 index 00000000..2b4f2508 --- /dev/null +++ b/osa_docs/epde/solver/solution.md @@ -0,0 +1,5 @@ +# Solution + + + +::: epde.solver.solution \ No newline at end of file diff --git a/osa_docs/epde/solver/solver.md b/osa_docs/epde/solver/solver.md new file mode 100644 index 00000000..bcd70f44 --- /dev/null +++ b/osa_docs/epde/solver/solver.md @@ -0,0 +1,5 @@ +# Solver + + + +::: epde.solver.solver \ No newline at end of file diff --git a/osa_docs/epde/solver/utils.md b/osa_docs/epde/solver/utils.md new file mode 100644 index 00000000..acc28803 --- /dev/null +++ b/osa_docs/epde/solver/utils.md @@ -0,0 +1,5 @@ +# Utils + + + +::: epde.solver.utils \ No newline at end of file diff --git a/osa_docs/epde/structure/Tokens.md b/osa_docs/epde/structure/Tokens.md new file mode 100644 index 00000000..bb1001a3 --- /dev/null +++ b/osa_docs/epde/structure/Tokens.md @@ -0,0 +1,5 @@ +# Tokens + + + +::: epde.structure.Tokens \ No newline at end of file diff --git a/osa_docs/epde/structure/encoding.md b/osa_docs/epde/structure/encoding.md new file mode 100644 index 00000000..2470c6a4 --- /dev/null +++ b/osa_docs/epde/structure/encoding.md @@ -0,0 +1,5 @@ +# Encoding + + + +::: epde.structure.encoding \ No newline at end of file diff --git a/osa_docs/epde/structure/factor.md b/osa_docs/epde/structure/factor.md new file mode 100644 index 00000000..27ddcb4c --- /dev/null +++ b/osa_docs/epde/structure/factor.md @@ -0,0 +1,5 @@ +# Factor + + + +::: epde.structure.factor \ No newline at end of file diff --git a/osa_docs/epde/structure/index.md b/osa_docs/epde/structure/index.md new file mode 100644 index 00000000..01fc3f78 --- /dev/null +++ b/osa_docs/epde/structure/index.md @@ -0,0 +1,9 @@ +# Structure Module + +## Overview + +The Structure module forms the architectural backbone of the EPDE framework, defining the fundamental data structures and classes necessary for representing and manipulating equations, terms, and genes within the evolutionary process. It provides the building blocks for constructing and evolving equation-based models. + +## Purpose + +The primary purpose of the Structure module is to provide a robust and flexible framework for representing mathematical equations and their components. This includes defining classes for genes, chromosomes, terms, equations, and systems of equations, as well as iterators for traversing these structures. The module also includes utilities for checking metaparameters and ensuring the uniqueness of objects within a given context. These components are essential for the symbolic regression tasks performed by EPDE, enabling the discovery of governing differential equations from data. diff --git a/osa_docs/epde/structure/main_structures.md b/osa_docs/epde/structure/main_structures.md new file mode 100644 index 00000000..445be20b --- /dev/null +++ b/osa_docs/epde/structure/main_structures.md @@ -0,0 +1,5 @@ +# Main Structures + + + +::: epde.structure.main_structures \ No newline at end of file diff --git a/osa_docs/epde/structure/structure_template.md b/osa_docs/epde/structure/structure_template.md new file mode 100644 index 00000000..c233e0d9 --- /dev/null +++ b/osa_docs/epde/structure/structure_template.md @@ -0,0 +1,5 @@ +# Structure Template + + + +::: epde.structure.structure_template \ No newline at end of file diff --git a/osa_docs/epde/supplementary.md b/osa_docs/epde/supplementary.md new file mode 100644 index 00000000..85c195e3 --- /dev/null +++ b/osa_docs/epde/supplementary.md @@ -0,0 +1,5 @@ +# Supplementar + + + +::: epde.supplementary \ No newline at end of file diff --git a/osa_docs/examples/index.md b/osa_docs/examples/index.md new file mode 100644 index 00000000..ff0b8960 --- /dev/null +++ b/osa_docs/examples/index.md @@ -0,0 +1,11 @@ +```markdown +# Examples + +## Overview + +The `examples` module provides example scripts and configurations that demonstrate how to use the EPDE framework. It serves as a practical guide for users to understand the various functionalities and workflows supported by the library. + +## Purpose + +The primary purpose of the `examples` module is to showcase the application of EPDE in different scenarios. By providing ready-to-use examples, it helps users quickly grasp the core concepts and adapt the framework to their specific problems of discovering and modeling differential equations from data. +``` \ No newline at end of file diff --git a/osa_docs/index.md b/osa_docs/index.md new file mode 100644 index 00000000..3a8740ce --- /dev/null +++ b/osa_docs/index.md @@ -0,0 +1,9 @@ +# EPDE + +## Overview + +The EPDE project provides a framework for discovering and modeling differential equations from data. It incorporates evolutionary algorithms and multi-objective optimization techniques to search for equation structures that best fit the observed data. The framework supports various data preprocessing methods, custom token definitions, and integration with numerical solvers. + +## Purpose + +The primary purpose of EPDE is to automate the process of identifying governing differential equations from experimental or observational data. This allows users to gain insights into the underlying dynamics of complex systems and build predictive models. diff --git a/osa_docs/projects/ODE/2nd_order.md b/osa_docs/projects/ODE/2nd_order.md new file mode 100644 index 00000000..2eea2c3c --- /dev/null +++ b/osa_docs/projects/ODE/2nd_order.md @@ -0,0 +1,5 @@ +# 2Nd Order + + + +::: projects.ODE.2nd_order \ No newline at end of file diff --git a/osa_docs/projects/ODE/ODE_data_preparation.md b/osa_docs/projects/ODE/ODE_data_preparation.md new file mode 100644 index 00000000..7d77e295 --- /dev/null +++ b/osa_docs/projects/ODE/ODE_data_preparation.md @@ -0,0 +1,5 @@ +# Ode Data Preparation + + + +::: projects.ODE.ODE_data_preparation \ No newline at end of file diff --git a/osa_docs/projects/ODE/example_ODE_2nd_order.md b/osa_docs/projects/ODE/example_ODE_2nd_order.md new file mode 100644 index 00000000..2711bd20 --- /dev/null +++ b/osa_docs/projects/ODE/example_ODE_2nd_order.md @@ -0,0 +1,5 @@ +# Example Ode 2Nd Order + + + +::: projects.ODE.example_ODE_2nd_order \ No newline at end of file diff --git a/osa_docs/projects/ODE/index.md b/osa_docs/projects/ODE/index.md new file mode 100644 index 00000000..e7b3ba43 --- /dev/null +++ b/osa_docs/projects/ODE/index.md @@ -0,0 +1,9 @@ +# ODE Module + +## Overview + +The ODE module within the EPDE project focuses on discovering and modeling ordinary differential equations from data. It provides tools for data preparation, equation discovery using evolutionary algorithms, and integration with numerical solvers to validate discovered equations. The module includes functionalities for solving second-order ODEs using the Runge-Kutta method, performing EPDE searches with custom tokens (e.g., trigonometric functions or inverse coordinate values), and extracting equations with specified complexity. + +## Purpose + +The primary purpose of the ODE module is to facilitate the identification of governing ordinary differential equations from data. This involves preparing data representing the solution of an ODE, setting up an EPDE search object, configuring its parameters, and then performing the search. It allows users to explore the solution space of possible equations and identify those that best fit the observed data, even incorporating custom functions and tokens to expand the search capabilities. diff --git a/osa_docs/projects/ODE/ode_textbook_interfaced.md b/osa_docs/projects/ODE/ode_textbook_interfaced.md new file mode 100644 index 00000000..6d3ac10f --- /dev/null +++ b/osa_docs/projects/ODE/ode_textbook_interfaced.md @@ -0,0 +1,5 @@ +# Ode Textbook Interfaced + + + +::: projects.ODE.ode_textbook_interfaced \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/Lorenz/data_preparation.md b/osa_docs/projects/benchmarking/Lorenz/data_preparation.md new file mode 100644 index 00000000..2352c1aa --- /dev/null +++ b/osa_docs/projects/benchmarking/Lorenz/data_preparation.md @@ -0,0 +1,5 @@ +# Data Preparation + + + +::: projects.benchmarking.Lorenz.data_preparation \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/Lorenz/equation_discovery.md b/osa_docs/projects/benchmarking/Lorenz/equation_discovery.md new file mode 100644 index 00000000..32b0cb06 --- /dev/null +++ b/osa_docs/projects/benchmarking/Lorenz/equation_discovery.md @@ -0,0 +1,5 @@ +# Equation Discover + + + +::: projects.benchmarking.Lorenz.equation_discovery \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/Lorenz/index.md b/osa_docs/projects/benchmarking/Lorenz/index.md new file mode 100644 index 00000000..b540db52 --- /dev/null +++ b/osa_docs/projects/benchmarking/Lorenz/index.md @@ -0,0 +1,13 @@ +# Lorenz + +## Overview + +This module focuses on simulating and analyzing the Lorenz system and similar dynamical systems, such as the Lotka-Volterra model. It provides tools for solving differential equations using numerical methods, visualizing solutions in both 2D and 3D plots, and discovering governing equations from simulation data. + +## Purpose + +The primary purpose of this module is to provide functionalities for simulating, visualizing, and discovering equations related to dynamical systems. It includes tools for: + +- Simulating the dynamics of systems like the Lorenz system and Lotka-Volterra model using the Runge-Kutta method. +- Visualizing the solutions of these systems through 2D and 3D plots. +- Discovering governing equations from simulation data and writing Pareto front solutions to files. diff --git a/osa_docs/projects/benchmarking/burgers_article.md b/osa_docs/projects/benchmarking/burgers_article.md new file mode 100644 index 00000000..99f27f0d --- /dev/null +++ b/osa_docs/projects/benchmarking/burgers_article.md @@ -0,0 +1,5 @@ +# Burgers Article + + + +::: projects.benchmarking.burgers_article \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/index.md b/osa_docs/projects/benchmarking/index.md new file mode 100644 index 00000000..41c2265d --- /dev/null +++ b/osa_docs/projects/benchmarking/index.md @@ -0,0 +1,19 @@ +```markdown +# Benchmarking + +## Overview + +This module focuses on applying the EPDE (Equation Parameter Discovery and Estimation) framework to identify governing differential equations for well-known physical systems, such as the Burgers', KdV, and Wave equations. It provides tools for configuring and executing EPDE searches, evaluating the discovered equations, and comparing the results against known solutions. The module supports both single-objective and multi-objective optimization modes. + +## Purpose + +The primary purpose of this module is to benchmark the EPDE framework's ability to rediscover known differential equations from data. It includes functionalities for: + +- Running equation searches for the Burgers', KdV, and Wave equations using the EPDE algorithm. +- Configuring search parameters, preprocessors, and custom tokens for each equation. +- Evaluating the identified equations and reporting the optimization metric. +- Translating and evaluating partial differential equations using EPDE. +- Simulating and analyzing dynamical systems like the Lorenz system and Lotka-Volterra model. +- Visualizing solutions of these systems through 2D and 3D plots. +- Discovering governing equations from simulation data and writing Pareto front solutions to files. +``` \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/kdv_article.md b/osa_docs/projects/benchmarking/kdv_article.md new file mode 100644 index 00000000..dcc54b48 --- /dev/null +++ b/osa_docs/projects/benchmarking/kdv_article.md @@ -0,0 +1,5 @@ +# Kdv Article + + + +::: projects.benchmarking.kdv_article \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/wave.md b/osa_docs/projects/benchmarking/wave.md new file mode 100644 index 00000000..8feccae9 --- /dev/null +++ b/osa_docs/projects/benchmarking/wave.md @@ -0,0 +1,5 @@ +# Wave + + + +::: projects.benchmarking.wave \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/wave_article.md b/osa_docs/projects/benchmarking/wave_article.md new file mode 100644 index 00000000..e9e120a9 --- /dev/null +++ b/osa_docs/projects/benchmarking/wave_article.md @@ -0,0 +1,5 @@ +# Wave Article + + + +::: projects.benchmarking.wave_article \ No newline at end of file diff --git a/osa_docs/projects/benchmarking/wave_article_ref.md b/osa_docs/projects/benchmarking/wave_article_ref.md new file mode 100644 index 00000000..e5ff4a51 --- /dev/null +++ b/osa_docs/projects/benchmarking/wave_article_ref.md @@ -0,0 +1,5 @@ +# Wave Article Ref + + + +::: projects.benchmarking.wave_article_ref \ No newline at end of file diff --git a/osa_docs/projects/burgers/burgers_eq.md b/osa_docs/projects/burgers/burgers_eq.md new file mode 100644 index 00000000..44f2024a --- /dev/null +++ b/osa_docs/projects/burgers/burgers_eq.md @@ -0,0 +1,5 @@ +# Burgers Eq + + + +::: projects.burgers.burgers_eq \ No newline at end of file diff --git a/osa_docs/projects/burgers/index.md b/osa_docs/projects/burgers/index.md new file mode 100644 index 00000000..ffba6e8e --- /dev/null +++ b/osa_docs/projects/burgers/index.md @@ -0,0 +1,9 @@ +# Burgers Equation Module + +## Overview + +This module focuses on providing utilities for formatting strings, specifically tailored for representing and manipulating equations related to Burgers' equation within the EPDE framework. It includes functionalities for replacing patterns and rounding floating-point numbers within a string representation of an equation. + +## Purpose + +The primary purpose of this module is to ensure consistent and readable output of Burgers' equation representations during the equation discovery and modeling process. By providing a dedicated formatting function, the module aims to improve the interpretability of the discovered equations and facilitate their integration with other components of the EPDE project. diff --git a/osa_docs/projects/epde_ts_pred/index.md b/osa_docs/projects/epde_ts_pred/index.md new file mode 100644 index 00000000..a98e62f7 --- /dev/null +++ b/osa_docs/projects/epde_ts_pred/index.md @@ -0,0 +1,9 @@ +# EPDE Time Series Prediction Module + +## Overview + +The EPDE Time Series Prediction module focuses on discovering and fitting differential equations to time series data. It includes functionality for determining parameter value differences, which are crucial for identifying unique equation structures and terms. This module is designed to work within the broader EPDE framework, leveraging evolutionary algorithms to find the best-fitting equations. + +## Purpose + +The primary purpose of this module is to facilitate the automated discovery of differential equations that accurately model time series data. It allows users to define acceptable parameter variations, ensuring that the identified equations are both accurate and structurally unique. This module contributes to the overall goal of EPDE by enabling the identification of governing equations from time-dependent data. diff --git a/osa_docs/projects/epde_ts_pred/ode_discovery.md b/osa_docs/projects/epde_ts_pred/ode_discovery.md new file mode 100644 index 00000000..344d681c --- /dev/null +++ b/osa_docs/projects/epde_ts_pred/ode_discovery.md @@ -0,0 +1,5 @@ +# Ode Discover + + + +::: projects.epde_ts_pred.ode_discovery \ No newline at end of file diff --git a/osa_docs/projects/example_COVID/index.md b/osa_docs/projects/example_COVID/index.md new file mode 100644 index 00000000..200eec59 --- /dev/null +++ b/osa_docs/projects/example_COVID/index.md @@ -0,0 +1,9 @@ +# Example COVID + +## Overview + +This module is designed to apply the EPDE framework to model and analyze data related to the COVID-19 pandemic. It leverages evolutionary algorithms and multi-objective optimization techniques to discover and model differential equations that describe the dynamics of the spread of COVID-19. + +## Purpose + +The primary purpose of this module is to automate the identification of governing differential equations from COVID-19 related data. This enables users to gain insights into the underlying dynamics of the pandemic and build predictive models for its spread. diff --git a/osa_docs/projects/hunter-prey/data_preparation.md b/osa_docs/projects/hunter-prey/data_preparation.md new file mode 100644 index 00000000..4aea1c48 --- /dev/null +++ b/osa_docs/projects/hunter-prey/data_preparation.md @@ -0,0 +1,5 @@ +# Data Preparation + + + +::: projects.hunter-prey.data_preparation \ No newline at end of file diff --git a/osa_docs/projects/hunter-prey/equation_discovery.md b/osa_docs/projects/hunter-prey/equation_discovery.md new file mode 100644 index 00000000..a1e8b06c --- /dev/null +++ b/osa_docs/projects/hunter-prey/equation_discovery.md @@ -0,0 +1,5 @@ +# Equation Discover + + + +::: projects.hunter-prey.equation_discovery \ No newline at end of file diff --git a/osa_docs/projects/hunter-prey/index.md b/osa_docs/projects/hunter-prey/index.md new file mode 100644 index 00000000..0adc6f58 --- /dev/null +++ b/osa_docs/projects/hunter-prey/index.md @@ -0,0 +1,9 @@ +# Hunter-Prey + +## Overview + +The Hunter-Prey module focuses on simulating and analyzing predator-prey dynamics using mathematical modeling and equation discovery techniques. It employs the Runge-Kutta method to simulate the Lotka-Volterra model, a classic representation of biological systems with interacting predator and prey species. Additionally, it incorporates functionality for identifying governing equations from simulation data, utilizing Pareto front analysis to find optimal solutions. + +## Purpose + +The primary purpose of this module is to provide tools for simulating and understanding the dynamics of predator-prey interactions. It allows users to model population changes over time using the Lotka-Volterra equations and the Runge-Kutta method. Furthermore, it facilitates the discovery of potential underlying equations by writing Pareto front solutions to files for further analysis. This enables the identification of equations that best describe the observed dynamics of the simulated system. diff --git a/osa_docs/projects/index.md b/osa_docs/projects/index.md new file mode 100644 index 00000000..aa209122 --- /dev/null +++ b/osa_docs/projects/index.md @@ -0,0 +1,11 @@ +```markdown +# Projects Module + +## Overview + +The Projects module serves as a collection of case studies and benchmarks for the EPDE (Equation Parameter Discovery and Estimation) framework. It encompasses a variety of submodules, each focusing on applying EPDE to specific equations, systems, or datasets. These submodules provide tools for data handling, equation search configuration, result evaluation, and visualization. The module facilitates the exploration of EPDE's capabilities in discovering governing equations across diverse scientific domains. + +## Purpose + +The primary purpose of the Projects module is to demonstrate and evaluate the EPDE framework's effectiveness in identifying governing equations from data across a range of applications. It provides concrete examples of how EPDE can be used to rediscover known equations, model complex systems, and gain insights into underlying dynamics. By showcasing the application of EPDE to specific problems, the module aims to guide users in applying the framework to their own research and modeling tasks. +``` \ No newline at end of file diff --git a/osa_docs/projects/multi_objective_algebraic_expression/index.md b/osa_docs/projects/multi_objective_algebraic_expression/index.md new file mode 100644 index 00000000..8a136d51 --- /dev/null +++ b/osa_docs/projects/multi_objective_algebraic_expression/index.md @@ -0,0 +1,9 @@ +# Multi Objective Algebraic Expression + +## Overview + +The `multi_objective_algebraic_expression` module is designed to handle data retrieval within the EPDE framework. It provides a function to access specific data points from a loaded dataset. + +## Purpose + +The module's primary purpose is to facilitate data access by index, ensuring that the correct data is available for the evolutionary algorithms and multi-objective optimization processes used in discovering and modeling differential equations. It serves as a utility for retrieving data points required for fitting equation structures to observed data. diff --git a/osa_docs/projects/multi_objective_algebraic_expression/load_data.md b/osa_docs/projects/multi_objective_algebraic_expression/load_data.md new file mode 100644 index 00000000..822f804b --- /dev/null +++ b/osa_docs/projects/multi_objective_algebraic_expression/load_data.md @@ -0,0 +1,5 @@ +# Load Data + + + +::: projects.multi_objective_algebraic_expression.load_data \ No newline at end of file diff --git a/osa_docs/projects/pic/data/ac/ac.md b/osa_docs/projects/pic/data/ac/ac.md new file mode 100644 index 00000000..307e4859 --- /dev/null +++ b/osa_docs/projects/pic/data/ac/ac.md @@ -0,0 +1,5 @@ +# Ac + + + +::: projects.pic.data.ac.ac \ No newline at end of file diff --git a/osa_docs/projects/pic/data/ac/index.md b/osa_docs/projects/pic/data/ac/index.md new file mode 100644 index 00000000..a5040d6d --- /dev/null +++ b/osa_docs/projects/pic/data/ac/index.md @@ -0,0 +1,9 @@ +# AC Module + +## Overview + +The AC module focuses on discovering and validating differential equations, specifically in the context of the Allen-Cahn equation. It provides functionalities for loading data, adding noise, comparing symbolic equations, and preparing fitness operators for equation discovery. The module also includes a test scenario to evaluate performance on the Allen-Cahn equation. + +## Purpose + +The primary purpose of the AC module is to facilitate the identification of the Allen-Cahn equation from data using the EPDE framework. It automates the process of loading data, adding noise, comparing symbolic equations, preparing fitness operators, and performing equation discovery. This allows users to gain insights into the underlying dynamics of the Allen-Cahn equation and build predictive models. diff --git a/osa_docs/projects/pic/data/burgers/burgers.md b/osa_docs/projects/pic/data/burgers/burgers.md new file mode 100644 index 00000000..c4d0dee8 --- /dev/null +++ b/osa_docs/projects/pic/data/burgers/burgers.md @@ -0,0 +1,5 @@ +# Burgers + + + +::: projects.pic.data.burgers.burgers \ No newline at end of file diff --git a/osa_docs/projects/pic/data/burgers/index.md b/osa_docs/projects/pic/data/burgers/index.md new file mode 100644 index 00000000..7be170a2 --- /dev/null +++ b/osa_docs/projects/pic/data/burgers/index.md @@ -0,0 +1,9 @@ +# Burgers Module + +## Overview + +This module focuses on discovering the Burgers' equation from data using the EPDE (Equation Parameter Discovery and Estimation) framework. It provides functionalities for loading and processing Burgers' equation data, adding noise, setting up and executing the EPDE search, and comparing identified equations with known correct and incorrect equations. The module also includes utilities for preparing sub-operators for compound fitness operators and loading pretrained Physics-Informed Neural Networks (PINNs). + +## Purpose + +The primary purpose of this module is to facilitate the identification of the Burgers' equation from noisy data. It automates the process of setting up and running an EPDE search, allowing users to discover the governing equation and verify its accuracy by comparing it with known equations. The module also provides tools for data preprocessing, noise addition, and model loading, making it easier to apply the EPDE framework to Burgers' equation discovery. diff --git a/osa_docs/projects/pic/data/darcy/darcy.md b/osa_docs/projects/pic/data/darcy/darcy.md new file mode 100644 index 00000000..62e7d855 --- /dev/null +++ b/osa_docs/projects/pic/data/darcy/darcy.md @@ -0,0 +1,5 @@ +# Darc + + + +::: projects.pic.data.darcy.darcy \ No newline at end of file diff --git a/osa_docs/projects/pic/data/darcy/index.md b/osa_docs/projects/pic/data/darcy/index.md new file mode 100644 index 00000000..d665163e --- /dev/null +++ b/osa_docs/projects/pic/data/darcy/index.md @@ -0,0 +1,9 @@ +# Darcy + +## Overview + +The `Darcy` module focuses on discovering and analyzing differential equations specifically within the context of Darcy flow. It provides tools for loading Darcy flow data, adding noise, comparing symbolic equations, and preparing sub-operators for fitness calculations within the EPDE framework. The module also includes functionality for testing the performance of EPDE search on the Darcy equation. + +## Purpose + +The primary purpose of this module is to facilitate the identification of governing equations from Darcy flow data using the EPDE framework. It enables users to load and preprocess Darcy flow data, incorporate custom tokens representing physical properties, and perform equation searches to discover the underlying dynamics of fluid flow in porous media. The module also supports the evaluation of identified equations against known correct and incorrect forms. diff --git a/osa_docs/projects/pic/data/index.md b/osa_docs/projects/pic/data/index.md new file mode 100644 index 00000000..17e54a61 --- /dev/null +++ b/osa_docs/projects/pic/data/index.md @@ -0,0 +1,11 @@ +```markdown +# Data Module + +## Overview + +The Data module is a collection of submodules focused on discovering and validating differential equations from data, tailored to specific equations and systems. It provides functionalities for loading data, adding noise, comparing symbolic equations, and preparing fitness operators for equation discovery using the EPDE framework. The module includes specialized tools for handling data, setting up equation searches, and evaluating performance, making it easier to apply the EPDE framework to various equation discovery tasks. + +## Purpose + +The primary purpose of the Data module is to facilitate the identification of governing equations from data for specific systems like Allen-Cahn, Burgers', Darcy flow, KdV, Lorenz, Lotka-Volterra, Van der Pol oscillator, wave equation and general ODEs and PDEs. It automates the process of data preprocessing, noise injection, equation search setup, and performance evaluation, enabling users to identify equations using different EPDE-based approaches. The module also provides functionalities for comparing discovered equations with known correct and incorrect equations, ensuring the accuracy and reliability of the equation discovery process. +``` \ No newline at end of file diff --git a/osa_docs/projects/pic/data/kdv/index.md b/osa_docs/projects/pic/data/kdv/index.md new file mode 100644 index 00000000..06ae4b1d --- /dev/null +++ b/osa_docs/projects/pic/data/kdv/index.md @@ -0,0 +1,9 @@ +# KdV Module + +## Overview + +The KdV module focuses on discovering the Korteweg-de Vries (KdV) equation from data using the EPDE (Equation Discovery) framework. It provides functionalities for loading and preparing KdV equation data from various sources (CSV, MAT files), adding noise to the data, and setting up EPDE searches with custom tokens and preprocessors. The module also includes tools for comparing symbolic equations, preparing sub-operators for compound fitness operators, and testing the performance of EPDE searches and symbolic genetic algorithms (SGA) on the KdV equation. It supports using pre-trained Physics-Informed Neural Networks (PINNs) and SINDy (Sparse Identification of Nonlinear Dynamics) for equation discovery. + +## Purpose + +The primary purpose of the KdV module is to facilitate the automated discovery of the KdV equation from noisy data. It offers a suite of tools for data handling, noise injection, equation search setup, and performance evaluation, enabling users to identify the KdV equation using different EPDE-based approaches, including finite difference preprocessors, symbolic genetic algorithms, and SINDy. The module also provides functionalities for comparing discovered equations with known correct and incorrect equations, ensuring the accuracy and reliability of the equation discovery process. diff --git a/osa_docs/projects/pic/data/kdv/kdv.md b/osa_docs/projects/pic/data/kdv/kdv.md new file mode 100644 index 00000000..907a2750 --- /dev/null +++ b/osa_docs/projects/pic/data/kdv/kdv.md @@ -0,0 +1,5 @@ +# Kdv + + + +::: projects.pic.data.kdv.kdv \ No newline at end of file diff --git a/osa_docs/projects/pic/data/lorenz/index.md b/osa_docs/projects/pic/data/lorenz/index.md new file mode 100644 index 00000000..849ea891 --- /dev/null +++ b/osa_docs/projects/pic/data/lorenz/index.md @@ -0,0 +1,11 @@ +```markdown +# Lorenz + +## Overview + +The `lorenz` module focuses on discovering and analyzing the Lorenz system of equations using the EPDE (Evolutionary Partial Differential Equations) framework. It provides functionalities for loading pre-trained neural networks, adding noise to data, comparing symbolic equations, preparing sub-operators for fitness evaluation, and performing the EPDE search to identify the Lorenz system's governing equations. + +## Purpose + +The primary purpose of this module is to facilitate the identification of the Lorenz system's underlying equations from data. It leverages the EPDE framework to automate the process of discovering these equations, incorporating functionalities for data preprocessing, symbolic equation comparison, and customized search configurations. This allows users to gain insights into the dynamics of the Lorenz system and build corresponding models. +``` \ No newline at end of file diff --git a/osa_docs/projects/pic/data/lorenz/lorenz.md b/osa_docs/projects/pic/data/lorenz/lorenz.md new file mode 100644 index 00000000..ce548f09 --- /dev/null +++ b/osa_docs/projects/pic/data/lorenz/lorenz.md @@ -0,0 +1,5 @@ +# Lorenz + + + +::: projects.pic.data.lorenz.lorenz \ No newline at end of file diff --git a/osa_docs/projects/pic/data/lv/index.md b/osa_docs/projects/pic/data/lv/index.md new file mode 100644 index 00000000..2dc628cb --- /dev/null +++ b/osa_docs/projects/pic/data/lv/index.md @@ -0,0 +1,9 @@ +# Lotka-Volterra System Equation Discovery (lv) + +## Overview + +This module focuses on discovering and modeling differential equations specifically for the Lotka-Volterra system using the EPDE framework. It provides functionalities for loading pre-trained models, adding noise to data, comparing symbolic equations, preparing sub-operators for fitness evaluation, and performing the equation discovery process. + +## Purpose + +The primary purpose of this module is to automate the identification of governing differential equations for the Lotka-Volterra system from data. It allows users to gain insights into the dynamics of predator-prey interactions and build predictive models for such systems. The module provides tools to compare equation stability and refine the search for the most accurate representation of the system's behavior. diff --git a/osa_docs/projects/pic/data/lv/lv.md b/osa_docs/projects/pic/data/lv/lv.md new file mode 100644 index 00000000..71ba2e8f --- /dev/null +++ b/osa_docs/projects/pic/data/lv/lv.md @@ -0,0 +1,5 @@ +# Lv + + + +::: projects.pic.data.lv.lv \ No newline at end of file diff --git a/osa_docs/projects/pic/data/ode/index.md b/osa_docs/projects/pic/data/ode/index.md new file mode 100644 index 00000000..a400a685 --- /dev/null +++ b/osa_docs/projects/pic/data/ode/index.md @@ -0,0 +1,11 @@ +```markdown +# ODE Module + +## Overview + +The ODE module provides functionalities for discovering and analyzing ordinary differential equations (ODEs) from data using the EPDE framework. It includes tools for loading pre-trained models, adding noise to data, comparing symbolic equations, and preparing sub-operators for fitness evaluation. The module also offers methods for testing the performance of the framework on simple ODEs and discovering ODEs from noisy data. + +## Purpose + +The primary purpose of this module is to facilitate the identification of governing ODEs from data, even when the data is corrupted by noise. It automates the process of setting up and executing EPDE searches, allowing users to explore the solution space of possible ODEs and discover equations that best fit the observed data. The module provides tools for data preprocessing, defining the search space, and evaluating the discovered equations. +``` \ No newline at end of file diff --git a/osa_docs/projects/pic/data/ode/ode.md b/osa_docs/projects/pic/data/ode/ode.md new file mode 100644 index 00000000..19d6233b --- /dev/null +++ b/osa_docs/projects/pic/data/ode/ode.md @@ -0,0 +1,5 @@ +# Ode + + + +::: projects.pic.data.ode.ode \ No newline at end of file diff --git a/osa_docs/projects/pic/data/pde_compound/index.md b/osa_docs/projects/pic/data/pde_compound/index.md new file mode 100644 index 00000000..f6ea948f --- /dev/null +++ b/osa_docs/projects/pic/data/pde_compound/index.md @@ -0,0 +1,14 @@ +# PDE Compound Module + +## Overview + +The PDE Compound module is a core component of the EPDE project, designed to facilitate the comparison of PDE equations and manage the experimental process of discovering PDEs from data. It includes functionalities for handling data, creating fitness operators, and conducting experiments to identify the best PDE representations. + +## Purpose + +The primary purpose of this module is to streamline the process of discovering and validating differential equations from data. It provides tools for: + +- Comparing PDE equations based on their fitness. +- Loading and preprocessing data for use in equation discovery. +- Creating and configuring fitness operators to evaluate equation performance. +- Conducting experiments to identify the most suitable PDE representations for a given dataset. diff --git a/osa_docs/projects/pic/data/pde_compound/pde_compound.md b/osa_docs/projects/pic/data/pde_compound/pde_compound.md new file mode 100644 index 00000000..07802a06 --- /dev/null +++ b/osa_docs/projects/pic/data/pde_compound/pde_compound.md @@ -0,0 +1,5 @@ +# De Compound + + + +::: projects.pic.data.pde_compound.pde_compound \ No newline at end of file diff --git a/osa_docs/projects/pic/data/pde_divide/index.md b/osa_docs/projects/pic/data/pde_divide/index.md new file mode 100644 index 00000000..dd42f066 --- /dev/null +++ b/osa_docs/projects/pic/data/pde_divide/index.md @@ -0,0 +1,9 @@ +# PDE Divide + +## Overview + +The PDE Divide module is designed to facilitate the analysis and experimentation of Partial Differential Equations (PDEs). It provides a structured environment for conducting experiments, handling data, comparing PDE equations, and creating fitness operators. The module includes functionalities for data loading, preprocessing, and comparison of different PDE structures to identify the best fit for observed data. + +## Purpose + +The primary purpose of the PDE Divide module is to offer a toolkit for dividing and conquering the complexities of PDE analysis within the EPDE framework. It automates the process of setting up and executing experiments, comparing different PDE forms, and evaluating their fitness against data. This enables users to efficiently explore the solution space of potential differential equations and identify those that best represent the underlying dynamics of a system. diff --git a/osa_docs/projects/pic/data/pde_divide/pde_divide.md b/osa_docs/projects/pic/data/pde_divide/pde_divide.md new file mode 100644 index 00000000..eefa034d --- /dev/null +++ b/osa_docs/projects/pic/data/pde_divide/pde_divide.md @@ -0,0 +1,5 @@ +# De Divide + + + +::: projects.pic.data.pde_divide.pde_divide \ No newline at end of file diff --git a/osa_docs/projects/pic/data/vdp/index.md b/osa_docs/projects/pic/data/vdp/index.md new file mode 100644 index 00000000..2cfb8820 --- /dev/null +++ b/osa_docs/projects/pic/data/vdp/index.md @@ -0,0 +1,9 @@ +# VDP Module + +## Overview + +The VDP (Van der Pol) module focuses on discovering and validating differential equations, specifically in the context of the Van der Pol oscillator. It provides functionalities for loading pre-trained models, adding noise to data, comparing symbolic equations, and preparing sub-operators for fitness evaluation. The module also includes methods for testing the EPDE search algorithm on the Van der Pol oscillator and discovering its governing equation from noisy data. + +## Purpose + +The primary purpose of the VDP module is to facilitate the application of the EPDE framework to the Van der Pol oscillator problem. It offers tools for data manipulation, equation comparison, and fitness operator preparation, enabling users to effectively discover and validate the underlying differential equation of the Van der Pol oscillator from noisy data. This module serves as a specialized component within the EPDE project, focusing on a specific application of equation discovery. diff --git a/osa_docs/projects/pic/data/vdp/vdp.md b/osa_docs/projects/pic/data/vdp/vdp.md new file mode 100644 index 00000000..3a444e23 --- /dev/null +++ b/osa_docs/projects/pic/data/vdp/vdp.md @@ -0,0 +1,5 @@ +# Vd + + + +::: projects.pic.data.vdp.vdp \ No newline at end of file diff --git a/osa_docs/projects/pic/data/wave/index.md b/osa_docs/projects/pic/data/wave/index.md new file mode 100644 index 00000000..768bb0f7 --- /dev/null +++ b/osa_docs/projects/pic/data/wave/index.md @@ -0,0 +1,11 @@ +```markdown +# Wave Module + +## Overview + +The Wave module focuses on discovering and validating the wave equation from data using the EPDE framework. It includes functionalities for loading wave data, adding noise, comparing symbolic equations, preparing sub-operators for compound fitness evaluation, and performing the EPDE search. The module also provides utilities for loading pre-trained Physics-Informed Neural Networks (PINNs). + +## Purpose + +The primary purpose of this module is to facilitate the identification of the wave equation from data. It automates the process of setting up and executing an EPDE search, comparing discovered equations with known symbolic representations, and preparing fitness operators for optimization. This allows users to analyze wave phenomena and validate equation discovery methods within the EPDE framework. +``` \ No newline at end of file diff --git a/osa_docs/projects/pic/data/wave/wave.md b/osa_docs/projects/pic/data/wave/wave.md new file mode 100644 index 00000000..d0969ff4 --- /dev/null +++ b/osa_docs/projects/pic/data/wave/wave.md @@ -0,0 +1,5 @@ +# Wave + + + +::: projects.pic.data.wave.wave \ No newline at end of file diff --git a/osa_docs/projects/pic/derivatives.md b/osa_docs/projects/pic/derivatives.md new file mode 100644 index 00000000..69a71b26 --- /dev/null +++ b/osa_docs/projects/pic/derivatives.md @@ -0,0 +1,5 @@ +# Derivatives + + + +::: projects.pic.derivatives \ No newline at end of file diff --git a/osa_docs/projects/pic/index.md b/osa_docs/projects/pic/index.md new file mode 100644 index 00000000..5af9a5eb --- /dev/null +++ b/osa_docs/projects/pic/index.md @@ -0,0 +1,11 @@ +```markdown +# Data Module + +## Overview + +The Data module is designed to facilitate the discovery and validation of differential equations from data, tailored to specific equations and systems. It provides functionalities for loading data, adding noise, comparing symbolic equations, and preparing fitness operators for equation discovery using the EPDE framework. The module includes specialized tools for handling data, setting up equation searches, and evaluating performance, making it easier to apply the EPDE framework to various equation discovery tasks. + +## Purpose + +The primary purpose of the Data module is to facilitate the identification of governing equations from data for specific systems like Allen-Cahn, Burgers', Darcy flow, KdV, Lorenz, Lotka-Volterra, Van der Pol oscillator, wave equation and general ODEs and PDEs. It automates the process of data preprocessing, noise injection, equation search setup, and performance evaluation, enabling users to identify equations using different EPDE-based approaches. The module also provides functionalities for comparing discovered equations with known correct and incorrect equations, ensuring the accuracy and reliability of the equation discovery process. +``` \ No newline at end of file diff --git a/osa_docs/projects/pic/pic_test_cases.md b/osa_docs/projects/pic/pic_test_cases.md new file mode 100644 index 00000000..19362787 --- /dev/null +++ b/osa_docs/projects/pic/pic_test_cases.md @@ -0,0 +1,5 @@ +# Ic Test Cases + + + +::: projects.pic.pic_test_cases \ No newline at end of file diff --git a/osa_docs/projects/pic/pic_wave_equation_test.md b/osa_docs/projects/pic/pic_wave_equation_test.md new file mode 100644 index 00000000..9f3cab61 --- /dev/null +++ b/osa_docs/projects/pic/pic_wave_equation_test.md @@ -0,0 +1,5 @@ +# Ic Wave Equation Test + + + +::: projects.pic.pic_wave_equation_test \ No newline at end of file diff --git a/osa_docs/projects/wSINDy/KdV_new.md b/osa_docs/projects/wSINDy/KdV_new.md new file mode 100644 index 00000000..7f82ab4e --- /dev/null +++ b/osa_docs/projects/wSINDy/KdV_new.md @@ -0,0 +1,5 @@ +# Kdv New + + + +::: projects.wSINDy.KdV_new \ No newline at end of file diff --git a/osa_docs/projects/wSINDy/burgers.md b/osa_docs/projects/wSINDy/burgers.md new file mode 100644 index 00000000..dc2c1c92 --- /dev/null +++ b/osa_docs/projects/wSINDy/burgers.md @@ -0,0 +1,5 @@ +# Burgers + + + +::: projects.wSINDy.burgers \ No newline at end of file diff --git a/osa_docs/projects/wSINDy/burgers_sindy_data.md b/osa_docs/projects/wSINDy/burgers_sindy_data.md new file mode 100644 index 00000000..b4e3ff7f --- /dev/null +++ b/osa_docs/projects/wSINDy/burgers_sindy_data.md @@ -0,0 +1,5 @@ +# Burgers Sindy Data + + + +::: projects.wSINDy.burgers_sindy_data \ No newline at end of file diff --git a/osa_docs/projects/wSINDy/index.md b/osa_docs/projects/wSINDy/index.md new file mode 100644 index 00000000..728aa2ce --- /dev/null +++ b/osa_docs/projects/wSINDy/index.md @@ -0,0 +1,11 @@ +```markdown +# wSINDy + +## Overview + +The `wSINDy` module focuses on applying and comparing two equation discovery methods, SINDy (Sparse Identification of Nonlinear Dynamics) and EPDE (Equation Parameter Discovery and Estimation), to various partial differential equations (PDEs). It includes functionalities for data preparation, derivative calculation, library construction, sparse regression, and equation formatting. The module provides tools to translate SINDy equations into human-readable formats and to write Pareto front solutions to files for multi-objective optimization results. + +## Purpose + +The primary purpose of the `wSINDy` module is to facilitate the identification of governing equations from data generated by known PDEs, such as the Burgers' equation, Korteweg-de Vries (KdV) equation, Lotka-Volterra equations, and the wave equation. It allows users to explore the effectiveness of SINDy and EPDE in discovering these equations, compare their performance, and analyze the resulting models. The module also aims to provide utilities for formatting and presenting the discovered equations in a clear and interpretable manner. +``` \ No newline at end of file diff --git a/osa_docs/projects/wSINDy/lotka-volterra.md b/osa_docs/projects/wSINDy/lotka-volterra.md new file mode 100644 index 00000000..ca1254ed --- /dev/null +++ b/osa_docs/projects/wSINDy/lotka-volterra.md @@ -0,0 +1,5 @@ +# Lotka-Volterra + + + +::: projects.wSINDy.lotka-volterra \ No newline at end of file diff --git a/osa_docs/projects/wSINDy/wave.md b/osa_docs/projects/wSINDy/wave.md new file mode 100644 index 00000000..63fea024 --- /dev/null +++ b/osa_docs/projects/wSINDy/wave.md @@ -0,0 +1,5 @@ +# Wave + + + +::: projects.wSINDy.wave \ No newline at end of file diff --git a/osa_docs/projects/wave/index.md b/osa_docs/projects/wave/index.md new file mode 100644 index 00000000..f78a7cca --- /dev/null +++ b/osa_docs/projects/wave/index.md @@ -0,0 +1,9 @@ +# Wave Module + +## Overview + +The Wave module focuses on translating and evaluating differential equations within the EPDE framework. It provides functionality to load data, set up coordinate grids, define terms for a differential equation, and perform a search for suitable equation structures. The module leverages evolutionary algorithms and multi-objective optimization techniques to identify equations that best fit the provided data. + +## Purpose + +The primary purpose of the Wave module is to facilitate the discovery of governing differential equations from data by translating and evaluating them. It automates the process of setting up and searching for equation structures, allowing users to gain insights into the underlying dynamics of complex systems. The module evaluates the generated equations by mapping them to a function, such as `np.mean`, to assess their performance. diff --git a/osa_docs/projects/wave/wave.md b/osa_docs/projects/wave/wave.md new file mode 100644 index 00000000..a7058a4c --- /dev/null +++ b/osa_docs/projects/wave/wave.md @@ -0,0 +1,5 @@ +# Wave + + + +::: projects.wave.wave \ No newline at end of file diff --git a/osa_docs/setup.md b/osa_docs/setup.md new file mode 100644 index 00000000..2f894561 --- /dev/null +++ b/osa_docs/setup.md @@ -0,0 +1,5 @@ +# Setu + + + +::: setup \ No newline at end of file diff --git a/osa_docs/tests/system/conftest.md b/osa_docs/tests/system/conftest.md new file mode 100644 index 00000000..b8bb8f20 --- /dev/null +++ b/osa_docs/tests/system/conftest.md @@ -0,0 +1,5 @@ +# Conftest + + + +::: tests.system.conftest \ No newline at end of file diff --git a/osa_docs/tests/system/ode_textbook.md b/osa_docs/tests/system/ode_textbook.md new file mode 100644 index 00000000..cd1d9afb --- /dev/null +++ b/osa_docs/tests/system/ode_textbook.md @@ -0,0 +1,5 @@ +# Ode Textbook + + + +::: tests.system.ode_textbook \ No newline at end of file diff --git a/osa_docs/tests/unit/adapter_test.md b/osa_docs/tests/unit/adapter_test.md new file mode 100644 index 00000000..44ebf2e0 --- /dev/null +++ b/osa_docs/tests/unit/adapter_test.md @@ -0,0 +1,5 @@ +# Adapter Test + + + +::: tests.unit.adapter_test \ No newline at end of file diff --git a/osa_docs/tests/unit/conftest.md b/osa_docs/tests/unit/conftest.md new file mode 100644 index 00000000..48469aac --- /dev/null +++ b/osa_docs/tests/unit/conftest.md @@ -0,0 +1,5 @@ +# Conftest + + + +::: tests.unit.conftest \ No newline at end of file diff --git a/osa_mkdocs.yml b/osa_mkdocs.yml new file mode 100644 index 00000000..460f2811 --- /dev/null +++ b/osa_mkdocs.yml @@ -0,0 +1,47 @@ +site_name: OSADocs +docs_dir: osa_docs +theme: + name: material + locale: en + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-7 + name: Switch to light mode + features: + - navigation.tracking + - navigation.instant + - navigation.tabs + - navigation.tabs.sticky + - navigation.footer + - navigation.indexes + - content.tabs.link + - content.code.copy +plugins: + - search + - mkdocstrings + +markdown_extensions: + - admonition + - codehilite + - footnotes + - meta + - pymdownx.tasklist: + custom_checkbox: true + - toc: + permalink: True + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences \ No newline at end of file diff --git a/projects/ODE/2nd_order.py b/projects/ODE/2nd_order.py index cdc6bdb0..ec1f769f 100644 --- a/projects/ODE/2nd_order.py +++ b/projects/ODE/2nd_order.py @@ -30,6 +30,21 @@ def epde_discovery(t, x, use_ann = False): + """ + Discovers a differential equation model from the provided data using an evolutionary algorithm. + + This method sets up and executes the EPDE search process to identify a governing differential equation that best describes the relationship between the input variables. It leverages multi-objective optimization to balance model complexity and accuracy. The method prioritizes finding a single equation with a complexity of 5. If a more comprehensive exploration of the solution space is needed, the `equation_search_results` method can be used to examine the Pareto frontier of optimal equations. + + Args: + t (np.ndarray): The time variable data. + x (np.ndarray): The dependent variable data. + use_ann (bool, optional): A flag to use an Artificial Neural Network (ANN) preprocessor. Defaults to False, which uses polynomial preprocessor. + + Returns: + tuple: A tuple containing: + - epde_search_obj (EpdeSearch): The `EpdeSearch` object used for the equation discovery process. + - sys (Equation): The discovered equation with complexity 5. + """ dimensionality = x.ndim - 1 diff --git a/projects/ODE/ODE_data_preparation.py b/projects/ODE/ODE_data_preparation.py index 947e3036..283f28bd 100644 --- a/projects/ODE/ODE_data_preparation.py +++ b/projects/ODE/ODE_data_preparation.py @@ -18,6 +18,27 @@ def second_order_ODE_by_RK(initial : tuple, timestep : float, steps : int, g1 : Callable, g2 : Callable, g3 : Callable, g4 : Callable): + """ + Solves a second-order ordinary differential equation using the Runge-Kutta method. + + This method approximates the solution to a second-order ODE of the form: + g1(t) * y''(t) + g2(t) * y'(t) + g3(t) * y(t) = g4(t) + using a fourth-order Runge-Kutta scheme. This is a crucial step in numerically solving differential equations that might arise during the equation discovery process. By providing a reliable and accurate solver, we can evaluate the fitness of candidate equations generated by the evolutionary algorithm. + + Args: + initial: Initial conditions (y(0), y'(0)). + timestep: The time step size for the Runge-Kutta method. + steps: The number of steps to take in the approximation. + g1: Callable representing the coefficient function g1(t). + g2: Callable representing the coefficient function g2(t). + g3: Callable representing the coefficient function g3(t). + g4: Callable representing the coefficient function g4(t). + + Returns: + np.ndarray: A NumPy array of shape (steps, 2) containing the approximate + solution. The first column represents y(t) and the second column + represents y'(t) at each time step. + """ res = np.full(shape = (steps, 2), fill_value = initial, dtype=np.float64) for step in range(steps-1): # print(res[step, :]) diff --git a/projects/ODE/__init__.py b/projects/ODE/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/ODE/example_ODE_2nd_order.py b/projects/ODE/example_ODE_2nd_order.py index c3055aa6..cfdc0b25 100644 --- a/projects/ODE/example_ODE_2nd_order.py +++ b/projects/ODE/example_ODE_2nd_order.py @@ -111,11 +111,33 @@ # 1-t^2 def c1(grid): + """ + Computes a component of the equation based on the input grid. + + This function calculates the sine of twice the input grid, + representing a specific term or feature within a broader differential equation. + It helps to explore potential relationships and dependencies within the data. + + Args: + grid (torch.Tensor): The input grid representing the independent variable(s). + + Returns: + torch.Tensor: The sine of twice the input grid, a component of the equation. + """ return torch.sin(2*grid) # -2t def c2(grid): + """ + Scales the input grid, adjusting the balance and influence of different components within the equation discovery process. + + Args: + grid: The input grid representing a set of candidate equation terms. + + Returns: + The scaled grid, where each element's magnitude is adjusted, effectively prioritizing or diminishing the contribution of corresponding terms during equation construction. This scaling helps refine the search for the optimal equation structure. + """ return -1.5 * grid diff --git a/projects/ODE/ode_textbook_interfaced.py b/projects/ODE/ode_textbook_interfaced.py index c2ba74aa..4421ad10 100755 --- a/projects/ODE/ode_textbook_interfaced.py +++ b/projects/ODE/ode_textbook_interfaced.py @@ -14,6 +14,15 @@ # if __name__ == '__main__': def old_main(): + """ + Extends the pool of available tokens by adding functions that represent the inverse of coordinate values (e.g., 1/x, 1/t). This is achieved by defining a lambda function, `custom_inverse_eval_fun`, to evaluate these tokens and then creating a corresponding family of tokens. This allows the algorithm to explore a wider range of potential equation structures, including those with inverse relationships to the independent variables. + + Args: + None + + Returns: + None + """ t = np.linspace(0, 4*np.pi, 1000) u = np.load('/home/maslyaev/epde/EPDE/tests/system/Test_data/fill366.npy') # loading data with the solution of ODE # Trying to create population for mulit-objective optimization with only @@ -125,6 +134,19 @@ def old_main(): epde_search_obj.equation_search_results(only_print = True, level_num = 1) # showing the Pareto-optimal set of discovered equations def new_main(): + """ + Performs an EPDE search to identify a differential equation that describes the given data. + + This method automates the process of discovering a differential equation from a given dataset. + It initializes the search space, configures the evolutionary algorithm, and executes the search. + The goal is to find the equation that best represents the underlying dynamics of the data. + + Args: + None + + Returns: + None + """ t = np.linspace(0, 4*np.pi, 1000) u = np.load('/home/maslyaev/epde/EPDE_rework/projects/ODE/data/fill366.npy') # loading data with the solution of ODE @@ -145,6 +167,17 @@ def new_main(): epde_search_obj.equation_search_results(only_print = True, level_num = 1) def new_main_with_custom_tokens(): + """ + Performs an EPDE search with custom trigonometric tokens to identify governing equations. + + This method configures and executes an EPDE search, leveraging custom trigonometric functions to enhance the discovery of differential equations from data. It involves loading data, defining the search space, setting up the MOEAD/D algorithm, and integrating custom trigonometric tokens with their evaluation functions and parameter ranges. The search is then initiated, and the resulting equations are displayed. This approach allows the framework to explore a broader range of potential equation structures, potentially leading to more accurate and insightful models. + + Args: + None + + Returns: + None + """ t = np.linspace(0, 4*np.pi, 1000) u = np.load('/home/maslyaev/epde/EPDE_rework/projects/ODE/data/fill366.npy') # loading data with the solution of ODE diff --git a/projects/__init__.py b/projects/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/benchmarking/Lorenz/__init__.py b/projects/benchmarking/Lorenz/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/benchmarking/Lorenz/data_preparation.py b/projects/benchmarking/Lorenz/data_preparation.py index 2b906c1c..a6b3c498 100644 --- a/projects/benchmarking/Lorenz/data_preparation.py +++ b/projects/benchmarking/Lorenz/data_preparation.py @@ -12,6 +12,31 @@ def Lotka_Volterra_by_RK(initial : tuple, timestep : float, steps : int, alpha : float, beta : float, delta : float, gamma : float): + """ + Calculates the Lotka-Volterra model using the Runge-Kutta method. + + This method simulates the Lotka-Volterra equations, a mathematical model + describing the dynamics of biological systems in which two species interact, + one as a predator and the other as prey, using the 4th order Runge-Kutta + method. It is used to generate synthetic data of interacting populations, + which can then be used to test equation discovery algorithms. By simulating + known dynamics, the accuracy and efficiency of these algorithms can be + evaluated. + + Args: + initial: Initial population sizes of prey and predator. + timestep: The size of the time step used in the Runge-Kutta method. + steps: The number of time steps to simulate. + alpha: The natural growth rate of prey. + beta: The death rate of prey due to predation. + delta: The growth rate of predators due to eating prey. + gamma: The natural death rate of predators. + + Returns: + np.ndarray: A 2D array where each row represents a time step and the + two columns represent the prey and predator population sizes, + respectively. + """ res = np.full(shape = (steps, 2), fill_value = initial, dtype=np.float64) for step in range(steps-1): # print(res[step, :]) @@ -34,6 +59,21 @@ def Lotka_Volterra_by_RK(initial : tuple, timestep : float, steps : int, alpha : return res def dxdt(x, t): + """ + Calculates the time derivative of the Lorenz system. + + This function is a crucial component for simulating the Lorenz attractor, + as it defines the system's dynamics. It computes the rate of change of the + system's state variables (x, y, z) at a given time, which is essential + for numerical integration and visualization of the chaotic behavior. + + Args: + x (np.ndarray): A 3D vector representing the current state of the system (x, y, z). + t (float): The current time. While not directly used in the calculation, it's a required argument for ODE solvers. + + Returns: + np.ndarray: A 3D vector representing the derivative of the system's state at the given time (dx/dt, dy/dt, dz/dt). + """ sigma = 10.; rho = 28.; beta = 8./3. res = np.empty(3) res[0] = sigma * (x[1] - x[0]) @@ -42,6 +82,25 @@ def dxdt(x, t): return res def solve(initial : tuple, timestep : float, steps : int): + """ + Solves a system of differential equations using the Runge-Kutta 4th order method to model the system's evolution. + + This method is crucial for simulating the behavior of a system over time, + given its initial state and the differential equations that govern its dynamics. + It iteratively approximates the system's state at discrete time steps, + allowing us to observe how the system evolves from its initial conditions. + + Args: + initial (tuple): The initial conditions for the system's variables. + timestep (float): The size of each time step. + steps (int): The number of steps to take. + + Returns: + np.ndarray: A 2D array where each row represents the state of the system + at a given time step. The shape of the array is (steps, len(initial)). + This array represents the trajectory of the system through time, + allowing for analysis of its behavior and characteristics. + """ res = np.full(shape = (steps, len(initial)), fill_value = initial, dtype=np.float64) for step in range(steps-1): k1s = dxdt(res[step, :], timestep * step) @@ -53,6 +112,21 @@ def solve(initial : tuple, timestep : float, steps : int): return res def plot3d(solution): + """ + Plots the discovered equation in 3D space to visualize its behavior. + + Args: + solution (np.ndarray): A NumPy array representing the solution to be plotted. + It is expected to have shape (N, 3), where N is the number of points + and each point has X, Y, and Z coordinates. + + Returns: + None. Displays the 3D plot using matplotlib. + + Why: Visualizing the solution in 3D helps in understanding the behavior of the + discovered equation and verifying if it accurately represents the underlying dynamics + of the system. + """ ax = plt.figure().add_subplot(projection='3d') # ax = plt.axes(projection='3d') ax.plot(*solution.T, 'black', lw = 0.3)#[:, 0], solution[:, 1], solution[:, 2], ) @@ -64,6 +138,24 @@ def plot3d(solution): def classical_plot(solution, time, time_max = -1, step = 1): + """ + Generates a classical plot of the solution over time to visualize the identified system dynamics. + + This method iterates through each variable in the solution, plotting its + values against the given time points. It enhances understanding of the system's behavior + by providing a visual representation of variable interactions over time. + It then adds a grid, legend, and axis labels to the plot before saving it to a file and displaying it. + + Args: + solution: The solution array, where each column represents a variable + and each row represents a time point. + time: The time array corresponding to the rows in the solution array. + time_max: The maximum time index to plot up to. If -1, plots all time points. Defaults to -1. + step: The step size for plotting time points. Defaults to 1. + + Returns: + None. The method saves the plot to a file and displays it. + """ colors = ['k', 'r', 'b'] for var_idx in range(solution.shape[1]): plt.scatter(time[:time_max:step], solution[:time_max:step, var_idx], color = colors[var_idx], diff --git a/projects/benchmarking/Lorenz/equation_discovery.py b/projects/benchmarking/Lorenz/equation_discovery.py index bc45ce14..a9d15673 100644 --- a/projects/benchmarking/Lorenz/equation_discovery.py +++ b/projects/benchmarking/Lorenz/equation_discovery.py @@ -20,6 +20,25 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))) def write_pareto(dict_of_exp): + """ + Writes Pareto front solutions to text files for each experiment. + + This function iterates through a dictionary of experimental results, extracting + Pareto front solutions and writing them to separate text files. The filename + is derived from the experiment's parameters. Each iteration's Pareto front + is written with equation strings to the file. This is done to store the discovered + equation structures that represent the best trade-offs between different objectives + at each stage of the evolutionary search process, allowing for later analysis and + selection of the most suitable models. + + Args: + dict_of_exp: Dictionary where keys are tuples representing experiment + parameters and values are lists of Pareto front solutions for each + iteration of the experiment. + + Returns: + None. This method writes data to files and does not return any value. + """ for key, item in dict_of_exp.items(): test_key = str(key[0]).replace('.', '_') + '__' + str(key[1]).replace('.', '_') with open('/home/maslyaev/epde/EPDE_main/projects/hunter-prey/param_var/'+test_key+'.txt', 'w') as f: diff --git a/projects/benchmarking/__init__.py b/projects/benchmarking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/benchmarking/burgers_article.py b/projects/benchmarking/burgers_article.py index ff2d19cd..4ebb03da 100644 --- a/projects/benchmarking/burgers_article.py +++ b/projects/benchmarking/burgers_article.py @@ -33,6 +33,20 @@ # TODO^ caching of the pre-calculated derivatives def run_burg_eq_search(multiobjective_mode, derivs): + """ + Discovers the Burgers' equation from provided data derivatives using an evolutionary algorithm. + + This method sets up and executes the equation search process, configuring the evolutionary algorithm + with appropriate parameters and custom tokens to explore the solution space effectively. The goal + is to identify the equation that best describes the relationship between the input data and its derivatives. + + Args: + multiobjective_mode: A boolean flag that enables multiobjective optimization, balancing equation complexity and accuracy. + derivs: Derivatives of the input data, precomputed to facilitate the equation search. + + Returns: + tuple: A tuple containing the EPDE search object, which holds the search results, and the optimization metric obtained during the search. + """ print(u.shape, grids[0].shape, grids[1].shape) epde_search_obj = epde_alg.EpdeSearch(multiobjective_mode=multiobjective_mode, use_solver=False, dimensionality=dimensionality, boundary=boundary, diff --git a/projects/benchmarking/kdv_article.py b/projects/benchmarking/kdv_article.py index 275083b2..37eae2dc 100644 --- a/projects/benchmarking/kdv_article.py +++ b/projects/benchmarking/kdv_article.py @@ -28,6 +28,27 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))) def run_KdV_eq_search(multiobjective_mode, derivs): + """ + Runs the KdV equation search using the EPDE algorithm. + + This method orchestrates the equation discovery process for the KdV equation. + It configures the search space, evolutionary algorithm parameters, and custom tokens + to effectively explore potential equation candidates. The goal is to identify the + equation that best describes the relationship between the provided data and its derivatives. + + Args: + multiobjective_mode: A boolean indicating whether to use multiobjective optimization, + allowing for trade-offs between different objectives such as accuracy + and equation complexity. + derivs: The derivatives of the data, precomputed and provided as input for the equation search. + + Returns: + tuple: A tuple containing the equation search results and the metric value. + The equation search results are a list of found equations, ranked by their + ability to fit the data and satisfy the search criteria. + The metric is a float representing the objective function value + for the best equation, quantifying its fitness based on the chosen optimization strategy. + """ epde_search_obj = epde_alg.EpdeSearch(multiobjective_mode=multiobjective_mode, use_solver=False, dimensionality=dimensionality, boundary=boundary, coordinate_tensors = grids) diff --git a/projects/benchmarking/wave.py b/projects/benchmarking/wave.py index cf87ea31..dd37190e 100644 --- a/projects/benchmarking/wave.py +++ b/projects/benchmarking/wave.py @@ -20,6 +20,24 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))) def run_wave_eq_search(multiobjective_mode, derivs = None): + """ + Runs the EPDE search for the wave equation. + + This method configures and executes the EPDE search algorithm to identify + the wave equation from the provided data. It sets up the search space by defining custom tokens and specifying the optimization parameters, + allowing the algorithm to explore potential equation structures. This automated search helps in discovering the underlying differential equation + that best describes the observed wave phenomena. + + Args: + multiobjective_mode (bool): A boolean indicating whether to use multiobjective + optimization. + derivs (None): Derivatives (not used). + + Returns: + tuple: A tuple containing the equation search results and the metric value. + The first element is a list of equation search results. + The second element is the metric value obtained during the search. + """ epde_search_obj = epde_alg.epde_search(multiobjective_mode=multiobjective_mode, use_solver = False, dimensionality = dimensionality, boundary = 10, coordinate_tensors = grids, verbose_params = {'show_moeadd_epochs' : True}) diff --git a/projects/benchmarking/wave_article.py b/projects/benchmarking/wave_article.py index 40a805c2..54713312 100644 --- a/projects/benchmarking/wave_article.py +++ b/projects/benchmarking/wave_article.py @@ -24,6 +24,23 @@ # TODO^ caching of the pre-calculated derivatives def translate_eq(): + """ + Translates and evaluates a partial differential equation (PDE) by constructing candidate equations and evaluating their fitness against the provided data. + + This method sets up the coordinate grid, defines the left-hand side (LHS) and right-hand side + (RHS) terms of the PDE, and then uses the EPDE algorithm to search for a solution by + exploring combinations of terms and coefficients. Finally, it evaluates the identified + equation and prints the equation's text form and the mean of the evaluated result, + demonstrating the equation discovery process. + + Args: + None + + Returns: + None. Prints the equation's text form and the mean of the evaluated result. The equation + represents the discovered relationship between the terms, and the mean provides a measure + of how well the equation fits the data. + """ u = np.loadtxt('/home/maslyaev/epde/EPDE_main/projects/wave/data.csv').reshape((101, 101, 101)) u = np.moveaxis(u, 2, 0) @@ -53,6 +70,17 @@ def map_to_equation(equation, function, function_kwargs = dict()): print(map_to_equation(test1.equation, np.mean)) def run_wave_eq_search(multiobjective_mode): + """ + Runs the equation search for the wave equation using the EPDE framework. + + This method leverages the EPDE (Equation Parameter Discovery and Estimation) search algorithm to automatically identify the mathematical equation that best describes the wave phenomenon, given the provided data and configuration. It defines the search space of possible equation terms, preprocesses the input data to enhance the search, and optimizes the equation parameters to achieve the best fit. This automated approach helps in understanding the underlying physics of wave propagation by discovering the governing equation directly from data. + + Args: + multiobjective_mode (bool): A boolean flag indicating whether to use multiobjective optimization. When True, the search aims to balance multiple objectives, such as accuracy and equation complexity. + + Returns: + list: A list of the best identified equations, ranked according to their fitness. Each equation represents a potential mathematical model for the wave equation. + """ epde_search_obj = epde_alg.EpdeSearch(multiobjective_mode=multiobjective_mode, use_solver=False, dimensionality=dimensionality, boundary=boundary, coordinate_tensors = grids, diff --git a/projects/benchmarking/wave_article_ref.py b/projects/benchmarking/wave_article_ref.py index 708a8ba8..4e0a7108 100644 --- a/projects/benchmarking/wave_article_ref.py +++ b/projects/benchmarking/wave_article_ref.py @@ -27,6 +27,25 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))) def run_wave_eq_search(multiobjective_mode, derivs = None): + """ + Discovers the wave equation using the EPDE framework. + + This method leverages the EPDE search algorithm to identify the wave equation + from the provided dataset. It explores the space of possible equation structures + using evolutionary algorithms, with the goal of finding the equation that best + represents the underlying dynamics of the data. The method supports both single-objective + and multi-objective optimization modes to refine the search process. + + Args: + multiobjective_mode (bool): A boolean flag indicating whether to use multi-objective optimization. + derivs (np.ndarray, optional): Pre-calculated derivatives. Defaults to None. + + Returns: + tuple: A tuple containing: + - The search results (equations found). + - The metric value of the best equation. + - The saved derivatives from the search process. + """ epde_search_obj = epde_alg.EpdeSearch(multiobjective_mode=multiobjective_mode, use_solver = False, dimensionality = dimensionality, boundary = 10, coordinate_tensors = grids) diff --git a/projects/burgers/__init__.py b/projects/burgers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/burgers/burgers_eq.py b/projects/burgers/burgers_eq.py index a3d282f8..01ceacf0 100644 --- a/projects/burgers/burgers_eq.py +++ b/projects/burgers/burgers_eq.py @@ -14,6 +14,21 @@ def out_formatting(string): + """ + Formats a string to improve readability of discovered equations by replacing symbolic representations and rounding numerical values. + + This function specifically targets the output format of the equation discovery process, + making it easier to interpret the resulting mathematical expressions. It performs + substitutions for common derivative notations and ensures that floating-point numbers + are displayed with a reasonable level of precision. + + Args: + string: The input string representing the discovered equation. + + Returns: + str: The formatted string with replaced patterns and rounded numbers, + suitable for human interpretation. + """ string1 = str.split(string, '\n') string = string1[0] string = string.replace("u{power: 1.0}", "u") diff --git a/projects/epde_ts_pred/__init__.py b/projects/epde_ts_pred/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/epde_ts_pred/ode_discovery.py b/projects/epde_ts_pred/ode_discovery.py index 75cc8a04..cdf0a24d 100644 --- a/projects/epde_ts_pred/ode_discovery.py +++ b/projects/epde_ts_pred/ode_discovery.py @@ -32,6 +32,19 @@ def equation_fit(grid,data): + """ + Defines the parameter value ranges within which functions are considered identical. + + This is crucial for constructing unique equation structures and their components. For instance, in the context of equation discovery, + the difference between `sin(3.135 * x)` and `sin(3.145 * x)` might be deemed negligible, treating them as equivalent. This allows to reduce number of similar equations. + + Args: + grid (np.ndarray): The grid on which the data is defined. + data (np.ndarray): The data to fit the equation to. + + Returns: + list: A list of tuples, where each tuple contains the solver form of an equation and its boundary conditions. + """ dimensionality = data.ndim # - 1 diff --git a/projects/example_COVID/__init__.py b/projects/example_COVID/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/hunter-prey/__init__.py b/projects/hunter-prey/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/hunter-prey/data_preparation.py b/projects/hunter-prey/data_preparation.py index 8b86ab09..3f1a76d0 100644 --- a/projects/hunter-prey/data_preparation.py +++ b/projects/hunter-prey/data_preparation.py @@ -11,6 +11,29 @@ def Lotka_Volterra_by_RK(initial : tuple, timestep : float, steps : int, alpha : float, beta : float, delta : float, gamma : float): + """ + Calculates the Lotka-Volterra model using the Runge-Kutta method. + + This method simulates the Lotka-Volterra equations, a mathematical model + describing the dynamics of biological systems in which two species interact, + one as a predator and the other as prey, using the 4th order Runge-Kutta + method. It is used to generate synthetic data of interacting populations + that can be used to test equation discovery algorithms. + + Args: + initial: Initial population sizes of prey and predator. + timestep: The size of the time step used in the Runge-Kutta method. + steps: The number of time steps to simulate. + alpha: The natural growth rate of prey. + beta: The death rate of prey due to predation. + delta: The growth rate of predators due to eating prey. + gamma: The natural death rate of predators. + + Returns: + np.ndarray: A 2D array where each row represents a time step and the + two columns represent the prey and predator population sizes, + respectively. + """ res = np.full(shape = (steps, 2), fill_value = initial, dtype=np.float64) for step in range(steps-1): # print(res[step, :]) diff --git a/projects/hunter-prey/equation_discovery.py b/projects/hunter-prey/equation_discovery.py index 6fdf437b..e754ce49 100644 --- a/projects/hunter-prey/equation_discovery.py +++ b/projects/hunter-prey/equation_discovery.py @@ -21,6 +21,25 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..'))) def write_pareto(dict_of_exp): + """ + Writes Pareto front solutions to text files for each experiment. + + This function iterates through a dictionary of experimental results, extracting + Pareto front solutions and writing them to separate text files. The filename + is derived from the experiment parameters. Each iteration's Pareto front is + written with equation strings to the file. This is done to store the discovered + equation structures representing the best trade-offs between different objectives + at each stage of the evolutionary process, enabling further analysis and selection + of the most suitable models. + + Args: + dict_of_exp: Dictionary where keys are tuples representing experiment + parameters and values are lists of Pareto front solutions for each + iteration of the experiment. + + Returns: + None. This method writes data to files and does not return any value. + """ for key, item in dict_of_exp.items(): test_key = str(key[0]).replace('.', '_') + '__' + str(key[1]).replace('.', '_') with open('/home/maslyaev/epde/EPDE_main/projects/hunter-prey/param_var/'+test_key+'.txt', 'w') as f: diff --git a/projects/multi_objective_algebraic_expression/__init__.py b/projects/multi_objective_algebraic_expression/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/multi_objective_algebraic_expression/load_data.py b/projects/multi_objective_algebraic_expression/load_data.py index b0e39edf..46511150 100644 --- a/projects/multi_objective_algebraic_expression/load_data.py +++ b/projects/multi_objective_algebraic_expression/load_data.py @@ -14,6 +14,22 @@ def get_data(idx: int) -> dict: + """ + Retrieves a specific data entry from the dataset. + + This function allows access to individual data points within the loaded dataset, + enabling further analysis or processing of specific instances. It is essential + for examining particular scenarios or events represented in the data. + + Args: + idx (int): The index of the desired data entry in the dataset. + + Returns: + dict: The data entry at the specified index. + + Raises: + IndexError: If the provided index is outside the valid range of the dataset. + """ try: return load_data[idx] except IndexError: diff --git a/projects/pic/__init__.py b/projects/pic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/__init__.py b/projects/pic/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/ac/__init__.py b/projects/pic/data/ac/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/ac/ac.py b/projects/pic/data/ac/ac.py index a37f0a3c..cee09b00 100644 --- a/projects/pic/data/ac/ac.py +++ b/projects/pic/data/ac/ac.py @@ -25,6 +25,18 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) from a file. + + This function attempts to load a previously trained PINN model, allowing the evolutionary process to start from a potentially good initial guess, + reducing the computational cost of training from scratch. If no pre-trained model is found, the process continues by retraining the ANN approximation. + + Args: + ann_filename: The filename of the pickled ANN data. + + Returns: + The loaded ANN data if the file exists, otherwise None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +47,43 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This function perturbs the data by adding Gaussian noise scaled by the data's standard deviation and a specified noise level. + This helps to evaluate the robustness of equation discovery algorithms when dealing with imperfect or noisy data. + + Args: + data (np.ndarray): The input data to which noise will be added. + noise_level (float): The standard deviation of the noise, expressed as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which better represents the underlying dynamics. + + This method translates both equations into a comparable format, applies a fitting procedure, + and then assesses their coefficient stability. The comparison helps identify equations that + are more robust and reliable in capturing the system's behavior. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the correct equation exhibits better coefficient stability + than the incorrect equation across all variables, indicating a superior + representation of the system's dynamics; False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +112,22 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator with necessary sub-operators for equation discovery. + + This method initializes and configures the sparsity and coefficient calculation sub-operators, + then integrates them into the provided compound fitness operator. The operator is mapped + between gene and chromosome levels based on a condition that checks if the fitness has + already been calculated. This ensures that the fitness calculation is performed only when + necessary during the evolutionary process of discovering differential equations. + + Args: + fitness_operator: The compound fitness operator to prepare. + operator_params: A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The prepared compound fitness operator, ready for use in the equation discovery process. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -84,6 +143,26 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di return fitness_operator def ac_data(filename: str): + """ + Loads data and generates corresponding grids for equation discovery. + + This function prepares the data by loading it from a file and constructing + the necessary coordinate grids. These grids represent the independent variables + over which the solution is defined, and are essential for evaluating candidate + differential equations. + + Args: + filename (str): The path to the data file ('.npy' format). + + Returns: + tuple: A tuple containing: + - grids (tuple of ndarray): Meshgrids representing the independent variables (t, x). + - data (ndarray): The loaded data representing the dependent variable. + + Why: + The grids are created to define the domain over which the differential equations + will be evaluated and compared against the loaded data. + """ t = np.linspace(0., 1., 51) x = np.linspace(-1., 0.984375, 128) data = np.load(filename) @@ -93,6 +172,17 @@ def ac_data(filename: str): def AC_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Test scenario to evaluate performance on Allen-Cahn equation. It aims to verify that the system can distinguish between the correct symbolic representation of the Allen-Cahn equation and an incorrect one based on the provided data and configurations. This is crucial for validating the equation discovery process. + + Args: + operator: Compound operator for equation comparison. + foldername: Name of the folder containing data files. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the symbolic equation and incorrect equation are different, False otherwise. + """ # Test scenario to evaluate performance on Allen-Cahn equation eq_ac_symbolic = '0.0001 * d^2u/dx1^2{power: 1.0} + -5.0 * u{power: 3.0} + 5.0 * u{power: 1.0} + 0.0 = du/dx0{power: 1.0}' eq_ac_incorrect = '-1.0 * d^2u/dx0^2{power: 1.0} + 1.5 * u{power: 1.0} + -0.0 = du/dx0{power: 1.0}' @@ -118,6 +208,22 @@ def AC_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def ac_discovery(foldername, noise_level): + """ + Discovers the governing equation from data using the EPDE framework. + + This method performs equation discovery on data loaded from a specified + folder, incorporating a given noise level. It leverages a pre-trained + PINN (Physics-Informed Neural Network) and the EPDE search algorithm + to identify the underlying equation. The method automates the process of identifying governing differential equations from data, + allowing users to gain insights into the underlying dynamics of complex systems and build predictive models. + + Args: + foldername: The name of the folder containing the data and PINN. + noise_level: The level of noise added to the data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations. + """ grid, data = ac_data(os.path.join(foldername, 'ac_data.npy')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) diff --git a/projects/pic/data/burgers/__init__.py b/projects/pic/data/burgers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/burgers/burgers.py b/projects/pic/data/burgers/burgers.py index caf6e8a2..2620f822 100644 --- a/projects/pic/data/burgers/burgers.py +++ b/projects/pic/data/burgers/burgers.py @@ -26,6 +26,19 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pretrained Physics-Informed Neural Network (PINN) model from a file. + + This function attempts to load a previously trained PINN model from disk. + This is useful for resuming training or using a pre-trained model + without retraining, saving computational resources. + + Args: + ann_filename: The filename of the pickled ANN model. + + Returns: + The loaded ANN model if the file exists; otherwise, returns None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -36,12 +49,44 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This helps to evaluate the robustness of discovered differential equations when data is imperfect. + + Args: + data (np.ndarray): The input data to add noise to. + noise_level (float): The level of noise to add, as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which better represents the underlying dynamics. + + This method translates both equations into a comparable form, applies a fitting + operator to assess their accuracy, and then evaluates their coefficient stability. + The comparison helps in identifying the equation that more robustly captures the + system's behavior. + + Args: + correct_symbolic (str): The correct symbolic equation as a string. + eq_incorrect_symbolic (str): The incorrect symbolic equation as a string. + search_obj (EpdeSearch): An EpdeSearch object containing the search pool and + necessary context for equation translation and fitting. + all_vars (List[str], optional): A list of variable names to consider. Defaults to ['u']. + + Returns: + bool: True if the correct equation exhibits better coefficient stability + than the incorrect equation across all variables, indicating a superior + representation of the system's dynamics. False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -70,6 +115,25 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound operator by setting sub-operators for sparsity and coefficient calculation. + + This method configures the provided compound operator by setting its + sub-operators, which are responsible for calculating sparsity and coefficients. + It then maps the operator between gene and chromosome levels based on a fitness + calculation condition. This ensures that the evolutionary process can + effectively explore the search space of possible equation structures. + + Args: + fitness_operator: The compound operator to prepare. + operator_params: Parameters to be set for the fitness operator. + + Returns: + The modified fitness operator with prepared sub-operators and level mapping. + This operator is now ready to be used in the evolutionary search for + differential equations. The mapping ensures compatibility between + different levels of representation (genes and chromosomes). + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +149,26 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di return fitness_operator def burgers_data(filename: str): + """ + Loads Burgers' equation data from a MATLAB file and prepares it for equation discovery. + + This function loads the solution data of Burgers' equation from a specified + MATLAB file. It extracts the solution `usol`, time `t`, and spatial `x` + data, transposes the solution data, and creates corresponding time and + spatial grids. This preprocessing step is crucial for preparing the data + into a suitable format that the equation discovery algorithms can utilize + to identify the underlying differential equation. + + Args: + filename: The path to the MATLAB file containing the Burgers' equation data. + + Returns: + A tuple containing: + - grids: A tuple of arrays representing the time and spatial grids, + structured for use in equation discovery. + - data: A NumPy array containing the transposed solution data, + ready for analysis. + """ burg = loadmat(filename) t = np.ravel(burg['t']) x = np.ravel(burg['x']) @@ -95,6 +179,24 @@ def burgers_data(filename: str): def burgers_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the EPDE search on the Burgers' equation. + + This method evaluates the EPDE framework's ability to identify the Burgers' equation + from noisy data. It sets up an EPDE search and compares the identified equation + against known correct and incorrect equations to assess the accuracy of the + equation discovery process. This test helps ensure the framework can reliably + extract governing equations from data, even in the presence of noise. + + Args: + operator: CompoundOperator object for equation comparison. + foldername: Path to the folder containing the Burgers' equation data. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the correct equation and differs + from the incorrect equation, False otherwise. + """ # Test scenario to evaluate performance on Allen-Cahn equation eq_burgers_symbolic = '0.0001 * d^2u/dx1^2{power: 1.0} + -5.0 * u{power: 3.0} + 5.0 * u{power: 1.0} + 0.0 = du/dx0{power: 1.0}' eq_burgers_incorrect = '-1.0 * d^2u/dx0^2{power: 1.0} + 1.5 * u{power: 1.0} + -0.0 = du/dx0{power: 1.0}' @@ -120,6 +222,18 @@ def burgers_test(operator: CompoundOperator, foldername: str, noise_level: int = def burgers_discovery(foldername, noise_level): + """ + Performs equation discovery for the Burgers' equation. + + This method leverages an evolutionary algorithm to identify the governing equation of the Burgers' equation from noisy data. It sets up and executes an EPDE search, incorporating finite difference (FD) preprocessing to estimate derivatives. Custom grid and trigonometric tokens are defined to enrich the search space. The model is then fitted to the data to find the equation that best describes the underlying dynamics. Finally, the discovered equations are printed and visualized to provide insights into the system's behavior. This automated equation discovery process helps to reveal the fundamental relationships within the data. + + Args: + foldername: The name of the folder containing the data files ('burgers.mat' and 'kdv_{noise_level}_ann.pickle'). + noise_level: The level of noise added to the data. + + Returns: + EpdeSearch: The trained EPDE search object containing the discovered equations and related information. + """ grid, data = burgers_data(os.path.join(foldername, 'burgers.mat')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) diff --git a/projects/pic/data/darcy/__init__.py b/projects/pic/data/darcy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/darcy/darcy.py b/projects/pic/data/darcy/darcy.py index e7868c4d..113f700f 100644 --- a/projects/pic/data/darcy/darcy.py +++ b/projects/pic/data/darcy/darcy.py @@ -25,6 +25,19 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) model from a file. + + This function attempts to load a previously trained PINN model from disk. + Loading a pre-trained model can save significant time by avoiding retraining, + especially when exploring different equation structures or refining existing models. + + Args: + ann_filename: The filename of the pickled ANN model. + + Returns: + The loaded ANN model, or None if the file is not found. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +48,43 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This function perturbs the data by adding Gaussian noise scaled by the data's standard deviation and a specified noise level. + This helps to evaluate the robustness of equation discovery algorithms when dealing with imperfect or noisy data. + + Args: + data (np.ndarray): The input data to which noise will be added. + noise_level (float): The standard deviation of the noise, expressed as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which better represents the underlying dynamics. + + This method translates both equations into a comparable format, applies a fitting procedure, + and then assesses their coefficient stability. The comparison helps identify equations that + are more robust and reliable in capturing the system's behavior. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the correct equation exhibits better coefficient stability + than the incorrect equation across all variables, indicating a superior + representation of the system's dynamics; False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +113,25 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator with necessary sub-operators. + + This method configures the provided compound operator by setting its + sub-operators for sparsity and coefficient calculation. These sub-operators + are essential for constructing the overall fitness evaluation process, ensuring + that the evolutionary algorithm can effectively explore the search space of + potential equation structures. The operator is then mapped between gene and + chromosome levels based on a fitness calculation condition to ensure proper + evaluation during the evolutionary process. This setup is crucial for + evaluating the fitness of candidate equations within the evolutionary process. + + Args: + fitness_operator (CompoundOperator): The compound fitness operator to prepare. + operator_params (dict): A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The prepared compound fitness operator. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +148,24 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def darcy_data(filename: str): + """ + Loads Darcy flow data, creating necessary grids and reshaping the data for EPDE analysis. + + This function loads a NumPy array representing a Darcy flow solution from a specified file, + reshapes it to a suitable format, and generates corresponding coordinate grids. The data is duplicated + along the time axis to simulate a time-dependent process, which is a common requirement + when modeling dynamical systems with EPDE. This duplication allows the framework to analyze + how the system evolves over time, even if the original data represents a steady-state solution. + + Args: + filename (str): The path to the NumPy file (.npy) containing the Darcy flow data. + + Returns: + tuple: A tuple containing: + - grids (tuple): A tuple of NumPy arrays representing the coordinate grids (t, x, y). + - data (np.ndarray): A NumPy array containing the processed Darcy flow data, reshaped and + duplicated along the time axis. The shape of the data is (2, 128, 128). + """ t = np.linspace(0., 1., 2) x = np.linspace(0., 1., 128) y = np.linspace(0., 1., 128) @@ -96,6 +177,22 @@ def darcy_data(filename: str): return grids, data def darcy_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance of the EPDE search on the Darcy equation. + + This method evaluates the EPDE framework's ability to identify the Darcy equation from noisy data. + It sets up an EPDE search with specific configurations, including data loading, preprocessing, + custom token definitions, and equation comparison. The goal is to assess whether the framework + can accurately rediscover the underlying equation despite the presence of noise and other challenges. + + Args: + operator: CompoundOperator instance for equation comparison. + foldername: Path to the folder containing the Darcy data. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the expected Darcy equation, False otherwise. + """ # Test scenario to evaluate performance on darcy equation # eq_darcy_symbolic = '-1.0 * du/dx1{power: 1.0} * dnu/dy{power: 1.0} + -1.0 * nu{power: 1.0} * d^2u/dx1^2{power: 1} + \ # -2.0 * nu{power: 1.0} * d^2u/dxdy{power: 1} + \ @@ -153,6 +250,24 @@ def darcy_test(operator: CompoundOperator, foldername: str, noise_level: int = 0 def darcy_discovery(foldername, noise_level): + """ + Discovers a differential equation governing Darcy flow from data. + + This method automates the identification of the underlying equation + describing Darcy flow by searching through possible equation structures. + It incorporates domain knowledge through custom tokens representing + permeability and its derivatives, and uses the EPDE framework to + efficiently explore the solution space. By finding the best equation + that fits the provided data, this method helps to understand and model + fluid flow in porous media. + + Args: + foldername: The name of the folder containing the Darcy flow data ('darcy_1.0.npy') and permeability data ('darcy_nu_1.0.npy'). + noise_level: The level of noise to add to the Darcy flow data. + + Returns: + EpdeSearch: The EPDE search object containing the results of the equation discovery process. + """ grid, data = darcy_data(os.path.join(foldername, 'darcy_1.0.npy')) nu = np.load('darcy_nu_1.0.npy') noised_data = noise_data(data, noise_level) diff --git a/projects/pic/data/kdv/__init__.py b/projects/pic/data/kdv/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/kdv/kdv.py b/projects/pic/data/kdv/kdv.py index 249b1cc2..51cd6c08 100644 --- a/projects/pic/data/kdv/kdv.py +++ b/projects/pic/data/kdv/kdv.py @@ -25,6 +25,17 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) model from a file. + + This function attempts to load a previously trained PINN model from disk. Loading a pre-trained model can save significant time by avoiding retraining, especially when exploring different equation structures or refining existing models within the EPDE framework. + + Args: + ann_filename: The filename of the pickled ANN model. + + Returns: + The loaded ANN model. Returns None if the file is not found, indicating that a new model may need to be trained from scratch. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +46,42 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This helps to evaluate the robustness of discovered differential equations when data is imperfect. + + Args: + data (np.ndarray): The input data to add noise to. + noise_level (float): The level of noise to add, as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which better represents the underlying dynamics of the system. + + It translates both equations into a comparable form, applies a fitting procedure, + and then assesses their stability based on their coefficients. This helps in + identifying the equation that more accurately captures the system's behavior. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the correct equation exhibits better coefficients stability + than the incorrect equation across all variables, indicating a superior + representation of the system's dynamics; False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +110,25 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator with necessary sub-operators. + + This method configures the provided compound operator by setting its + sub-operators for sparsity and coefficient calculation. These sub-operators + are essential for constructing the overall fitness evaluation process, ensuring + that the evolutionary algorithm can effectively explore the search space of + potential equation structures. The operator is then mapped between gene and + chromosome levels based on a fitness calculation condition to ensure proper + evaluation during the evolutionary process. This setup is crucial for + evaluating the fitness of candidate equations within the evolutionary process. + + Args: + fitness_operator (CompoundOperator): The compound fitness operator to prepare. + operator_params (dict): A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The prepared compound fitness operator. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +145,23 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def kdv_data(filename, shape=80): + """ + Loads data and creates corresponding grids for equation discovery. + + This method reads data from a specified file, and constructs spatial and temporal grids + to align with the data. These grids are essential for representing the domain + over which the differential equation is defined. The shape of the grid is fixed to 80. + This setup facilitates the application of evolutionary algorithms to discover the underlying equation. + + Args: + filename (str): The name of the data file to load (CSV format). + + Returns: + tuple: A tuple containing the grids and the data. + The grids are a meshgrid of time and space coordinates, + representing the independent variables of the differential equation. + The data is a NumPy array loaded from the file, representing the dependent variable. + """ shape = 80 print(os.path.dirname(__file__)) @@ -96,6 +173,35 @@ def kdv_data(filename, shape=80): def kdv_data_h(filename, shape=80): + """ + Loads KdV equation data and prepares it for equation discovery. + + This function loads data representing the solution of the KdV equation + from a specified file. It also generates the corresponding time and + space grids necessary for representing the data as a function of + these independent variables. This setup is crucial for algorithms + aiming to identify the underlying differential equation. + + Args: + filename (str): The path to the .npy file containing the KdV equation data. + shape (int, optional): An integer intended to define the shape of the data. + This parameter is overridden internally and does not affect the output. Defaults to 80. + + Returns: + tuple: A tuple containing the time and space grids (t, x) and the loaded data. + - grids (tuple of numpy.ndarray): A tuple containing two numpy arrays, + representing the time and space grids, respectively. These grids are + created using np.meshgrid with t ranging from 0 to 1 (120 points) + and x ranging from -3 to 3 (480 points). + - data (numpy.ndarray): The loaded KdV equation data from the specified file. + + Why: + This function prepares the KdV equation data and its corresponding grids + so that equation discovery algorithms can use them to learn the underlying + differential equation. The grids provide the coordinate system in which + the data is defined, which is essential for calculating derivatives and + evaluating potential equation candidates. + """ shape = 119 print(os.path.dirname(__file__)) @@ -108,6 +214,30 @@ def kdv_data_h(filename, shape=80): def kdv_data_sga(filename): + """ + Loads KdV data from a .mat file and prepares it for symbolic equation discovery. + + This function loads the KdV data, extracts the solution `u`, spatial grid `x`, + and temporal grid `t`, and then creates a meshgrid from `t` and `x`. This + structured data is essential for representing the solution on a grid, which + is a prerequisite for applying symbolic regression techniques to discover the + underlying partial differential equation. + + Args: + filename: The name of the .mat file containing the KdV data. + + Returns: + tuple: A tuple containing: + - grids: A tuple representing the meshgrid of t and x. + - u: The solution data. + + Why: + The KdV data is loaded and prepared in this way to create a structured + representation of the solution on a grid. This is necessary for + subsequent symbolic regression to discover the underlying partial + differential equation. The meshgrid provides the coordinates for each + point in the solution, which is used to evaluate candidate equations. + """ data = scio.loadmat(filename) u = data.get("uu").T n, m = u.shape @@ -117,6 +247,24 @@ def kdv_data_sga(filename): return grids, u def kdv_sindy_data(filename): + """ + Prepares data from a MATLAB file for equation discovery. + + Loads data from a specified MATLAB file, extracts the time series, + spatial coordinates, and solution data, and prepares them for use in + equation discovery algorithms. This function ensures that the data is + correctly formatted and accessible for subsequent analysis, enabling + the identification of potential governing equations. + + Args: + filename: The name of the MATLAB file containing the data. + + Returns: + tuple: A tuple containing: + - grids: A tuple of meshgrids representing the time and spatial + coordinates. + - u: A NumPy array representing the solution data. + """ data = scio.loadmat(filename) t = np.ravel(data['t']) x = np.ravel(data['x']) @@ -126,6 +274,23 @@ def kdv_sindy_data(filename): return grids, u def KdV_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance of the EPDE search on the Korteweg-de Vries equation. + + This method aims to validate the equation discovery process by attempting to identify the KdV equation from noisy data. + It compares the identified equation against known correct and incorrect formulations to assess the accuracy of the search. + The method employs a finite difference preprocessor to prepare the data for the equation search. + This test helps ensure that the EPDE framework can reliably extract governing equations from data, even in the presence of noise. + + Args: + operator: The compound operator to use for equation comparison. + foldername: The name of the folder containing the data files ('data.csv' and 'kdv_0_ann.pickle'). + noise_level: The level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the correct equation and + does not match the incorrect equation, False otherwise. + """ # Test scenario to evaluate performance on Korteweg-de Vries equation # eq_kdv_symbolic = '-6.0 * du/dx1{power: 1.0} * u{power: 1.0} + -1.0 * d^3u/dx1^3{power: 1.0} + \ # 1.0 * sin{power: 1, freq: 1.0, dim: 1} * cos{power: 1, freq: 1.0, dim: 0} + \ @@ -182,6 +347,25 @@ def KdV_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def KdV_h_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance on the Korteweg-de Vries equation. + + This method evaluates the ability of the EPDE framework to identify the + Korteweg-de Vries (KdV) equation from data. It sets up a test scenario + involving data loading, noise addition, and equation search, ultimately + assessing whether the identified equation matches the known KdV equation. + This serves as a benchmark to ensure the framework can accurately + recover governing equations from noisy data, a crucial aspect of its + equation discovery capabilities. + + Args: + operator: CompoundOperator object for equation comparison. + foldername: Path to the folder containing the data files. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the expected KdV equation, False otherwise. + """ # Test scenario to evaluate performance on Korteweg-de Vries equation eq_kdv_symbolic = '-6.0 * du/dx1{power: 1.0} * u{power: 1.0} + -1.0 * d^3u/dx1^3{power: 1.0} + \ 0.0 = du/dx0{power: 1.0}' @@ -214,6 +398,21 @@ def KdV_h_test(operator: CompoundOperator, foldername: str, noise_level: int = 0 def KdV_sga_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance of the symbolic genetic algorithm (SGA) on the Korteweg-de Vries (KdV) equation. + + This method evaluates the SGA's ability to identify the underlying differential equation of the KdV system from data. + It sets up a controlled experiment by loading KdV data, introducing noise to simulate real-world imperfections, and then employing the SGA to discover the equation. + The goal is to assess whether the algorithm can accurately extract the governing equation, demonstrating its capability to automate the discovery of differential equations from data. + + Args: + operator: The CompoundOperator instance to be tested. + foldername: The name of the folder containing the KdV data and pretrained PINN model. + noise_level: The level of noise to add to the data (default: 0). + + Returns: + bool: True if the discovered equation matches the expected KdV equation, False otherwise. + """ # Test scenario to evaluate performance on Korteweg-de Vries equation eq_kdv_symbolic = '-1 * du/dx1{power: 1.0} * u{power: 1.0} + -0.0025 * d^3u/dx1^3{power: 1.0} + \ 0.0 = du/dx0{power: 1.0}' @@ -246,6 +445,25 @@ def KdV_sga_test(operator: CompoundOperator, foldername: str, noise_level: int = def kdv_discovery(foldername, noise_level): + """ + Discovers the KdV equation using the EPDE framework. + + This method automates the process of identifying the Korteweg-de Vries (KdV) + equation from data. It leverages the EPDE framework to explore potential + equation structures that best represent the provided data, incorporating + noise handling, custom token definitions, and model fitting. The goal is to + uncover the underlying dynamics of the KdV equation from data, providing + a symbolic representation of the system's behavior. + + Args: + foldername: The name of the folder containing the data.csv file and the + pretrained PINN model. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EPDE search object, containing the discovered + equation and related information. + """ grid, data = kdv_data(os.path.join(foldername, 'data.csv')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) @@ -299,6 +517,22 @@ def kdv_discovery(foldername, noise_level): def kdv_h_discovery(foldername, noise_level): + """ + Discovers the governing equation for the KdV equation from noisy data using EPDE. + + This method applies the EPDE framework to identify the Korteweg-de Vries (KdV) equation + from noisy data. It involves loading data, adding noise, preprocessing it using finite differences, + and then searching for the equation that best represents the data's dynamics. The goal is to + automatically learn the underlying equation without relying on specific prior assumptions + about its form, showcasing EPDE's ability to extract governing equations from complex datasets. + + Args: + foldername: The name of the folder containing the data. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations. + """ grid, data = kdv_data_h(os.path.join(foldername, 'data_kdv_homogen.npy')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) @@ -355,6 +589,24 @@ def kdv_h_discovery(foldername, noise_level): return epde_search_obj def kdv_sga_discovery(foldername, noise_level): + """ + Discovers the KdV equation using the EPDE framework with SGA. + + This method leverages the EPDE framework's evolutionary algorithms to + identify the KdV equation from data. It prepares the data, defines the + search space with custom tokens relevant to the KdV equation, and then + employs a Sequential Genetic Algorithm (SGA) to explore potential equation + structures. The discovered equations are then presented and visualized. + This automated discovery process helps in understanding the underlying + dynamics of the system by finding the best-fit differential equation. + + Args: + foldername: The name of the folder containing the data and pretrained PINN. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EPDE search object. + """ grid, data = kdv_data_sga(os.path.join(foldername, 'Kdv.mat')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) @@ -413,6 +665,22 @@ def kdv_sga_discovery(foldername, noise_level): def kdv_sindy_discovery(foldername, noise_level): + """ + Performs KdV SINDy discovery using EPDE. + + This method leverages EPDE to identify the underlying KdV equation from noisy data. + It automates the equation discovery process by setting up an EPDE search, defining + relevant equation terms (including custom trigonometric functions), and fitting the model + to the provided data. The goal is to find the equation that best represents the + system's dynamics, even in the presence of noise. + + Args: + foldername: The name of the folder containing the data and pretrained PINN. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations. + """ grid, data = kdv_sindy_data(os.path.join(foldername, 'kdv_sindy.mat')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) diff --git a/projects/pic/data/lorenz/__init__.py b/projects/pic/data/lorenz/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/lorenz/lorenz.py b/projects/pic/data/lorenz/lorenz.py index b9ab07e3..90416057 100644 --- a/projects/pic/data/lorenz/lorenz.py +++ b/projects/pic/data/lorenz/lorenz.py @@ -25,6 +25,18 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) from a file. + + This function attempts to load a previously trained PINN model, allowing the evolutionary process to start from a potentially good initial guess, + reducing the computational cost of training from scratch. If no pre-trained model is found, the process continues by retraining the ANN approximation. + + Args: + ann_filename: The filename of the pickled ANN data. + + Returns: + The loaded ANN data if the file exists, otherwise None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +47,44 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This function perturbs the data by adding Gaussian noise scaled by the data's standard deviation and a specified noise level. + This helps to evaluate the robustness of equation discovery algorithms when dealing with imperfect or noisy data. + + Args: + data (np.ndarray): The input data to which noise will be added. + noise_level (float): The standard deviation of the noise, expressed as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which one better represents the underlying dynamics of the system. + + It translates both the correct and incorrect symbolic equations into a + form suitable for comparison, applies a fitting operator to estimate the coefficients, and then + assesses their stability. The equation with more stable coefficients is considered a better + representation of the system's dynamics. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the coefficient stability of the correct equation is + less than that of the incorrect equation for all variables, + False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +113,24 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound operator by setting sub-operators for sparsity and coefficient calculation. + + This method configures the provided compound operator by setting its + sub-operators, which are essential for determining the equation's structure + and coefficients. It then maps the operator between gene and chromosome levels + based on a fitness calculation condition, ensuring that the equation discovery + process is aligned with the evolutionary algorithm's search strategy. This + preparation is crucial for effectively exploring the space of possible equations + and identifying those that best fit the observed data. + + Args: + fitness_operator (CompoundOperator): The compound operator to prepare. + operator_params (dict): Parameters to be set for the fitness operator. + + Returns: + CompoundOperator: The modified fitness operator with prepared sub-operators and level mapping. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +147,20 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def lorenz_discovery(noise_level): + """ + Discovers the Lorenz system equations using the EPDE framework. + + This method leverages the EPDE search algorithm to identify the governing equations of the Lorenz attractor directly from data. + It automates the equation discovery process by exploring a space of potential equation structures defined by trigonometric and grid tokens. + The method preprocesses the data, configures the search space, sets up the EPDE search object with specific parameters, and then performs the search. + The goal is to find a balance between model complexity and accuracy in representing the underlying dynamics. + + Args: + noise_level: This parameter is not used in the function. + + Returns: + EpdeSearch: The trained EPDE search object, containing the discovered equations. + """ t_file = os.path.join(os.path.dirname( __file__ ), 't.npy') t = np.load(t_file) data_file = os.path.join(os.path.dirname(__file__), 'lorenz.npy') diff --git a/projects/pic/data/lv/__init__.py b/projects/pic/data/lv/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/lv/lv.py b/projects/pic/data/lv/lv.py index ffb20955..ea93cbe2 100644 --- a/projects/pic/data/lv/lv.py +++ b/projects/pic/data/lv/lv.py @@ -24,6 +24,18 @@ import scipy.io as scio def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) from a file. + + This function attempts to load a previously trained PINN model, allowing the evolutionary process to start from a potentially good initial guess, + reducing the computational cost of training from scratch. If no pre-trained model is found, the process continues by retraining the ANN approximation. + + Args: + ann_filename: The filename of the pickled ANN data. + + Returns: + The loaded ANN data if the file exists, otherwise None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -34,12 +46,42 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This helps to evaluate the robustness of discovered differential equations when data is imperfect. + + Args: + data (np.ndarray): The input data to add noise to. + noise_level (float): The level of noise to add, as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to assess which one better represents the underlying dynamics. + + This method translates symbolic equations into a comparable format, applies a fitting procedure, + and then evaluates their coefficient stability. The comparison helps determine which equation + more accurately captures the system's behavior. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the coefficient stability of the correct equation is + less than that of the incorrect equation for all variables, + False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -68,6 +110,23 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator with necessary sub-operators. + + This method configures the sparsity and coefficient calculation sub-operators, + and sets them within the provided fitness operator. It also maps the operator + between gene and chromosome levels based on a fitness calculation condition. + This step is crucial for ensuring that the evolutionary process can effectively + evaluate the fitness of candidate equations by considering both their sparsity + and the accuracy of their coefficients. + + Args: + fitness_operator (CompoundOperator): The compound fitness operator to prepare. + operator_params (dict): A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The modified fitness operator with prepared sub-operators. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -84,6 +143,23 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def lv_discovery(noise_level): + """ + Discovers the Lotka-Volterra system's governing equations using the EPDE framework. + + This method automates the identification of differential equations that describe the dynamics + of the Lotka-Volterra system. By loading system data and configuring the EPDE search object + with specific parameters, it performs an equation discovery process, aiming to find the + equations that best represent the system's behavior. The discovered equations are then printed + to provide insights into the underlying dynamics. This approach allows for automated modeling + and analysis of the system's behavior directly from data. + + Args: + noise_level: This parameter is not used in the method. + + Returns: + EpdeSearch: The configured EpdeSearch object after the fitting process, containing the + discovered equations and related information. + """ t_file = os.path.join(os.path.dirname( __file__ ), 't_20.npy') t = np.load(t_file) data_file = os.path.join(os.path.dirname(__file__), 'data_20.npy') diff --git a/projects/pic/data/ode/__init__.py b/projects/pic/data/ode/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/ode/ode.py b/projects/pic/data/ode/ode.py index 66cf2ab7..4ed8f89c 100644 --- a/projects/pic/data/ode/ode.py +++ b/projects/pic/data/ode/ode.py @@ -25,6 +25,19 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pretrained Physics-Informed Neural Network (PINN) model from a file. + + This function attempts to load a previously trained PINN model from disk. + Loading a pre-trained model can save significant time by avoiding retraining, + especially when exploring different equation structures or refining existing models. + + Args: + ann_filename (str): The filename of the pickled ANN model. + + Returns: + object: The loaded ANN model if the file exists; otherwise, returns None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +48,43 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This helps to evaluate the robustness of discovered differential equations when data is imperfect. + + Args: + data (np.ndarray): The input data to add noise to. + noise_level (float): The level of noise to add, as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which one better represents the underlying dynamics of the system. + + It translates both the correct and incorrect symbolic equations into a + form suitable for comparison, applies a fitting operator to estimate the coefficients, and then + assesses their stability. The equation with more stable coefficients is considered a better + representation of the system's dynamics. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the coefficient stability of the correct equation is + less than that of the incorrect equation for all variables, + False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +113,23 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator by configuring its sub-operators for sparsity and coefficient calculation. + + This method instantiates and configures the sparsity and coefficient calculation sub-operators, + then sets them within the provided compound fitness operator. It also maps the operator + between gene and chromosome levels based on a condition that checks if the fitness has already been calculated. + This ensures that the fitness calculation is performed efficiently and only when necessary during the evolutionary process + of discovering differential equations. + + Args: + fitness_operator (CompoundOperator): The compound fitness operator to prepare. + operator_params (dict): A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The prepared compound fitness operator with configured + sub-operators and level mapping. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +146,22 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def ODE_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the ability to identify a known 2nd order ODE from data. + + This test evaluates whether the framework can rediscover the equation + x'' + sin(2t) x' + 4 x = 1.5 t, represented as g1 x'' + g2 x' + g3 x = g4, + from a dataset generated by its solution. This serves as a validation + of the core equation discovery process. + + Args: + operator: CompoundOperator object, responsible for equation manipulation. + foldername: The name of the folder containing the data files ('ode_data.npy' and 'ode_0_ann.pickle'). + noise_level: The level of noise to add to the data, simulating real-world imperfections. + + Returns: + bool: True if the discovered symbolic equation matches the expected equation, indicating successful identification, False otherwise. + """ # Test scenario to evaluate performance on simple 2nd order ODE # x'' + sin(2t) x' + 4 x = 1.5 t, written as $g_{1} x'' + g_{2} x' + g_{3} x = g_{4} # g1 = lambda x: 1. @@ -124,6 +201,22 @@ def ODE_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def ODE_discovery(foldername, noise_level): + """ + Discovers the underlying ODE from noisy data. + + This method orchestrates the search for a differential equation that best describes the provided data, even when the data is corrupted by noise. + It prepares the data, defines the possible equation components, and then employs an optimization algorithm to find the equation that best fits the data. + This is done to automatically identify the mathematical relationships governing the observed system's behavior. + + Args: + foldername: The name of the folder containing the 'ode_data.npy' file + and the 'ode_0_ann.pickle' file, which store the data and a pretrained neural network, respectively. + noise_level: The level of noise added to the data, influencing the search's robustness. + + Returns: + EpdeSearch: The trained EPDE search object, containing the discovered + equations and related information, allowing for further analysis and validation of the identified model. + """ step = 0.05 steps_num = 320 t = np.arange(start=0., stop=step * steps_num, step=step) @@ -165,6 +258,23 @@ def ODE_discovery(foldername, noise_level): return epde_search_obj def ODE_simple_discovery(foldername, noise_level): + """ + Performs a simple ODE discovery using the EPDE framework. + + This method demonstrates the core functionality of EPDE by discovering an ordinary + differential equation (ODE) from a synthetically generated dataset. It showcases + the process of defining the search space using token sets, configuring the EPDE + search object with appropriate parameters, and visualizing the discovered solutions. + The goal is to find a balance between model complexity and accuracy in representing + the underlying dynamics of the system. + + Args: + foldername: The name of the folder to store results. + noise_level: The level of noise to add to the data (currently unused in the provided code). + + Returns: + EpdeSearch: The configured and executed EpdeSearch object, containing the discovered equations and related information. + """ C = 1.3 t = np.linspace(0, 4 * np.pi, 200) x = np.sin(t) + C * np.cos(t) diff --git a/projects/pic/data/pde_compound/__init__.py b/projects/pic/data/pde_compound/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/pde_compound/pde_compound.py b/projects/pic/data/pde_compound/pde_compound.py index f8829afe..b2a90bfb 100644 --- a/projects/pic/data/pde_compound/pde_compound.py +++ b/projects/pic/data/pde_compound/pde_compound.py @@ -26,24 +26,42 @@ class PDEComparator: - """Class for comparing PDE equations and their fitness.""" + """ + Class for comparing PDE equations and their fitness. + """ + def __init__(self, fitness_operator: CompoundOperator): + """ + Initializes the FitnessProportionateSelection object. + + This class implements fitness proportionate selection, also known as roulette wheel selection. + It selects individuals from a population based on their fitness, where the probability of + selection is proportional to their fitness value. This ensures that equation structures with better fitness (i.e., those that better describe the data) are more likely to be selected for the next generation of the evolutionary process. + + Args: + fitness_operator: The operator used to calculate the fitness of individuals. + + Returns: + None + """ self.fitness_operator = fitness_operator def compare(self, correct_symbolic: str, incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u']) -> bool: """ - Compare two equations based on their fitness values and coefficient stability. - - Args: - correct_symbolic: Symbolic form of the correct equation - incorrect_symbolic: Symbolic form of the incorrect equation - search_obj: EpdeSearch object containing the token pool - all_vars: List of variable names to compare - - Returns: - bool: True if correct equation has better stability for all variables + Compares two symbolic equations to determine which better represents the underlying dynamics based on coefficient stability. + + This comparison helps refine the search for the most accurate equation by favoring those with more stable coefficients across different variables. + + Args: + correct_symbolic: Symbolic form of the equation presumed to be more accurate. + incorrect_symbolic: Symbolic form of the equation presumed to be less accurate. + search_obj: EpdeSearch object containing the token pool and search parameters. + all_vars: List of variable names to compare coefficient stability. Defaults to ['u']. + + Returns: + bool: True if the 'correct' equation exhibits better coefficient stability than the 'incorrect' equation for all specified variables, indicating a potentially better representation of the underlying dynamics. """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} @@ -58,7 +76,21 @@ def compare(self, correct_symbolic: str, incorrect_symbolic: str, def _prepare_equation(self, eq_symbolic: str, search_obj: EpdeSearch, all_vars: List[str], metaparams: dict): - """Translate symbolic equation and set its parameters.""" + """ + Transforms a symbolic equation into an executable form and configures its variables for the equation discovery process. This involves translating the equation string into a computational representation and associating each variable with relevant search parameters. + + Args: + eq_symbolic (str): The symbolic representation of the equation. + search_obj (EpdeSearch): The EPDE search object containing the token pool and search configurations. + all_vars (List[str]): A list of all variable names present in the equation. + metaparams (dict): A dictionary of metaparameters to be assigned to each variable. + + Returns: + Equation: The translated equation object with configured variables. + + Why: + This step is crucial for converting the symbolic equation into a format that can be evaluated and optimized within the EPDE framework. By setting the metaparameters for each variable, the search algorithm can effectively explore the solution space and identify the best-fitting equation. + """ equation = translate_equation(eq_symbolic, search_obj.pool, all_vars=all_vars) for var in all_vars: equation.vals[var].main_var_to_explain = var @@ -67,12 +99,31 @@ def _prepare_equation(self, eq_symbolic: str, search_obj: EpdeSearch, return equation def _evaluate_equations(self, *equations): - """Apply fitness operator to all provided equations.""" + """ + Evaluate the fitness of the provided equations using the configured fitness operator. This step is crucial for assessing how well each equation represents the underlying dynamics of the system, guiding the evolutionary search towards better models. + + Args: + *equations: One or more equation objects to be evaluated. + + Returns: + None + """ for eq in equations: self.fitness_operator.apply(eq, {}) def _print_comparison(self, correct_eq, incorrect_eq, all_vars): - """Print comparison metrics for the equations.""" + """ + Prints comparison of fitness value, coefficients stability and AIC for each variable in correct and incorrect equations. + This is useful for understanding how well the discovered equation matches the data compared to other candidates. + + Args: + correct_eq (Equation): The correct equation object. + incorrect_eq (Equation): The incorrect equation object. + all_vars (list): A list of all variables. + + Returns: + None + """ print([[correct_eq.vals[var].fitness_value, incorrect_eq.vals[var].fitness_value] for var in all_vars]) print([[correct_eq.vals[var].coefficients_stability, incorrect_eq.vals[var].coefficients_stability] @@ -81,11 +132,24 @@ def _print_comparison(self, correct_eq, incorrect_eq, all_vars): class DataHandler: - """Handles data loading and preprocessing operations.""" + """ + Handles data loading and preprocessing operations. + """ + @staticmethod def load_pretrained_pinn(ann_filename: str): - """Load pretrained PINN model from file.""" + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) model from a specified file. + + This allows to reuse previously trained models, avoiding the need to retrain them from scratch, which saves computational resources and time when exploring different equation structures or refining existing models. + + Args: + ann_filename (str): The path to the file containing the serialized PINN model. + + Returns: + The loaded PINN model if the file exists; otherwise, returns None and prints a message indicating that the model will be retrained. + """ try: with open(ann_filename, 'rb') as f: return pickle.load(f) @@ -95,13 +159,31 @@ def load_pretrained_pinn(ann_filename: str): @staticmethod def add_noise(data: np.ndarray, noise_level: float) -> np.ndarray: - """Add Gaussian noise to the input data.""" + """ + Add Gaussian noise to the input data to simulate real-world measurement errors or to augment the dataset for more robust equation discovery. + + Args: + data (np.ndarray): The input data to which noise will be added. + noise_level (float): The standard deviation of the Gaussian noise, expressed as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added Gaussian noise. + """ noise_amplitude = noise_level * 0.01 * np.std(data) return noise_amplitude * np.random.normal(size=data.shape) + data @staticmethod def load_pc_data(filename: str, shape: int = 80) -> Tuple[List[np.ndarray], np.ndarray]: - """Load and prepare PC data with corresponding grid.""" + """ + Load PC data from a file, create a corresponding grid, and return both for usage in equation discovery. + + Args: + filename (str): The path to the .npy file containing the PC data. + shape (int): The shape of the grid to be created (default: 80). + + Returns: + Tuple[List[np.ndarray], np.ndarray]: A tuple containing the grid and the loaded PC data. The grid is a list of numpy arrays representing the coordinate axes. + """ data = np.load(filename) nx, nt = 100, 251 x = np.linspace(1, 2, nx) @@ -111,17 +193,42 @@ def load_pc_data(filename: str, shape: int = 80) -> Tuple[List[np.ndarray], np.n class OperatorFactory: - """Factory for creating and configuring fitness operators.""" + """ + Factory for creating and configuring fitness operators. + """ + @staticmethod def create_fitness_operator(operator_class, params: dict) -> CompoundOperator: - """Create and configure a fitness operator.""" + """ + Create and configure a fitness operator. + + This operator is a crucial part of the equation discovery process, responsible for evaluating the fitness of candidate equations based on how well they describe the observed data. + + Args: + operator_class: The class of the fitness operator to create. + params (dict): A dictionary of parameters to configure the operator. These parameters define the specific settings and options used during the fitness evaluation process. + + Returns: + CompoundOperator: A configured fitness operator, ready to be used in the evolutionary search for differential equations. + """ fitness_operator = operator_class(list(params.keys())) return OperatorFactory._prepare_suboperators(fitness_operator, params) @staticmethod def _prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: - """Configure suboperators for the fitness operator.""" + """ + Configures the fitness operator with necessary sub-operators and applies level mapping. + + The fitness operator requires specific sub-operators for calculating sparsity and coefficients. This method sets these sub-operators and then maps the fitness operator between the gene and chromosome levels. This mapping ensures that the fitness evaluation is correctly applied at the appropriate level of the evolutionary process, contributing to the overall goal of discovering accurate differential equation models. + + Args: + fitness_operator (CompoundOperator): The fitness operator to configure. + operator_params (dict): Parameters for the fitness operator. + + Returns: + CompoundOperator: The configured fitness operator after level mapping. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -142,15 +249,45 @@ def _prepare_suboperators(fitness_operator: CompoundOperator, operator_params: d class PCExperiment: - """Class for conducting PC (PDE Compound) experiments.""" + """ + Class for conducting PC (PDE Compound) experiments. + """ + def __init__(self, foldername: str, fitness_operator: CompoundOperator = None): + """ + Initializes a PCExperiment instance, preparing the environment for equation discovery within a specified folder. + + The initialization process sets up the necessary components for conducting experiments, + including the fitness evaluation mechanism used to assess candidate equations. + + Args: + foldername (str): The name of the folder where experiment data and results will be stored. + fitness_operator (CompoundOperator, optional): The operator used to evaluate the fitness of candidate equations. + Defaults to None. If None, no fitness evaluation is performed during the experiment setup. + + Returns: + None + + Class Fields: + foldername (str): The name of the folder associated with this experiment. + fitness_operator (CompoundOperator): The fitness operator used for evaluating equations. + comparator (PDEComparator): The comparator object, initialized based on the fitness operator, used to compare different equations. + """ self.foldername = foldername self.fitness_operator = fitness_operator self.comparator = PDEComparator(fitness_operator) if fitness_operator else None def run_test(self, noise_level: int = 0) -> bool: - """Run the PC test scenario comparing known correct and incorrect equations.""" + """ + Compares the performance of the equation discovery process with a known correct equation and a deliberately incorrect equation. This comparison helps assess the robustness and accuracy of the equation discovery algorithm. + + Args: + noise_level (int, optional): The level of noise added to the data. Defaults to 0. + + Returns: + bool: True if the correct equation is identified as superior to the incorrect one, False otherwise. + """ correct_eq = '1.0 * du/dx1{power: 2.0} + 1.0 * d^2u/dx1^2{power: 1.0} * u{power: 1.0} + \ 0.0 = du/dx0{power: 1.0}' incorrect_eq = '0.04 * d^2u/dx1^2{power: 1} + 0. = d^2u/dx0^2{power: 1}' @@ -191,7 +328,17 @@ def run_test(self, noise_level: int = 0) -> bool: return self.comparator.compare(correct_eq, incorrect_eq, search_obj) def run_discovery(self, noise_level: int = 0) -> EpdeSearch: - """Run PC equation discovery experiment.""" + """ + Discovers a differential equation from data using a combination of finite difference preprocessing, custom token definitions, and multi-objective evolutionary optimization. + + This method sets up and executes the equation discovery process. It involves loading data, adding noise, defining custom tokens representing known relationships, and configuring the evolutionary search to find the equation that best fits the data. The method leverages finite difference approximations for derivative calculations and custom tokens to represent specific functional forms, enhancing the search's ability to identify relevant equation structures. The evolutionary algorithm explores the space of possible equations, balancing model complexity and accuracy to identify the governing dynamics. + + Args: + noise_level (int, optional): The level of noise to add to the data. Defaults to 0. + + Returns: + EpdeSearch: The EpdeSearch object containing the results of the equation discovery process. + """ grid, data = DataHandler.load_pc_data(os.path.join(self.foldername, 'PDE_compound.npy')) noised_data = DataHandler.add_noise(data, noise_level) dimensionality = data.ndim - 1 @@ -268,7 +415,17 @@ def run_discovery(self, noise_level: int = 0) -> EpdeSearch: def main(): - """Main execution function.""" + """ + Initializes the environment, configures the fitness operator, and executes the equation discovery experiment. + + This function sets up the necessary components for discovering differential equations, including checking CUDA availability, initializing the fitness operator with specified parameters, and configuring the experiment environment. It then runs the discovery process to identify potential equation candidates. + + Args: + None + + Returns: + None + """ print("CUDA available:", torch.cuda.is_available()) # Initialize fitness operator diff --git a/projects/pic/data/pde_divide/__init__.py b/projects/pic/data/pde_divide/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/pde_divide/pde_divide.py b/projects/pic/data/pde_divide/pde_divide.py index 8ddd523a..aabffa18 100644 --- a/projects/pic/data/pde_divide/pde_divide.py +++ b/projects/pic/data/pde_divide/pde_divide.py @@ -21,16 +21,51 @@ class PDEAnalysis: - """Base class for PDE analysis operations.""" + """ + Base class for PDE analysis operations. + """ + def __init__(self, foldername: str, fitness_operator: CompoundOperator = None): + """ + Initializes the analysis environment for a given dataset. + + This setup prepares the necessary components for comparing and evaluating potential differential equation models against the data stored in the specified folder. The fitness operator defines how well a model fits the data, and the comparator uses this operator to rank different models. + + Args: + foldername (str): The name of the folder containing the dataset to be analyzed. + fitness_operator (CompoundOperator, optional): The operator used to evaluate the fitness of candidate equations. Defaults to None. + + Returns: + None + + Class Fields: + foldername (str): The name of the folder containing the dataset. + fitness_operator (CompoundOperator): The fitness operator used for equation evaluation. + comparator (PDEComparator): An object responsible for comparing and ranking candidate equations based on their fitness. + """ self.foldername = foldername self.fitness_operator = fitness_operator self.comparator = PDEComparator(fitness_operator) if fitness_operator else None @staticmethod def load_data(filename: str) -> Tuple[List[np.ndarray], np.ndarray]: - """Load and prepare data with corresponding grid.""" + """ + Load data from a file and generate corresponding space-time grid. + + The data is assumed to represent a solution of a differential equation + on a 2D spatial-temporal domain. The method prepares the data and + creates a grid representing the domain, which is essential for + further analysis and equation discovery. + + Args: + filename (str): The name of the file containing the data ('.npy' format). + + Returns: + Tuple[List[np.ndarray], np.ndarray]: A tuple containing: + - grids (List[np.ndarray]): A list of two numpy arrays representing the spatial and temporal grids. + - data (np.ndarray): The loaded data from the file. + """ data = np.load(filename) nx, nt = 100, 251 x = np.linspace(1, 2, nx) @@ -40,7 +75,15 @@ def load_data(filename: str) -> Tuple[List[np.ndarray], np.ndarray]: @staticmethod def create_custom_tokens(grid: List[np.ndarray]) -> Tuple[CacheStoredTokens, CustomTokens]: - """Create custom grid and trigonometric tokens.""" + """ + Creates the grid and trigonometric tokens required for symbolic regression. These tokens define the basis functions used to represent candidate equation terms. The grid tokens represent the independent variables, while the trigonometric tokens introduce non-polynomial terms, enriching the search space for potential solutions. + + Args: + grid (List[np.ndarray]): A list containing the grid points for the independent variables (e.g., time and space). + + Returns: + Tuple[CacheStoredTokens, CustomTokens]: A tuple containing the grid tokens and the trigonometric tokens. + """ grid_tokens = CacheStoredTokens( token_type='grid', token_labels=['t', 'x'], @@ -68,10 +111,24 @@ def create_custom_tokens(grid: List[np.ndarray]) -> Tuple[CacheStoredTokens, Cus class PDEDivideExperiment(PDEAnalysis): - """Class for conducting PDE divide experiments.""" + """ + Class for conducting PDE divide experiments. + """ + def run_test(self, noise_level: int = 0) -> bool: - """Run the PDE divide test scenario comparing known correct and incorrect equations.""" + """ + Compares the performance of the equation discovery process on a known correct equation against an incorrect one. + + This test evaluates the ability of the EPDE framework to distinguish between a true governing equation and a false one. + It assesses whether the search algorithm correctly identifies the known equation within the solution space. + + Args: + noise_level (int): The level of noise to add to the data. Defaults to 0. + + Returns: + bool: True if the correct equation is preferred over the incorrect one, False otherwise. + """ correct_eq = ( '1.0 * du/dx1{power: 1.0} + 0.25 * d^2u/dx1^2{power: 1.0} * x{power: 1.0} + ' '0.0 = du/dx0{power: 1.0} * x{power: 1.0}' @@ -114,7 +171,18 @@ def run_test(self, noise_level: int = 0) -> bool: return self.comparator.compare(correct_eq, incorrect_eq, search_obj) def run_discovery(self, noise_level: int = 0) -> EpdeSearch: - """Run PDE divide equation discovery experiment.""" + """ + Discovers a differential equation that describes the provided dataset by exploring combinations of mathematical terms and evaluating their fit to the data. This method leverages evolutionary algorithms to efficiently search the space of possible equations. + + Args: + noise_level (int, optional): The level of noise to add to the data. Defaults to 0. + + Returns: + EpdeSearch: The `EpdeSearch` object containing the results of the equation discovery process. The object stores the discovered equations and related information. + + Why: + This method automates the process of finding a differential equation that best represents the underlying dynamics of the provided data. By using evolutionary algorithms, it efficiently explores a wide range of possible equation structures, identifying the ones that accurately capture the relationships within the data. + """ grid, data = self.load_data(os.path.join(self.foldername, 'PDE_divide.npy')) noised_data = DataHandler.add_noise(data, noise_level) dimensionality = data.ndim - 1 @@ -172,11 +240,24 @@ def run_discovery(self, noise_level: int = 0) -> EpdeSearch: class DataHandler: - """Handles data loading and preprocessing operations.""" + """ + Handles data loading and preprocessing operations. + """ + @staticmethod def load_pretrained_pinn(ann_filename: str): - """Load pretrained PINN model from file.""" + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) model from a specified file. + + This allows to reuse previously trained models, avoiding the need to retrain them from scratch, which saves computational resources and time when exploring different equation structures or refining existing models. + + Args: + ann_filename (str): The path to the file containing the serialized PINN model. + + Returns: + The loaded PINN model if the file exists; otherwise, returns None and prints a message indicating that the model will be retrained. + """ try: with open(ann_filename, 'rb') as f: return pickle.load(f) @@ -186,21 +267,66 @@ def load_pretrained_pinn(ann_filename: str): @staticmethod def add_noise(data: np.ndarray, noise_level: float) -> np.ndarray: - """Add Gaussian noise to the input data.""" + """ + Add Gaussian noise to the input data to simulate real-world measurement errors or to augment the dataset for more robust equation discovery. + + Args: + data (np.ndarray): The input data to which noise will be added. + noise_level (float): The standard deviation of the Gaussian noise, expressed as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added Gaussian noise. + """ noise_amplitude = noise_level * 0.01 * np.std(data) return noise_amplitude * np.random.normal(size=data.shape) + data class PDEComparator: - """Class for comparing PDE equations and their fitness.""" + """ + Class for comparing PDE equations and their fitness. + """ + def __init__(self, fitness_operator: CompoundOperator): + """ + Initializes the FitnessProportionateSelection object. + + This class implements fitness proportionate selection, where the + probability of selecting an individual is proportional to its fitness. + This initialization is crucial for the evolutionary process, ensuring + that individuals (potential equation structures) are selected for + reproduction based on how well they fit the observed data. + + Args: + fitness_operator (CompoundOperator): The operator used to calculate + the fitness of an individual, quantifying how well a candidate + equation matches the data. + + Returns: + None. + + Class Fields: + fitness_operator (CompoundOperator): The operator used to calculate + the fitness of an individual. + """ self.fitness_operator = fitness_operator def compare(self, correct_symbolic: str, incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u']) -> bool: """ - Compare two equations based on their fitness values and coefficient stability. + Compares two symbolic equations to determine which better represents the underlying dynamics of the system. + + This comparison is based on the stability of their coefficients after numerical evaluation. + The equation with more stable coefficients is considered to be a better representation. + + Args: + correct_symbolic (str): Symbolic representation of the equation considered to be more correct. + incorrect_symbolic (str): Symbolic representation of the equation considered to be less correct. + search_obj (EpdeSearch): The search object containing the data and settings for equation discovery. + all_vars (List[str], optional): List of dependent variables. Defaults to ['u']. + + Returns: + bool: True if the 'correct' equation exhibits more stable coefficients than the 'incorrect' one across all variables, False otherwise. """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} @@ -215,7 +341,21 @@ def compare(self, correct_symbolic: str, incorrect_symbolic: str, def _prepare_equation(self, eq_symbolic: str, search_obj: EpdeSearch, all_vars: List[str], metaparams: dict): - """Translate symbolic equation and set its parameters.""" + """ + Transforms a symbolic equation into an executable form and configures its variables for the equation discovery process. This involves translating the equation string into a computational representation and associating each variable with relevant search parameters. + + Args: + eq_symbolic (str): The symbolic representation of the equation. + search_obj (EpdeSearch): The EPDE search object containing the token pool and search configurations. + all_vars (List[str]): A list of all variable names present in the equation. + metaparams (dict): A dictionary of metaparameters to be assigned to each variable. + + Returns: + Equation: The translated equation object with configured variables. + + Why: + This step is crucial for converting the symbolic equation into a format that can be evaluated and optimized within the EPDE framework. By setting the metaparameters for each variable, the search algorithm can effectively explore the solution space and identify the best-fitting equation. + """ equation = translate_equation(eq_symbolic, search_obj.pool, all_vars=all_vars) for var in all_vars: equation.vals[var].main_var_to_explain = var @@ -224,12 +364,32 @@ def _prepare_equation(self, eq_symbolic: str, search_obj: EpdeSearch, return equation def _evaluate_equations(self, *equations): - """Apply fitness operator to all provided equations.""" + """ + Evaluate the fitness of the provided equations using the configured fitness operator. This step is crucial for assessing how well each equation represents the underlying dynamics of the system, guiding the evolutionary search towards better models. + + Args: + *equations: One or more equations to be evaluated. + + Returns: + None. The fitness is applied to the equations in place. + """ for eq in equations: self.fitness_operator.apply(eq, {}) def _print_comparison(self, correct_eq, incorrect_eq, all_vars): - """Print comparison metrics for the equations.""" + """ + Prints comparison metrics of the discovered and the ground truth equations. + It compares fitness values, coefficient stability, and AIC scores for each variable in both equations. + This helps to evaluate how well the discovered equation approximates the ground truth equation based on multiple criteria. + + Args: + correct_eq (Equation): The ground truth equation. + incorrect_eq (Equation): The discovered equation. + all_vars (list): A list of all variables in the equations. + + Returns: + None + """ print([[correct_eq.vals[var].fitness_value, incorrect_eq.vals[var].fitness_value] for var in all_vars]) print([[correct_eq.vals[var].coefficients_stability, incorrect_eq.vals[var].coefficients_stability] @@ -238,17 +398,42 @@ def _print_comparison(self, correct_eq, incorrect_eq, all_vars): class OperatorFactory: - """Factory for creating and configuring fitness operators.""" + """ + Factory for creating and configuring fitness operators. + """ + @staticmethod def create_fitness_operator(operator_class, params: dict) -> CompoundOperator: - """Create and configure a fitness operator.""" + """ + Create and configure a fitness operator. + + This operator is a crucial part of the equation discovery process, responsible for evaluating the fitness of candidate equations based on the provided data. + + Args: + operator_class: The class of the fitness operator to create. + params (dict): A dictionary of parameters to configure the operator. These parameters define the specific settings and configurations used during the fitness evaluation. + + Returns: + CompoundOperator: A configured fitness operator, ready to be used in the evolutionary search process. The returned operator encapsulates the logic for assessing how well a given equation fits the observed data. + """ fitness_operator = operator_class(list(params.keys())) return OperatorFactory._prepare_suboperators(fitness_operator, params) @staticmethod def _prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: - """Configure suboperators for the fitness operator.""" + """ + Configures the fitness operator with necessary sub-operators and applies level mapping. + + The fitness operator requires specific sub-operators for calculating sparsity and coefficients. This method sets these sub-operators and then maps the fitness operator between the gene and chromosome levels. This mapping ensures that the fitness evaluation is correctly applied at the appropriate level of the evolutionary process, contributing to the overall goal of discovering accurate differential equation models. + + Args: + fitness_operator (CompoundOperator): The fitness operator to configure. + operator_params (dict): Parameters for the fitness operator. + + Returns: + CompoundOperator: The configured fitness operator after level mapping. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -269,7 +454,17 @@ def _prepare_suboperators(fitness_operator: CompoundOperator, operator_params: d def main(): - """Main execution function.""" + """ + Initializes the environment, configures the fitness operator, and executes the equation discovery process. + + This function sets up the necessary components for discovering differential equations, including checking CUDA availability, initializing the fitness operator with specified parameters, and configuring the experiment environment. It then orchestrates the execution of the equation discovery process. + + Args: + None + + Returns: + None + """ print("CUDA available:", torch.cuda.is_available()) # Initialize fitness operator diff --git a/projects/pic/data/vdp/__init__.py b/projects/pic/data/vdp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/vdp/vdp.py b/projects/pic/data/vdp/vdp.py index 106de2f2..8e51fca0 100644 --- a/projects/pic/data/vdp/vdp.py +++ b/projects/pic/data/vdp/vdp.py @@ -25,6 +25,18 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) from a file. + + This function attempts to load a previously trained PINN model, allowing the evolutionary process to start from a potentially good initial guess, + reducing the computational cost of training from scratch. If no pre-trained model is found, the process continues by retraining the ANN approximation. + + Args: + ann_filename: The filename of the pickled ANN data. + + Returns: + The loaded ANN data if the file exists, otherwise None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +47,44 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This helps to evaluate the robustness of discovered differential equations when data is imperfect. + + Args: + data (np.ndarray): The input data to add noise to. + noise_level (float): The level of noise to add, as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which better represents the underlying dynamics. + + This method translates both equations into a comparable form, applies a fitting + operator to assess their accuracy, and then evaluates their coefficient stability. + The comparison helps in identifying the equation that more robustly captures the + system's behavior. + + Args: + correct_symbolic (str): The correct symbolic equation as a string. + eq_incorrect_symbolic (str): The incorrect symbolic equation as a string. + search_obj (EpdeSearch): An EpdeSearch object containing the search pool and + necessary data for equation fitting. + all_vars (List[str], optional): A list of variable names to consider. Defaults to ['u']. + + Returns: + bool: True if the correct equation exhibits better coefficient stability + than the incorrect equation across all variables, indicating a superior + representation of the system's dynamics. False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +113,22 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator with necessary sub-operators for equation discovery. + + This method initializes and configures the sparsity and coefficient calculation sub-operators, + then integrates them into the provided compound fitness operator. The operator is mapped + between gene and chromosome levels based on a condition that checks if the fitness has + already been calculated. This ensures that the fitness calculation is performed only when + necessary during the evolutionary process of discovering differential equations. + + Args: + fitness_operator: The compound fitness operator to prepare. + operator_params: A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The prepared compound fitness operator, ready for use in the equation discovery process. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +145,23 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def VdP_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance on the Van-der-Pol oscillator. + + This method evaluates the ability to identify the Van-der-Pol oscillator equation from data. + It sets up a test scenario by loading data, adding noise, defining symbolic and incorrect equations, + creating tokens representing potential equation terms, and then runs a search to compare identified equations + against the known symbolic form. This helps assess the framework's capability to rediscover governing equations + from noisy data. + + Args: + operator: The compound operator to be tested. + foldername: The name of the folder containing the data files. + noise_level: The level of noise to add to the data. Defaults to 0. + + Returns: + bool: True if the comparison of equations passes, False otherwise. + """ # u'' + E (u^2 - 1)u' + u = 0, where $\mathcal{E}$ is a positive constant (in the example we will use $\mathcal{E} = 0.2$) # Test scenario to evaluate performance on Van-der-Pol oscillator eq_vdp_symbolic = '-0.2 * u{power: 2.0} * du/dx0{power: 1.0} + 0.2 * du/dx0{power: 1.0} + -1.0 * u{power: 1.0} + -0.0 \ @@ -120,6 +197,18 @@ def VdP_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def vdp_discovery(foldername, noise_level): + """ + Discovers the governing equation of the Van der Pol oscillator from noisy data. + + This method automates the process of identifying the underlying differential equation by employing evolutionary algorithms and multi-objective optimization to find equation structures that best fit the provided data. It leverages data preprocessing, custom token definitions, and numerical solvers to achieve accurate discovery. The method is applied to the Van der Pol oscillator to demonstrate its capability in uncovering governing equations from complex systems. + + Args: + foldername: The name of the folder containing the data and pretrained model. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations and solutions. + """ step = 0.05; steps_num = 320 t = np.arange(start=0., stop=step * steps_num, step=step) diff --git a/projects/pic/data/wave/__init__.py b/projects/pic/data/wave/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/pic/data/wave/wave.py b/projects/pic/data/wave/wave.py index 62aac6b4..d08f3f06 100644 --- a/projects/pic/data/wave/wave.py +++ b/projects/pic/data/wave/wave.py @@ -25,6 +25,19 @@ def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained neural network model from a file. + + This function attempts to load a previously trained Physics-Informed Neural Network (PINN) + from the specified file. This allows to reuse already trained models, avoiding retraining + and saving computational resources when exploring different equation candidates. + + Args: + ann_filename (str): The filename of the pickled ANN data. + + Returns: + object: The loaded ANN data if the file exists; otherwise, None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -35,12 +48,43 @@ def load_pretrained_PINN(ann_filename): def noise_data(data, noise_level): + """ + Adds random noise to the input data, scaling it based on the data's standard deviation and a specified noise level. + + This helps to evaluate the robustness of discovered differential equations by simulating real-world measurement errors. + + Args: + data (np.ndarray): The input data (e.g., a time series) to which noise will be added. + noise_level (float): A percentage determining the magnitude of the noise relative to the data's standard deviation. + + Returns: + np.ndarray: The data with added Gaussian noise, having the same shape as the input data. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u', ]) -> bool: + """ + Compares two symbolic equations to determine which one better represents the underlying dynamics of the system. + + It translates both the correct and incorrect symbolic equations into a form suitable + for comparison, applies a fitting operator to estimate the coefficients, and then + assesses their stability. The equation with more stable coefficients is considered + a better representation of the system's dynamics. + + Args: + correct_symbolic: The correct symbolic equation as a string. + eq_incorrect_symbolic: The incorrect symbolic equation as a string. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the coefficient stability of the correct equation is + less than that of the incorrect equation for all variables, + False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars=all_vars) @@ -69,6 +113,25 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the compound fitness operator with necessary sub-operators. + + This method configures the provided compound operator by setting its + sub-operators for sparsity and coefficient calculation. These sub-operators + are essential for constructing the overall fitness evaluation process, ensuring + that the evolutionary algorithm can effectively explore the search space of + potential equation structures. The operator is then mapped between gene and + chromosome levels based on a fitness calculation condition to ensure proper + evaluation during the evolutionary process. This setup is crucial for + evaluating the fitness of candidate equations within the evolutionary process. + + Args: + fitness_operator (CompoundOperator): The compound fitness operator to prepare. + operator_params (dict): A dictionary of parameters for the fitness operator. + + Returns: + CompoundOperator: The prepared compound fitness operator. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -85,6 +148,23 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def wave_data(filename): + """ + Generates spatio-temporal data from a file. + + This method loads data representing a physical system from a specified file, + creates corresponding time and space grids, and returns both the grids and the loaded data. + The data is assumed to be comma-separated values. This is a crucial step + in preparing the data for equation discovery, as it establishes the domain + over which the differential equations will be defined and evaluated. + + Args: + filename: The name of the file containing the wave data. + + Returns: + tuple: A tuple containing the grids and the data. The grids are a + numpy array representing the time and space coordinates, and the + data is a numpy array loaded from the file. + """ shape = 80 # print(os.path.dirname( __file__ )) @@ -96,6 +176,24 @@ def wave_data(filename): def wave_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the equation discovery process for the wave equation. + + This method validates the equation discovery pipeline on the wave equation. + It sets up an EPDE search, preprocesses data, and checks if the discovered + equation matches the expected symbolic form. This ensures the framework + can accurately identify known relationships within a given dataset. + + Args: + operator: CompoundOperator object for equation comparison. + foldername: Path to the folder containing the data files + ('wave_sln_80.csv' and 'ann_pretrained.pickle'). + noise_level: Level of noise to add to the data. Defaults to 0. + + Returns: + bool: True if the discovered equation matches the expected symbolic + representation, False otherwise. + """ # eq_wave_symbolic = '1. * d^2u/dx1^2{power: 1} + 0. = d^2u/dx0^2{power: 1}' eq_wave_symbolic = '0.04 * d^2u/dx1^2{power: 1} + 0. = d^2u/dx0^2{power: 1}' eq_wave_incorrect = '0.04 * d^2u/dx1^2{power: 1} * du/dx0{power: 1} + 0. = d^2u/dx0^2{power: 1} * du/dx0{power: 1}' @@ -123,6 +221,23 @@ def wave_test(operator: CompoundOperator, foldername: str, noise_level: int = 0) def wave_discovery(foldername, noise_level): + """ + Performs wave equation discovery using an evolutionary search. + + This method orchestrates the search for a differential equation that describes + wave propagation. It involves loading data, adding noise to simulate real-world + conditions, setting up a preprocessor for data conditioning, and then + fitting the evolutionary search object to the prepared data. The goal is to + find the equation that best represents the underlying dynamics of the wave. + + Args: + foldername (str): The name of the folder containing the data files + ('wave_sln_80.csv' and 'ann_pretrained.pickle'). + noise_level (float): The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EPDE search object, containing the discovered equation. + """ grid, data = wave_data(os.path.join(foldername, 'wave_sln_80.csv')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, 'ann_pretrained.pickle')) diff --git a/projects/pic/derivatives.py b/projects/pic/derivatives.py index e31692fa..84791241 100644 --- a/projects/pic/derivatives.py +++ b/projects/pic/derivatives.py @@ -4,6 +4,32 @@ def kdv_data(filename): + """ + Loads data, computes time and space steps for differential equation discovery. + + This function reads data from a CSV file, transposes it to align with + the expected format (likely time series data across spatial points), + and calculates the temporal (dt) and spatial (dx) step sizes. These + step sizes are crucial for numerical differentiation and integration + schemes used in discovering the underlying differential equation. + + Args: + filename (str): The name of the CSV file containing the data. + The data is expected to be organized such that each row + represents a spatial point and each column represents a time step. + + Returns: + tuple: A tuple containing: + - data (ndarray): The processed data array. + - dt (float): The calculated time step size. + - dx (float): The calculated space step size. + + Why: + The function prepares the data and calculates essential parameters + (dt, dx) required for subsequent numerical analysis, which is a + fundamental step in identifying the governing differential equation + from the provided dataset. + """ shape = 80 data = np.loadtxt(filename, delimiter=',').T t = np.linspace(0, 1, shape + 1) @@ -13,6 +39,18 @@ def kdv_data(filename): return data, dt, dx def ac_data(filename: str): + """ + Loads simulation data from a file and determines the temporal and spatial resolution. This is a preliminary step to analyze the data and identify the underlying differential equations. The increments are needed to properly discretize the domain when comparing discovered equations with the data. + + Args: + filename (str): The name of the file containing the simulation data. + + Returns: + tuple: A tuple containing: + - data (np.ndarray): The loaded simulation data. + - dt (float): The time increment between data points. + - dx (float): The space increment between data points. + """ t = np.linspace(0., 1., 51) x = np.linspace(-1., 0.984375, 128) data = np.load(filename) @@ -21,6 +59,23 @@ def ac_data(filename: str): return data, dt, dx def darcy_data(filename: str): + """ + Loads Darcy flow data and grid spacing from a file. + + This function is used to load the precomputed solutions of the Darcy flow equation, + which serve as a testbed for evaluating the equation discovery process. The data + represents the flow field, and the grid spacing is essential for numerical + computations and comparisons. + + Args: + filename (str): The name of the file containing the Darcy flow data. + + Returns: + tuple: A tuple containing: + - data (numpy.ndarray): The Darcy flow data loaded from the file. + - dx (float): The grid spacing in the x-direction. + - dy (float): The grid spacing in the y-direction. + """ x = np.linspace(0., 1., 128) y = np.linspace(0., 1., 128) data = np.load(filename) @@ -29,6 +84,23 @@ def darcy_data(filename: str): return data, dx, dy def wave_data(filename): + """ + Loads wave data and prepares it for differential equation discovery. + + This method reads wave data from a CSV file, transposes it to the correct format, + and calculates the time and space increments necessary for subsequent + differential equation modeling. The data and increments are essential + for setting up the problem domain and numerical computations within the EPDE framework. + + Args: + filename: The name of the CSV file containing the wave data. + + Returns: + A tuple containing: + - data: A NumPy array representing the wave data. + - dt: The time increment. + - dx: The space increment. + """ shape = 80 data = np.loadtxt(filename, delimiter=',').T t = np.linspace(0, 1, shape + 1) diff --git a/projects/pic/pic_test_cases.py b/projects/pic/pic_test_cases.py index 3eed3385..da6d8fdc 100644 --- a/projects/pic/pic_test_cases.py +++ b/projects/pic/pic_test_cases.py @@ -30,6 +30,22 @@ # np.random.seed(0) def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) from a file to potentially accelerate the equation discovery process. + + This method attempts to load a pickled PINN model from the specified file. + If the file is not found, it prints a message indicating that a new model + will be trained from scratch. Loading a pre-trained model can save + computational resources by providing a good initial guess for the network's + parameters, which is especially useful when dealing with complex datasets + or high-dimensional problems. + + Args: + ann_filename: The filename of the pickled PINN model. + + Returns: + The loaded PINN model if the file is found, otherwise None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -41,6 +57,25 @@ def load_pretrained_PINN(ann_filename): def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, search_obj: EpdeSearch, all_vars: List[str] = ['u',]) -> bool: + """ + Compares two symbolic equations to determine which better represents the underlying dynamics. + + It translates both equations into a usable format, applies a fitting operator to + assess their accuracy, and then compares the coefficient stability of the variables + in both equations. This helps in identifying equations that are more robust and + reliable representations of the system. + + Args: + correct_symbolic: The symbolic representation of the correct equation. + eq_incorrect_symbolic: The symbolic representation of the incorrect equation. + search_obj: An EpdeSearch object containing the search pool. + all_vars: A list of variable names to consider (default: ['u']). + + Returns: + bool: True if the coefficient stability of all variables in the + correct equation is less than that of the incorrect equation, + False otherwise. + """ metaparams = {('sparsity', var): {'optimizable': False, 'value': 1E-6} for var in all_vars} correct_eq = translate_equation(correct_symbolic, search_obj.pool, all_vars = all_vars) @@ -66,6 +101,22 @@ def compare_equations(correct_symbolic: str, eq_incorrect_symbolic: str, def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: dict) -> CompoundOperator: + """ + Prepares the sub-operators required for the compound fitness operator. + + This method configures the sparsity and coefficient calculation sub-operators, + and sets them within the provided fitness operator. It also maps the operator + between gene and chromosome levels ensuring compatibility between the symbolic + representation of equations and the data. This setup is crucial for evaluating + the fitness of candidate equations during the evolutionary search process. + + Args: + fitness_operator: The compound fitness operator to prepare. + operator_params: A dictionary of parameters for the fitness operator. + + Returns: + The prepared compound fitness operator, ready for use in the evolutionary process. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() @@ -82,11 +133,45 @@ def prepare_suboperators(fitness_operator: CompoundOperator, operator_params: di def noise_data(data, noise_level): + """ + Adds random noise to the input data based on its standard deviation. + + This function perturbs the data by adding Gaussian noise scaled + by the data's standard deviation and a specified noise level. + This is useful for simulating real-world measurement errors or + for data augmentation purposes, ensuring the discovered equations + are robust to small variations in the input. + + Args: + data (np.ndarray): The input data to which noise will be added. + noise_level (float): The standard deviation of the noise, expressed as a percentage of the data's standard deviation. + + Returns: + np.ndarray: The data with added noise. + """ # add noise level to the input data return noise_level * 0.01 * np.std(data) * np.random.normal(size=data.shape) + data def ODE_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the ability of the EPDE framework to identify a simple 2nd order ODE from data. + + This method defines a symbolic representation of the ODE + x'' + sin(2t) x' + 4 x = 1.5 t, sets up an EPDE search problem, loads data (potentially adding noise), + and then checks if the identified equation is closer to the correct symbolic representation + than to an intentionally incorrect one. This verifies that the search process can distinguish + between plausible and implausible equation forms based on the provided data. + + Args: + operator: CompoundOperator object, not used in the current implementation. + foldername: Path to the folder containing the data and pretrained PINN model. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the comparison between the identified equation and the + incorrect equation passes, indicating the test was successful. + """ # Test scenario to evaluate performance on simple 2nd order ODE # x'' + sin(2t) x' + 4 x = 1.5 t, written as $g_{1} x'' + g_{2} x' + g_{3} x = g_{4} # g1 = lambda x: 1. @@ -125,6 +210,23 @@ def ODE_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def VdP_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance on the Van-der-Pol oscillator. + + This method evaluates the EPDE framework's ability to identify the Van-der-Pol oscillator equation from data. + It sets up a test scenario by loading data, adding noise, defining a symbolic representation of the equation, + and creating a pool of candidate terms. The method then compares the discovered equation with an incorrect one + to assess the search's accuracy. This helps to ensure that the evolutionary algorithm can effectively + distinguish the true equation from other possibilities. + + Args: + operator: Compound operator. + foldername: Name of the folder containing the data. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the symbolic and incorrect equations are the same, False otherwise. + """ # u'' + E (u^2 - 1)u' + u = 0, where $\mathcal{E}$ is a positive constant (in the example we will use $\mathcal{E} = 0.2$) # Test scenario to evaluate performance on Van-der-Pol oscillator eq_vdp_symbolic = '-0.2 * u{power: 2.0} * du/dx0{power: 1.0} + 0.2 * du/dx0{power: 1.0} + -1.0 * u{power: 1.0} + -0.0 \ @@ -160,6 +262,23 @@ def VdP_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def lorenz_discovery(foldername, noise_level): + """ + Discovers the Lorenz system equations using the EPDE framework. + + This method leverages the EPDE framework to identify the underlying equations + governing the Lorenz attractor directly from data. It automates the equation + discovery process by configuring an EPDE search object, performing a search + for the best equation structures, and presenting the discovered equations. + This is done to provide insights into the dynamics of the Lorenz system + and build a predictive model. + + Args: + foldername: The name of the folder containing the data. (Not used in the provided code snippet) + noise_level: The level of noise in the data. (Not used in the provided code snippet) + + Returns: + EpdeSearch: The trained EPDE search object, containing the discovered equations and search results. + """ t_file = os.path.join(os.path.dirname( __file__ ), 'data\\lorenz\\t.npy') t = np.load(t_file) data_file = os.path.join(os.path.dirname(__file__), 'data\\lorenz\\lorenz.npy') @@ -200,6 +319,25 @@ def lorenz_discovery(foldername, noise_level): def lv_discovery(foldername, noise_level): + """ + Discovers the underlying equations governing the Lotka-Volterra system. + + This method automates the process of identifying the differential equations + that best describe the dynamics of the Lotka-Volterra system, given time-series data. + It configures and executes an EPDE search, leveraging preprocessors and + multi-objective optimization to find a set of equations that accurately + capture the relationships between the variables. The method returns the + trained EPDE search object, which contains the discovered equations and + related information. The method is doing that to provide insights into the + underlying dynamics of the Lotka-Volterra system and build predictive models. + + Args: + foldername: The name of the folder where the data is stored (not used). + noise_level: The noise level in the data (not used). + + Returns: + EpdeSearch: The trained EPDE search object containing the discovered equations. + """ t_file = os.path.join(os.path.dirname( __file__ ), 'data\\lv\\t_20.npy') t = np.load(t_file) data_file = os.path.join(os.path.dirname(__file__), 'data\\lv\\data_20.npy') @@ -239,6 +377,23 @@ def lv_discovery(foldername, noise_level): def ac_data(filename: str): + """ + Loads data and generates corresponding grid coordinates. + + This function is designed to load data from a specified file and create a grid + representing the coordinate system in which the data is defined. This is a + necessary step to work with the data in a structured manner, allowing for + further analysis and processing within the framework. + + Args: + filename (str): The path to the file containing the data to be loaded. + + Returns: + tuple: A tuple containing two elements: + - grids (tuple of numpy.ndarray): A tuple of arrays representing the meshgrid + of the coordinate system. + - data (numpy.ndarray): The loaded data from the specified file. + """ t = np.linspace(0., 1., 51) x = np.linspace(-1., 0.984375, 128) data = np.load(filename) @@ -248,6 +403,22 @@ def ac_data(filename: str): def AC_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance on the Allen-Cahn equation. + + This method evaluates the ability to identify the Allen-Cahn equation from data. + It sets up a test scenario by loading data, adding noise, and then checking + if the discovered equation matches the known symbolic representation. This + validation is crucial to ensure the reliability of the equation discovery process. + + Args: + operator: Compound operator to test. + foldername: Name of the folder containing the data and pretrained model. + noise_level: Level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the expected equation, False otherwise. + """ # Test scenario to evaluate performance on Allen-Cahn equation eq_ac_symbolic = '0.0001 * d^2u/dx1^2{power: 1.0} + -5.0 * u{power: 3.0} + 5.0 * u{power: 1.0} + 0.0 = du/dx0{power: 1.0}' eq_ac_incorrect = '-1.0 * d^2u/dx0^2{power: 1.0} + 1.5 * u{power: 1.0} + -0.0 = du/dx0{power: 1.0}' @@ -273,6 +444,20 @@ def AC_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def wave_data(filename): + """ + Generates grid coordinates and wave data from a file. + + This method loads wave data from a CSV file and creates a corresponding grid of coordinates. + The grid and data are structured to facilitate the discovery of underlying differential equations. + + Args: + filename (str): The name of the CSV file containing the wave data. + + Returns: + tuple (np.ndarray, np.ndarray): A tuple containing two numpy arrays: + - grids: A numpy array representing the grid coordinates. + - data: A numpy array containing the wave data loaded from the file. + """ shape = 80 # print(os.path.dirname( __file__ )) @@ -284,6 +469,22 @@ def wave_data(filename): def wave_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the equation comparison within the `EpdeSearch` object to ensure accurate equation discovery. + + This test validates the ability of the `compare_equations` function to differentiate + between a correct symbolic equation representing the wave equation and an incorrect one. + It's crucial for verifying that the search process can reliably distinguish between + candidate equations based on their structure and fit to the data. + + Args: + operator: CompoundOperator object. + foldername: Path to the folder containing the wave data and pretrained ANN. + noise_level: Level of noise to add to the wave data. Defaults to 0. + + Returns: + bool: True if the comparison passes, otherwise raises an AssertionError. + """ # eq_wave_symbolic = '1. * d^2u/dx1^2{power: 1} + 0. = d^2u/dx0^2{power: 1}' eq_wave_symbolic = '0.04 * d^2u/dx1^2{power: 1} + 0. = d^2u/dx0^2{power: 1}' eq_wave_incorrect = '1. * d^2u/dx1^2{power: 1} * du/dx1{power: 1} + 2.3 * d^2u/dx0^2{power: 1} + 0. = du/dx0{power: 1}' @@ -309,6 +510,24 @@ def wave_test(operator: CompoundOperator, foldername: str, noise_level: int = 0) def kdv_data(filename, shape = 80): + """ + Loads data and creates corresponding spatial and temporal grids. + + This function is essential for preparing the data into a suitable format + that can be used for equation discovery. It reads the raw data, and then + generates the spatial and temporal grids necessary for representing the + solution domain. This is a preliminary step before the evolutionary + search can be applied to find the best fitting differential equation. + + Args: + filename (str): The name of the data file to load (CSV format). + shape (int, optional): The shape of the grid to create. Defaults to 80. + + Returns: + tuple: A tuple containing the spatial and temporal grids and the loaded data. + The first element is a tuple of two numpy arrays representing the grids. + The second element is a numpy array containing the loaded data. + """ shape = 80 print(os.path.dirname( __file__ )) @@ -320,6 +539,30 @@ def kdv_data(filename, shape = 80): def kdv_data_sga(filename): + """ + Loads KdV data from a .mat file and prepares it for symbolic equation discovery. + + This function loads the KdV data, extracts the solution `u`, spatial grid `x`, + and temporal grid `t`, and then creates a meshgrid from `t` and `x`. This + structured data is essential for representing the solution on a grid, which + is a prerequisite for applying symbolic regression techniques to discover the + underlying partial differential equation. + + Args: + filename: The name of the .mat file containing the KdV data. + + Returns: + tuple: A tuple containing: + - grids: A tuple representing the meshgrid of t and x. + - u: The solution data. + + Why: + The KdV data is loaded and prepared in this way to create a structured + representation of the solution on a grid. This is necessary for + subsequent symbolic regression to discover the underlying partial + differential equation. The meshgrid provides the coordinates for each + point in the solution, which is used to evaluate candidate equations. + """ data = scio.loadmat(filename) u = data.get("uu").T n, m = u.shape @@ -330,6 +573,23 @@ def kdv_data_sga(filename): def KdV_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance on the Korteweg-de Vries equation. + + This method evaluates the ability of the EPDE framework to rediscover the + Korteweg-de Vries (KdV) equation from data. It sets up a controlled experiment + by loading data, optionally adding noise, and then using the EPDE search + algorithm to identify the equation that best describes the data. This test + validates the framework's capacity to automatically identify governing equations. + + Args: + operator: The compound operator to be used in the EPDE search. + foldername: The name of the folder containing the data files. + noise_level: The level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the expected KdV equation, False otherwise. + """ # Test scenario to evaluate performance on Korteweg-de Vries equation eq_kdv_symbolic = '-6.0 * du/dx1{power: 1.0} * u{power: 1.0} + -1.0 * d^3u/dx1^3{power: 1.0} + \ 1.0 * sin{power: 1, freq: 1.0, dim: 1} * cos{power: 1, freq: 1.0, dim: 0} + \ @@ -367,6 +627,30 @@ def KdV_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): def kdv_data_h(filename, shape=80): + """ + Loads KdV equation data and prepares it for equation discovery. + + This function loads data representing the solution of the KdV equation + from a specified file and generates corresponding time and space grids. + These grids and the data are structured to be used as input for + discovering the underlying differential equation. The shape parameter + is overridden internally to ensure consistency with the grid dimensions. + + Args: + filename (str): The name of the file containing the KdV equation data. + shape (int, optional): An integer intended to define the shape of the data (unused). Defaults to 80. + + Returns: + tuple (np.ndarray, np.ndarray): A tuple containing the grids (t, x) and the loaded data. + The grids are created using np.meshgrid with 120 time points + between 0 and 1, and 480 spatial points between -3 and 3. + The data is loaded from the specified file using np.load. + + Why: + This function prepares the data into a format suitable for the equation discovery process. + The grids provide the independent variable values (time and space) corresponding to the + solution data, which is necessary for algorithms to learn the underlying equation. + """ shape = 119 print(os.path.dirname(__file__)) @@ -379,6 +663,20 @@ def kdv_data_h(filename, shape=80): def KdV_h_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance of the EPDE search on the Korteweg-de Vries equation. + + This method sets up and executes an EPDE search to identify the KdV equation + from noisy data. It then validates whether the discovered equation aligns with the known KdV equation, ensuring the framework's ability to accurately identify governing equations from data. + + Args: + operator: CompoundOperator object, not used in the current implementation. + foldername: Path to the folder containing the data and pretrained PINN. + noise_level: Level of noise to add to the data. + + Returns: + bool: True if the comparison of equations passes, False otherwise. + """ # Test scenario to evaluate performance on Korteweg-de Vries equation eq_kdv_symbolic = '-6.0 * du/dx1{power: 1.0} * u{power: 1.0} + -1.0 * d^3u/dx1^3{power: 1.0} + \ 0.0 = du/dx0{power: 1.0}' @@ -411,6 +709,21 @@ def KdV_h_test(operator: CompoundOperator, foldername: str, noise_level: int = 0 def KdV_sga_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance of the symbolic genetic algorithm (SGA) on the Korteweg-de Vries (KdV) equation. + + This method evaluates the SGA's ability to identify the underlying differential equation of the KdV system from data. + It sets up a controlled experiment by loading KdV data, introducing noise, and defining a search space for potential equation candidates. + The SGA then explores this space to find an equation that accurately represents the system's dynamics. This process demonstrates the framework's ability to discover governing equations from data. + + Args: + operator: The CompoundOperator instance to be tested. + foldername: The name of the folder containing the KdV data and pretrained PINN model. + noise_level: The level of noise to add to the data (default: 0). + + Returns: + bool: True if the discovered equation matches the expected KdV equation, False otherwise. + """ # Test scenario to evaluate performance on Korteweg-de Vries equation eq_kdv_symbolic = '-1 * du/dx1{power: 1.0} * u{power: 1.0} + -0.0025 * d^3u/dx1^3{power: 1.0} + \ 0.0 = du/dx0{power: 1.0}' @@ -442,6 +755,24 @@ def KdV_sga_test(operator: CompoundOperator, foldername: str, noise_level: int = assert compare_equations(eq_kdv_symbolic, eq_kdv_incorrect, epde_search_obj) def darcy_data(filename: str): + """ + Loads Darcy flow data from a file and prepares it for use in equation discovery. + + This method loads a NumPy array representing a Darcy flow solution from the specified file, + reshapes it to be compatible with the expected input format, and creates a corresponding + coordinate grid. The coordinate grid and processed data are then returned for use in + identifying the underlying differential equation. + + Args: + filename: The name of the file containing the Darcy flow data + in NumPy format (.npy). + + Returns: + A tuple containing: + - grids: A tuple of NumPy arrays representing the coordinate grid. + - data: A NumPy array containing the processed Darcy flow data, + reshaped for compatibility with the equation discovery process. + """ t = np.linspace(0., 1., 2) x = np.linspace(0., 1., 128) y = np.linspace(0., 1., 128) @@ -453,6 +784,24 @@ def darcy_data(filename: str): return grids, data def darcy_test(operator: CompoundOperator, foldername: str, noise_level: int = 0): + """ + Tests the performance of the equation discovery process on the Darcy equation. + + This method evaluates the ability of the EPDE framework to identify the Darcy equation + from noisy data. It involves data loading, preprocessing, defining custom tokens + representing domain-specific knowledge, and comparing the identified equation with + known correct and incorrect forms. The goal is to assess whether the framework can + accurately extract the underlying equation despite the presence of noise and complexity. + + Args: + operator: The CompoundOperator instance to use for equation comparison. + foldername: The name of the folder containing the Darcy data. + noise_level: The level of noise to add to the data (default: 0). + + Returns: + bool: True if the identified equation matches the expected Darcy equation, + False if it matches the incorrect equation. + """ # Test scenario to evaluate performance on darcy equation # eq_darcy_symbolic = '-1.0 * du/dx1{power: 1.0} * dnu/dy{power: 1.0} + -1.0 * nu{power: 1.0} * d^2u/dx1^2{power: 1} + \ # -2.0 * nu{power: 1.0} * d^2u/dxdy{power: 1} + \ @@ -509,6 +858,24 @@ def darcy_test(operator: CompoundOperator, foldername: str, noise_level: int = 0 assert compare_equations(eq_darcy_symbolic, eq_darcy_incorrect, epde_search_obj) def darcy_discovery(foldername, noise_level): + """ + Discovers a differential equation governing Darcy flow from data using the EPDE framework. + + This method automates the identification of the underlying differential equation + that describes Darcy flow by analyzing provided data. It preprocesses the data, + defines custom tokens relevant to the flow characteristics (permeability and its + gradients, mixed derivative of pressure), and employs the EPDE framework to + search for the equation that best fits the data. The goal is to uncover the + mathematical relationship governing the flow based on observed data, even in the + presence of noise. + + Args: + foldername: The name of the folder containing the Darcy flow data ('darcy_1.0.npy') and optionally a pretrained ANN model ('ann_pretrained.pickle'). + noise_level: The level of noise to add to the Darcy flow data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations and related information. + """ grid, data = darcy_data(os.path.join(foldername, 'darcy_1.0.npy')) nu = np.load(r'C:\Users\user\PycharmProjects\EPDE\EPDE\projects\pic\data\darcy\darcy_nu_1.0.npy') noised_data = noise_data(data, noise_level) @@ -570,6 +937,21 @@ def darcy_discovery(foldername, noise_level): return epde_search_obj def ODE_discovery(foldername, noise_level): + """ + Discovers the underlying ODE from noisy data using the EPDE framework. + + This method performs equation discovery on ODE data, leveraging a combination + of finite difference preprocessing, neural network priors, and evolutionary + algorithms to identify the governing equations. The method aims to find the best equation structure that accurately represents the dynamics of the system described by the provided data. This is achieved by exploring a space of possible equations and selecting those that minimize the error between the model's predictions and the observed data. + + Args: + foldername: The name of the folder containing the 'ode_data.npy' file + and the pretrained PINN model 'ode_0_ann.pickle'. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EpdeSearch object, containing the discovered equations. + """ step = 0.05 steps_num = 320 t = np.arange(start=0., stop=step * steps_num, step=step) @@ -611,6 +993,23 @@ def ODE_discovery(foldername, noise_level): return epde_search_obj def vdp_discovery(foldername, noise_level): + """ + Discovers the governing equation of the Van der Pol oscillator from noisy data. + + This method leverages the EPDE framework to identify the underlying differential equation + that describes the dynamics of the Van der Pol oscillator. It starts by loading the data, + introducing noise to simulate real-world conditions, and utilizing a pre-trained neural network + to approximate the solution. EPDE then explores the space of possible equation structures to + find the one that best fits the data. This approach allows us to automatically infer the + mathematical model from the observed behavior of the system. + + Args: + foldername: The name of the folder containing the data and pretrained neural network. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EPDE search object, which encapsulates the discovered equation and related information. + """ step = 0.05; steps_num = 320 t = np.arange(start=0., stop=step * steps_num, step=step) @@ -653,6 +1052,23 @@ def vdp_discovery(foldername, noise_level): def kdv_discovery(foldername, noise_level): + """ + Discovers the governing equation for the KdV equation from data using an evolutionary approach. + + This method leverages the EPDE framework to identify the Korteweg-de Vries (KdV) + equation. It begins by loading data and configuring the EPDE search + object with settings appropriate for the KdV equation. Custom tokens are defined + to guide the search process, and finally, the model is trained to find the + equation that best describes the provided data. This automated discovery + process helps in understanding the underlying dynamics of the system. + + Args: + foldername: The name of the folder containing the data. + noise_level: The level of noise in the data. + + Returns: + EpdeSearch: The trained EPDE search object, containing the discovered equation. + """ grid, data = kdv_data(os.path.join(foldername, 'data.csv')) # grid, data = kdv_data(os.path.join(foldername, 'Kdv.mat')) # noised_data = noise_data(data, noise_level) @@ -705,6 +1121,22 @@ def kdv_discovery(foldername, noise_level): return epde_search_obj def kdv_h_discovery(foldername, noise_level): + """ + Discover the governing equation for the KdV equation with added noise using EPDE. + + This method leverages the EPDE framework to identify the underlying equation + of the KdV system from noisy data. By setting up an EPDE search object, + defining custom tokens relevant to the KdV equation, and fitting the model + to the provided data, it aims to find the equation that best describes the system's behavior. + This is done to provide insights into the system's dynamics and build a predictive model. + + Args: + foldername: The name of the folder containing the data and pretrained PINN. + noise_level: The level of noise added to the data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations. + """ grid, data = kdv_data_h(os.path.join(foldername, 'data_kdv_homogen.npy')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) @@ -761,6 +1193,24 @@ def kdv_h_discovery(foldername, noise_level): return epde_search_obj def kdv_sga_discovery(foldername, noise_level): + """ + Discovers the KdV equation by fitting equation structures to data using an evolutionary algorithm. + + This method employs the EPDE framework with a Sequential Genetic Algorithm (SGA) to + identify the governing equation for the KdV equation. It involves loading and + preprocessing data, adding noise to test robustness, defining custom equation components, + and then fitting the EPDE model to find the equation that best describes the data. + This approach automates the equation discovery process, providing insights into the + system's dynamics by finding a balance between model complexity and accuracy in + representing the observed data. + + Args: + foldername: The name of the folder containing the data and pretrained PINN. + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EPDE search object. + """ grid, data = kdv_data_sga(os.path.join(foldername, 'Kdv.mat')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) @@ -817,6 +1267,24 @@ def kdv_sga_discovery(foldername, noise_level): return epde_search_obj def wave_discovery(foldername, noise_level): + """ + Performs wave equation discovery using the EPDE framework. + + This method leverages the EPDE search to identify the underlying equation + governing wave propagation from provided data, even in the presence of noise. + It prepares the data, configures the search space with appropriate tokens + and preprocessors, and then fits the EPDE model to the data to find the + best equation. This is done to automatically extract the mathematical + representation of the wave phenomenon from the observed data. + + Args: + foldername: The name of the folder containing the data files + ('wave_sln_80.csv' and 'ann_pretrained.pickle'). + noise_level: The level of noise to add to the data. + + Returns: + EpdeSearch: The trained EPDE search object. + """ grid, data = wave_data(os.path.join(foldername, 'wave_sln_80.csv')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, 'ann_pretrained.pickle')) @@ -863,6 +1331,22 @@ def wave_discovery(foldername, noise_level): return epde_search_obj def ac_discovery(foldername, noise_level): + """ + Discovers the governing equation from data using the EPDE framework. + + This method performs equation discovery on data loaded from a specified + folder, incorporating a given noise level. It leverages a pre-trained + PINN (Physics-Informed Neural Network) and the EPDE search algorithm + to identify the underlying equation. The method automates the process of identifying governing differential equations from data, + allowing users to gain insights into the underlying dynamics of complex systems and build predictive models. + + Args: + foldername: The name of the folder containing the data and PINN. + noise_level: The level of noise added to the data. + + Returns: + EpdeSearch: The EPDE search object containing the discovered equations. + """ grid, data = ac_data(os.path.join(foldername, 'ac_data.npy')) noised_data = noise_data(data, noise_level) data_nn = load_pretrained_PINN(os.path.join(foldername, f'kdv_{noise_level}_ann.pickle')) diff --git a/projects/pic/pic_wave_equation_test.py b/projects/pic/pic_wave_equation_test.py index 31bae804..1c43b417 100644 --- a/projects/pic/pic_wave_equation_test.py +++ b/projects/pic/pic_wave_equation_test.py @@ -35,6 +35,22 @@ # return (data, grid) def load_data(filename): + """ + Loads data representing a physical field from a file and creates a corresponding grid. + + This function reads data from the specified file, assuming comma-separated values, + and constructs a grid of coordinates that represents the spatial or temporal domain + over which the data is defined. This is a preliminary step for discovering + the underlying differential equation. + + Args: + filename: The name of the file containing the data. + + Returns: + A tuple containing: + - grids: A NumPy array representing the grid coordinates. + - data: A NumPy array containing the loaded data. + """ shape = 80 # print(os.path.dirname( __file__ )) @@ -45,6 +61,19 @@ def load_data(filename): def load_pretrained_PINN(ann_filename): + """ + Loads a pre-trained Physics-Informed Neural Network (PINN) from a file to accelerate the equation discovery process. + + This method attempts to load a pickled PINN model from the specified file. + If the file is not found, it prints a message indicating that the model + will be retrained and returns None. Loading a pre-trained model can significantly reduce the computational cost of identifying the underlying differential equation, especially when starting from scratch. + + Args: + ann_filename (str): The filename of the pickled PINN model. + + Returns: + object: The loaded PINN model if the file is found, otherwise None. + """ try: with open(ann_filename, 'rb') as data_input_file: data_nn = pickle.load(data_input_file) @@ -54,6 +83,20 @@ def load_pretrained_PINN(ann_filename): return data_nn def prepare_suboperators(fitness_operator: CompoundOperator) -> CompoundOperator: + """ + Prepares sub-operators required to calculate the fitness of an equation. + + This method configures and assigns sparsity and coefficient calculation + sub-operators to the provided fitness operator. It maps these sub-operators + between 'gene level' and 'chromosome level' and then sets them as + sub-operators within the fitness operator. This setup is crucial for evaluating the equation's performance by considering both its complexity (sparsity) and accuracy (coefficient calculation) when fitting the data. + + Args: + fitness_operator (CompoundOperator): The compound operator to prepare. + + Returns: + CompoundOperator: The modified fitness operator with the prepared sub-operators. + """ sparsity = LASSOSparsity() coeff_calc = LinRegBasedCoeffsEquation() diff --git a/projects/wSINDy/KdV_new.py b/projects/wSINDy/KdV_new.py index c26ec327..868e42da 100644 --- a/projects/wSINDy/KdV_new.py +++ b/projects/wSINDy/KdV_new.py @@ -12,6 +12,18 @@ def sindy_out_format(model, max_deriv_order): + """ + Formats the SINDy output equation for enhanced readability, focusing on derivative representation. + + This method refines the SINDy model equation's presentation by converting derivative notations into a more human-readable format, inserting spaces, and adding multiplication symbols where appropriate. This improves the interpretability of the discovered equations. + + Args: + model: The SINDy model object containing the equation to format. + max_deriv_order: The maximum derivative order to consider during formatting. + + Returns: + str: The formatted SINDy equation as a string. + """ def pure_derivs_format(string, max_deriv_order, deriv_symbol='\d'): # производная переменной x2 или x1 if deriv_symbol == '\d': diff --git a/projects/wSINDy/__init__.py b/projects/wSINDy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/wSINDy/burgers.py b/projects/wSINDy/burgers.py index b5f774ca..35cdd7b0 100644 --- a/projects/wSINDy/burgers.py +++ b/projects/wSINDy/burgers.py @@ -37,6 +37,23 @@ from epde.interface.solver_integration import BoundaryConditions, BOPElement def epde_discovery(grids, data, derivs, use_ann = False): + """ + Performs equation discovery using the EPDE algorithm. + + This method orchestrates the search for governing equations by configuring and executing an EPDE search. + It leverages the provided data and derivative information to explore the space of possible equations, + aiming to identify the equation structures that best represent the underlying dynamics of the system. + The method configures the search space, optimization parameters, and token library before executing the search. + + Args: + grids: Spatial and temporal grid coordinates. + data: The data to fit the equation to. + derivs: Precomputed derivatives of the data. + use_ann: Flag to use artificial neural networks. Defaults to False. + + Returns: + EpdeSearch: The fitted EPDE search object containing the discovered equations. + """ multiobjective_mode = True dimensionality = data.ndim - 1 @@ -74,6 +91,21 @@ def epde_discovery(grids, data, derivs, use_ann = False): def sindy_discovery(grids, u):#, u_dot): + """ + Performs sparse identification of differential equations (SINDy) to discover governing equations from data. + + This method takes spatial and temporal grids and corresponding data 'u', + constructs a library of candidate functions using `PDELibrary`, and then + uses SINDy to identify a sparse model that describes the dynamics of the data. + It automates the process of identifying governing differential equations from data. + + Args: + grids (tuple): A tuple containing the temporal and spatial grids. + u (numpy.ndarray): The data to be modeled. + + Returns: + sindy.SINDy: The fitted SINDy model, representing the discovered differential equation. + """ t = np.unique(grids[0]) x = np.unique(grids[1]) dt = t[1] - t[0] @@ -105,6 +137,21 @@ def sindy_discovery(grids, u):#, u_dot): return model def translate_sindy_eq(equation: str): + """ + Translates a SINDy equation into a human-readable format suitable for further processing within the EPDE framework. + + This method takes a SINDy equation string as input, replaces the + shorthand notation with a more descriptive form, and appends " = du/dx1{power: 1.0}" + to the end of the translated equation. It relies on a predefined + correspondence dictionary to map the shorthand notations to their + corresponding representations. This translation is crucial for ensuring that the discovered equations are interpretable and can be seamlessly integrated into subsequent analysis or simulation steps within the EPDE workflow. + + Args: + equation (str): The SINDy equation string to translate. + + Returns: + str: The translated SINDy equation. + """ correspondence = {"0" : "u{power: 1.0}", "0_1" : "du/dx2{power: 1.0}", "0_11" : "d^2u/dx2^2{power: 1.0}"} @@ -126,6 +173,24 @@ def replace(term): return terms_comb def sindy_examplar_search(x, t, u, u_dot = None): + """ + Identifies governing equations using SINDy exemplar search. + + This method employs the SINDy algorithm with a PDE library to discover + the underlying differential equations that describe the provided data. + It constructs a library of candidate functions and uses sparse regression + to select the most relevant terms, effectively identifying the equation's structure. + This approach helps automate the process of identifying governing differential equations from data. + + Args: + x (np.ndarray): Spatial grid points. + t (np.ndarray): Time points. + u (np.ndarray): Input data representing the system's state. + u_dot (np.ndarray, optional): Time derivatives of the input data. If None, it will be calculated using finite differences. Defaults to None. + + Returns: + ps.SINDy: The fitted SINDy model, representing the identified governing equations. + """ dt = t[1] - t[0] dx = x[1] - x[0] diff --git a/projects/wSINDy/burgers_sindy_data.py b/projects/wSINDy/burgers_sindy_data.py index 1fef3421..2129e6f5 100644 --- a/projects/wSINDy/burgers_sindy_data.py +++ b/projects/wSINDy/burgers_sindy_data.py @@ -36,6 +36,22 @@ from epde.interface.solver_integration import BoundaryConditions, BOPElement def epde_discovery(x, t, u, use_ann = False): #(grids, data, use_ann = False): + """ + Performs EPDE to find governing equations from data. + + This method sets up and executes the EPDE algorithm to identify the underlying equation that best describes the provided data. + It involves preprocessing, defining custom tokens, and fitting the EPDE search object to the data. + The method aims to find a balance between model complexity and accuracy by searching for the most concise equation that adequately represents the data. + + Args: + x: Spatial grid. + t: Temporal grid. + u: Data corresponding to the spatial and temporal grids. + use_ann: Flag indicating whether to use an artificial neural network (ANN) preprocessor. Defaults to False. + + Returns: + epde_search_obj: The fitted EPDE search object containing the discovered equations. + """ u = u.T grids = np.meshgrid(t, x, indexing = 'ij') print(u.shape, grids[0].shape, grids[1].shape) @@ -78,6 +94,23 @@ def epde_discovery(x, t, u, use_ann = False): #(grids, data, use_ann = False): def sindy_examplar_search(x, t, u): + """ + Performs SINDy exemplar search to identify governing equations. + + This method applies the SINDy algorithm to discover the underlying equations from the provided data. + It leverages finite differences to estimate derivatives, constructs a library of potential terms + using a PDE library, and then employs sparse regression to identify the most significant terms. + This approach automates the identification of governing differential equations from data, + allowing users to gain insights into the dynamics of the system. + + Args: + x (np.ndarray): Spatial coordinates. + t (np.ndarray): Time coordinates. + u (np.ndarray): Input data representing the system's state. + + Returns: + None. The method prints the identified model to the console. + """ dt = t[1] - t[0] dx = x[1] - x[0] diff --git a/projects/wSINDy/lotka-volterra.py b/projects/wSINDy/lotka-volterra.py index bc4b00ab..73de06bc 100644 --- a/projects/wSINDy/lotka-volterra.py +++ b/projects/wSINDy/lotka-volterra.py @@ -38,6 +38,22 @@ SOLVER_STRATEGY = 'autograd' def write_pareto(dict_of_exp): + """ + Writes Pareto front solutions to text files for each experiment. + + This function iterates through a dictionary of experimental results, extracting + Pareto front solutions and writing them to separate text files. The filename + is derived from the experiment's key. Each iteration's Pareto front is + written with equation strings separated by newlines. This is done to store the discovered equations + in a readable format for further analysis and validation. + + Args: + dict_of_exp: Dictionary where keys are tuples representing experiment + parameters and values are lists of lists of Pareto front objects. + + Returns: + None. This method writes data to files and does not return any value. + """ for key, item in dict_of_exp.items(): test_key = str(key[0]).replace('.', '_') + '__' + str(key[1]).replace('.', '_') with open('/home/maslyaev/epde/EPDE_main/projects/hunter-prey/param_var/'+test_key+'.txt', 'w') as f: @@ -47,6 +63,26 @@ def write_pareto(dict_of_exp): f.write(ind + '\n\n') def epde_discovery(t, x, y, use_ann = False): + """ + Discovers a Pareto set of differential equation systems that fit the provided data. + + This method leverages evolutionary algorithms and multi-objective optimization to explore the space of possible equation structures. + The goal is to identify a set of equations that accurately describe the relationships between the input variables and their derivatives, + balancing model complexity and accuracy. The method preprocesses the input data, defines the search space using tokens and constraints, + and then iteratively refines the equation candidates based on their ability to fit the data. The Pareto set represents a collection of + equation systems that offer different trade-offs between model complexity and accuracy. + + Args: + t (np.ndarray): Time coordinate tensor. + x (np.ndarray): Independent variable data. + y (np.ndarray): Dependent variable data. + use_ann (bool, optional): Whether to use an Artificial Neural Network (ANN) preprocessor. Defaults to False. + + Returns: + tuple: A tuple containing: + - epde_search_obj (EpdeSearch): The EpdeSearch object containing the search results. + - sys (list): The discovered system of equations with specified complexity. + """ dimensionality = x.ndim - 1 ''' @@ -86,6 +122,21 @@ def epde_discovery(t, x, y, use_ann = False): def sindy_discovery(t, x, y): + """ + Performs sparse identification of nonlinear dynamics using the SINDy algorithm. + + This method identifies a dynamical system from time series data by constructing a polynomial feature library and applying sparse regression. + It aims to find a parsimonious model that accurately describes the system's evolution, effectively discovering the underlying equations governing the observed dynamics. + This is crucial for automating the process of identifying governing differential equations from data, a core objective of the EPDE project. + + Args: + t (np.ndarray): Time vector corresponding to the data samples. + x (np.ndarray): First state variable's time series data. + y (np.ndarray): Second state variable's time series data. + + Returns: + ps.SINDy: The fitted SINDy model. + """ poly_order = 5 threshold = 0.05 diff --git a/projects/wSINDy/wave.py b/projects/wSINDy/wave.py index f5af9a20..da7c8f31 100644 --- a/projects/wSINDy/wave.py +++ b/projects/wSINDy/wave.py @@ -36,6 +36,24 @@ from epde.interface.solver_integration import BoundaryConditions, BOPElement def write_pareto(dict_of_exp): + """ + Writes Pareto front solutions to text files for each experiment. + + This function iterates through a dictionary of experimental results, extracting + Pareto front solutions and writing them to separate text files. The filename + is derived from the experiment parameters (dictionary key), and each iteration's + Pareto front is written with equation strings separated by newlines. This + facilitates the analysis and comparison of discovered equation structures + across different experimental settings, aiding in the identification of robust + and generalizable models. + + Args: + dict_of_exp: Dictionary where keys are tuples representing experiment + parameters and values are lists of lists of Pareto front objects. + + Returns: + None. This method writes data to files and does not return any value. + """ for key, item in dict_of_exp.items(): test_key = str(key[0]).replace('.', '_') + '__' + str(key[1]).replace('.', '_') with open('/home/maslyaev/epde/EPDE_main/projects/hunter-prey/param_var/'+test_key+'.txt', 'w') as f: @@ -45,6 +63,24 @@ def write_pareto(dict_of_exp): f.write(ind + '\n\n') def epde_discovery(grids, data, derivs, use_ann = False): + """ + Performs symbolic regression to identify governing equations from data using an evolutionary algorithm. + + This method sets up and executes the search algorithm to discover + equations that describe the provided data. It configures the search space, + defines token types, and fits the model to the data by searching for the best equation structure. + + Args: + grids: Spatial and temporal grid coordinates. These coordinates provide the independent variable values for the data. + data: The data to fit the equations to. This is the dependent variable data that the algorithm attempts to model. + derivs: Precomputed derivatives of the data. (Not used in the current implementation) + + Returns: + tuple: A tuple containing: + - The EPDE search object, which holds the state and results of the search. + - The discovered equations with complexity 2, representing the simplest identified relationships. + - The saved derivatives from the EPDE search object. These derivatives can be used for further analysis or validation. + """ multiobjective_mode = True dimensionality = data.ndim - 1 @@ -120,6 +156,21 @@ def epde_discovery(grids, data, derivs, use_ann = False): # return model def sindy_discovery(grids, u): + """ + Discovers a sparse representation of the governing PDE from the given data using SINDy. + + This method constructs a SINDy model, fits it to the provided spatio-temporal data, + and returns the fitted model. It leverages a PDELibrary for feature engineering, + allowing for custom basis functions and derivative calculations, and SR3 for sparse + regression. This enables the identification of the most relevant terms in the PDE. + + Args: + grids: Grid points for the data. Assumed to contain time and space grids. + u: Solution data to be modeled. + + Returns: + The fitted SINDy model, representing the discovered PDE. + """ t = np.unique(grids[0]) x = np.unique(grids[1]) dt = t[1] - t[0] diff --git a/projects/wave/__init__.py b/projects/wave/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/projects/wave/wave.py b/projects/wave/wave.py index 123e2a3d..5785ecde 100644 --- a/projects/wave/wave.py +++ b/projects/wave/wave.py @@ -18,6 +18,23 @@ # TODO^ caching of the pre-calculated derivatives def translate_eq(): + """ + Translates and evaluates a differential equation to assess its fitness. + + This method orchestrates the process of loading data representing a physical system, + defining the search space of potential differential equations, and evaluating how well + a candidate equation describes the observed data. It involves setting up coordinate + grids, defining equation terms, searching for the best equation structure, and + quantifying the equation's accuracy using a specified evaluation function. The goal + is to find a differential equation that accurately captures the underlying dynamics + of the system represented by the input data. + + Args: + None + + Returns: + None + """ u = np.loadtxt('/home/maslyaev/epde/EPDE_main/projects/wave/data.csv').reshape((101, 101, 101)) u = np.moveaxis(u, 2, 0) diff --git a/setup.py b/setup.py index 84ca1bf7..8e4fba7b 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,19 @@ def read(*names, **kwargs): + """ + Reads the content of a file, ensuring proper handling of character encoding. + + This is essential for reliably loading equation definitions, data, or + configuration files used within the EPDE framework. + + Args: + *names: Path segments to the file, relative to the directory of the current file. + **kwargs: Keyword arguments. May include 'encoding' to specify the file encoding. + + Returns: + str: The content of the file as a string. + """ with open( join(dirname(__file__), *names), encoding=kwargs.get('encoding', 'utf8') @@ -27,10 +40,37 @@ def read(*names, **kwargs): def extract_requirements(file_name): + """ + Extracts and filters lines from a file, treating each line as a potential requirement. + + This function is used to prepare a list of requirements from a specified file, + which is a preliminary step for the equation discovery process. By removing + empty lines and comments, it ensures that only valid requirements are considered + during the search for governing equations. + + Args: + file_name (str): The name of the file containing the requirements. + + Returns: + list[str]: A list of strings, where each string represents a valid requirement + extracted from the file. Empty lines and comments are excluded. + """ return [r for r in read(file_name).split('\n') if r and not r.startswith('#')] def get_requirements(): + """ + Extracts and returns the requirements from a file. + + This function is crucial for setting up the necessary environment + by identifying and listing the required Python packages. + + Args: + None + + Returns: + list: A list of strings, where each string is a requirement. + """ requirements = extract_requirements('requirements.txt') return requirements diff --git a/tests/system/conftest.py b/tests/system/conftest.py index d28b1208..c83cd196 100644 --- a/tests/system/conftest.py +++ b/tests/system/conftest.py @@ -9,10 +9,33 @@ import pytest def pytest_addoption(parser): + """ + Adds a command-line option `--path` to pytest. + + This option allows users to specify a path relevant to equation discovery, + potentially influencing data loading or saving locations. + + Args: + parser: The pytest argument parser. + + Returns: + None + """ parser.addoption("--path", action="store") @pytest.fixture(scope='session') def name(request): + """ + Return the path to the data directory specified in the pytest configuration. + + This fixture retrieves the path to the data directory. If no path is provided via the command line, the test is skipped, ensuring that tests requiring data are only executed when the data path is explicitly specified. + + Args: + request: The pytest request object, used to access configuration values. + + Returns: + str: The path to the data directory. + """ path_value = request.config.option.path if path_value is None: pytest.skip() diff --git a/tests/system/ode_textbook.py b/tests/system/ode_textbook.py index d3fb97d7..1ccdf7ad 100644 --- a/tests/system/ode_textbook.py +++ b/tests/system/ode_textbook.py @@ -35,15 +35,17 @@ #if __name__ == '__main__': def test_ode_auto(): - ''' + """ + Discovers a differential equation of the form u sin(x) + u' cos(x) = 1, given its solution u = sin(x) + C cos(x) with a particular solution where C = 1.3. - В этой задаче мы ищем уравнение u sin(x) + u' cos(x) = 1 по его решению: u = sin(x) + C cos(x), - где у частного решения C = 1.3. + The method sets up a search space of possible equations and uses multi-objective optimization to find the equation that best fits the provided data. The search prioritizes simplicity and accuracy, aiming to identify the underlying relationship between the function and its derivatives. - Задаём x - координатную ось по времени; ts - временной ряд условных измерений - ff_filename - имя файла, куда сохраняется временной ряд; output_file_name - имя файла для производных - step - шаг по времени - ''' + Args: + None + + Returns: + None + """ # delim = '/' if sys.platform == 'linux' else '\\' diff --git a/tests/unit/adapter_test.py b/tests/unit/adapter_test.py index 7be07dff..96caa537 100644 --- a/tests/unit/adapter_test.py +++ b/tests/unit/adapter_test.py @@ -30,6 +30,23 @@ # from epde.interface.equation_translator import translate_equation def get_basic_var_family(var_name, deriv_names, deriv_orders): + """ + Creates a foundational TokenFamily for a variable and its derivatives. + + This method constructs a TokenFamily object, essential for representing a variable + within the equation discovery process. It configures the TokenFamily to handle + derivatives and sets up its evaluation mechanism. This ensures that the variable + and its derivatives can be properly incorporated into candidate equations. + + Args: + var_name: The name of the variable. + deriv_names: A list of derivative names. + deriv_orders: A list of derivative orders corresponding to deriv_names. + + Returns: + TokenFamily: A configured TokenFamily object representing the variable + and its derivatives, ready for use in equation discovery. + """ entry_token_family = TokenFamily(var_name, family_of_derivs = True) entry_token_family.set_status(demands_equation=True, unique_specific_token=False, unique_token_type=False, s_and_d_merged = False, @@ -40,6 +57,25 @@ def get_basic_var_family(var_name, deriv_names, deriv_orders): return entry_token_family def prepare_basic_inputs(): + """ + Prepares the foundation for equation discovery by initializing essential components. + + This method configures the necessary data structures and objects + to facilitate the equation discovery process. It involves setting up grids, + defining variables and their derivatives, preprocessing the data to + extract relevant features, and constructing a token pool that defines the search space. + This setup ensures that the subsequent search algorithms have a well-defined + and preprocessed input to efficiently explore potential equation candidates. + + Args: + None + + Returns: + tuple: A tuple containing: + - grids: A list of NumPy arrays representing the grids over which the data is defined. + - pool: A TF_Pool object containing the variable family and + trigonometric tokens, which constitute the building blocks for constructing equations. + """ grids = [np.linspace(0, 4*np.pi, 1000),] var_name = 'u' u = np.sin(x) + 1.3 * np.cos(x)#np.load('/home/maslyaev/epde/EPDE_main/tests/system/Test_data/fill366.npy') @@ -67,6 +103,20 @@ def prepare_basic_inputs(): return grids, pool def mock_equation(): + """ + Generates a mock equation and input grids for testing the equation discovery process. + + This method sets up a simplified environment with predefined input grids and a mock token pool. + It then translates a symbolic equation using this mock pool. This is essential for verifying + the correctness and stability of the equation translation and subsequent steps in the equation + discovery pipeline, such as equation simplification and numerical evaluation. + + Returns: + tuple: A tuple containing the input grids and the translated equation. + - grids (tuple): The basic input grids prepared by `prepare_basic_inputs`. + - translated_equation (torch.Tensor): The equation translated into a numerical representation + using the mock token pool. + """ grids, mock_pool = prepare_basic_inputs() print('Mock pool families:', [family.tokens for family in mock_pool.families]) text_form = ('1.0 * u{power: 1} * sin{freq: 1, power: 1, dim: 0} + 1. = ' @@ -74,6 +124,20 @@ def mock_equation(): return grids, translate_equation(text_form, mock_pool) def test_adapter_form_only(): + """ + Tests the SolverFormAdapter's form method, ensuring consistent equation form generation. + + This method verifies that the SolverFormAdapter produces equivalent equation forms + regardless of whether grid data is explicitly provided. This ensures that the equation + discovery process remains consistent and reliable, even when grid information is + implicitly defined within the dataset. + + Args: + None + + Returns: + None + """ grids, equation = mock_equation() solver_form_adapter = SolverFormAdapter() equation_form_no_grids = solver_form_adapter.form() @@ -84,5 +148,34 @@ def test_adapter_form_only(): -def test_adapter_full_solution(): +def test_adapter_full_solution(): """ + Tests a full adapter solution. + + This method sets up and executes a complete adapter-based workflow, + verifying the interaction between different components. It doesn't + take any direct input parameters but relies on predefined configurations + and data within the test environment. + + Returns: + """ + Tests the end-to-end functionality of the equation discovery process. + + This method orchestrates a complete EPDE workflow, from data input to equation + output, ensuring that all components work together seamlessly. It validates + the system's ability to identify governing equations from a dataset, reflecting + a real-world application scenario. The test relies on predefined configurations + and data within the test environment to simulate a typical equation discovery task. + + Args: + None + + Returns: + None. The method implicitly asserts the correctness of the discovered + equations and the overall workflow execution. It checks if the system + can successfully process data and identify a meaningful equation. + """ + None. This method doesn't return any value; it asserts the + correctness of the adapter solution's behavior. + """ + \ No newline at end of file diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index d28b1208..b997944b 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -9,10 +9,33 @@ import pytest def pytest_addoption(parser): + """ + Adds a command-line option `--path` to pytest. + + This option allows users to specify a path relevant to equation discovery, + potentially influencing data loading or saving locations. + + Args: + parser: The pytest argument parser. + + Returns: + None. + """ parser.addoption("--path", action="store") @pytest.fixture(scope='session') def name(request): + """ + Return the path to the data directory specified in the pytest configuration. + + This fixture retrieves the path to the data directory. If no path is provided via the command line, the test is skipped, ensuring that tests requiring data are only executed when the data path is explicitly specified. + + Args: + request: The pytest request object, used to access configuration values. + + Returns: + str: The path to the data directory. + """ path_value = request.config.option.path if path_value is None: pytest.skip()