-
Notifications
You must be signed in to change notification settings - Fork 0
Add function which calculates Polarization & propagating uncertainties #97
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
Conversation
esmith1729
commented
Jan 30, 2025
- Polarization/asymmetry #58
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.
Most of these things are relatively minor - core implementation looks good.
("a", "b", "variance_a", "variance_b", "expected_value", "expected_uncertainty"), | ||
[ | ||
# Case 1: Symmetric case with equal uncertainties | ||
(5.0, 3.0, 0.1, 0.1, 0.25, 0.05762215285808055), # comment where tf this number came from |
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.
comment where tf this number came from
good idea
# Case 2: Asymmetric case with different uncertainties | ||
(10.0, 6.0, 0.2, 0.3, 0.25, 0.04764984588117782), | ||
# Case 3: Case with larger values and different uncertainty magnitudes | ||
(100.0, 60.0, 1.0, 2.0, 0.25, 0.0120017902310447), |
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.
In our chat we also had some "simple" example cases - the sort which could be worked out easily "in your head". Those would make good test cases to add here.
var_a = sc.Variable(dims=[], values=a, variances=variance_a, unit="", dtype="float64") | ||
var_b = sc.Variable(dims=[], values=b, variances=variance_b, unit="", dtype="float64") |
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.
From https://scipp.github.io/generated/classes/scipp.Variable.html#scipp.Variable:
This constructor is meant primarily for internal use. Use one of the Specialized creation functions instead. See in particular scipp.array() and scipp.scalar().
(scalar is what you want here) - it still gives you a sc.Variable
object in the end though.
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 added sc.scalar in the tests where it had Variable... there was an error arising in the assert result_value == pytest.approx(expected_value)
lines, but I changed it to assert result_value.value == pytest.approx(expected_value)
instead, and that seems to have silenced Pylance at least
polarization(var_a, var_b) | ||
|
||
|
||
# test cases uncert = 0 |
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.mark.parametrize( | ||
("a", "b", "variances_a", "variances_b", "expected_values", "expected_uncertainties"), | ||
[ | ||
# Case 1: Symmetric case with equal uncertainties |
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 sure what "symmetric" means here - could you clarify?
Also the equal uncertainties seems not true.
Copy/paste?
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.
symmetric means that the uncertainty values for both a and b are the same. It seems not true in that the value is wrong? I need to check, yes this was copy/paste but I think it might have been correlated uncertainty.
doc/devices/dae.md
Outdated
Polarization refers to the property of transverse waves which specifies the geometrical orientation of the | ||
oscillations. | ||
|
||
The polarization funtion provided will calculate the polarization between two values, A and B, which | ||
have different definitions based on the instrument context. | ||
|
||
Instrument-Specific Interpretations | ||
SANS Instruments (e.g., LARMOR) | ||
A: Intensity in DAE period before switching a flipper. | ||
B: Intensity in DAE period after switching a flipper. | ||
|
||
Reflectometry Instruments (e.g., POLREF) | ||
Similar to LARMOR, A and B represent intensities before and after flipper switching. | ||
|
||
Muon Instruments | ||
A and B refer to Measurements from different detector banks. |
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 think we need to re-word this section a bit. Not your fault, this is kind of specific science knowledge here - which I'm no expert on myself but have picked up bits & pieces from talking to scientists.
How about something like:
Polarization/asymmetry
ibex_bluesky_core
provides a helper method,
{py:obj}ibex_bluesky_core.devices.simpledae.reducers.polarization
, for calculating the quantity(a-b)/(a+b)
. This quantity is used, for example, in neutron polarization measurements, and in calculating asymmetry for muon measurements.For this expression,
scipp
's default uncertainty propagation rules cannot be used as the uncertainties on(a-b)
are correlated with those of(a+b)
in the division step - butscipp
assumes uncorrelated data. This helper method calculates the uncertainties following linear error propagation theory, using the partial derivatives of the above expression.The partial derivatives are:
[INSERT MATHS HERE - and see the fitting models docs pages for how to render nice maths equations in docs]
Which then means the variances computed by this helper function are:
[INSERT MATHS HERE]
Co-authored-by: Tom Willemsen <tom.willemsen@stfc.ac.uk>
Co-authored-by: Tom Willemsen <tom.willemsen@stfc.ac.uk>
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.
Couple of tiny comments and then we're there I think...
doc/devices/dae.md
Outdated
Muon Instruments | ||
A and B refer to Measurements from different detector banks. | ||
|
||
[`polarization`](ibex_bluesky_core.devices.simpledae.polarization) |
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 sure this is needed - there's a link to the function in the first sentence?
a_ufloat = ufloat(a, variance_a) | ||
b_ufloat = ufloat(b, variance_b) | ||
|
||
# polarization value, i.e. (a - b) / (a + b) | ||
polarization_ufloat = (a_ufloat.n - b_ufloat.n) / (a_ufloat.n + b_ufloat.n) | ||
|
||
# the partial derivatives of a and b, calculated with 'uncertainties' library's ufloat type | ||
partial_a = (2 * b_ufloat.n) / ((a_ufloat.n + b_ufloat.n) ** 2) | ||
partial_b = (-2 * a_ufloat.n) / ((a_ufloat.n + b_ufloat.n) ** 2) | ||
|
||
# variance calculated with 'uncertainties' library | ||
variance = (partial_a**2 * a_ufloat.s) + (partial_b**2 * b_ufloat.s) | ||
uncertainty = variance**0.5 # uncertainty is sqrt of variance | ||
|
||
# Two scipp scalars, to test our polarization function | ||
var_a = sc.scalar(value=a, variance=variance_a, unit="", dtype="float64") | ||
var_b = sc.scalar(value=b, variance=variance_b, unit="", dtype="float64") | ||
result_value = polarization(var_a, var_b) | ||
result_uncertainy = (result_value.variance) ** 0.5 # uncertainty is sqrt of variance |
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 quite understand what's going on in this test; we shouldn't need partial derivatives with the uncertainties library as that's what the library is doing for us. I think conceptually it should be:
a_ufloat = ...
b_ufloat = ...
a_scipp = ...
b_scipp = ...
pol_ufloat = (a-b)/(a+b)
pol_scipp = polarization(a, b)
assert pol_ufloat.nominal == pol_scipp.value
assert pol_ufloat.stddev == sqrt(pol_scipp.variance)
i.e. similar to the test below
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.
whoops yes sorry I meant to remove the unnecessary bits and forgot, will do it now.
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.
Actually sorry, pretty sure we need those; when calculating the polarization from the nominal values, the variances do not get carried into that value, so we need to calculate it in long form. It was easier with the arrays, because it was just adding an array of ufloats. But in this test we are calculating with the ufloat's nominal values themselves.
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 whatever reason, ufloat library really didn't like doing this
polarization_ufloat = (a_ufloat - b_ufloat) / (a_ufloat + b_ufloat)
And needs it to be nominal values.... which then creates polarization_ufloat
as a "float" and not a ufloat
. We could possibly get around this by instead making two arrays of one element, sort of copying the array test, but it might be more confusing in future for the next person looking at this code to do it that way.