Skip to content

q_to_tth & tth_to_q #178

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 11 commits into from
Nov 28, 2024
Merged

q_to_tth & tth_to_q #178

merged 11 commits into from
Nov 28, 2024

Conversation

yucongalicechen
Copy link
Contributor

closes #174
@sbillinge ready for review!

Copy link

codecov bot commented Nov 15, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 99.62%. Comparing base (9789eac) to head (d481253).
Report is 17 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #178      +/-   ##
==========================================
+ Coverage   99.17%   99.62%   +0.44%     
==========================================
  Files           7        7              
  Lines         242      265      +23     
==========================================
+ Hits          240      264      +24     
+ Misses          2        1       -1     
Files with missing lines Coverage Δ
...ils/scattering_objects/test_diffraction_objects.py 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls see comments inline. Per our earlier convo, I think we should be working on these in the tests first. Let's converge the tests first, then write the code. What is the behavior that we want to handle? I know it is something like tth exceeding 180 deg, but when would this come about in practice? We only need to handle it if it is a user-facing issue, so what would a user do where this would be encountered? then how do we want the code to behave to alert the user?

"""
q = self.on_q[0]
q = np.asarray(q)
wavelength = float(self.wavelength)
pre_factor = wavelength / (4 * np.pi)
if np.any(np.abs(q * pre_factor) > 1):
raise ValueError(
"Invalid input for arcsin: some values exceed the range [-1, 1]. Check wavelength or q values."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the users will not have a clear idea what has gone wrong and how to fix it from this. I think we should not raise a ValueError, but just let the math fail, but overload the error message with advice about how to fix it.

of ``wavelength``
"""
two_theta = np.asarray(np.deg2rad(self.on_tth[0]))
if np.any(two_theta > np.pi):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above, I think this isn't doing quite what we want.

"""
q = self.on_q[0]
q = np.asarray(q)
wavelength = float(self.wavelength)
pre_factor = wavelength / (4 * np.pi)
if np.any(np.abs(q * pre_factor) > 1):
raise ValueError("Please check if you entered an incorrect wavelength or q value.")
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 think we would want to raise a value error here because otherwise the code proceeds with tth = nan.

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's first work on the test and then the code, so don't modify the code yet, just the test. The test should test the error message that is sent to users under different situations. The most likely one we discussed is that they give a wrong wavelength. They could also give no wavelength. What behavior do we want then? Less likely but possible is they give a tth array that goes above 180. What behavior do we want then?

@yucongalicechen
Copy link
Contributor Author

Let's first work on the test and then the code, so don't modify the code yet, just the test. The test should test the error message that is sent to users under different situations. The most likely one we discussed is that they give a wrong wavelength. They could also give no wavelength. What behavior do we want then? Less likely but possible is they give a tth array that goes above 180. What behavior do we want then?

Yes, sorry, I added more tests now. @sbillinge it looks like in DO tth is implemented on degree scale only, shall we make another issue to allow tth to compute in radian?

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see inline

actual = DiffractionObject(wavelength=4 * np.pi)
setattr(actual, "on_q", [[0.6, 0.8, 1, 1.2], [1, 2, 3, 4]])
params_q_to_tth_bad = [
# UC1: user did not specify wavelength
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to not allow missing wavelengths? This makes these DOs harder to use, but also makes them less useful. There may be a middle ground where we allow it, but let the user know that much of the functionality goes away without it. We could also trigger a workflow that requests the wavelength, but still allows users to override that. This would encourage them to enter a wavelength but not insist?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this sounds good. I think user can also directly set attributes so that they can use DO without a wavelength. I would suggest to prompt user inputs in the insert_scattering_quantity function instead of this one when it wants to set arrays on all tth/q/dspace, so that it can avoid calling these functions?
When people use this function directly we would want them to speicify a wavelength, so it's better if we raise an error?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, how about we just print a warning message if no wavelength is supplied. Something like "INFO: no wavelength has been specified. You can continue to use the DiffractionObject but some of it's powerful features will not be available. To specify a wavelength...."

# UC2: user specified invalid q values that result in tth > 180 degrees
(
[4 * np.pi, [0.2, 0.4, 0.6, 0.8, 1, 1.2]],
"Wavelength * q > 4 * pi. Please check if you entered an incorrect wavelength or q value.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this behavior is good. The first sentence is a bit mathematical and cryptic. Is there a more "chemist friendly" way of saying it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does "The combination of wavelength and q values is too large" sound good?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about: "the supplied q-array and wavelength will result in an impossible two-theta. Please check these values and re-instantiate the DiffractionObject"

[100, [0, 0.2, 0.4, 0.6, 0.8, 1]],
"Wavelength * q > 4 * pi. Please check if you entered an incorrect wavelength or q value.",
),
# UC4: user specified an empty q array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ok to specify no q-array (you can instantiate on tth for example). Presumably this is only an error if the the DO is being created with q data. So the behavior is ok, but we may want to tweak the error message. I think a more general error is if the x and y arrays are not the same length, whether they are q, tth or d. I suggest to handle that more general case?

Finally, do we also want to specify what error is raised as well as the message?

@yucongalicechen
Copy link
Contributor Author

@sbillinge ready for review

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see inline

"INFO: no wavelength has been specified. You can continue "
"to use the DiffractionObject but some of its powerful features "
"will not be available. To specify a wavelength, you can use "
"DiffractionObject(wavelength=0.71)."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.71? Or something like "wavelength=) where you replace with the actual wavelength in the units of..." Etc. Also, are these the correct instructions? This seems to be instantiating a new DO not adding a wavelength to an existing DO.

Let's also make sure to add these instructions to the documentation.

[
ValueError,
"The supplied q-array and wavelength will result in an impossible two-theta. "
"Please check these values and re-instantiate the DiffractionObject.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add ."... With correct values"

[100, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5, 6]],
[
ValueError,
"The supplied q-array and wavelength will result in an impossible two-theta. "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above. Also since we are reusing the error message do we want to minimize word by defining it in a variable once and reusing the variable in multiple tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I will store them in diffraction_objects.py file

[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, "invalid"]],
"Invalid value found in q array. Please ensure all values are numeric.",
[4 * np.pi, [0, 0.2, 0.4, 0.6, 0.8, 1], [1, 2, 3, 4, 5]],
[IndexError, "Please ensure q array and intensity array are the same length."],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think RunTime wrote may be more appropriate here since I think we will raise it ourselves?

@yucongalicechen
Copy link
Contributor Author

@sbillinge thanks for the comments. Since the tests are almost there I'll start implementing the functions.

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good now good to go on the code.

@@ -231,6 +231,158 @@ def test_diffraction_objects_equality(inputs1, inputs2, expected):
assert (diffraction_object1 == diffraction_object2) == expected


def _test_valid_diffraction_objects(actual_diffraction_object, function, expected_array):
"""Checks the behavior of the DiffractionObject:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test functions don't need docstring, though if it's ok to leave this now you did it.

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks good. this needs a news and possibly some update in the docs. You don't have to explain error messages in docs, but it might be nice to explain correct usage.

@yucongalicechen
Copy link
Contributor Author

looks good. this needs a news and possibly some update in the docs. You don't have to explain error messages in docs, but it might be nice to explain correct usage.

@sbillinge I add the news. I can add another issue on adding example/utility for functionalities we updated this PR?

@sbillinge
Copy link
Contributor

looks good. this needs a news and possibly some update in the docs. You don't have to explain error messages in docs, but it might be nice to explain correct usage.

@sbillinge I add the news. I can add another issue on adding example/utility for functionalities we updated this PR?

Why not just do it on this PR?

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nicely done there! Please see my comments and then I can merge this.


# convert q to tth
from diffpy.utils.scattering_objects.diffraction_objects import DiffractionObject
test = DiffractionObject(wavelength=1.54)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of test let's call it my_diffraction_pattern

We are not writing all of the docs here, so you can start with something like "assuming we have created a DiffractionObject called my_diffraction_pattern from a measured diffraction pattern, and we have specified the wavelenth (see Section ??) we can use the q_to_tth and tth_to_q functions to convert between Q and two-theta by typing my_diffraction_pattern.q_to_tth()..." ..and so on. Let's make an issue to add to docs "How to set a wavelength" and how to instantiate a diffraction object and how to add a diffraction pattern.

for managing and analyzing diffraction data, including angle-space conversions
and interactions between diffraction data.

- ``q_to_tth()``: Converts an array of q values to their corresponding two theta values, based on specified wavelength.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this here. It will be auto-generated in the API docs, and it is shown as an aexample above.

@yucongalicechen
Copy link
Contributor Author

@sbillinge ready for review! I added an issue for additional docs (#183).

Copy link
Contributor

@sbillinge sbillinge left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need these methods at all, at least as public methods. Please see my comment.

To load the converted array, you can either call ``test.q_to_tth()`` or ``test.on_q[0]``.

Similarly, use the function ``tth_to_q`` to convert two theta values in degrees to q values. ::
To load the converted array, you can either call ``test.q_to_tth()`` or ``test.on_q[0]``. ::
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is confusing. Doesn't this function do it in place and just set the tth array? In this respect, shouldn't this just be a private function and not used by the user at all? In other words, I would load my diffraction data into the object and the object automatically populates all the different arrays. So to get my data on q if I loaded it on tth I would just do my_diffraction_data.on_q

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 think this is a private function for now (I didn't see the "_" in the function name)? We need to call insert_scattering_quantity in order for it to populate on all arrays automatically. If they just do my_diffraction_pattern = DiffractionObjects() and my_diffraction_pattern.on_q = ... it is not automatically populated to tth.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but if they do that they are just instantiating an empty DO so there is nothing to populate anywhere..... So that is desired behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is what behavior do we want? I want to be able to get my intensity data on all the different x-grids, but I can do that by typing my_pattern.on_q orwhatever. What is the UC where I would want to run that function?

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 see. Does this sound better?

Assuming we have created a DiffractionObject called my_diffraction_pattern from a measured diffraction pattern, and we have specified the wavelength (see Section ??, to be added), we can use the q_to_tth and tth_to_q functions to convert between q and two-theta. For example, my_diffraction_pattern.q_to_tth() converts q to two-theta, while my_diffraction_pattern.tth_to_q() converts two-theta to q. The converted array can be accessed using by calling my_diffraction_pattern.on_q[0] for q, or my_diffraction_pattern.on_tth[0] for two-theta.

@sbillinge sbillinge merged commit d3f83b2 into diffpy:main Nov 28, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

differaction objects improve functions q_to_tth, tth_to_q
2 participants