Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@nvaytet
Copy link
Member

@nvaytet nvaytet commented Mar 1, 2023

This PR adds the beam_center function to find the beam center in a SANS experiment.
The prodecure to determine the precise location of the beam center is the following:

  1. obtain an initial guess by computing the center-of-mass of the pixels, weighted by the counts on each pixel
  2. from that initial guess, divide the panel into 4 quadrants
  3. compute $I(Q)$ inside each quadrant and compute the residual difference between all 4 quadrants
  4. iteratively move the centre position and repeat 2. and 3. until all 4 $I(Q)$ curves lie on top of each other

This is described in detail in a new notebook, and is used in the existing SANS notebooks.

I do not know how to add good unit tests for the beam center finder... Suggestions welcome.

@nvaytet nvaytet marked this pull request as ready for review March 1, 2023 11:01
@nvaytet
Copy link
Member Author

nvaytet commented Mar 1, 2023

@wpotrzebowski the final $I(Q)$results have changed, because we are masking both numerator and denominator now (this was an oversight in the old code).
We should carefully go over the results to make sure I haven't messed up.

Copy link
Member

@SimonHeybrock SimonHeybrock left a comment

Choose a reason for hiding this comment

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

Some comments for now, I did not look at everything in detail yet.

from .normalization import normalize


def _center_of_mass(data):
Copy link
Member

Choose a reason for hiding this comment

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

Type hints missing everywhere

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

summed = data.sum(list(set(data.dims) - set(data.meta['position'].dims)))
v = sc.values(summed.data)
com = sc.sum(summed.meta['position'] * v) / v.sum()
return com.fields.x, com.fields.y
Copy link
Member

Choose a reason for hiding this comment

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

This assumes that the beam direction is z. Should this be checked somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

I tried to make it general in terms of normal to the direction of the beam. please check

c = ((data['top'] - ref)**2 + (data['left'] - ref)**2 +
(data['bottom'] - ref)**2) / ref**2
out = c.sum().value
print(xy, out)
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
print(xy, out)

?

Copy link
Member Author

Choose a reason for hiding this comment

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

This should go to the logger 👍

Comment on lines 63 to 69
pi = sc.constants.pi.value
phi_offset = sc.scalar(pi / 4, unit='rad')
phi = (sc.atan2(y=data.coords['position'].fields.y,
x=data.coords['position'].fields.x) +
phi_offset) % (2 * (pi * sc.units.rad))
phi_bins = sc.linspace('phi', 0, pi * 2, 5, unit='rad')
quadrants = ['right', 'top', 'left', 'bottom']
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 I follow entirely, but given the pi/4 and naming (right, top, ...), it looks like you make an X cut into quadrants? I imagine this is fine for square detector banks, but what if the bank is rectangular? Wouldn't you get very different pixel counts in the (left/right) vs (top/bottom) quadrants?

Can you make a + cut instead to avoid this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice observations. I tested a + cut instead and I get nice results, so I will go with that. Thanks.

Comment on lines 44 to 48
# Make a copy of the original data
data = sc.DataArray(data=sample.data)
coord_list = ['position', 'sample_position', 'source_position']
for c in coord_list:
data.coords[c] = sample.meta[c].copy(deep=True)
Copy link
Member

Choose a reason for hiding this comment

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

  • Why are you not using data=data.copy()? Is it do avoid copying wrong wavelength/Q coords from previous iterations?
  • data = sc.DataArray(data=sample.data) will not make a copy of the data values, is that intentional? Note that if you have event data then this will also affect event coords.
  • Are there no prior masks (such as TOF, or dead pixels) that need to be copied?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is it do avoid copying wrong wavelength/Q coords from previous iterations?

Yes

will not make a copy of the data values, is that intentional?

Yes, I don't think we need a deep copy.

Note that if you have event data then this will also affect event coords.

Not sure what you mean by this will also affect event coords.

Are there no prior masks (such as TOF, or dead pixels) that need to be copied?

hmm yes, masks are missing! Thanks. I would like to change to use da.copy() and then da.coords.clear() once I can use the latest version of Scipp, but we first need to deal with the variances broadcast issue before we can do this.

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 you mean by this will also affect event coords

I mean, not copying the data implies not copying the event coords (since they are "within" the data values).



def sans_elastic(gravity: bool = False) -> dict:
def phi(position: 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.

Should at the very least document that is for a beam direction along z. I do not know how precisely this is fulfilled in practice.

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm nice catch, we are not using this anymore. I forgot to remove it.

Copy link
Member

Choose a reason for hiding this comment

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

There are also other places where the same assumption is made in this code.

Copy link
Member Author

Choose a reason for hiding this comment

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

I left this in the end and generalized it so that we don't assume the beam is along z.

Comment on lines 197 to 202
lu = sc.lookup(mask, mask.dim)
if da.coords.is_edges(mask.dim):
sampling = sc.midpoints(da.coords[mask.dim])
else:
sampling = da.coords[mask.dim]
da.masks[name] = lu[sampling]
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, is considering the midpoints the correct solution? Shouldn't we mask all bins that have overlap with a masked region instead? Something like

dim = mask.dim
edges = sc.lookup(mask, dim)[da.coords[dim]]
da.masks[name] = edges[dim, 1:] | edges[dim, :-1]

Copy link
Member Author

Choose a reason for hiding this comment

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

I made a function mask_range which basically implements what you suggest, and works for both binned and dense data. Not sure if that should go somewhere more general than in the SANS submodule, as it's fully generic.

@nvaytet
Copy link
Member Author

nvaytet commented Mar 20, 2023

Few comments about functionality

We don't need BCF for every data set. As I understand this notebooks serves as an example of existing functionality rather than something that serves as a user workflow

Yes

Tof bins are no longer used (at least explicetly) - I guess it is because we don't use DataSet any more?

Correct (and we will use DataGroup in the very near future, once we address the issue of the variances being broadcast).

Pixel height and width are now incorporated into files is it the same true for sample_pos_z_offset and monitor4_pos_z_offset?

Yes

Comment to code:
In sans2d_reduction.ipynb masking is done on sample directly (sample.masks['holder_mask'] = holder_mask). Shouldn't be dg['sample']?

They are the same python object, so it should not matter. But I need to check about maybe also masking the background run...

Comment on lines 27 to 31
Cost function for determining how close the I(Q) curves are in all four quadrants.
"""
ref = data['north-east']
c = ((data['north-west'] - ref)**2 + (data['south-west'] - ref)**2 +
(data['south-east'] - ref)**2) / ref**2
Copy link
Member

Choose a reason for hiding this comment

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

This it biased and not symmetric. One would expect a result like

AB
AA

to have the same cost as others such as

BA
AA

but this is not the case in this implementation.

Copy link
Member Author

Choose a reason for hiding this comment

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

What would you suggest? Compute the mean of all 4 and use that as ref?

Copy link
Member

Choose a reason for hiding this comment

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

That could work, yes.

q_bins:
The binning in the Q dimension to be used.
masking_radius:
The radius of the circular mask to apply to the data while iterating.
Copy link
Member

Choose a reason for hiding this comment

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

Which circular mask? To mask the beam stop?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's this mask:
Screenshot at 2023-03-23 13-10-51

I added more details in the docstring.

if (dim in da.coords) and (da.coords[dim].ndim > 1):
raise sc.DimensionError(
'Cannot mask range on data with multi-dimensional coordinate. '
f'Found dimensions {da.coords[dim].dims} for coordinate {dim}.')
Copy link
Member

Choose a reason for hiding this comment

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

Is it worth checking if there is an existing mask of the same name? Or else we risk dropping it silently.

Comment on lines +95 to +97
"# Add X, Y coordinates\n",
"sample.coords['x'] = sample.coords['position'].fields.x\n",
"sample.coords['y'] = sample.coords['position'].fields.y\n",
Copy link
Member

Choose a reason for hiding this comment

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

Why is this necessary?

Copy link
Member Author

Choose a reason for hiding this comment

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

Convenience. We hist in x and y to make images of the detector panel, instead of using the instrument view which increases size bloat of the docs pages.

"id": "afbf9a8c-4fc7-4eb3-a477-111baa047b18",
"metadata": {},
"source": [
"## Making 4 quadrants\n",
Copy link
Member

Choose a reason for hiding this comment

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

Is all of this repeating the code from the Python module?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, it was. I was never happy with the duplication.
I now tried to do what I did in the other notebooks, which is to break up the BCF code into smaller functions, and use those inside the notebook.

Comment on lines 121 to 132
"""
Find the beam center of a SANS scattering pattern.
Description of the procedure:
#. obtain an initial guess by computing the center-of-mass of the pixels,
weighted by the counts on each pixel
#. from that initial guess, divide the panel into 4 quadrants
#. compute :math:`I(Q)` inside each quadrant and compute the residual difference
between all 4 quadrants
#. iteratively move the centre position and repeat 2. and 3. until all 4
:math:`I(Q)` curves lie on top of each other
Copy link
Member

Choose a reason for hiding this comment

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

We had talked about documenting all of the things that may be done wrong, i.e., all the things you learning during implementation. Shouldn't this be documented here somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

As discussed, I added some notes in the docstring of the beam_center function.

iofq = iofq_in_quadrants(xy, *args)
all_q = sc.concat(list(iofq.values()), dim='quadrant')
ref = sc.values(all_q.mean('quadrant'))
c = sc.abs(all_q - ref) / ref
Copy link
Member

Choose a reason for hiding this comment

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

Didn't you have squares in the old implementation?

Copy link
Member Author

Choose a reason for hiding this comment

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

I did, I am not sure it matters?

Copy link
Member

Choose a reason for hiding this comment

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

Does it not affect convergence speed, maybe?

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't seem to, the number of iterations is the same as before.

Comment on lines +259 to +260
Notes
-----
Copy link
Member

Choose a reason for hiding this comment

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

👍

Maybe place under the parameter section, since users will often want to read the params, but not the notes?

Comment on lines +211 to +214
all_q = sc.concat([sc.values(da) for da in iofq.values()], dim='quadrant')
ref = all_q.mean('quadrant')
c = (all_q - ref)**2
out = (sc.sum(ref * c) / sc.sum(ref)).value
Copy link
Member

Choose a reason for hiding this comment

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

You mention above that you now use a weighted mean, but I cannot really see this here. I expected that the variances would be used as weights?

Copy link
Member Author

Choose a reason for hiding this comment

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

I thought you meant that the signal intensity should be used as weights, to give less importance to the noisy parts with low signal?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm slightly reluctant to use variances as weights, as we are not computing them accurately?

@nvaytet nvaytet merged commit c8cceea into main Mar 28, 2023
@nvaytet nvaytet deleted the beam_centre_finder branch March 28, 2023 14:30
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.

4 participants