Skip to content
This repository was archived by the owner on Sep 2, 2024. It is now read-only.

370 implement rotation nexus#736

Merged
DominicOram merged 40 commits into
mainfrom
370_implement_rotation_nexus_again
Jun 26, 2023
Merged

370 implement rotation nexus#736
DominicOram merged 40 commits into
mainfrom
370_implement_rotation_nexus_again

Conversation

@d-perl

@d-perl d-perl commented Jun 14, 2023

Copy link
Copy Markdown
Contributor

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

@codecov

codecov Bot commented Jun 14, 2023

Copy link
Copy Markdown

Codecov Report

Merging #736 (554e603) into main (042c950) will increase coverage by 0.23%.
The diff coverage is 97.56%.

@@            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     
Impacted Files Coverage Δ
src/artemis/parameters/internal_parameters.py 98.68% <ø> (ø)
...plan_specific/grid_scan_with_edge_detect_params.py 95.12% <50.00%> (-4.88%) ⬇️
...is/parameters/plan_specific/fgs_internal_params.py 98.33% <96.96%> (-1.67%) ⬇️
src/artemis/experiment_plans/rotation_scan_plan.py 100.00% <100.00%> (ø)
...ternal_interaction/callbacks/fgs/nexus_callback.py 100.00% <100.00%> (ø)
...l_interaction/callbacks/rotation/nexus_callback.py 100.00% <100.00%> (ø)
...callbacks/rotation/rotation_callback_collection.py 100.00% <100.00%> (ø)
.../artemis/external_interaction/nexus/nexus_utils.py 100.00% <100.00%> (+8.69%) ⬆️
.../artemis/external_interaction/nexus/write_nexus.py 100.00% <100.00%> (+3.19%) ⬆️
...ers/plan_specific/rotation_scan_internal_params.py 100.00% <100.00%> (+1.88%) ⬆️

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@DominicOram DominicOram left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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": [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great addition, thanks!


@subs_decorator(list(subscriptions))
@bpp.subs_decorator(list(subscriptions))
@bpp.set_run_key_decorator("run_gridscan_move_and_tidy")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should: This isn't the gridscan

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we even need the run key here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could: I think a docstring would still be helpful

Comment on lines +142 to +171
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice test, thanks!

)


def test_generic_create_gonio_creates_same_gonio_as_defined_with_individual_params(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment on lines 27 to +30
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@d-perl d-perl Jun 16, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Comment on lines +40 to +43
if isinstance(rotation_direction, str):
return RotationDirection[rotation_direction]
else:
return RotationDirection(rotation_direction)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 DominicOram left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment thread src/artemis/parameters/internal_parameters.py Outdated
Comment on lines +180 to +185
def get_scan_points(cls):
"""Get points of the scan as calculated by scanspec."""
...

@abstractmethod
def get_data_shape(cls):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should: Can we have type hints on the return values of these?

...

@abstractmethod
def get_scan_points(cls):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should: I think it will be slightly cleaner if we return the ScanPath here, then we can do the consume().midpoints in one place

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment on lines +59 to +90
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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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().midpoints

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Co-authored-by: Dominic Oram <dominic.oram@diamond.ac.uk>
@d-perl
d-perl requested a review from DominicOram June 20, 2023 09:23

@DominicOram DominicOram left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great, thank you!

@DominicOram
DominicOram merged commit 45279b2 into main Jun 26, 2023
@DominicOram
DominicOram deleted the 370_implement_rotation_nexus_again branch June 26, 2023 12:01
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make nexus writer deal with different experiment types

2 participants