Skip to content
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

Simplify AMOR/reflectometry workflow #107

Merged
merged 38 commits into from
Jan 16, 2025
Merged

Simplify AMOR/reflectometry workflow #107

merged 38 commits into from
Jan 16, 2025

Conversation

jokasimr
Copy link
Contributor

@jokasimr jokasimr commented Dec 3, 2024

A while back I realized the reflectometry workflow can be made a lot simpler than it is now.

I think it was worthwhile to do this refactoring even if Amor is not an ESS instrument because the data reduction is very similar to Estia, and this new version serves as a better template for the Estia data reduction.

There are some things to fix here that I am aware of:

  • I've renamed and created some new domain types, but I think some of the names are not the best, I'll ask (IDS) Nico what names he thinks makes most sense. (Mainly: ReducibleData ReducedReference Sample Reference) Name suggestions are appreciated!
  • Documentation for some of the new coordinate transformations. Done. This version does not use the coordinate transformations in scippneutron, mainly because most of them just are not in scippneutron and are only used here, and partly because it was just simpler to keep the transformations directly in the package.
  • There's one or two hardcoded constants that I haven't gotten around to move and name properly. Those should of course get proper names and be documented.

@jokasimr jokasimr requested a review from nvaytet December 3, 2024 14:01
@jokasimr jokasimr marked this pull request as ready for review December 6, 2024 14:30
@jokasimr jokasimr requested review from jl-wynen and removed request for nvaytet December 12, 2024 09:02
Copy link
Member

@jl-wynen jl-wynen left a comment

Choose a reason for hiding this comment

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

  • There's one or two hardcoded constants that I haven't gotten around to move and name properly. Those should of course get proper names and be documented.

Please do this. But not here, instead:

This PR is very large and after more than 1h and 13 comments, I am still no where near reviewing it all. Can you please split it into smaller PRs.
It is particularly difficult to tell for a change whether something was simply renamed or moved or modified in a meaningful way. Also, this PR seems to contain changes related to several aspects of the workflow such as loading, saving, coord transformations, normalisations, etc.

docs/user-guide/amor/compare-to-eos.ipynb Show resolved Hide resolved
@@ -111,11 +93,6 @@ class SampleSize(sciline.Scope[RunType, sc.Variable], sc.Variable):
"""Size of the sample. If None it is assumed to be the same as the reference."""


Gravity = NewType("Gravity", sc.Variable)
"""This parameter determines if gravity is taken into account
when computing the scattering angle and momentum transfer."""
Copy link
Member

Choose a reason for hiding this comment

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

Why did you remove this? Now the user can no longer control the gravity correction.

BeamDivergenceLimits: (
sc.scalar(-0.75, unit='deg').to(unit='rad'),
sc.scalar(0.75, unit='deg').to(unit='rad'),
),
Copy link
Member

Choose a reason for hiding this comment

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

This is not very user friendly. Ideally, the code that uses these should convert to the unit it needs.

Copy link
Contributor Author

@jokasimr jokasimr Dec 12, 2024

Choose a reason for hiding this comment

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

On the other hand it's messy and somewhat error prone to have unit conversion code all over the package (forgetting a unit conversion somewhere might lead to UnitErrors and having extraneous unit conversions in every provider obscures the intention of the code, which is also bad user experience) 🤔

It's easiest to have unit conversion code on the boundaries of the application and only work with a set of predetermined units in the "internal" code.

One way to achieve that would be to create a separate provider just for converting the unit of the input to the common unit. But doing that for every input would clutter the workflow graph and make it considerably less readable.

Copy link
Member

Choose a reason for hiding this comment

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

It's easiest to have unit conversion code on the boundaries of the application and only work with a set of predetermined units in the "internal" code.

I disagree. It is often very difficult to understand which units are required by any given piece of code. And we don't encode them in our interfaces.

forgetting a unit conversion somewhere might lead to UnitErrors

Yes, the same as using an incorrect unit in a parameter. Except that the latter UnitErrors are user errors and it is typically very difficulty to figure out which unit was wrong because those errors can happen after many processing steps.

having extraneous unit conversions in every provider obscures the intention of the code, which is also bad user experience

True. That is a tradeoff. But we accepted that tradeoff in ScippNeutron in favour of reducing the number of user-facing UnitErrors. In fact, we took great care to make coord transform kernels work with any units and do conversions in an optimal way.

I don't expect the same amount of care in all code. But I do think that users should not have to know which units are required. Especially given that the requirements are not document anywhere.

def _pixel_coordinate_in_detector_system(
pixelID: sc.Variable,
) -> tuple[sc.Variable, sc.Variable]:
def pixel_coordinates_in_detector_system() -> tuple[sc.Variable, sc.Variable]:
Copy link
Member

Choose a reason for hiding this comment

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

The return annotation is incorrect now. And I am not sure the function name still makes sense either.

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'll fix the annotation. What name would you have?

src/ess/amor/geometry.py Show resolved Hide resolved
fp: Filename[RunType],
) -> AngleCenterOfIncomingToHorizon[RunType]:
(kad,) = load_nx(fp, "NXentry/NXinstrument/master_parameters/kad")
return sc.scalar(kad['value'].data['dim_1', 0]['time', 0].value, unit='deg')
Copy link
Member

Choose a reason for hiding this comment

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

Why does it access the 0th element in dim_1 and time? What are the other elements?

Copy link
Contributor Author

@jokasimr jokasimr Dec 12, 2024

Choose a reason for hiding this comment

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

It's stored as an NXLog and when we read that it ends up this shape, but it doesn't make sense for this to be anything other than a scalar, so we only read the first value.

Copy link
Member

Choose a reason for hiding this comment

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

Ok. Can you check that there is indeed a single entry? Otherwise, it may silently mess up when there is a time-dependent log. You could just use squeeze.

Copy link
Contributor Author

@jokasimr jokasimr Dec 13, 2024

Choose a reason for hiding this comment

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

It's not unusual that there is more than one entry. But it has been determined that it's correct to read the first entry. See the other parameters that are read the same way, sample_rotation and detector_rotation.

I don't think it makes sense to complicate the processing or raise errors just because there are more than a single entry here.

Copy link
Member

Choose a reason for hiding this comment

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

In that case, please add a comment that explains the choice and why it is ok to use the first element.

src/ess/amor/conversions.py Show resolved Hide resolved
"divergence_angle": "pixel_divergence_angle",
"wavelength": wavelength,
"theta": theta,
"angle_of_divergence": angle_of_divergence,
Copy link
Member

Choose a reason for hiding this comment

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

What is the difference of 'divergence_angle' and 'angle_of_divergence'? If they are different, can you rename one? The two are synonyms in English.

Copy link
Contributor Author

@jokasimr jokasimr Dec 12, 2024

Choose a reason for hiding this comment

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

That's a very good point. It's confusing. divergence_angle is the divergence angle on the detector, angle_of_divergence is the divergence angle of the incoming beam on the sample.
Without gravity they are identical.

I'll try to come up with better names.

src/ess/amor/conversions.py Outdated Show resolved Hide resolved
src/ess/amor/conversions.py Show resolved Hide resolved
@jokasimr
Copy link
Contributor Author

This PR is very large and after more than 1h and 13 comments, I am still no where near reviewing it all. Can you please split it into smaller PRs.

I understand it's a lot, the changes are large.
How about this: Let's resolve the comments you have already made here and when that's done I'll split the changes in a number of separate PRs.

@jokasimr
Copy link
Contributor Author

I'll close this and split it up into smaller PRs.

@jokasimr jokasimr closed this Dec 16, 2024
src/ess/reflectometry/corrections.py Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

Unused AngleCenterOfIncomingToHorizon in this file? Where does the correction happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The correction happens in normalize.py/reduce_reference.

Not sure what you mean by AngleCenterOfIncomingToHorizon is unused? Do you mean that it should be used, or do you mean that it's imported but unused? I can't see that it occurs in the file.

Copy link
Member

Choose a reason for hiding this comment

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

Do you mean that it should be used, or do you mean that it's imported but unused?

Yes, this is what I meant. It should have been caught by ruff, I think. But it looks like the setup in ESSreflectometry is rather old.

Copy link
Contributor Author

@jokasimr jokasimr Dec 20, 2024

Choose a reason for hiding this comment

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

AngleCenterOfIncomingToHorizon does not occur in the file src/ess/reflectometry/supermirror/__init__.py

@jokasimr jokasimr reopened this Dec 18, 2024
src/ess/amor/normalization.py Outdated Show resolved Hide resolved
data.coords["chopper_frequency"] = chopper_frequency
data.coords["chopper_separation"] = sc.abs(chopper_separation)
data.coords["L1"] = sc.abs(chopper_distance)
data.coords["L2"] = data.coords['distance_in_detector'] + Detector.distance
Copy link
Member

Choose a reason for hiding this comment

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

As a reminder, this should be done by adapting the bemaline graph for coord transforms from ScippNeutron.

Copy link
Contributor Author

@jokasimr jokasimr Dec 19, 2024

Choose a reason for hiding this comment

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

The beamline graph defines the following quantities:

  • incident_beam is the vector of incoming neutrons on the sample
  • L1
  • scattered_beam is the vector of neutrons that were scattered off the sample
  • L2
  • Ltotal

But some of those are not used in this workflow, and some are to be replaced anyway:

  • incident_beam - this quantity is not needed in the workflow
  • L1 - should be replaced
  • scattered_beam - this quantity is not used in the workflow
  • L2 - should be replaced
  • Ltotal - this is used in the workflow

In the end, the only useful quantity we get from the graph is Ltotal. But that is neither difficult to compute or likely to change.

For this reason I don't think it's necessary to use that graph here. Adapting it to fit here is likely more code than saved by using it.

I think it's good to take into consideration that it becomes harder for a user to follow what transformations were applied if they have to go and look in a different package to find the definitions. And while that's sometimes unavoidable, it doesn't seem like it is here.

Copy link
Member

Choose a reason for hiding this comment

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

Take a look at the LoKI workflow: https://scipp.github.io/esssans/user-guide/loki/loki-iofq.html It has a provider for its coord transform graphs. This way, we can have the same code do the actual transforms for multiple instruments. We only need to replace the graphs.

@@ -54,7 +54,7 @@ def mask_events_where_supermirror_does_not_cover(
theta(
sam.bins.coords["wavelength"],
sam.coords["pixel_divergence_angle"],
ref.coords["L2"],
sam.coords["L2"],
Copy link
Member

Choose a reason for hiding this comment

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

Is this a bugfix? If yes, please add a test!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not a bugfix, they are equivalent, I just found the second version more clear.

src/ess/amor/resolution.py Show resolved Hide resolved
src/ess/amor/load.py Outdated Show resolved Hide resolved
sam.bins.coords["wavelength"],
sam.coords["pixel_divergence_angle"],
sam.coords["L2"],
ref.coords["sample_rotation"],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be good to use the AmorCoordinates transformation graph to compute supermirror reflectivity here, because as it is now it looks like the coordinate transformations can be changed everywhere by replacing AmorCoordinates but they can't because here they are hardcoded.

However, I wasn't able to use transform_coords here in a way that doesn't rely on knowing the input arguments to the coordinate transformation functions in the graph.

If you have any suggestions about how to do this, please tell me.

Copy link
Member

Choose a reason for hiding this comment

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

What do you mean by

However, I wasn't able to use transform_coords here in a way that doesn't rely on knowing the input arguments to the coordinate transformation functions in the graph.

What about the inputs would you have to know that you don't have to know otherwise?

Copy link
Contributor Author

@jokasimr jokasimr Jan 16, 2025

Choose a reason for hiding this comment

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

Sorry that was unclear. You have to know it either way. It's just that since you have to know it either way we don't get any benefit from using transform_coords here and then I thought this version was more explicit and easier to understand.

)
out = sc.asin(out, out=out)
out -= sample_rotation
out -= sample_rotation.to(unit='rad')
Copy link
Member

Choose a reason for hiding this comment

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

Are the variables you are converting all scalar? For arrays, I would use copy=False.

@@ -9,6 +9,8 @@
AngularResolution = NewType("AngularResolution", sc.Variable)
SampleSizeResolution = NewType("SampleSizeResolution", sc.Variable)

AmorCoordinates = NewType("AmorCoordinates", dict)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
AmorCoordinates = NewType("AmorCoordinates", dict)
CoordTransformationGraph = NewType("CoordTransformationGraph", dict)

to match the name in ESSsans.

Why did you include the word 'Amor' in the name?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because it's specific to the Amor beamline

Copy link
Member

Choose a reason for hiding this comment

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

But you didn't prefix the other domain types with 'Amor'. The concrete value is specific, but the concept of a coord transform graph is generic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed the name

sam.bins.coords["wavelength"],
sam.coords["pixel_divergence_angle"],
sam.coords["L2"],
ref.coords["sample_rotation"],
Copy link
Member

Choose a reason for hiding this comment

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

What do you mean by

However, I wasn't able to use transform_coords here in a way that doesn't rely on knowing the input arguments to the coordinate transformation functions in the graph.

What about the inputs would you have to know that you don't have to know otherwise?

@jokasimr jokasimr merged commit e43cfaf into main Jan 16, 2025
4 checks passed
@jokasimr jokasimr deleted the simplify-amor branch January 16, 2025 13:17
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.

2 participants