JointFitter and two examples. - #288
Conversation
|
Nice. I want to add a sensible “what if” to think about. What if a user wants to use a prior distribution that is not a uniform prior? Bounding the parameters is equivalent to a uniform prior. Say I want to use a truncated normal distribution on a parameter, or a Jeffreys prior. How hard would that be to implement as this code is written, or as Astropy modeling is implemented? |
|
@settwi , that's a good point and worth thinking about. The permanent fix would be to PR AstropyPersonally, I think that the place to allow this would be in the module level If we could just allow the bounds attribute for the parameter object to accept a prior function of the user's choice (either in place of a simple length 2 tuple or as well as) then I think the code-block that enforces the given bounds at the minute (here) could just run the value through the parameter's "bound function" to get the corresponding value. This would make the bounds on any parameter far more powerful for any fitting. The temporary fix (while we wait for Astropy to accept)I can think of two ways:
|
samaloney
left a comment
There was a problem hiding this comment.
This is really nice!
I wonder would it make sense to extract an ABC which defines the API then make this a concrete implementation?
The tests cover:
- unitless models,
- forward-direction tying
but misses: - fixed params
- bounds enforcement check,
- weights,
- Quantity units.
The units-handling branch and the weights/fkwarg paths used in are untested. I do think tests for any feature expose by the API would be needed - could be done in a later PR but would be better here.
It would be good to try and find some specific examples of the thick target function issues so we can pin it down and hopefully fix.
On the optimiser and statistic I think the way it set up now makes sense if need to define our own API and then map the the underling fitters - other subclasses can override the methods and raise error is not supported
| ## should really call self._opt_method() | ||
| # optimize.least_squares just minimises the square of what comes out of self.objective_function | ||
| # need the lambda function for now since `minimize` won't accept other kwargs | ||
| fun = lambda x: self.objective_function(x, *(models, farg), **fkwarg) # noqa | ||
| result = minimize( | ||
| fun, | ||
| jmodel_params, | ||
| bounds=tuple(zip(jmodel_bounds[0], jmodel_bounds[1])), | ||
| method="Nelder-Mead", | ||
| tol=1e-8, | ||
| ) |
There was a problem hiding this comment.
As partially noted in the PR description the __init__ validates and stores self._opt_method/self._stat_method here always calls scipy.optimize.minimize(..., method="Nelder-Mead") with a hardcoded sum-of-squares.
Both gallery examples call JointFitter(optimizer=SLSQP, statistic=leastsquare) which doesn't actually use the optim/stats functions.
There was a problem hiding this comment.
Absolutely, and I don't like this either. I wanted to get the code up here to discuss first but I definitely thought this is something to fix. I'm hoping my pseudo code gets at this and I'm happy to build on this.
|
|
||
| for mod_num, model in enumerate(models): | ||
| # units, make sure we have units | ||
| if parameter_units is not None: |
There was a problem hiding this comment.
mod_params is only assigned when parameter_units is not None. It's unreachable via the public call path (_get_param_units never returns None for non-empty model lists)
|
|
||
|
|
||
| ## KRIS: All I did was add ``model_list`` | ||
| def fitter_to_model_params_array(model, fps, use_min_max_bounds=True, *, fit_param_indices=None, model_list=None): |
There was a problem hiding this comment.
Should see if we can eventually PR this to astropy
There was a problem hiding this comment.
Absolutely. I Think that pretty much all the JointFitter code and fitters.py code as it stands could live in Astropy eventually. The only thing I've essentially done (after a lot of work to get there) was to:
- Get the fitter to accept (model, x, y) groups instead of just one
- This mainly meant I had to add in some loops to loop through the groups (+minimal other things)
- Make those two module level functions (only because one of these functions uses the other) accept this
model_listparameter
I don't see why the other fitting classes in Astropy couldn't just be made to do this. Then we only have to have our own fitters like the NelderMeadLeastSquaresFitter fitter with our own optimiser and statistics modules to let us do whatever we want.
Thanks! I've been happy with how it's turned out so far. I would love to do that! It's something that the API in Astropy isn't very solid in and I don't like exactly how I've done it here. The way Astropy does it is by defining the base I can try and make E.g., from scipy.optimize import minimize
from some_module import least_squares_func
class JointFitter(Fitter):
def __init__(self, optimizer, statistic):
super().__init__(optimizer=optimizer, statistic=statistic)
def joint_model_to_fit_params(self, models):
...
def _update_model_params(self, models, fps, jfit_param_indices):
...
def _verify_input(self, args):
...
@staticmethod
def _get_param_units(models):
...
def _run_fitter(self, models, farg, fkwarg=None):
raise NotImplementedError("Subclasses should implement this method.")
def objective_function(self, models, farg, fkwarg=None):
raise NotImplementedError("Subclasses should implement this method.")
def __call__(self, *args, fkwarg=None, inplace=False):
# will check inputs then return output of `self._run_fitter`
...
return self._run_fitter(...)
class NelderMeadLeastSquaresFitter(JointFitter):
def __init__(self):
super().__init__(optimizer=minimize, statistic=least_squares_func)
def objective_function(self, fps, *args, weights=None, jfit_param_indices=None, parameter_units=None):
# gathers parameters and evaluates model to run`self._stat_method` which is `least_squares_func` here
...
self._stat_method()
def _run_fitter(self, models, farg, fkwarg=None):
# sets up and runs `self._opt_method` which is `minimize` here
...
self._opt_method(...)
...The user could then just: fitter = NelderMeadLeastSquaresFitter()
result = fitter(model0, x0, y0, ..., modelN, xN, yN)The
Agreed! I'll work through this list and update the tests to account for them.
Agreed again and I'm working on another branch to do this.
Have I answered this with the pseudo code above? |
|
New tests to add:
|
|
Don't believe the failed ubuntu-latest test. I've seen it fail then pass with no change to the code here and in PR #289 |
Adds the first version of the new
JointFitterand two examples for the example gallery.This is still a work in progress.
My local docs building is not working.To add:
pre-commitThoughts
This is workable but there are still somethings to do or clean-up. I'm not sure if the following should be this PR or another one.
JointFitterneeds to be initialised with one of each since I'm trying to follow their set-up. The issue is, not all of their other fitters follow this and end up just bypassing this and hardcoding in their optimiser and statistic choices anyway. How do we want to do this?JointFitterjust handles units briefly and crudely to get stuff to work. What I've written isn't complicated and we should look into altering the Astropy decorator that handles units.@jajmitchell , @samaloney , let me know what you both think about this. I think the
JointFitterdoc-strings could use a little work but other than that, what would be crucial to get this initial stuff merged in your eyes?