Skip to content

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

Merged
merged 33 commits into from
Jun 19, 2025

Conversation

esmith1729
Copy link
Contributor

@pull-request-size pull-request-size bot added size/L and removed size/M labels Feb 5, 2025
@esmith1729 esmith1729 linked an issue Feb 6, 2025 that may be closed by this pull request
5 tasks
@esmith1729 esmith1729 changed the title Initial polarization func and test Polarization/asymmetry function Feb 6, 2025
Copy link
Member

@Tom-Willemsen Tom-Willemsen left a 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
Copy link
Member

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),
Copy link
Member

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.

Comment on lines 956 to 957
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")
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

@esmith1729 esmith1729 Jun 5, 2025

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.

Comment on lines 262 to 277
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.
Copy link
Member

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 - but scipp 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]

Copy link
Member

@Tom-Willemsen Tom-Willemsen left a 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...

Muon Instruments
A and B refer to Measurements from different detector banks.

[`polarization`](ibex_bluesky_core.devices.simpledae.polarization)
Copy link
Member

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?

Comment on lines +983 to +1001
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
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@jackbdoughty jackbdoughty changed the title Polarization/asymmetry function Add function which calculates Polarization & propagating uncertainties Jun 19, 2025
@jackbdoughty jackbdoughty merged commit 240ba51 into main Jun 19, 2025
13 checks passed
@jackbdoughty jackbdoughty deleted the 58_polarization_asymmetry branch June 19, 2025 14:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Polarization/asymmetry
3 participants