Source code for coexist.combiners

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# File   : combiners.py
# License: GNU v3.0
# Author : Jack Sykes <jas653@student.bham.ac.uk>
# Date   : 17.03.2022


'''Classes for combining multiple errors into a single value to be minimised.

The evolutionary algorithm will then naturally optimise all errors at the same
time. For example, the ``Product`` class will multiply all given errors
(optionally "weighting" them by raising them to different powers) and minimise
their product.

To combine multiple errors differently, create a new class defining the method
``.combine(error)``, which will be called with the error values found for a
given simulation.

For example, a GranuDrum Digital Twin may be calibrated for two different
flowing regimes at the same time; instead of doing separate ACCES runs, one
for, say, 15 rpm, and one for 45 rpm, you can instead use the multi-objective
functionality of ACCES to optimise both cases simultaneously.
'''


import  textwrap
import  numpy as  np




[docs]def combiner(func): '''Make a user-defined function a multi-objective error combiner. Examples -------- To simply multiply all error values together: >>> import numpy as np >>> from coexist.combiners import combiner >>> >>> @combiner >>> def multiply(errors: np.ndarray) -> float: >>> return np.prod(errors) The input argument "errors" will always be a 1D NumPy array, even when the simulation error is a simple, single number. If you know you will have only two error values, you can sum them up like this: >>> from coexist.combiners import combiner >>> >>> @combiner >>> def sum_errors(errors): >>> return errors[0] + errors[1] Then you can simply supply your function to ACCES: >>> import coexist >>> coexist.Access("<simulation_script>").learn( >>> multi_objective = sum_errors, >>> random_seed = 42, >>> ) ''' # Attach a method called "combine" that simply calls the function itself func.combine = func.__call__ return func
[docs]class Product: '''Class for combining multiple errors by multiplying them. The multi-objective functionality allows multiple error values to be calibrated / optimised at the same time. Attributes ---------- weights: float, optional Raise each error to a power corresponding to its respective weight. If unset, all errors are multiplied with no exponentiation. Examples -------- Use the ``Product`` combiner in an ACCES run: >>> import coexist >>> access = coexist.Access("<simulation_script>") >>> access.learn( >>> multi_objective = coexist.combiners.Product(), >>> random_seed = 42, >>> ) To test what a combiner will output, use its ``.combine(errors)`` method - this is what will be called by ACCES: >>> import coexist >>> comb = coexist.combiners.Product(weights = [2, 3]) >>> errors = [2, 2] >>> comb.combine(errors) 32.0 '''
[docs] def __init__(self, weights = None): # If `weights` values are given, put them into an array if weights is not None: if not hasattr(weights, "__iter__"): weights = [weights] weights = np.array(weights, dtype = float) # Initialised parameter self.weights = weights
[docs] def combine(self, errors): '''Combine the given errors to a total error value. ''' # No `weights` values, simply mutliply the errors if self.weights is None: return np.prod(errors) # Error-check to see if number of weights matches number of errors if len(errors) != len(self.weights): raise ValueError(textwrap.fill(( "Number of weights given is not equal to the number of errors " f"set in the simulation script. Received {len(self.weights)} " f"weights, but found {len(errors)} errors." ))) # Return the product of the errors with `weights` values as exponents return np.prod(errors ** self.weights)
[docs]class Sum: '''Class for combining errors by summing them together. The multi-objective functionality allows multiple error values to be calibrated / optimised at the same time. Attributes ---------- weights: float, optional Multiply each error with its respective weight. If unset, all errors are multiplied with no weighting. Examples -------- Use the ``Sum`` combiner in an ACCES run: >>> import coexist >>> access = coexist.Access("<simulation_script>") >>> access.learn( >>> multi_objective = coexist.combiners.Sum(), >>> random_seed = 42, >>> ) To test what a combiner will output, use its ``.combine(errors)`` method - this is what will be called by ACCES: >>> import coexist >>> comb = coexist.combiners.Sum(weights = [2, 3]) >>> errors = [2, 2] >>> comb.combine(errors) 10.0 '''
[docs] def __init__(self, weights = None): # If `weights` values are given, put them into an array if weights is not None: if not hasattr(weights, "__iter__"): weights = [weights] weights = np.array(weights, dtype = float) # Initialised parameter self.weights = weights
[docs] def combine(self, errors): '''Combine the given errors to a total error value. ''' # No `weights` values, simply sum the errors if self.weights is None: return np.sum(errors) # Error-check to see if number of weights matches number of errors if len(errors) != len(self.weights): raise ValueError(textwrap.fill(( "Number of weights given is not equal to the number of errors " f"set in the simulation script. Received {len(self.weights)} " f"weights, but found {len(errors)} errors." ))) # Return the sum of the errors multiplied by their `weights` values return np.sum(errors * self.weights)