|
30 | 30 | def test_stage_positions(position: Any, pexpectation: Sequence[float]) -> None: |
31 | 31 | position = useq.Position.model_validate(position) |
32 | 32 | assert (position.x, position.y, position.z) == pexpectation |
| 33 | + |
| 34 | + |
| 35 | +_ABSOLUTE_GRID_PLANS = [ |
| 36 | + pytest.param( |
| 37 | + {"top": 1, "bottom": -1, "left": 0, "right": 0}, |
| 38 | + id="GridFromEdges", |
| 39 | + ), |
| 40 | + pytest.param( |
| 41 | + {"vertices": [(0, 0), (4, 0), (2, 4)], "fov_width": 2, "fov_height": 2}, |
| 42 | + id="GridFromPolygon", |
| 43 | + ), |
| 44 | +] |
| 45 | + |
| 46 | +_RELATIVE_GRID_PLANS = [ |
| 47 | + pytest.param({"rows": 2, "columns": 2}, id="GridRowsColumns"), |
| 48 | + pytest.param( |
| 49 | + useq.RandomPoints(num_points=3, max_width=100, max_height=100), |
| 50 | + id="RandomPoints", |
| 51 | + ), |
| 52 | +] |
| 53 | + |
| 54 | + |
| 55 | +@pytest.mark.parametrize("grid_plan", _ABSOLUTE_GRID_PLANS) |
| 56 | +def test_position_warns_on_absolute_sub_sequence_grid( |
| 57 | + grid_plan: dict, |
| 58 | +) -> None: |
| 59 | + """Position clears x/y and warns at construction when sub-sequence uses an absolute grid.""" |
| 60 | + with pytest.warns(UserWarning, match="is ignored when a position sequence uses"): |
| 61 | + pos = useq.Position(x=1, y=2, sequence={"grid_plan": grid_plan}) |
| 62 | + assert pos.x is None |
| 63 | + assert pos.y is None |
| 64 | + |
| 65 | + |
| 66 | +@pytest.mark.parametrize("grid_plan", _RELATIVE_GRID_PLANS) |
| 67 | +def test_position_no_warn_on_relative_sub_sequence_grid(grid_plan: Any) -> None: |
| 68 | + """Position keeps x/y when sub-sequence uses a relative grid.""" |
| 69 | + pos = useq.Position(x=1, y=2, sequence={"grid_plan": grid_plan}) |
| 70 | + assert pos.x == 1 |
| 71 | + assert pos.y == 2 |
| 72 | + |
| 73 | + |
| 74 | +# --- __add__ / __radd__ ----------------------------------------------------------- |
| 75 | + |
| 76 | +_ADD_CASES = [ |
| 77 | + pytest.param( |
| 78 | + useq.Position(x=1, y=2, z=3), |
| 79 | + useq.RelativePosition(x=5, y=10, z=1), |
| 80 | + (6, 12, 4), |
| 81 | + id="both_have_values", |
| 82 | + ), |
| 83 | + pytest.param( |
| 84 | + useq.Position(x=None, y=None, z=3), |
| 85 | + useq.RelativePosition(x=5, y=10, z=0), |
| 86 | + (5, 10, 3), |
| 87 | + id="none_falls_back_to_other", |
| 88 | + ), |
| 89 | +] |
| 90 | + |
| 91 | + |
| 92 | +@pytest.mark.parametrize("pos, rel, expected", _ADD_CASES) |
| 93 | +def test_position_add( |
| 94 | + pos: useq.Position, rel: useq.RelativePosition, expected: tuple |
| 95 | +) -> None: |
| 96 | + result = pos + rel |
| 97 | + assert (result.x, result.y, result.z) == expected |
| 98 | + |
| 99 | + |
| 100 | +def test_position_radd() -> None: |
| 101 | + """__radd__ supports reversed addition (RelativePosition + AbsolutePosition).""" |
| 102 | + pos = useq.Position(x=1, y=2, z=3) |
| 103 | + rel = useq.RelativePosition(x=5, y=10, z=0) |
| 104 | + result = rel + pos |
| 105 | + assert (result.x, result.y, result.z) == (6, 12, 3) |
| 106 | + |
| 107 | + |
| 108 | +# --- RelativePosition rejected in stage_positions -------------------------------- |
| 109 | + |
| 110 | + |
| 111 | +def test_relative_position_rejected_in_stage_positions() -> None: |
| 112 | + """RelativePosition is always rejected in stage_positions.""" |
| 113 | + with pytest.raises(Exception, match="RelativePosition cannot be used"): |
| 114 | + useq.MDASequence(stage_positions=[useq.RelativePosition(x=1, y=2, z=3)]) |
| 115 | + |
| 116 | + |
| 117 | +# --- Global grid + position interactions ----------------------------------------- |
| 118 | + |
| 119 | + |
| 120 | +def test_warns_global_abs_grid_does_not_mutate_original_position() -> None: |
| 121 | + """Clearing x/y for a global absolute grid must not mutate the original Position.""" |
| 122 | + pos = useq.Position(x=1, y=2, z=3) |
| 123 | + with pytest.warns(UserWarning, match="is ignored when using"): |
| 124 | + seq = useq.MDASequence( |
| 125 | + stage_positions=[pos], |
| 126 | + grid_plan={"top": 1, "bottom": -1, "left": 0, "right": 0}, |
| 127 | + ) |
| 128 | + assert pos.x == 1 # original must be untouched |
| 129 | + assert pos.y == 2 |
| 130 | + assert seq.stage_positions[0].x is None # sequence copy is updated |
| 131 | + assert seq.stage_positions[0].y is None |
| 132 | + |
| 133 | + |
| 134 | +@pytest.mark.parametrize("grid_plan", _ABSOLUTE_GRID_PLANS) |
| 135 | +def test_z_only_position_iterates_with_absolute_grid(grid_plan: dict) -> None: |
| 136 | + """Position(x=None, y=None, z=3) iterates correctly: grid provides x/y, pos z.""" |
| 137 | + seq = useq.MDASequence( |
| 138 | + stage_positions=[useq.Position(x=None, y=None, z=3)], |
| 139 | + grid_plan=grid_plan, |
| 140 | + ) |
| 141 | + events = list(seq) |
| 142 | + assert len(events) > 0 |
| 143 | + for event in events: |
| 144 | + assert event.x_pos is not None |
| 145 | + assert event.y_pos is not None |
| 146 | + assert event.z_pos == 3.0 |
0 commit comments