-
Notifications
You must be signed in to change notification settings - Fork 17
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
Improve test_finite_difference and test_fit_result #289
Conversation
The test test_constrained/test_constrained_dependent_on_matrixmodel didn't fail because of this PR. If I clone the master branch, the test doesn't work either. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy holidays :)
I have some comments for you on test_finite_difference so far. I'm not completely happy with test_fit_result either, but I don't quite see how it should be done. Could you look up how Pytest deals with fixtures that return "multiple" items? That way all the *_result
fixtures could be condensed into one. Maybe you can do that by doing something like:
@pytest.mark.parametrize('..., minimizer',
[
(..., MINPACK),
(..., [BFGS, NelderMead),
(..., BasinHopping),
...
]
)
@pytest.fixture()
def fit_result(...)
In addition, do we really need all the Parameter/Variable fixtures? Maybe we do, I'm not sure. If so, we can consider creating an alphabet in conf.py
. which pytest will magically import.
I could compress the fixtures to three: parameters a, b and the result_dict. In this case, all the results would be built in a single fixture, which returns a dict with all of them. By the way, I inquire about Fixtures with several return-items. |
I stick to the solution with the dictionary after long research :D |
Could you share some of your considerations? That way we can have a short discussion about it here, without me also having to do the research. |
Just for clearness: You want to create a parametrized fixture. The params of the fixture are the different results of the models. Further, the fixture will be applied to different tests. But now it is difficult to create tests, with each model having a different comparison partner. In this case, there should be a new parallel parameterization. Besides, models often have to be left out (or only one model is tested) and in my eyes, you quickly lose the overview. The tests are not only made for TravisCI, but also for people who need to understand :D In my eyes, the dictionary allows models to be added speedy, tests to be implemented easily and clearly, and bugs to be viewed quickly. |
Very clear, thanks for the write-up. To sum up: the tests are too diverse? Feel free to reorganise/change the tests so that they become more uniform, if you think that makes sense. As for the tests in finite_difference, we'll need to find a way to use models in |
On the one hand it's difficult to uniform the tests. For example, only two results are using constraints. We have to separate them. Also On the other hand the tests can be more uniformed than I initially thought. In some cases, a model didn't mention even though it would be passed. EDIT: I thought a long time, that it's not possible to put fixtures in a parametrizations, because it was a feature request some years ago, but It didn't realise that an offshoot of Pytest can do it. Now I would have with pytest-cases some new possibilities, for example to replace the |
I never said it would be easy :) |
It's never easy ;-) Some ideas...
assert isinstance(result_name.minimizer, result_minimizer)
if isinstance(result_name.minimizer, ChainedMinimizer):
for minimizer, cls is zip(result_name.minimizer.minimizers, [BFGS, NelderMead]):
assert isinstance(minimizer, cls) (Maybe we could connect the |
Sure. You could even do a
Not necessarily. Maybe in the current tests, but it's not generally true.
I then prefer kicking all chained minimizer tests to either a separate test, or even a separate file. As you said earlier: making the tests understandable is at least as important. |
Allright....Then I have ean idea of the new test_fit_result file :) But I won't work on it until next year :P Guten Rutsch :) |
Hey, Does it make sense to continue working on this PR, or have there been many new tests in the meantime? |
Heyhey, it's been quiet here as well. If you want to pick this up again then by all means go ahead. |
Bevor I start, I want to sum up the requested changes: test_finite_difference test_fit_result So far correct? |
I think so, yes. @pytest.mark.parametrize("model, expected_values, initial_values, xdata, noise", (
[a*x**2 + b*x + c, {'a': 1, 'b': 2, 'c': 3}, {'a': 0, 'b': 0, 'c': 0}, {'x': np.arange(10)}, 0.05]
)
def test_something(model, expected_values, initial_values, xdata, noise):
if not initial_values:
initial_values = expected_values.copy()
y_data = model(**xdata, **expected_values)
y_data *= np.random.normal(loc=1, scale=noise, shape=y_data.shape) # doesn't actually work, but you get the idea
... # more things here
assert result == expected_values |
Alright! |
* Added HadamardPower printing * Import HadamardPower only for sympy.__version__ > (1,4) * Fixed the HadamardPower printer. Co-authored-by: Martin Roelfs <u0114255@kuleuven.be>
Adjust auto_variables import
I removed TC and COBYLA from no_constraints for the moment. I will open an issue about wrapping the remaining scipy methods in the next days. About removing TC + CNM: Pytest implemented skipif to exclude a parametrized testcases under defined circumstances. But we can't parametrize one variable (eg constraints) and exclude the combination TC + CMN, because our "circumstances" refer to another parametrize variable (eg minimizer). small_constraint = [Le(a,b)]
big_constraint = [
Le(a, b),
CallableNumericalModel.as_constraint(
{z: ge_constraint}, connectivity_mapping={z: {a}},
constraint_type=Ge, model=Model({y: a * x ** b})
)
]
@pytest.mark.parametrize('minimizer, constraints',
# Added None to check the defaul objective
# Removed Truple with TrustConstr, because it cannot handle CallableNumericalModel
[(min, constr) for (min, constr) in itertools.product(list(subclasses(ConstrainedMinimizer) | {None}), [small_constraint, big_constraint]) \
if (min,constr) != (TrustConstr, big_constraint)]
)
... Another idea is a separate test for TrustConstr with constraints :D Concerning LL: |
Heyhey!
I don't really like the code snippet you suggest, since it puts a lot of logic in the "wrong" place. Separating TC + constraints is almost the correct idea I think: it's not TC+constraints, or even TC+CNM that's the exception, but CallableNumericalModel constraints are the exception. In summary, test 1: all constrained minimizers with symbolic constraints; test 2: all constrained minimizers except TC with CNM constraints. You may want to create a separate helper function that performs the actual test. |
Insert MINPACK test comments and alignments
Should I open another issue because of LBFGSB (next to the issue with TrustConst and COBYLA) ? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice! A few small comments and then I think I'm actually happy. Before then I'll also go over all the changes again to refresh my memory though.
- Could you move _run_tests to the top of the file?
- For all tests you have
expected_got
, did you meanexpected_gof
(goodness of fit)? - Do we also want to test with
objective=None
? Or is that more for testing the Fit object, i.e. another test file/PR.
Sounds good to me. |
It's propably a typo :) sorry for that! :D
I think I've added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there! There's still a few things to polish off.
Also, I don't see any place where you have None
as objective. You do have it as minimizer though. Could you add a case or 2?
Once this is done this is suddenly a very thorough test set, I'm very happy with it!
# Test objective LogLikelihood | ||
(dict(objective=LogLikelihood, x=np.linspace(1, 10, 10)), {a: 62.56756, b: 758.08369}, # TODO Adjust x_data | ||
{a: float('nan'), b: float('nan')}, LogLikelihood, | ||
(dict(objective=LogLikelihood, x=np.linspace(1, 10, 10)), # TODO Adjust x_data |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still TODO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kind of. This PR shouldn't be merged before we have good data for LL so that every test passes.
…erged constraints tests
I decided to add an if-clause to the test-methods because we did the same thing with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just went over the complete PR (again), and it's looking great!
Besides the new comments, here's what I think still needs doing:
- Rebase on master
- Find working LL parameters/cases
- Rethink the constrained test cases critically so we can be sure the constraints are doing something.
Very short list :D
requirements.txt
Outdated
scipy >= 1.0 | ||
scipy >= 1.6 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this should be in this PR.
w, x, y, z = sf.variables('w, x, y, z') | ||
a, b, c, d = sf.parameters('a, b, c, d') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As alternative, call initVariables
and initParameters
. And I must say I'm not super thrilled with the camelCase naming of those two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pytest doesn't like calling fixtures directly :/
Fixture "init_variables" called directly. Fixtures are not meant to be called directly,
but are created automatically when test functions request them as parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok. Then leave it as is.
tests/test_fit_result.py
Outdated
import pytest | ||
import pickle | ||
from collections import OrderedDict | ||
|
||
from sympy.core.relational import Eq | ||
|
||
from tests.auto_variables import a, b, x, y, z |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the autofixtures to work, don't you need to import init_parameters
and init_variables
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right 🤔 It's crazy that it worked without it
tests/test_fit_result.py
Outdated
|
||
dumped = pickle.dumps(fit_result) | ||
new_result = pickle.loads(dumped) | ||
assert sorted(fit_result.__dict__.keys()) == sorted(new_result.__dict__.keys()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit
assert sorted(fit_result.__dict__.keys()) == sorted(new_result.__dict__.keys()) | |
assert set(fit_result.__dict__.keys()) == set(new_result.__dict__.keys()) |
tests/test_fit_result.py
Outdated
|
||
|
||
@pytest.mark.parametrize('minimizer', | ||
# Removed TrustConstr and COBYLA because their results differ greatly #TODO add them if fixed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this still true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not anymore 🙌 The results of COBYLA and TrustConstr are correct with more iterations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: TrustConstr runs with LogLikelihood, 1e8 as the maximum number of iterations and no constraints forever. I think that's because of the LL, but we should keep an eye on it.
([Le(a, b)], | ||
dict(x=np.linspace(1, 10, 10), y=3 * np.linspace(1, 10, 10) ** 2), | ||
{a: 2.152889, b: 2.152889}, {a: 0.2371545, b: 0.05076355}, | ||
LeastSquares, | ||
{'r_squared': 0.99791, 'objective_value': 98.83587, 'chi_squared': 197.671746}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not completely sure this is the best possible model, since it really has only 1 minimum. Maybe a case with 2 degenerate global minima, where the constraint selects which one to find would be better? OTOH, this seems to work fine, so leave it.
([CNM_CONSTRAINT], | ||
dict(x=np.linspace(1, 10, 10), y=3 * np.linspace(1, 10, 10) ** 2), | ||
{a: 3, b: 1.9999999}, {a: 1.4795115e-8, b: 2.28628889e-8}, | ||
LeastSquares, | ||
{'r_squared': 1.0, 'objective_value': 1.870722e-13, 'chi_squared': 3.741445e-13}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment here, how can we be sure the CNM_CONSTRAINT does anything, since the outcome is the same as without constraint.
LogLikelihood, | ||
{'objective_value': -float('inf'), 'log_likelihood': float('inf'), 'likelihood': float('inf')}), | ||
# No special objective | ||
([Le(a, b), CNM_CONSTRAINT], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
_run_tests(fit_result, expected_par, expected_std, expected_obj, expected_gof) | ||
|
||
|
||
def test_fitting_2(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you also want to move this to the parametrized version, or rather keep it as is?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not even sure about the benefit of this test :D
Thank you for the review (again) 🚀 |
By using a different Model. For example, if you're not fitting but just minimizing a function, that function can have multiple (local) minima. The combination of initial guesses and constraints will decide which one you'll find. When fitting functions it's a bit harder to see, but I think we can achieve the desired effect by taking a model with degenerate/redundant parameters (but I'm not completely sure here). For example: |
Thank you for the explanation! I'll look what I can do with this new information :) |
Alright :D It doesn't matter what I try the results don't change :)) I'm just not in the topic :) sooorry :) |
No problem. Could you rebase your branch on master? I can then take a look at the LL testcases, and maybe the constrained ones as well. |
Thank you very much for doing this! :) |
Closed because it's old :) |
Hey guys,
I worked on test_finite_difference and test_fit_result so that they correspond more to the pytest style.
Some words to 'test_fit_result.py': It's not possible to use a fixture in Parameters, that's why I used a dict to store an call the results.
I didn't touch the last test, because the converted version would be more confusing than helpful.
Merry Christmas and a Happy New Year