Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling of constraints for polydisperse parameters (fix #1588) #2348

Merged
merged 37 commits into from
Nov 23, 2022

Conversation

gonzalezma
Copy link
Contributor

@gonzalezma gonzalezma commented Oct 31, 2022

Description

Changes to handle correctly the constraints for polydisperse (PD) parameters.
Now PD params will appear in the combo box menu of the Complex Constraint window, and they will behave as the main parameters (constraints can be added, selected/deselected, edited, etc.)

Fixes #1588

How Has This Been Tested?

Tested on the 3 AOT data sets of the simultaneous 1D fitting tutorial, combining standard and PD constraints and trying the available options (check/uncheck, remove constraint, using a formula) and everything worked as expected.

Known issue

A pending problem is that the PD constrains are not loaded with the project, so they have to be reintroduced every time one loads a project. This is not unexpected, as there are comments (TODO: add polidyspersity and magnetism) in FittingPage.readFitPage and FittingPage.saveToFitPage. The PD constraints are nevertheless written into the project json file, but not loaded. But I suggest to merge this PR and open a separate issue for this.

Review Checklist (please remove items if they don't apply):

  • Code has been reviewed
  • Functionality has been tested
  • Windows installer (GH artifact) has been tested (installed and worked)
  • MacOSX installer (GH artifact) has been tested (installed and worked)
  • User documentation is available and complete (if required)
  • Developers documentation is available and complete (if required)
  • The introduced changes comply with SasView license (BSD 3-Clause)

rozyczko and others added 27 commits July 19, 2019 13:53
Now, editing the constraint on a poly param is possible.
Do a better check for polydisp. parameters.
Created a dictionary within the FittingWidget class, named model_dict,  to contain the main models of SasView. The keys are “standard”, “poly” and “magnet” relating to the main model, polydisperse model and magnet model respectively.
Many functions in the code have been refactored to accept an argument, called model_key, which corresponds to a key in model_dict. This allows the model to be passed to a function without having to pass the bumps object, which can lead to the code misbehaving.
…training or removing a constraint on right click.
Required updating the getSymbolDict function in line with previous commits on this branch
Small edit to the getActiveConstraintList function
@gonzalezma
Copy link
Contributor Author

Last commit affects only the interface and should allow closing issue #2372. With this commit all parameters (either adjustable, fixed or constrained) will appear on the RHS of the Complex Constraint interface and can be used in the definition of a new constraint.

It also fixes the FittingWidget.paramHasConstraint method, which was working for polydisperse parameters but not for the standard one. With the changes in this commit, this method is no longer useful, but I decided to keep it as well as the commented command in ComplexConstraint.setupParamWidgets, in case we could adopt a different logic in future and decide that constrained parameters should not be used in the definition of a new constraint.

src/sas/sascalc/fit/BumpsFitting.py Show resolved Hide resolved
stderr.append(p.value.s)
# p constrained based on another parameter
# Warning: This will not work for parameters that are tied
# to other parameter already constrained.
# Is this still needed with the added _allComputedParamsUncertaintiesDefined test?
else:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you ever get into the else condition?

I think you can replace the entire if-else block with:

assert isinstance(p.value, (uncertainties.core.Variable, uncertainties.core.AffineScalarFunc))
# value.n returns value p
pvec.append(p.value.n)
# value.s returns error in p
stderr.append(p.value.s)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that everything seems fixed, the execution flow is not entering the else condition any more in any of my tests. So I removed the else block entirely. But as a measure of precaution, I kept a try/except, so that if by any strange reason a parameter is still not an uncertainty object, at least the user will get the parameter value (+ some error message). But if this is judge completely unnecessary, I do not have any objection in removing the try/except.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My inclination would be to drop it, but I don't object to leaving it in.

In principle one should strive for 100% code coverage with tests. The except block is untestable (you can't trigger the except without extraordinary measures) . Let the higher up exception handler catch future broken code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do that then. I just removed a few more lines :-)

@rozyczko rozyczko assigned rozyczko and unassigned rozyczko Nov 11, 2022
@pkienzle
Copy link
Contributor

I've only reviewed the code in sascalc not the GUI and I haven't tried running it, so I'll leave it to others to approve the PR.

Copy link
Contributor

@wpotrzebowski wpotrzebowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works as expected if the weights are not modified. When I changed weights 1.5 form M1, and then change them all to 1.0 weight for M2 and M3 were not reset (see attached image, parameters and log). I think it is unrealeted issue and therefore I would vote for merging this one and addressing weighting separetly.
Screenshot 2022-11-15 at 11 16 17

@gonzalezma
Copy link
Contributor Author

The weighting is correct (at least the fact that the values shown in the log are not the same as those given by the user in the interface is the expected behaviour, not a bug). I realize that this is confusing, but the idea is that the weights shown in the log are the effective weights of each data set that are passed to bumps in order to try that each data set contributes to the fit with a "force" proportional to the user weights given by the user in the interface (sorry, not sure that this is very clear either!).

When you give your 3 data sets a weight of 1, the effective weights shown in the log are automatically calculated to take into account the different number of points and error bars in each set, such that the 3 sets contribute more or less equally to the set.

A clear documentation explaining how the weighting works is needed, but that is a separate issue.

@wpotrzebowski
Copy link
Contributor

What is bit puzzling with weighting is that one cannot come to defaults vaules i.e. if all values are set to 1.0 in parameter table the info in log shows:

09:03:44 - INFO: ('FitPage1', 1.0)
09:03:44 - INFO: ('FitPage2', 1.0)
09:03:44 - INFO: ('FitPage3', 1.0)

Once the values are modified e.g. M1 weight is set to 1.5 and then you run fit the values changes (that's expected) but when you change them all to 1.0 again the "INFO" weights won't come back to one. I agree is beyond the scope of this PR and may not be desired behavior.

@wpotrzebowski
Copy link
Contributor

I figured one potential issue while doing the test on the latest code. This may be however more general issue with constrained fitting. When you set up restraint M2.radius = M1.radius and then in the M1 model panel deselect radius the fit still seems to start, however it returns the error:

0```
9:14:06 - ERROR: Fitting failed: Traceback (most recent call last):
File "sas/qtgui/Perspectives/Fitting/FitThread.py", line 79, in compute
File "sas/qtgui/Perspectives/Fitting/FitThread.py", line 19, in map_apply
File "sas/qtgui/Perspectives/Fitting/FitThread.py", line 16, in map_getattr
File "sas/sascalc/fit/BumpsFitting.py", line 336, in fit
AssertionError


I guess the proper treatment of this would be to remove constraint when certain parameter is disabled? Again, it may be beyond the scope of this PR

@gonzalezma
Copy link
Contributor Author

Concerning the weights, the way to recover the default values is to deactivate the 'Modify weighting' option. But I agree that the difference between the values in the log info and the user interface is puzzling. So this is something to discuss and probably to improve in the future.

@gonzalezma
Copy link
Contributor Author

Well spotted the error when a parameter is tied to a parameter that it is not a free parameter. The final cause for the error is that the fix parameter is not an uncertainty object, so the tied param will not be it either, and after the last changes discussed with Paul K this result in an assertion error. The question is if such a constraint definition makes sense and if we should allow it or not.

Note that fixed parameters are still allowed and correctly handled when used as part of a formula. E.g. defining
M2.scale = M1.scale + 0.001 * M1.sld_core (where M1.scale is a free param and M1.sld_core is fixed) results in M2.scale having the expected value according to the formula and the same uncertainty as M1.scale.

So the quick fix here is to recover the try/except with a warning to the user that for a given parameter the uncertainty could not be computed, such that at least SasView does not stop.

Otherwise, we need to address the root problem as suggested by Paul K, meaning that we need to implement a method to identify such problematic definitions and then decide what to do, as I can see several possibilities:

  1. Deactivate the constraint, as you propose.
  2. Assume that the user intention was correct, so "unfree" the constrained parameter and fix it to the same value as the param on the RHS.
  3. Inactivate the fit button and show a warning to the user explaining that this is an unclear case and asking him/her to intervene manually (either setting the RHS param as a free param or setting the LHS param as a fixed param with the value directly given by the user).

I can give a look to that, but I'm not sure that I can manage to find a useful solution in a reasonable time, so I would propose to go for the quick fix in this PR and then discuss in one of our calls if something better is needed.

@pkienzle
Copy link
Contributor

The constraints should be independent of fitting. That is, I want to be able to set the two radii to be equal (so tied by an expression) even if neither are selected for fitting. The GUI should allow this without having to toggle fitting on to set up the constraint and off again to suppress fitting.

You can easily replace the assert with a condition, setting stderr to either NaN or zero for propagation of unconstrained values. I suggest replacing the entire if success-else block with the following (untested):

# Note: Derived values will not have uncertainty if the inputs are fixed, so no 'n' or 's'.
fitting_result.pvec = np.array([getattr(p.value, 'n', p.value) for p in pars])
fitting_result.stderr = np.array([getattr(p.value, 's', 0) for p in pars])
DOF = max(1, fitness.numpoints() - len(fitness.fitted_pars))
fitting_result.fitness = np.sum(fitting_result.residuals ** 2) / DOF

This produces the χ2 associated with the current set of parameter values, and the fitted parameters uncertainty estimates even if the fit quits early. Leave it to the GUI to decide how to inform the user on the basis of fitting_result.success rather than throwing away the information that we have.

For now you can reproduce current behaviour by adding:

# TODO: Let the GUI decided how to handle success/failure.
if not fitting_result.success:
    fitting_result.stderr[:] = np.NaN
    fitting_result.fitness = np.NaN

There are other code simplifications that could be done but this is enough for now.

@gonzalezma
Copy link
Contributor Author

gonzalezma commented Nov 17, 2022

The problem is that we may have some ambiguous situations, where it is not completely clear the user intention. Some examples and the current behaviour of SasView are:

  1. M1 = 1 (not selected for fitting), M2 = 2 (not selected for fitting), M2 = M1
    ==> Should M2 be set equal to 1 or 2?
    Currently, in SasView it is not possible to define the constraint M2=M1 from the menu (as only fittable parameters are shown in the LHS of the constraint menu), but it is possible to edit a line to add such constraint. In this case, the constraint will be applied and M2 = 1.

  2. M1 = 1 (not selected for fitting), M2 = 2 (selected for fitting), M2 = M1
    ==> Should M2 be a free parameter (and therefore the constraint ignored)?
    ==> Should the constraint take preference and thus M2 be fixed to 1? This is what SasView does now.
    ==> Should the constraint take even more preference and assume that M1 should be fitted as well, so that M2 = M1 = fitted value?

In any case, the current behaviour does not seem unreasonable to me, so I would vote to keep it as it is. The errors that we have observed do not appear during the fitting, but in the post-processing of the uncertainties. I will try the new suggestion from Paul K. Otherwise, I will go back to a simple try/except block to take different actions depending if M1 is just a number (fixed parameter) or an uncertainty object (fitted param). This works also, and it is not very clear to me which information we would be throwing away.

@gonzalezma
Copy link
Contributor Author

I applied the suggested changes and tested. It works fine. Only minor drawback that I found: Now the only sign that something "strange" with the constraints definitions could be happening, is that the error of the tied parameter will appear as 0 in the corresponding FitPage. We can discuss if it is needed to check this and send a explicit warning to the log explorer asking the user to verify the fit conditions.

@pkienzle
Copy link
Contributor

The existing constraint system is purely assignment. With a mathematical constraint such as R' = (R^2 L)^{1/3} you could imagine that any of pair of R', R, and L would be fittable and you could solve for the remaining parameter, but the code is not that clever. Instead you need to structure your constraint so that the fitted parameters are on the RHS, with the LHS computed from their values, and never fitted. In that context there is no ambiguity: M1 = M2 should be interpreted as M1 is assigned the value of M2, with M1 never fitted.

Given that constraints are only being used to tie parameters between models and almost(?) never to calculate arbitrary expressions you could scrap the existing GUI and replace it with something easier to control. This could build up the set of expressions needed to apply the constraints. That is outside the scope of this PR and better left as a discussion.

@rozyczko
Copy link
Member

  1. Create two constraints, one of nonpolydisp. params and one of polydisp. params. E.g. M2:radius.width = M1.radius.width and M2:radius = M1.radius.
  2. Go to M2 and right click -> Edit constraint on the radius.width row. This works.
  3. Go to M2 and right click -> Edit constraint on the non-pd radius row.
11:42:16 - ERROR: Traceback (most recent call last):
  File "D:\projects\sasview_new\src\sas\qtgui\Perspectives\Fitting\FittingWidget.py", line 1050, in editConstraint
    if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
  File "D:\projects\sasview_new\src\sas\qtgui\Perspectives\Fitting\FittingWidget.py", line 1050, in <listcomp>
    if not np.any([FittingUtilities.isParamPolydisperse(p, self.model_parameters, is2D=self.is2D) for p in params_list]):
  File "D:\projects\sasview_new\src\sas\qtgui\Perspectives\Fitting\FittingUtilities.py", line 967, in isParamPolydisperse
    if '.width' in param_name:
TypeError: argument of type 'NoneType' is not iterable

You are probably trying to query all constraints based on the model of the first constraint rather than on both models?

@gonzalezma
Copy link
Contributor Author

@rozyczko Yes, this is something that I already noted on the code camp, but did not try to fix yet. I described the problem in issue 2355. All the work and testing in this PR was done using only the Const. & Simul. Fit tab. It's in my plans to try to solve that issue too, but I don't know when I'll have time. In the meantime, I would argue that this PR fixes the main problem of being able to constraint also polydisp. parameters and that I expect most users will use this functionality from the simultaneous tab, so issue 2355 is a minor problem and should not delay merging this PR. To discuss in the next call?

@rozyczko
Copy link
Member

@rozyczko Yes, this is something that I already noted on the code camp, but did not try to fix yet. I described the problem in issue 2355.

Ah! OK then, as long as we have this listed as an issue then I agree it's better to have the functionality in, before the codebase diverges and we have to spend time solving merge conflicts again, considering how big this PR is.

@wpotrzebowski wpotrzebowski added Discuss At The Call Issues to be discussed at the fortnightly call and removed Discuss At The Call Issues to be discussed at the fortnightly call labels Nov 21, 2022
@wpotrzebowski
Copy link
Contributor

As discussed at the biweekly call we are merging this PR and saving related issues/discussion for the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5.0 no constraint for polydispersity ?
7 participants