-
Notifications
You must be signed in to change notification settings - Fork 24
change: Handle uncertainties as None rather than 1s
#193
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
Changes from all commits
1eb61e2
fa2f373
8a4900f
74cc053
b0cac01
3a52223
04bb2f4
c248fe7
0383095
f5478d1
29ce00d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| **Added:** | ||
|
|
||
| * <news item> | ||
|
|
||
| **Changed:** | ||
|
|
||
| * Change ``ProfileParser.parse_file`` to set unavailable ``dx``/``dy`` uncertainties to ``None`` instead of ``0``, matching the format-specific parsers. | ||
| * Change ``Profile.set_observed_profile`` to leave ``dyobs`` as ``None`` when no uncertainties are observed, instead of silently replacing them with an array of ones. The calculated ``dy`` still defaults to 1 at every calculation point, so unweighted fits refine exactly as before. | ||
| * Change ``Profile._validate`` to accept a ``None`` ``dyobs``, since observed uncertainties are optional. ``x``, ``y``, ``dy``, ``xobs`` and ``yobs`` are still required. | ||
|
|
||
| **Deprecated:** | ||
|
|
||
| * <news item> | ||
|
|
||
| **Removed:** | ||
|
|
||
| * <news item> | ||
|
|
||
| **Fixed:** | ||
|
|
||
| * Fix ``ProfileParser.parse_file`` producing zero-valued uncertainties for absent or invalid ``dx``/``dy`` columns, which gave non-finite residuals for any file without usable uncertainties instead of falling back to an unweighted fit. | ||
| * Fix ``Profile.dyobs`` reporting an array of ones for data that carries no uncertainties, which made an unweighted profile indistinguishable from one whose uncertainties were genuinely all 1. | ||
|
|
||
| **Security:** | ||
|
|
||
| * <news item> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,7 +20,7 @@ | |
| import matplotlib.pyplot as plt | ||
| import numpy as np | ||
| import pytest | ||
| from numpy import array_equal, dot, linspace, pi, sin | ||
| from numpy import array_equal, dot, linspace, ones_like, pi, sin | ||
| from scipy.optimize import leastsq | ||
|
|
||
| from diffpy.srfit.fitbase import FitResults, ProfileParser | ||
|
|
@@ -516,7 +516,7 @@ def test_initialize_recipe_from_recipe(build_recipes_one_contribution): | |
| assert sorted(list(expected_values)) == sorted(list(actual_values)) | ||
|
|
||
|
|
||
| def test_initialize_recipe_from_recipe_bad(build_recipe_two_contributions): | ||
| def test_initialize_recipe_from_recipe_bad(): | ||
| # Case: User tries to initialize a FitRecipe from a non recipe object | ||
| # expected: raised ValueError with message | ||
| recipe_bad = 12345 # not a FitRecipe object | ||
|
|
@@ -1060,5 +1060,86 @@ def test_plot_recipe_reset_all_defaults(build_recipes_one_contribution): | |
| assert actual_legend == expected_legend | ||
|
|
||
|
|
||
| # Observed uncertainties are optional. A profile loaded without an | ||
| # uncertainty column keeps dyobs as None and is refined unweighted, with | ||
| # dy falling back to one at every calculation point. The cases below | ||
| # refine the same noiseless sine profile so that the known solution | ||
| # (A=1, k=1, c=0) is recovered no matter how the fit is weighted. | ||
|
|
||
|
|
||
| # make_input_dyobs returns a function that evaluates dyobs on the array | ||
| # of xobs for insertion into the build_recipe_with_uncertainty fixture. | ||
| @pytest.mark.parametrize( | ||
| "make_input_dyobs, expected_dyobs_is_set", | ||
| [ | ||
| # C1: No uncertainties are observed, as for a file with no | ||
| # uncertainty column. | ||
| # Expected: The refinement converges and dyobs stays None. | ||
| (lambda xobs: None, False), | ||
| # C2: Uniform uncertainties of one are observed explicitly. | ||
| # Expected: The refinement converges to the same values as C1, | ||
| # since an unweighted fit is weighted by one everywhere. | ||
| (lambda xobs: ones_like(xobs), True), | ||
| # C3: Uncertainties vary across the profile, so the refinement | ||
| # is weighted point by point. | ||
| # Expected: The refinement still converges to the known values. | ||
| (lambda xobs: linspace(0.1, 1.0, len(xobs)), True), | ||
| ], | ||
| ) | ||
| def test_refine_with_and_without_uncertainty( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This tests that the refinement still works with or without uncertainties |
||
| build_recipe_with_uncertainty, make_input_dyobs, expected_dyobs_is_set | ||
| ): | ||
| recipe, profile = build_recipe_with_uncertainty(make_input_dyobs) | ||
| optimize_recipe(recipe) | ||
| actual_dyobs_is_set = profile.dyobs is not None | ||
| actual_names = recipe.get_names() | ||
| actual_values = recipe.get_values() | ||
| expected_names = ["A", "k", "c"] | ||
| expected_values = [1.0, 1.0, 0.0] | ||
| assert actual_dyobs_is_set == expected_dyobs_is_set | ||
| assert actual_names == expected_names | ||
| assert actual_values == pytest.approx(expected_values, abs=1e-5) | ||
|
|
||
|
|
||
| # The residual that is optimized is (ycalc - y)/dy, so dy is what | ||
| # actually weights a refinement. These cases check that a profile with | ||
| # no observed uncertainties is weighted identically to one whose | ||
| # uncertainties are all one, and that observed uncertainties are carried | ||
| # through to the residual unchanged. | ||
|
|
||
|
|
||
| # make_input_dyobs returns a function that evaluates dyobs on the array | ||
| # of xobs for insertion into the build_recipe_with_uncertainty fixture. | ||
| @pytest.mark.parametrize( | ||
| "make_input_dyobs, make_expected_dy", | ||
| [ | ||
| # C1: No uncertainties are observed. | ||
| # Expected: dy falls back to one everywhere, so the residual is | ||
| # unweighted. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. copy paste the comment from above here in case in the future we are looking just at this test case. |
||
| (lambda xobs: None, lambda xobs: ones_like(xobs)), | ||
| # C2: Uniform uncertainties of one are observed explicitly. | ||
| # Expected: dy is one everywhere, matching C1. | ||
| (lambda xobs: ones_like(xobs), lambda xobs: ones_like(xobs)), | ||
| # C3: Uncertainties vary across the profile. | ||
| # Expected: dy keeps the observed values, so the residual is | ||
| # weighted point by point. | ||
| ( | ||
| lambda xobs: linspace(0.1, 1.0, len(xobs)), | ||
| lambda xobs: linspace(0.1, 1.0, len(xobs)), | ||
| ), | ||
| ], | ||
| ) | ||
| def test_residual_is_weighted_by_uncertainty( | ||
| build_recipe_with_uncertainty, make_input_dyobs, make_expected_dy | ||
| ): | ||
| recipe, profile = build_recipe_with_uncertainty(make_input_dyobs) | ||
| actual_residual = recipe.residual(recipe.values) | ||
| actual_dy = profile.dy | ||
| expected_dy = make_expected_dy(profile.xobs) | ||
| expected_residual = (profile.ycalc - profile.y) / expected_dy | ||
| assert actual_dy == pytest.approx(expected_dy) | ||
| assert actual_residual == pytest.approx(expected_residual) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() | ||
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.
add a comment; "return a function that evaluates dyobs on the array of xobs for insertion into the build_recipe_with_uncertainty fixture."