European-XFEL / EXtra-geom

Use EuXFEL detector geometry to assemble images
https://extra-geom.rtfd.io
BSD 3-Clause "New" or "Revised" License
5 stars 4 forks source link

Geometry update by motor positions of quadrants #269

Closed egorsobolev closed 6 months ago

egorsobolev commented 9 months ago

This implements the update of geometry according to the motors of detector quadrants. The motor position can be write to and read from Crystfel geometry files (write as comments). There is also the helper to read motors from experimental data.

@takluyver @philsmt @JamesWrigley

takluyver commented 8 months ago

Hi Egor, sorry it has taken me a while to look at this. Before I get into commenting on specific bits of the code, I have a few broader questions & thoughts.

Am I understanding correctly that we can only calculate quadrant shifts based on motor movements, not absolute positions? I.e. we can't translate motor positions directly to quadrant positions, we need a previous geometry and the difference in motor positions between the two?

I think the code to read motor positions from saved data (what's currently the read_motors_from_data() function) would be a more natural fit as an EXtra component which knows about the source name patterns - this is quite similar to the pattern of components we already have or have planned. It could look something like this:

from extra.components import AGIPDQuadrantMotors

quad_motors = AGIPDQuadrantMotors(run)
quad_motors.get_positions(atol=0.01)

I don't particularly like the idea of storing motor positions in comments in .geom files - both because it's essentially defining a new ad-hoc file format, and also because if someone modifies the geometry with a tool that ignores the motor positions, they can be out of sync with the module positions they're stored with. But I also don't really have a better idea for how to store these at the moment.

egorsobolev commented 8 months ago

Am I understanding correctly that we can only calculate quadrant shifts based on motor movements, not absolute positions? I.e. we can't translate motor positions directly to quadrant positions, we need a previous geometry and the difference in motor positions between the two?

Yes. But its generalization. If you wanna use absolute position, you just define the reference quadrant positions for zero motor positions:

    quad_pos_for_zero_motor_pos = [(-525, 625), (-550, -10), (520, -160), (542.5, 475)]

    geom = AGIPD_1MGeometry.from_quad_positions(quad_pos_for_zero_motor_pos)
    geom2 = geom.move_by_motors(motor_pos)

I think the code to read motor positions from saved data (what's currently the read_motors_from_data() function) would be a more natural fit as an EXtra component which knows about the source name patterns - this is quite similar to the pattern of components we already have or have planned. It could look something like this:

from extra.components import AGIPDQuadrantMotors

quad_motors = AGIPDQuadrantMotors(run)
quad_motors.get_positions(atol=0.01)

I have no objections. I put all in one place to start discussion. It would be super if it would not need source names. But they depends on the instrument. (yes, we can hard code different sources depending on agipd sources in a run)

I don't particularly like the idea of storing motor positions in comments in .geom files - both because it's essentially defining a new ad-hoc file format, and also because if someone modifies the geometry with a tool that ignores the motor positions, they can be out of sync with the module positions they're stored with. But I also don't really have a better idea for how to store these at the moment.

I also don't like. But I think its good enough for now until we find better way. I don't think that is the problem, because if someone modifies geometry with a tool which doesn't know about motors, than we don't know the reason of modification. It's better to discard the motors in this case. Then we need motors only for reference geometry, which instruments supposed to make and keep. Later, I hope, we will store reference geometries in caldb and find a way how to pack the motor positions there.

egorsobolev commented 8 months ago

What do you think about mixin implementation?

takluyver commented 8 months ago

Are those reference positions you gave real, and accurate enough to be useful? If we can convert absolute motor positions into absolute quadrant positions reliably, I think that opens the door to simpler interfaces, like:

quad_motors = AGIPDQuadrantMotors(run)

geom = AGIPD_1MGeometry.from_motor_positions(
    quad_motors.get_positions(atol=0.01), reference="MID",
)
egorsobolev commented 8 months ago

The simpler interface would be good. I gave the numbers from example, but there are real numbers for AGIPD@MID: [(-542, 660), (-608, -35), (534, -221), (588, 474)].

The problem that these numbers are not guaranteed. They can stay the same for any long time, but also may be changed any moment (for example by re-calibrating encoders, and that is not a single option). It means that it is not a good idea to hard code them in library. Thus, simpler interface can look:

quad_motors = AGIPDQuadrantMotors(run)

geom = AGIPD_1MGeometry.from_motor_positions(
    quad_motors.get_positions(atol=0.01),
    reference_quad_pos=[(-542, 660), (-608, -35), (534, -221), (588, 474)]
)

Which is actually shortcut replacing just two calls:

geom = AGIPD_1MGeometry.from_quad_positions(reference_quad_pos).move_by_motors(motor_pos)

Another problem, that ideal geometry doesn't work for SFX experiments, which are sensitive to the module rotations. That mean we cannot use ideal geometry and need to use refined stored in the file. But we potentially could refine geometry (if we do it with our tools) relative some hard coded origin (move there after refinement using motor positions during the calibration run, instead storing them in the geometry). But then we cannot distinguish geometry files which respect that convention and others.

I would go with explicit way to store and specify motor positions.

takluyver commented 7 months ago

Looking at this again:

egorsobolev commented 7 months ago

Looking at all suggestions, I decided to refactor it a bit:

Usage without reference motor positions:

    geom = AGIPD_1MGeometry.from_quad_positions(quad_pos)
    tracker = AGIPD_1MMotors(geom)
    geom2 = tracker.move_geom_by(motor_pos)

Usage with reference motor positions:

    geom = AGIPD_1MGeometry.from_quad_positions(quad_pos)
    tracker = AGIPD_1MMotors.with_reference_positions(geom)
    geom2 = tracker.geom_at(motor_pos)

    tracker = AGIPD_1MMotors.with_reference_positions(geom, ref_motor_pos)
    geom2 = tracker.geom_at(motor_pos)
takluyver commented 6 months ago

I think the structure of this looks good, thanks! I have a few comments about details above.

takluyver commented 6 months ago

Thanks, this LGTM now. Would you be able to write a bit of documentation before we merge it? Even just an example notebook showing how to use motors to adjust geometry?

review-notebook-app[bot] commented 6 months ago

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

egorsobolev commented 6 months ago

Thanks, this LGTM now. Would you be able to write a bit of documentation before we merge it? Even just an example notebook showing how to use motors to adjust geometry?

Sure. I've pushed it.

I don't quite understand why we're storing motor positions in the .geom files? I assume the point of saving the metadata is for the sake of reproducibility/provenance; if so, don't we need both the motor positions and the reference geometry?

The main goal is to store the refined geometry (by SFX data, for example) together with corresponding motor positions. At them moment we use crystfel geometry and sending the geometry files around. Thus, this solution seems natural and transparent.

This solution is not ideal (see the first comments from @takluyver), but there is no obvious better option for now. If we store geometry in CalDB, we likely can get rid of this hack.

JamesWrigley commented 6 months ago

The main goal is to store the refined geometry (by SFX data, for example) together with corresponding motor positions. At them moment we use crystfel geometry and sending the geometry files around. Thus, this solution seems natural and transparent.

Sure, but what exactly can you do with that? Do you plan to use them to uniquely identify a particular geometry, or are the positions used by crystfel or something? Sorry if this is a dumb question :sweat_smile: It's just not clear to me how the motor positions are useful without the reference geometry.

egorsobolev commented 6 months ago

But the reference geometry is in the same file.

We store refined geometry in a file (refined by special procedure using the SFX data but not motor changes) and also store corresponding motor positions there (motor positions at the moment when data for refinement were taken).

Then operators move motors and ask me to generate new geometry (for example). I read current motor position from Karabo devices, but I need the difference between current positions and positions which correspond to the refined geometry. The last are missed until I store them somewhere (better togehter with refined geometry, which is reference geometry now).

I have implemented writing and reading motor position in the geometry file especially for this particular case. In other cases, this is just a metadata which may be used or not.

For example, motor positions can be used to check if the geometry is still valid. You just compare motor positions stored in the geometry file and motor positions in data. If they are different, then you need to update geometry. For SFX it means full refinement

egorsobolev commented 6 months ago

if there are no more comments... do I merge?

egorsobolev commented 6 months ago

@takluyver and @JamesWrigley, thank you both for review