370 implement rotation nexus#736
Conversation
Codecov Report
@@ Coverage Diff @@
## main DiamondLightSource/hyperion#736 +/- ##
==========================================
+ Coverage 93.94% 94.18% +0.23%
==========================================
Files 36 36
Lines 1718 1736 +18
==========================================
+ Hits 1614 1635 +21
+ Misses 104 101 -3
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
DominicOram
left a comment
There was a problem hiding this comment.
Great, mostly the refactor we discussed and some minor comments. Also the __init__ file under the external interactions unit tests is spelt wrong
| @@ -0,0 +1,19 @@ | |||
| { | |||
| "folders": [ | |||
There was a problem hiding this comment.
Great addition, thanks!
|
|
||
| @subs_decorator(list(subscriptions)) | ||
| @bpp.subs_decorator(list(subscriptions)) | ||
| @bpp.set_run_key_decorator("run_gridscan_move_and_tidy") |
There was a problem hiding this comment.
Should: This isn't the gridscan
There was a problem hiding this comment.
Do we even need the run key here?
There was a problem hiding this comment.
yes, setting the run key is what makes a section generate a start doc
| with h5py.File(temp_filename, "r+") as nxsfile: | ||
| nxsfile["entry"].create_dataset( | ||
| "end_time", data=np.string_(get_current_time()) | ||
| "end_time", data=np.string_(get_current_time() + "Z") |
There was a problem hiding this comment.
Should: get_current_time looks like it already ends with a Z, do we need two? If we do this should be put in get_current_time, not here
| Goniometer: A Goniometer description for nexgen | ||
| """ | ||
| # Axis: name, depends, type, vector, start | ||
| def create_goniometer_axes( |
There was a problem hiding this comment.
Could: I think a docstring would still be helpful
| class FGSNexusWriter(NexusWriter): | ||
| grid_scan: dict | ||
|
|
||
| def __init__(self, parameters: FGSInternalParameters, grid_scan: dict) -> None: | ||
| self.scan_points = grid_scan | ||
| super().__init__(parameters) | ||
| self.goniometer = create_gridscan_goniometer_axes( | ||
| parameters.artemis_params.detector_params, | ||
| parameters.experiment_params, | ||
| grid_scan, | ||
| ) | ||
| self.grid_scan = grid_scan | ||
|
|
||
| def _get_data_shape_for_vds(self) -> tuple[int | float, ...]: | ||
| ax = list(self.grid_scan.keys())[0] | ||
| num_frames_in_vds = len(self.grid_scan[ax]) | ||
| def _get_data_shape_for_vds(self) -> tuple[int, ...]: | ||
| ax = list(self.scan_points.keys())[0] | ||
| num_frames_in_vds = len(self.scan_points[ax]) | ||
| nexus_detector_params: EigerDetector = self.detector.detector_params | ||
| return (num_frames_in_vds, *nexus_detector_params.image_size) | ||
|
|
||
|
|
||
| class RotationNexusWriter(NexusWriter): | ||
| def __init__( | ||
| self, parameters: RotationInternalParameters, rotation_scan: dict | ||
| ) -> None: | ||
| super().__init__(parameters) | ||
| self.goniometer = create_rotation_goniometer_axes( | ||
| parameters.artemis_params.detector_params, | ||
| parameters.experiment_params, | ||
| rotation_scan, | ||
| def __init__(self, parameters: RotationInternalParameters) -> None: | ||
| scan_spec = Line( | ||
| axis="omega", | ||
| start=parameters.experiment_params.omega_start, | ||
| stop=( | ||
| parameters.experiment_params.rotation_angle | ||
| + parameters.experiment_params.omega_start | ||
| ), | ||
| num=parameters.experiment_params.get_num_images(), | ||
| ) | ||
| scan_path = ScanPath(scan_spec.calculate()) | ||
| self.scan_points = scan_path.consume().midpoints | ||
| super().__init__(parameters) | ||
|
|
||
| def _get_data_shape_for_vds(self) -> tuple[int | float, ...]: | ||
| return (1,) | ||
| # ax = list(self.grid_scan.keys())[0] | ||
| # num_frames_in_vds = len(self.grid_scan[ax]) | ||
| # return (num_frames_in_vds, *self.detector.detector_params.image_size) | ||
| def _get_data_shape_for_vds(self) -> tuple[int, ...]: | ||
| nexus_detector_params: EigerDetector = self.detector.detector_params | ||
| return (self.full_num_of_images, *nexus_detector_params.image_size) |
There was a problem hiding this comment.
Should: As discussed, if the scan is held in the InternalParameters, which it probably should be anyway then the two different __init__s are redundant. Then, if we get the data shape from the scan itself the separate _get_data_shape functions are redundant, making these classes redundant
| return plan() | ||
|
|
||
|
|
||
| def test_rotation_scan_nexus_output_compared_to_existing_file( |
| ) | ||
|
|
||
|
|
||
| def test_generic_create_gonio_creates_same_gonio_as_defined_with_individual_params( |
There was a problem hiding this comment.
Should: This test is great to confirm what we've done in this issue but seems a bit pointless going forward given the old way will no longer exist, suggest we remove it.
| omega_start: float = 0.0 | ||
| phi_start: float = 0.0 | ||
| chi_start: Optional[float] = None | ||
| kappa_start: Optional[float] = None | ||
| chi_start: float | None = None | ||
| kappa_start: float | None = None |
There was a problem hiding this comment.
Could: Probably a new ticket but sort of feels like all of these are optional (apart from maybe the one you are rotating) and if not provided you just use the current value
There was a problem hiding this comment.
Agree in principle but that's an issue with the rotation scan plan, not nexus writing - that did make me realise though that we're not even using these at all currently
issue for this: #743
| if isinstance(rotation_direction, str): | ||
| return RotationDirection[rotation_direction] | ||
| else: | ||
| return RotationDirection(rotation_direction) |
There was a problem hiding this comment.
Nit: I would say we shouldn't allow the integer representation, only the string as it makes the API messier but not that fussed
DominicOram
left a comment
There was a problem hiding this comment.
Some comments in code. As discussed, I think this is cleaner than before but not 100%, I've made a few tickets to clean up more such as https://github.com/DiamondLightSource/python-artemis/issues/747
| def get_scan_points(cls): | ||
| """Get points of the scan as calculated by scanspec.""" | ||
| ... | ||
|
|
||
| @abstractmethod | ||
| def get_data_shape(cls): |
There was a problem hiding this comment.
Should: Can we have type hints on the return values of these?
| ... | ||
|
|
||
| @abstractmethod | ||
| def get_scan_points(cls): |
There was a problem hiding this comment.
Should: I think it will be slightly cleaner if we return the ScanPath here, then we can do the consume().midpoints in one place
There was a problem hiding this comment.
currently we're using it to calculate the data shape, so we would still need to do it in multiple places, but less consistently. I think we should change this in #745 instead
| ... | ||
|
|
||
| @abstractmethod | ||
| def get_data_shape(cls): |
There was a problem hiding this comment.
As discussed I don't think we need the data_shape if we have the scan, spun into https://github.com/DiamondLightSource/python-artemis/issues/745
| if scan_number == 1: | ||
| x_axis = self.experiment_params.x_axis | ||
| y_axis = self.experiment_params.y_axis | ||
| spec = Line( | ||
| "sam_y", | ||
| y_axis.start, | ||
| y_axis.end, | ||
| y_axis.full_steps, | ||
| ) * ~Line( | ||
| "sam_x", | ||
| x_axis.start, | ||
| x_axis.end, | ||
| x_axis.full_steps, | ||
| ) | ||
| scan_path = ScanPath(spec.calculate()) | ||
| return scan_path.consume().midpoints | ||
| elif scan_number == 2: | ||
| x_axis = self.experiment_params.x_axis | ||
| z_axis = self.experiment_params.z_axis | ||
| spec = Line( | ||
| "sam_z", | ||
| z_axis.start, | ||
| z_axis.end, | ||
| z_axis.full_steps, | ||
| ) * ~Line( | ||
| "sam_x", | ||
| x_axis.start, | ||
| x_axis.end, | ||
| x_axis.full_steps, | ||
| ) | ||
| scan_path = ScanPath(spec.calculate()) | ||
| return scan_path.consume().midpoints |
There was a problem hiding this comment.
Nit: I think you can clean this up a bit using something like:
```suggestion
def create_line(axis_name, axis_data):
return Line(
axis_name,
axis_data.start,
axis_data.end,
axis_data.full_steps,
)
x_line = create_line("sam_x", self.experiment_params.x_axis)
y_line = create_line("sam_y", self.experiment_params.y_axis)
z_line = create_line("sam_z", self.experiment_params.z_axis)
if scan_number == 1:
spec = y_line *~ x_line
scan_path = ScanPath(spec.calculate())
return scan_path.consume().midpoints
elif scan_number == 2:
spec = z_line *~ x_line
scan_path = ScanPath(spec.calculate())
return scan_path.consume().midpointsThere was a problem hiding this comment.
I did something similar, but I wasn't keen on calculating unused lines so I still kept those separate
| num_frames_in_vds = len(scan_points[ax]) | ||
| return (num_frames_in_vds, size.width, size.height) | ||
|
|
||
| def get_omega_start(self, scan_number: int) -> float: |
There was a problem hiding this comment.
I think we can improve this, spun into https://github.com/DiamondLightSource/python-artemis/issues/746
Co-authored-by: Dominic Oram <dominic.oram@diamond.ac.uk>
The history got too messy on the other branch so this is a new one
Fixes #370
Requires Dodal PR: https://github.com/DiamondLightSource/dodal/tree/artemis_370_implement_rotation_nexus
Requires Nexgen PR DiamondLightSource/nexgen#120
Requires GDA changes: current_energy parameter renamed to current_energy_ev
To test:
Run tests