Skip to content
Merged
10 changes: 5 additions & 5 deletions src/easyscience/Objects/ObjectClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from easyscience.Utils.classTools import addLoggedProp

from .core import ComponentSerializer
from .new_variable import Parameter as new_Parameter
from .new_variable import Parameter as NewParameter
from .new_variable.descriptor_base import DescriptorBase
from .Variable import Descriptor
from .Variable import Parameter
Expand Down Expand Up @@ -161,7 +161,7 @@ def constraints(self) -> List[C]:
return constraints

## TODO clean when full move to new_variable
def get_parameters(self) -> Union[List[Parameter], List[new_Parameter]]:
def get_parameters(self) -> Union[List[Parameter], List[NewParameter]]:
"""
Get all parameter objects as a list.

Expand All @@ -171,7 +171,7 @@ def get_parameters(self) -> Union[List[Parameter], List[new_Parameter]]:
for key, item in self._kwargs.items():
if hasattr(item, 'get_parameters'):
par_list = [*par_list, *item.get_parameters()]
elif isinstance(item, Parameter) or isinstance(item, new_Parameter):
elif isinstance(item, Parameter) or isinstance(item, NewParameter):
par_list.append(item)
return par_list

Expand All @@ -191,7 +191,7 @@ def _get_linkable_attributes(self) -> List[V]:
return item_list

## TODO clean when full move to new_variable
def get_fit_parameters(self) -> Union[List[Parameter], List[new_Parameter]]:
def get_fit_parameters(self) -> Union[List[Parameter], List[NewParameter]]:
"""
Get all objects which can be fitted (and are not fixed) as a list.

Expand All @@ -201,7 +201,7 @@ def get_fit_parameters(self) -> Union[List[Parameter], List[new_Parameter]]:
for key, item in self._kwargs.items():
if hasattr(item, 'get_fit_parameters'):
fit_list = [*fit_list, *item.get_fit_parameters()]
elif isinstance(item, Parameter) or isinstance(item, new_Parameter):
elif isinstance(item, Parameter) or isinstance(item, NewParameter):
if item.enabled and not item.fixed:
fit_list.append(item)
return fit_list
Expand Down
88 changes: 62 additions & 26 deletions src/easyscience/fitting/minimizers/minimizer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,61 @@
from abc import ABCMeta
from abc import abstractmethod
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

import numpy as np

from easyscience.Objects.ObjectClasses import BaseObj
from easyscience.Objects.Variable import Parameter

from ..Constraints import ObjConstraint
from .utils import FitError
from .utils import FitResults

MINIMIZER_PARAMETER_PREFIX = 'p'


class MinimizerBase(metaclass=ABCMeta):
"""
This template class is the basis for all fitting engines in `EasyScience`.
This template class is the basis for all minimizer engines in `EasyScience`.
"""

wrapping: str = None

def __init__(self, obj, fit_function: Callable, method: Optional[str] = None):
def __init__(self, obj: BaseObj, fit_function: Callable, method: Optional[str] = None):
if method not in self.available_methods():
raise FitError(f'Method {method} not available in {self.__class__}')
self._object = obj
self._original_fit_function = fit_function
self._method = method
self._cached_pars = {}
self._cached_pars_vals = {}
self._cached_pars: Dict[str, Parameter] = {}
self._cached_pars_vals: Dict[str, Tuple[float]] = {}
self._cached_model = None
self._fit_function = None
self._constraints = []

@property
def all_constraints(self) -> list:
def all_constraints(self) -> List[ObjConstraint]:
return [*self._constraints, *self._object._constraints]

def fit_constraints(self) -> list:
def fit_constraints(self) -> List[ObjConstraint]:
return self._constraints

def set_fit_constraint(self, constraints):
def set_fit_constraint(self, constraints: List[ObjConstraint]):
self._constraints = constraints

def add_fit_constraint(self, constraint):
def add_fit_constraint(self, constraint: ObjConstraint):
self._constraints.append(constraint)

def remove_fit_constraint(self, index: int):
def remove_fit_constraint(self, index: int) -> None:
del self._constraints[index]

@abstractmethod
def make_model(self, pars=None):
def make_model(self, pars: List[Parameter] = None):
"""
Generate an engine model from the supplied `fit_function` and parameters in the base object.

Expand All @@ -66,7 +78,7 @@ def fit(
self,
x: np.ndarray,
y: np.ndarray,
weights: Optional[Union[np.ndarray]] = None,
weights: Optional[np.ndarray] = None,
model=None,
parameters=None,
method=None,
Expand All @@ -89,37 +101,52 @@ def fit(
:return: Fit results
"""

def evaluate(self, x: np.ndarray, parameters: dict = None, **kwargs) -> np.ndarray:
def evaluate(self, x: np.ndarray, minimizer_parameters: dict[str, float] = None, **kwargs) -> np.ndarray:
"""
Evaluate the fit function for values of x. Parameters used are either the latest or user supplied.
If the parameters are user supplied, it must be in a dictionary of {'parameter_name': parameter_value,...}.

:param x: x values for which the fit function will be evaluated
:type x: np.ndarray
:param parameters: Dictionary of parameters which will be used in the fit function. They must be in a dictionary
:param minimizer_parameters: Dictionary of parameters which will be used in the fit function. They must be in a dictionary
of {'parameter_name': parameter_value,...}
:type parameters: dict
:type minimizer_parameters: dict
:param kwargs: additional arguments
:return: y values calculated at points x for a set of parameters.
:rtype: np.ndarray
"""
""" # noqa: E501
if minimizer_parameters is None:
minimizer_parameters = {}
if not isinstance(minimizer_parameters, dict):
raise TypeError("minimizer_parameters must be a dictionary")

if self._fit_function is None:
# This will also generate self._cached_pars
self._fit_function = self._generate_fit_function()

if not isinstance(parameters, (dict, type(None))):
raise AttributeError
minimizer_parameters = self._prepare_parameters(minimizer_parameters)

return self._fit_function(x, **minimizer_parameters, **kwargs)

def _prepare_parameters(self, parameters: dict[str, float]) -> dict[str, float]:
"""
Prepare the parameters for the minimizer.

:param parameters: Dict of parameters for the minimizer with names as keys.
"""
pars = self._cached_pars
new_parameters = parameters
if new_parameters is None:
new_parameters = {}

for name, item in pars.items():
fit_name = 'p' + str(name)
if fit_name not in new_parameters.keys():
new_parameters[fit_name] = item.raw_value
parameter_name = MINIMIZER_PARAMETER_PREFIX + str(name)
if parameter_name not in parameters.keys():
## TODO clean when full move to new_variable
from easyscience.Objects.new_variable import Parameter as NewParameter

return self._fit_function(x, **new_parameters, **kwargs)
if isinstance(item, NewParameter):
parameters[parameter_name] = item.value
else:
parameters[parameter_name] = item.raw_value
return parameters

@abstractmethod
def convert_to_pars_obj(self, par_list: Optional[Union[list]] = None):
Expand All @@ -131,9 +158,18 @@ def convert_to_pars_obj(self, par_list: Optional[Union[list]] = None):
:return: engine Parameters compatible object
"""

@abstractmethod
def available_methods(self) -> List[str]:
"""
Return a list of available methods for the engine.

:return: List of available methods
:rtype: List[str]
"""

@staticmethod
@abstractmethod
def convert_to_par_object(obj):
def convert_to_par_object(obj: BaseObj):
"""
Convert an `EasyScience.Objects.Base.Parameter` object to an engine Parameter object.
"""
Expand All @@ -149,7 +185,7 @@ def _set_parameter_fit_result(self, fit_result):
"""

@abstractmethod
def _gen_fit_results(self, fit_results, **kwargs) -> 'FitResults':
def _gen_fit_results(self, fit_results, **kwargs) -> FitResults:
"""
Convert fit results into the unified `FitResults` format.

Expand Down
32 changes: 18 additions & 14 deletions src/easyscience/fitting/minimizers/minimizer_bumps.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from bumps.names import FitProblem
from bumps.parameter import Parameter as BumpsParameter

from .minimizer_base import MINIMIZER_PARAMETER_PREFIX
from .minimizer_base import MinimizerBase
from .utils import FitError
from .utils import FitResults
Expand Down Expand Up @@ -57,10 +58,10 @@ def make_func(x, y, weights):
par = {}
if not pars:
for name, item in obj._cached_pars.items():
par['p' + str(name)] = obj.convert_to_par_object(item)
par[MINIMIZER_PARAMETER_PREFIX + str(name)] = obj.convert_to_par_object(item)
else:
for item in pars:
par['p' + item.unique_name] = obj.convert_to_par_object(item)
par[MINIMIZER_PARAMETER_PREFIX + item.unique_name] = obj.convert_to_par_object(item)
return Curve(fit_func, x, y, dy=weights, **par)

return make_func
Expand Down Expand Up @@ -99,8 +100,7 @@ def fit_function(x: np.ndarray, **kwargs):
for name, value in kwargs.items():
par_name = name[1:]
if par_name in self._cached_pars.keys():

## TODO clean when full move to new_variable
## TODO clean when full move to new_variable
from easyscience.Objects.new_variable import Parameter

if isinstance(self._cached_pars[par_name], Parameter):
Expand All @@ -109,7 +109,7 @@ def fit_function(x: np.ndarray, **kwargs):
else:
if self._cached_pars[par_name].raw_value != value:
self._cached_pars[par_name].value = value

# update_fun = self._cached_pars[par_name]._callback.fset
# if update_fun:
# update_fun(value)
Expand All @@ -125,8 +125,9 @@ def fit_function(x: np.ndarray, **kwargs):
# f = (x, a=1, b=2)...
# Where we need to be generic. Note that this won't hold for much outside of this scope.

## TODO clean when full move to new_variable
## TODO clean when full move to new_variable
from easyscience.Objects.new_variable import Parameter

if isinstance(parameter, Parameter):
default_value = parameter.value
else:
Expand All @@ -137,7 +138,7 @@ def fit_function(x: np.ndarray, **kwargs):
inspect.Parameter('x', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=inspect._empty),
*[
inspect.Parameter(
'p' + str(name),
MINIMIZER_PARAMETER_PREFIX + str(name),
inspect.Parameter.POSITIONAL_OR_KEYWORD,
annotation=inspect._empty,
default=default_value,
Expand Down Expand Up @@ -204,8 +205,9 @@ def fit(
model = model(x, y, weights)
self._cached_model = model

## TODO clean when full move to new_variable
## TODO clean when full move to new_variable
from easyscience.Objects.new_variable import Parameter

if isinstance(self._cached_pars[list(self._cached_pars.keys())[0]], Parameter):
self._p_0 = {f'p{key}': self._cached_pars[key].value for key in self._cached_pars.keys()}
else:
Expand Down Expand Up @@ -252,16 +254,17 @@ def convert_to_par_object(obj) -> BumpsParameter:
:return: bumps Parameter compatible object.
:rtype: BumpsParameter
"""
## TODO clean when full move to new_variable

## TODO clean when full move to new_variable
from easyscience.Objects.new_variable import Parameter

if isinstance(obj, Parameter):
value = obj.value
else:
value = obj.raw_value

return BumpsParameter(
name='p' + obj.unique_name,
name=MINIMIZER_PARAMETER_PREFIX + obj.unique_name,
value=value,
bounds=[obj.min, obj.max],
fixed=obj.fixed,
Expand Down Expand Up @@ -314,16 +317,17 @@ def _gen_fit_results(self, fit_results, **kwargs) -> FitResults:

## TODO clean when full move to new_variable
from easyscience.Objects.new_variable import Parameter

if isinstance(pars[dict_name], Parameter):
item[name] = pars[dict_name].value
item[name] = pars[dict_name].value
else:
item[name] = pars[dict_name].raw_value

results.p0 = self._p_0
results.p = item
results.x = self._cached_model.x
results.y_obs = self._cached_model.y
results.y_calc = self.evaluate(results.x, parameters=results.p)
results.y_calc = self.evaluate(results.x, minimizer_parameters=results.p)
results.y_err = self._cached_model.dy
# results.residual = results.y_obs - results.y_calc
# results.goodness_of_fit = np.sum(results.residual**2)
Expand Down
Loading