DAQ: updated face_detection added operations/face_detection fodler including service and models, new tests of face_detection
Build and Publish / test (push) Failing after 1m4s
Build and Publish / Build and Deploy Docs (push) Has been skipped
Build and Publish / build (push) Has been skipped

This commit is contained in:
appleb_m committed 2026-04-30 12:44:55 +02:00
1 parent 2b7c634227
commit a00f0b3dff
6 files changed
+783 -206

No files matched your search

@@ -0,0 +1,216 @@
import types
import pytest
from aare.common.coordinate import Coordinate, SmargonCoordinate
from aare.common.models import BoundingBoxModel, MLBoxModel, MLBoxType, ZoomModeEnum
from aare.daq.operations.face_detection.models import (
FaceDetectionContext,
FaceDetectionResult,
)
from aare.daq.operations.face_detection.service import FaceDetectionService
class DummyLogger:
def info(self, *args, **kwargs):
pass
def debug(self, *args, **kwargs):
pass
def error(self, *args, **kwargs):
pass
def log(self, *args, **kwargs):
pass
class DummyGeometry:
def __init__(self):
self.beam_location_pxl = Coordinate(x=100.0, y=100.0, z=0.0)
def picture_to_smargon(self, coord: Coordinate) -> Coordinate:
return Coordinate(x=coord.x / 100.0, y=coord.y / 100.0, z=0.0)
def _box(cls: MLBoxType, x1: float, y1: float, x2: float, y2: float) -> MLBoxModel:
return MLBoxModel(
cls=cls,
box=BoundingBoxModel(
top_x=x1,
top_y=y1,
bottom_x=x2,
bottom_y=y2,
),
conf=0.95,
)
@pytest.fixture
def context():
progress_events = []
devs = types.SimpleNamespace(
lamp_light=0.0,
zoom=200.0,
aerotech_omega=0.0,
smargon_pos=None,
set_zoom=lambda zoom_value, wait=True: None,
smargon_wait=lambda timeout=60: None,
)
cfg = types.SimpleNamespace(zoom_mode=ZoomModeEnum.User)
ctx = FaceDetectionContext(
cfg=cfg,
devs=devs,
mlbox=types.SimpleNamespace(
predict=lambda **kwargs: types.SimpleNamespace(
box=None,
target_point=None,
focus=None,
)
),
sample_geometry_provider=lambda: DummyGeometry(),
emit_progress=lambda payload: progress_events.append(payload),
)
ctx._progress_events = progress_events
return ctx
def test_service_returns_empty_payload_when_no_boxes(monkeypatch, context):
service = FaceDetectionService(context=context, logger=DummyLogger())
monkeypatch.setattr(
context.mlbox,
"predict",
lambda **kwargs: types.SimpleNamespace(
box=None,
target_point=None,
focus=None,
),
)
result = service.run(steps=2, step_size=30)
assert isinstance(result, FaceDetectionResult)
assert result.success is True
assert result.payload["running"] is False
assert result.payload["samples"] == []
assert result.payload["height_fit"] == {}
assert result.payload["area_fit"] == {}
assert context.cfg.zoom_mode == ZoomModeEnum.User
assert len(context._progress_events) >= 1
assert context._progress_events[-1]["running"] is False
def test_service_prefers_face_boxes_when_ratio_is_high(monkeypatch, context):
service = FaceDetectionService(context=context, logger=DummyLogger())
predictions = iter(
[
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 12, 20, 32, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 16, 20, 36, 40), target_point=None, focus=None),
]
)
monkeypatch.setattr(context.mlbox, "predict", lambda **kwargs: next(predictions))
monkeypatch.setattr(
"aare.daq.operations.face_detection.service.fd.get_flat_face",
lambda boxes, start_angle, end_angle, use_area: (
45 if use_area else 50,
{"A": 1.0, "B": 2.0, "phi_rad": 0.1, "C": 3.0},
),
)
monkeypatch.setattr(
"aare.daq.operations.face_detection.service.fd.choose_best_fit",
lambda fit_results: (47, {"A": 1.0}, "Area"),
)
monkeypatch.setattr(
"aare.daq.operations.face_detection.service.fd.get_samples_out",
lambda boxes: [{"angle": angle, "box": box} for angle, box in sorted(boxes.items())],
)
result = service.run(steps=3, step_size=30, face_min_ratio=0.5)
assert result.success is True
assert result.payload["running"] is False
assert result.payload["height_fit"]["best_angle_deg"] == 50
assert result.payload["area_fit"]["best_angle_deg"] == 45
assert context.devs.aerotech_omega == 47
assert [sample["angle"] for sample in result.payload["samples"]] == [0, 30, 90]
assert context._progress_events[-1]["running"] is False
def test_service_falls_back_to_loop_all_when_face_ratio_is_low(monkeypatch, context):
service = FaceDetectionService(context=context, logger=DummyLogger())
predictions = iter(
[
types.SimpleNamespace(box=_box(MLBoxType.LOOP_FACE, 10, 20, 30, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 11, 20, 31, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 12, 20, 32, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 13, 20, 33, 40), target_point=None, focus=None),
types.SimpleNamespace(box=_box(MLBoxType.LOOP_ALL, 14, 20, 34, 40), target_point=None, focus=None),
]
)
monkeypatch.setattr(context.mlbox, "predict", lambda **kwargs: next(predictions))
captured = {}
def fake_get_flat_face(boxes, start_angle, end_angle, use_area):
captured["boxes_used"] = dict(boxes)
return 60, {"A": 1.0, "B": 2.0, "phi_rad": 0.2, "C": 4.0}
monkeypatch.setattr("aare.daq.operations.face_detection.service.fd.get_flat_face", fake_get_flat_face)
monkeypatch.setattr(
"aare.daq.operations.face_detection.service.fd.choose_best_fit",
lambda fit_results: (60, {"A": 1.0}, "Height"),
)
monkeypatch.setattr(
"aare.daq.operations.face_detection.service.fd.get_samples_out",
lambda boxes: [{"angle": angle} for angle in sorted(boxes.keys())],
)
result = service.run(steps=4, step_size=30, face_min_ratio=0.5)
assert result.success is True
assert len(captured["boxes_used"]) == 4
assert sorted(captured["boxes_used"].keys()) == [30, 60, 90, 120]
assert context.devs.aerotech_omega == 60
assert context._progress_events[-1]["running"] is False
def test_service_applies_centre_correction_when_target_is_far_from_beam(monkeypatch, context):
service = FaceDetectionService(context=context, logger=DummyLogger())
model = _box(MLBoxType.LOOP_FACE, 40, 160, 80, 200)
service._centre_correction(model, tolerance=0.2)
assert isinstance(context.devs.smargon_pos, SmargonCoordinate)
assert context.devs.smargon_pos.sh_mm.x == pytest.approx(1.0)
assert context.devs.smargon_pos.sh_mm.y == pytest.approx(1.8)
def test_service_returns_failed_result_when_prediction_raises(monkeypatch, context):
service = FaceDetectionService(context=context, logger=DummyLogger())
monkeypatch.setattr(
context.mlbox,
"predict",
lambda **kwargs: (_ for _ in ()).throw(RuntimeError("prediction failed")),
)
result = service.run(steps=1, step_size=30)
assert result.success is False
assert isinstance(result.error, RuntimeError)
assert result.comment == "Face detection sequence failed"
assert result.payload["running"] is False
assert context.cfg.zoom_mode == ZoomModeEnum.User
assert context._progress_events[-1]["running"] is False
+105
View File
@@ -0,0 +1,105 @@
import types
from aare.common.models import DAQOperation
from aare.daq.operations.face_detection.models import FaceDetectionResult
def test_execute_face_detection_reports_failure(monkeypatch):
from aare.daq.daq import AareDAQ
daq = object.__new__(AareDAQ)
calls = {"set_state": [], "handle_error": []}
monkeypatch.setattr(
daq,
"_AareDAQ__set_state",
lambda state: calls["set_state"].append(state),
)
monkeypatch.setattr(
daq,
"_create_face_detection_service",
lambda: types.SimpleNamespace(
run=lambda **kwargs: FaceDetectionResult(
success=False,
payload={"running": False, "samples": [], "height_fit": {}, "area_fit": {}},
error=RuntimeError("fd failed"),
comment="face detection sequence failed",
)
),
)
monkeypatch.setattr(
daq,
"_handle_operation_error",
lambda **kwargs: calls["handle_error"].append(kwargs),
)
monkeypatch.setattr(
type(daq),
"sample",
property(lambda self: types.SimpleNamespace(sample_name="sample-1", db_id=1)),
)
result = daq._execute_face_detection(steps=7, step_size=30, report_error=True)
assert result.success is False
assert len(calls["set_state"]) == 1
assert len(calls["handle_error"]) == 1
assert calls["handle_error"][0]["operation"] == DAQOperation.FACE_CENTERING
def test_execute_face_detection_can_skip_error_reporting(monkeypatch):
from aare.daq.daq import AareDAQ
daq = object.__new__(AareDAQ)
handle_error_calls = []
monkeypatch.setattr(daq, "_AareDAQ__set_state", lambda state: None)
monkeypatch.setattr(
daq,
"_create_face_detection_service",
lambda: types.SimpleNamespace(
run=lambda **kwargs: FaceDetectionResult(
success=False,
payload={"running": False, "samples": [], "height_fit": {}, "area_fit": {}},
error=RuntimeError("fd failed"),
comment="face detection sequence failed",
)
),
)
monkeypatch.setattr(
daq,
"_handle_operation_error",
lambda **kwargs: handle_error_calls.append(kwargs),
)
result = daq._execute_face_detection(report_error=False)
assert result.success is False
assert handle_error_calls == []
def test_public_face_detection_uses_execute_face_detection(monkeypatch):
from aare.daq.daq import AareDAQ
daq = object.__new__(AareDAQ)
cfg = types.SimpleNamespace(
try_set_busy=lambda timeout=360: None,
state_busy=False,
)
setattr(daq, "_AareDAQ__cfg", cfg)
monkeypatch.setattr(
daq,
"_execute_face_detection",
lambda **kwargs: FaceDetectionResult(
success=True,
payload={"running": False, "samples": [{"angle": 45}], "height_fit": {}, "area_fit": {}},
),
)
result = daq.face_detection(steps=7, step_size=30)
assert result["running"] is False
assert result["samples"] == [{"angle": 45}]
assert cfg.state_busy is False