Skip to content

Commit

Permalink
Fixed params and constraints (#165) (#168)
Browse files Browse the repository at this point in the history
This fixes a problem which appeared when using constraints and fixed parameters at the same time.It was caused due to constriants not being provided the fixed parameters in the same way that the objective function was.

* Constraints are partialed with data before being passed to the minimizer, since minimizers don't know about data.

* Partial constraints with fixed parameters.

* Updated resize_jac to facilitate Constraint.eval_jacobian

* Introduce ScipyConstrainedMinimize.

This brings constraints on the same footing as jacobians by bringing the conversion to scipy constraints to a seperate subclass. Inherites from ScipyGradientMinimize because we need this to wrap the jacobian of the constraints in order to feed them to SLSQP, but this might be factored out because scipy's COBYLA does not accept a jacobian for it's constraints.

* Made sure we cannot np.squeeze down to 0-dimensional

* Cleaned up the inheretence tree. Now only SLSQP adds jacobians to constraints.

Also, a COBYLA method has been added for gradient-less constrained optimization.

* Use numpy masking to slice the jacobian to the right shape.

* Added a wrapper to scipys COBYLA

* Use @wraps on decorators

* SLSQP has to pass the wrapped_jacobian explicitely.

* Added very complete tests on the basis of #165.

* Renamed wrap_func to a more descriptive list2kwargs

* Added basic docstring to all objects in minimze.py

* Fixed py27 issue by using functools32.wraps

* Added a repeatable partial function for python versions older than 3.5

* Added tests for repeatable_partial

* Made key2str use the type of the input Mapping

* Prevent potential future namespace conflicts by renaming partial derivatives

* Fixed minor autodoc indentation problem.

* Removed a rogue print statement

* Fixed some typos in the docstring

* Changed the name of the file containg repeatable_partial to _repeatable_partial.py, and added comments.
  • Loading branch information
tBuLi authored Jul 27, 2018
1 parent d2b861e commit ee2d6e0
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 91 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ numpy >= 1.12
scipy >= 1.0
sympy <= 1.1.1
funcsigs; python_version < '3.0'
functools32; python_version < '3.0'
41 changes: 41 additions & 0 deletions symfit/core/_repeatable_partial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from functools import partial

class repeatable_partial(partial):
"""
In python < 3.5, stacked partials on the same function do not add more args
and kwargs to the function being partialed, but rather partial the
partial.
This is unlogical behavior, which has been corrected in py35. This objects
rectifies this behavior in earlier python versions as well.
"""
def __new__(*args, **keywords):
"""
This is essentially just a copy-paste of python 3.5's __new__ method,
but made python 2.7 friendly.
:param args:
:param keywords:
:return:
"""
if not args:
raise TypeError("descriptor '__new__' of partial needs an argument")
if len(args) < 2:
raise TypeError("type 'partial' takes at least one argument")
cls = args[0]
func = args[1]
args = args[2:]
if not callable(func):
raise TypeError("the first argument must be callable")
args = tuple(args)
# I would prefer isinstance(func, partial), but the standard lib does
# this so best copy that for now.
if hasattr(func, "func"):
args = func.args + args
tmpkw = func.keywords.copy()
tmpkw.update(keywords)
keywords = tmpkw
del tmpkw
func = func.func

return super(repeatable_partial, cls).__new__(cls, func, *args, **keywords)
1 change: 0 additions & 1 deletion symfit/core/argument.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,6 @@ def __init__(self, name=None, value=1.0, min=None, max=None, fixed=False, **assu

if min is not None and max is not None and min > max:
if not self.fixed:
print(min, max)
raise ValueError('The value of `min` should be less than or'
' equal to the value of `max`.')
else:
Expand Down
23 changes: 11 additions & 12 deletions symfit/core/fit.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from collections import namedtuple, Mapping, OrderedDict, Sequence
import copy
from functools import partial
import sys
import warnings
from abc import abstractmethod
Expand All @@ -12,7 +11,8 @@
from scipy.integrate import odeint

from symfit.core.argument import Parameter, Variable
from .support import seperate_symbols, keywordonly, sympy_to_py, cache, key2str
from .support import \
seperate_symbols, keywordonly, sympy_to_py, cache, key2str, partial

from .minimizers import (
BFGS, SLSQP, LBFGSB, BaseMinimizer, GradientMinimizer, ConstrainedMinimizer,
Expand Down Expand Up @@ -425,7 +425,7 @@ def numerical_jacobian(self):
"""
:return: lambda functions of the jacobian matrix of the function, which can be used in numerical optimization.
"""
return [[sympy_to_py(partial, self.independent_vars, self.params) for partial in row] for row in self.jacobian]
return [[sympy_to_py(partial_dv, self.independent_vars, self.params) for partial_dv in row] for row in self.jacobian]

@property
# @cache
Expand Down Expand Up @@ -465,7 +465,7 @@ def eval_jacobian(self, *args, **kwargs):
"""
# Evaluate the jacobian at specified points
jac = [
[partial(*args, **kwargs) for partial in row ] for row in self.numerical_jacobian
[partial_dv(*args, **kwargs) for partial_dv in row ] for row in self.numerical_jacobian
]
for idx, comp in enumerate(jac):
# Find out how many datapoints this component has. We need to do
Expand Down Expand Up @@ -647,7 +647,7 @@ def numerical_jacobian(self):
"""
:return: lambda functions of the jacobian matrix of the function, which can be used in numerical optimization.
"""
return [[sympy_to_py(partial, self.model.vars, self.model.params) for partial in row] for row in self.jacobian]
return [[sympy_to_py(partial_dv, self.model.vars, self.model.params) for partial_dv in row] for row in self.jacobian]

def _make_signature(self):
# Handle args and kwargs according to the allowed names.
Expand Down Expand Up @@ -1371,13 +1371,12 @@ def _init_minimizer(self, minimizer, **minimizer_options):
minimizer_options['jacobian'] = self.objective.eval_jacobian

if issubclass(minimizer, ConstrainedMinimizer):
if issubclass(minimizer, ScipyMinimize):
minimizer_options['constraints'] = minimizer.scipy_constraints(
self.constraints,
self.data
)
else:
minimizer_options['constraints'] = self.constraints
# Minimizers are agnostic about data, they just know about
# objective functions. So we partial away the data at this point.
minimizer_options['constraints'] = [
partial(constraint, **key2str(self.data))
for constraint in self.constraints
]
return minimizer(self.objective, self.model.params, **minimizer_options)

def _init_constraints(self, constraints):
Expand Down
Loading

0 comments on commit ee2d6e0

Please sign in to comment.