|
20 | 20 | import matplotlib.pyplot as plt |
21 | 21 | import numpy as np |
22 | 22 | import pytest |
23 | | -from numpy import array_equal, dot, linspace, pi, sin |
| 23 | +from numpy import array_equal, dot, linspace, ones_like, pi, sin |
24 | 24 | from scipy.optimize import leastsq |
25 | 25 |
|
26 | 26 | from diffpy.srfit.fitbase import FitResults, ProfileParser |
@@ -516,7 +516,7 @@ def test_initialize_recipe_from_recipe(build_recipes_one_contribution): |
516 | 516 | assert sorted(list(expected_values)) == sorted(list(actual_values)) |
517 | 517 |
|
518 | 518 |
|
519 | | -def test_initialize_recipe_from_recipe_bad(build_recipe_two_contributions): |
| 519 | +def test_initialize_recipe_from_recipe_bad(): |
520 | 520 | # Case: User tries to initialize a FitRecipe from a non recipe object |
521 | 521 | # expected: raised ValueError with message |
522 | 522 | recipe_bad = 12345 # not a FitRecipe object |
@@ -1060,5 +1060,86 @@ def test_plot_recipe_reset_all_defaults(build_recipes_one_contribution): |
1060 | 1060 | assert actual_legend == expected_legend |
1061 | 1061 |
|
1062 | 1062 |
|
| 1063 | +# Observed uncertainties are optional. A profile loaded without an |
| 1064 | +# uncertainty column keeps dyobs as None and is refined unweighted, with |
| 1065 | +# dy falling back to one at every calculation point. The cases below |
| 1066 | +# refine the same noiseless sine profile so that the known solution |
| 1067 | +# (A=1, k=1, c=0) is recovered no matter how the fit is weighted. |
| 1068 | + |
| 1069 | + |
| 1070 | +# make_input_dyobs returns a function that evaluates dyobs on the array |
| 1071 | +# of xobs for insertion into the build_recipe_with_uncertainty fixture. |
| 1072 | +@pytest.mark.parametrize( |
| 1073 | + "make_input_dyobs, expected_dyobs_is_set", |
| 1074 | + [ |
| 1075 | + # C1: No uncertainties are observed, as for a file with no |
| 1076 | + # uncertainty column. |
| 1077 | + # Expected: The refinement converges and dyobs stays None. |
| 1078 | + (lambda xobs: None, False), |
| 1079 | + # C2: Uniform uncertainties of one are observed explicitly. |
| 1080 | + # Expected: The refinement converges to the same values as C1, |
| 1081 | + # since an unweighted fit is weighted by one everywhere. |
| 1082 | + (lambda xobs: ones_like(xobs), True), |
| 1083 | + # C3: Uncertainties vary across the profile, so the refinement |
| 1084 | + # is weighted point by point. |
| 1085 | + # Expected: The refinement still converges to the known values. |
| 1086 | + (lambda xobs: linspace(0.1, 1.0, len(xobs)), True), |
| 1087 | + ], |
| 1088 | +) |
| 1089 | +def test_refine_with_and_without_uncertainty( |
| 1090 | + build_recipe_with_uncertainty, make_input_dyobs, expected_dyobs_is_set |
| 1091 | +): |
| 1092 | + recipe, profile = build_recipe_with_uncertainty(make_input_dyobs) |
| 1093 | + optimize_recipe(recipe) |
| 1094 | + actual_dyobs_is_set = profile.dyobs is not None |
| 1095 | + actual_names = recipe.get_names() |
| 1096 | + actual_values = recipe.get_values() |
| 1097 | + expected_names = ["A", "k", "c"] |
| 1098 | + expected_values = [1.0, 1.0, 0.0] |
| 1099 | + assert actual_dyobs_is_set == expected_dyobs_is_set |
| 1100 | + assert actual_names == expected_names |
| 1101 | + assert actual_values == pytest.approx(expected_values, abs=1e-5) |
| 1102 | + |
| 1103 | + |
| 1104 | +# The residual that is optimized is (ycalc - y)/dy, so dy is what |
| 1105 | +# actually weights a refinement. These cases check that a profile with |
| 1106 | +# no observed uncertainties is weighted identically to one whose |
| 1107 | +# uncertainties are all one, and that observed uncertainties are carried |
| 1108 | +# through to the residual unchanged. |
| 1109 | + |
| 1110 | + |
| 1111 | +# make_input_dyobs returns a function that evaluates dyobs on the array |
| 1112 | +# of xobs for insertion into the build_recipe_with_uncertainty fixture. |
| 1113 | +@pytest.mark.parametrize( |
| 1114 | + "make_input_dyobs, make_expected_dy", |
| 1115 | + [ |
| 1116 | + # C1: No uncertainties are observed. |
| 1117 | + # Expected: dy falls back to one everywhere, so the residual is |
| 1118 | + # unweighted. |
| 1119 | + (lambda xobs: None, lambda xobs: ones_like(xobs)), |
| 1120 | + # C2: Uniform uncertainties of one are observed explicitly. |
| 1121 | + # Expected: dy is one everywhere, matching C1. |
| 1122 | + (lambda xobs: ones_like(xobs), lambda xobs: ones_like(xobs)), |
| 1123 | + # C3: Uncertainties vary across the profile. |
| 1124 | + # Expected: dy keeps the observed values, so the residual is |
| 1125 | + # weighted point by point. |
| 1126 | + ( |
| 1127 | + lambda xobs: linspace(0.1, 1.0, len(xobs)), |
| 1128 | + lambda xobs: linspace(0.1, 1.0, len(xobs)), |
| 1129 | + ), |
| 1130 | + ], |
| 1131 | +) |
| 1132 | +def test_residual_is_weighted_by_uncertainty( |
| 1133 | + build_recipe_with_uncertainty, make_input_dyobs, make_expected_dy |
| 1134 | +): |
| 1135 | + recipe, profile = build_recipe_with_uncertainty(make_input_dyobs) |
| 1136 | + actual_residual = recipe.residual(recipe.values) |
| 1137 | + actual_dy = profile.dy |
| 1138 | + expected_dy = make_expected_dy(profile.xobs) |
| 1139 | + expected_residual = (profile.ycalc - profile.y) / expected_dy |
| 1140 | + assert actual_dy == pytest.approx(expected_dy) |
| 1141 | + assert actual_residual == pytest.approx(expected_residual) |
| 1142 | + |
| 1143 | + |
1063 | 1144 | if __name__ == "__main__": |
1064 | 1145 | unittest.main() |
0 commit comments