Skip to content
This repository has been archived by the owner on Nov 7, 2024. It is now read-only.

Commit

Permalink
Feat(NXdetector): Add support for pixel gaps
Browse files Browse the repository at this point in the history
  • Loading branch information
danesss committed Jul 24, 2023
1 parent 7808e7c commit e7fb503
Show file tree
Hide file tree
Showing 7 changed files with 382 additions and 45 deletions.
7 changes: 7 additions & 0 deletions nexus_constructor/geometry/pixel_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class PixelGrid(PixelData):
The corner that counting starts in, and whether counting should first happen along rows or columns can be set with
the 'count_direction' and 'initial_count_corner' attributes, which respectively take 'CountDirection' and 'Corner'
Enum values.
If there are gaps between pixels at regular intervals, the number of pixels between two gaps is configured
via the 'gap_every_rows' / 'gap_every_columns' attributes, and the size of the gap is set with 'row_gap_height'
/ 'column_gap_width'.
"""

rows = attr.ib(default=1, type=int)
Expand All @@ -51,6 +54,10 @@ class PixelGrid(PixelData):
first_id = attr.ib(default=0, type=int)
count_direction = attr.ib(default=CountDirection.ROW, type=CountDirection)
initial_count_corner = attr.ib(default=Corner.BOTTOM_LEFT, type=Corner)
gap_every_rows = attr.ib(default=0, type=int)
gap_every_columns = attr.ib(default=0, type=int)
row_gap_height = attr.ib(default=0, type=float)
column_gap_width = attr.ib(default=0, type=float)


@attr.s
Expand Down
40 changes: 32 additions & 8 deletions nexus_constructor/geometry/pixel_data_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,22 @@ def get_x_offsets_from_pixel_grid(grid: PixelGrid) -> Union[np.ndarray, float]:
Returns an array of x-offsets. Each value in the array is the x position of a pixel instance defined in the
PixelGrid.
"""
half_distance = grid.col_width / 2
end = half_distance * (grid.columns - 1)

offsets = np.linspace(start=-end, stop=end, num=grid.columns)
if grid.gap_every_columns > 0:
total_width = (
grid.columns * grid.col_width
+ ((grid.columns - 1) // grid.gap_every_columns) * grid.column_gap_width
)
else:
total_width = grid.columns * grid.col_width

start = -total_width / 2 + grid.col_width / 2
offsets = np.zeros(grid.columns)
gap_counter = 0
for i in range(grid.columns):
offsets[i] = start + i * grid.col_width + gap_counter * grid.column_gap_width
# If there is a gap, increment the gap counter
if grid.gap_every_columns > 0 and (i + 1) % grid.gap_every_columns == 0:
gap_counter += 1
return np.tile(offsets, (grid.rows, 1))


Expand All @@ -69,10 +81,22 @@ def get_y_offsets_from_pixel_grid(grid: PixelGrid) -> Union[np.ndarray, float]:
Returns an array of y-offsets. Each value in the array is the y position of a pixel instance defined in the
PixelGrid.
"""
half_distance = grid.row_height / 2
end = half_distance * (grid.rows - 1)

offsets = np.linspace(start=end, stop=-end, num=grid.rows)
if grid.gap_every_rows > 0:
total_height = (
grid.rows * grid.row_height
+ ((grid.rows - 1) // grid.gap_every_rows) * grid.row_gap_height
)
else:
total_height = grid.rows * grid.row_height

start = total_height / 2 - grid.row_height / 2
offsets = np.zeros(grid.rows)
gap_counter = 0
for i in range(grid.rows):
offsets[i] = start - i * grid.row_height - gap_counter * grid.row_gap_height
# If there is a gap, increment the gap counter
if grid.gap_every_rows > 0 and (i + 1) % grid.gap_every_rows == 0:
gap_counter += 1
return np.tile(offsets, (grid.columns, 1)).transpose()


Expand Down
14 changes: 14 additions & 0 deletions nexus_constructor/model/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,20 @@ def record_pixel_grid(self, pixel_grid: PixelGrid, unit: str = ""):
np.array(get_detector_ids_from_pixel_grid(pixel_grid)),
ValueTypes.INT,
)
if pixel_grid.gap_every_rows and pixel_grid.row_gap_height:
self.attributes.set_attribute_value(
"pixelgrid_gap_every_rows", pixel_grid.gap_every_rows
)
self.attributes.set_attribute_value(
"pixelgrid_row_gap_height", pixel_grid.row_gap_height
)
if pixel_grid.gap_every_columns and pixel_grid.column_gap_width:
self.attributes.set_attribute_value(
"pixelgrid_gap_every_columns", pixel_grid.gap_every_columns
)
self.attributes.set_attribute_value(
"pixelgrid_column_gap_width", pixel_grid.column_gap_width
)

def record_pixel_mapping(self, pixel_mapping: PixelMapping):
"""
Expand Down
50 changes: 50 additions & 0 deletions nexus_constructor/pixel_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ def _fill_single_pixel_fields(self, component_to_edit: Component):
self.start_counting_combo_box.setCurrentText(start_counting_text)
self.count_first_combo_box.setCurrentText(count_along_text)

# Set pixel gap information from attributes
(
gap_every_rows,
gap_every_columns,
row_gap_height,
column_gap_width,
) = self._get_pixel_gaps_information(component_to_edit)
self.gap_every_rows_spin_box.setValue(gap_every_rows)
self.gap_every_columns_spin_box.setValue(gap_every_columns)
self.row_gap_height_spin_box.setValue(row_gap_height)
self.column_gap_width_spin_box.setValue(column_gap_width)

else:
# If the pixel offset information represents a single pixel
pass
Expand Down Expand Up @@ -249,6 +261,40 @@ def _get_detector_number_information(

return first_id, start_counting_text, count_along_text

@staticmethod
def _get_pixel_gaps_information(
component_to_edit: Component,
) -> Tuple[Optional[int], Optional[int], Optional[float], Optional[float]]:
"""
Loads pixel gap information from NXdetector custom attributes.
"""
gap_every_rows = (
component_to_edit.attributes.get_attribute_value("pixelgrid_gap_every_rows")
or 0
)
gap_every_columns = (
component_to_edit.attributes.get_attribute_value(
"pixelgrid_gap_every_columns"
)
or 0
)
row_gap_height = (
component_to_edit.attributes.get_attribute_value("pixelgrid_row_gap_height")
or 0
)
column_gap_width = (
component_to_edit.attributes.get_attribute_value(
"pixelgrid_column_gap_width"
)
or 0
)
return (
int(gap_every_rows),
int(gap_every_columns),
float(row_gap_height),
float(column_gap_width),
)

def _fill_entire_shape_fields(self, component_to_edit: Component):
"""
Fill the "entire shape" fields a component that is being edited and contains pixel data.
Expand Down Expand Up @@ -492,6 +538,10 @@ def generate_pixel_data(self) -> PixelData:
initial_count_corner=INITIAL_COUNT_CORNER[
self.start_counting_combo_box.currentText()
],
gap_every_rows=self.gap_every_rows_spin_box.value(),
gap_every_columns=self.gap_every_columns_spin_box.value(),
row_gap_height=self.row_gap_height_spin_box.value(),
column_gap_width=self.column_gap_width_spin_box.value(),
)

if self.entire_shape_radio_button.isChecked():
Expand Down
165 changes: 161 additions & 4 deletions tests/geometry/test_pixel_data_to_nexus_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,47 @@
ROW_COL_VALS = [4, 7]


@pytest.fixture(params=[0, 1, 4])
def pixel_grid_gap_every_rows(request):
return request.param


@pytest.fixture(params=[0, 1, 3])
def pixel_grid_gap_every_columns(request):
return request.param


@pytest.fixture(scope="function")
def pixel_grid():
def pixel_grid(pixel_grid_gap_every_rows, pixel_grid_gap_every_columns):
return PixelGrid(
rows=5,
columns=7,
row_height=0.873,
col_width=2.0 / 3,
first_id=0,
count_direction=CountDirection.ROW,
initial_count_corner=Corner.BOTTOM_LEFT,
gap_every_rows=pixel_grid_gap_every_rows,
gap_every_columns=pixel_grid_gap_every_columns,
row_gap_height=0.1,
column_gap_width=0.7,
)


@pytest.fixture(scope="function")
def pixel_grid_without_gaps():
return PixelGrid(
rows=3,
columns=4,
row_height=0.873,
col_width=2.0 / 3,
first_id=0,
count_direction=CountDirection.ROW,
initial_count_corner=Corner.BOTTOM_LEFT,
gap_every_rows=0,
gap_every_columns=3,
row_gap_height=0.1,
column_gap_width=0,
)


Expand Down Expand Up @@ -89,11 +120,63 @@ def test_GIVEN_single_id_WHEN_calling_detector_number_THEN_list_is_not_returned(
assert get_detector_number_from_pixel_mapping(pixel_mapping) == [pixel_id]


def test_GIVEN_simple_pixel_grid_WHEN_calling_pixel_grid_offsets_THEN_correct_offset_lists_are_returned():
pixel_grid = PixelGrid(
rows=4,
columns=4,
row_height=0.9,
col_width=2.0 / 3,
first_id=0,
count_direction=CountDirection.ROW,
initial_count_corner=Corner.BOTTOM_LEFT,
gap_every_rows=2,
gap_every_columns=1,
row_gap_height=0.25,
column_gap_width=0.7,
)

expected_x_offsets = [
[
(pixel_grid.column_gap_width + pixel_grid.col_width) * -1.5,
(pixel_grid.column_gap_width + pixel_grid.col_width) * -0.5,
(pixel_grid.column_gap_width + pixel_grid.col_width) * 0.5,
(pixel_grid.column_gap_width + pixel_grid.col_width) * 1.5,
]
for _ in range(pixel_grid.rows)
]
expected_y_offsets = [
[
(1.5 * pixel_grid.row_height + 0.5 * pixel_grid.row_gap_height)
for _ in range(pixel_grid.columns)
],
[
(0.5 * pixel_grid.row_height + 0.5 * pixel_grid.row_gap_height)
for _ in range(pixel_grid.columns)
],
[
(-0.5 * pixel_grid.row_height + -0.5 * pixel_grid.row_gap_height)
for _ in range(pixel_grid.columns)
],
[
(-1.5 * pixel_grid.row_height + -0.5 * pixel_grid.row_gap_height)
for _ in range(pixel_grid.columns)
],
]

assert np.allclose(
np.array(expected_x_offsets), get_x_offsets_from_pixel_grid(pixel_grid)
)
assert np.allclose(
np.array(expected_y_offsets), get_y_offsets_from_pixel_grid(pixel_grid)
)


@pytest.mark.parametrize("rows", ROW_COL_VALS)
@pytest.mark.parametrize("columns", ROW_COL_VALS)
def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_x_offsets_THEN_correct_x_offset_list_is_returned(
pixel_grid, rows, columns
def test_GIVEN_pixel_grid_without_gaps_WHEN_calling_pixel_grid_x_offsets_THEN_correct_x_offset_list_is_returned(
pixel_grid_without_gaps, rows, columns
):
pixel_grid = pixel_grid_without_gaps
pixel_grid.columns = columns
pixel_grid.rows = rows

Expand All @@ -110,11 +193,50 @@ def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_x_offsets_THEN_correct_x_offse

@pytest.mark.parametrize("rows", ROW_COL_VALS)
@pytest.mark.parametrize("columns", ROW_COL_VALS)
def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_y_offsets_THEN_correct_y_offset_list_is_returned(
def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_x_offsets_THEN_correct_x_offset_list_is_returned(
pixel_grid, rows, columns
):
pixel_grid.columns = columns
pixel_grid.rows = rows
number_of_column_gaps = (
(pixel_grid.columns - 1) // pixel_grid.gap_every_columns
if pixel_grid.gap_every_columns
else 0
)

offset_offset = ((pixel_grid.columns - 1) * pixel_grid.col_width / 2) + (
pixel_grid.column_gap_width * number_of_column_gaps / 2
)
expected_x_offsets = [
[0 for _ in range(pixel_grid.columns)] for _ in range(pixel_grid.rows)
]
gap_counter = 0
for i in range(pixel_grid.columns):
for j in range(pixel_grid.rows):
expected_x_offsets[j][i] = (
(i * pixel_grid.col_width)
+ gap_counter * pixel_grid.column_gap_width
- offset_offset
)
if (
pixel_grid.gap_every_columns > 0
and (i + 1) % pixel_grid.gap_every_columns == 0
):
gap_counter += 1

assert np.allclose(
np.array(expected_x_offsets), get_x_offsets_from_pixel_grid(pixel_grid)
)


@pytest.mark.parametrize("rows", ROW_COL_VALS)
@pytest.mark.parametrize("columns", ROW_COL_VALS)
def test_GIVEN_pixel_grid_without_gaps_WHEN_calling_pixel_grid_y_offsets_THEN_correct_y_offset_list_is_returned(
pixel_grid_without_gaps, rows, columns
):
pixel_grid = pixel_grid_without_gaps
pixel_grid.columns = columns
pixel_grid.rows = rows

offset_offset = (pixel_grid.rows - 1) * pixel_grid.row_height / 2
expected_y_offsets = [
Expand All @@ -127,6 +249,41 @@ def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_y_offsets_THEN_correct_y_offse
)


@pytest.mark.parametrize("rows", ROW_COL_VALS)
@pytest.mark.parametrize("columns", ROW_COL_VALS)
def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_y_offsets_THEN_correct_y_offset_list_is_returned(
pixel_grid, rows, columns
):
pixel_grid.columns = columns
pixel_grid.rows = rows
number_of_row_gaps = (
(pixel_grid.rows - 1) // pixel_grid.gap_every_rows
if pixel_grid.gap_every_rows
else 0
)

offset_offset = ((pixel_grid.rows - 1) * pixel_grid.row_height / 2) + (
pixel_grid.row_gap_height * number_of_row_gaps / 2
)
expected_y_offsets = [
[0 for _ in range(pixel_grid.columns)] for _ in range(pixel_grid.rows)
]
gap_counter = 0
for i in range(pixel_grid.rows):
for j in range(pixel_grid.columns):
expected_y_offsets[i][j] = (
offset_offset
- (i * pixel_grid.row_height)
- gap_counter * pixel_grid.row_gap_height
)
if pixel_grid.gap_every_rows > 0 and (i + 1) % pixel_grid.gap_every_rows == 0:
gap_counter += 1

assert np.allclose(
np.array(expected_y_offsets), get_y_offsets_from_pixel_grid(pixel_grid)
)


@pytest.mark.parametrize("rows", ROW_COL_VALS)
@pytest.mark.parametrize("columns", ROW_COL_VALS)
def test_GIVEN_pixel_grid_WHEN_calling_pixel_grid_z_offsets_THEN_z_offsets_are_all_zero(
Expand Down
Loading

0 comments on commit e7fb503

Please sign in to comment.