diff --git a/tests/test_phasor.py b/tests/test_phasor.py index 033b6f5..4afb189 100644 --- a/tests/test_phasor.py +++ b/tests/test_phasor.py @@ -382,3 +382,116 @@ def test_phasor_settings_are_served(serve_state): assert payload['values']['phasor_filter'] == 'none' keys = [entry['key'] for entry in payload['schema']] assert 'phasor_filter' in keys and 'irf' in keys + + +def test_a_polygon_selects_one_population(two_populations): + real, imag, mean = two_populations + + masks = phasor.cursor_masks( + real, imag, mean, + cursors=[{'id': 'p1', 'type': 'polygon', + 'vertices': [[0.25, 0.35], [0.35, 0.35], + [0.35, 0.45], [0.25, 0.45]]}], + min_photons=1.0) + + mask = masks['p1'] + assert mask[:8].all() + assert not mask[8:].any() + + +def test_a_polygon_and_an_ellipse_share_one_request(two_populations): + real, imag, mean = two_populations + + masks = phasor.cursor_masks( + real, imag, mean, + cursors=[ + {'id': 'p1', 'type': 'polygon', + 'vertices': [[0.25, 0.35], [0.35, 0.35], + [0.35, 0.45], [0.25, 0.45]]}, + {'id': 'e1', 'center_g': 0.70, 'center_s': 0.35, 'radius': 0.05}, + ], + min_photons=1.0) + + assert masks['p1'].sum() == 128 + assert masks['e1'].sum() == 128 + assert not (masks['p1'] & masks['e1']).any() + + +def test_a_polygon_drops_low_photon_pixels(two_populations): + real, imag, mean = two_populations + mean = mean.copy() + mean[0, :] = 0.0 + + masks = phasor.cursor_masks( + real, imag, mean, + cursors=[{'id': 'p1', 'type': 'polygon', + 'vertices': [[0.25, 0.35], [0.35, 0.35], + [0.35, 0.45], [0.25, 0.45]]}], + min_photons=10.0) + + assert not masks['p1'][0].any() + assert masks['p1'][1:8].all() + + +def test_a_polygon_needs_three_vertices(two_populations): + real, imag, mean = two_populations + + with pytest.raises(ValueError, match='three vertices'): + phasor.cursor_masks( + real, imag, mean, + cursors=[{'id': 'p1', 'type': 'polygon', + 'vertices': [[0.25, 0.35], [0.35, 0.45]]}], + min_photons=1.0) + + +def test_a_polygon_with_no_vertices_is_rejected(two_populations): + real, imag, mean = two_populations + + with pytest.raises(ValueError, match='three vertices'): + phasor.cursor_masks( + real, imag, mean, + cursors=[{'id': 'p1', 'type': 'polygon'}], + min_photons=1.0) + + +def test_an_unknown_cursor_type_is_rejected(two_populations): + real, imag, mean = two_populations + + with pytest.raises(ValueError, match='unknown cursor type'): + phasor.cursor_masks( + real, imag, mean, + cursors=[{'id': 'x1', 'type': 'freehand'}], + min_photons=1.0) + + +def test_a_polygon_gets_its_own_label(two_populations): + real, imag, mean = two_populations + + labels = phasor.label_image( + real, imag, mean, + cursors=[ + {'id': 'e1', 'center_g': 0.70, 'center_s': 0.35, 'radius': 0.05}, + {'id': 'p1', 'type': 'polygon', + 'vertices': [[0.25, 0.35], [0.35, 0.35], + [0.35, 0.45], [0.25, 0.45]]}, + ], + min_photons=1.0) + + assert set(np.unique(labels)) == {1, 2} + assert (labels[8:] == 1).all() + assert (labels[:8] == 2).all() + + +def test_a_polygon_reports_lifetimes_like_an_ellipse(two_populations): + real, imag, mean = two_populations + cursors = [{'id': 'p1', 'type': 'polygon', + 'vertices': [[0.25, 0.35], [0.35, 0.35], + [0.35, 0.45], [0.25, 0.45]]}] + + masks = phasor.cursor_masks(real, imag, mean, cursors, min_photons=1.0) + found = phasor.cursor_stats(real, imag, mean, masks, 80.0) + + assert len(found) == 1 + assert found[0]['n_pixels'] == 128 + assert found[0]['tau_phi_ns'] > 0 + assert found[0]['mean_g'] == pytest.approx(0.30, abs=0.01) diff --git a/tests/test_phasor_endpoints.py b/tests/test_phasor_endpoints.py index b55830a..f57c580 100644 --- a/tests/test_phasor_endpoints.py +++ b/tests/test_phasor_endpoints.py @@ -161,3 +161,84 @@ def test_mask_options_ride_on_the_body(dataset): }) assert payload['options']['phasor_filter'] == 'median' + + +FULL_PLANE = [[-5.0, -5.0], [5.0, -5.0], [5.0, 5.0], [-5.0, 5.0]] +LEFT_HALF = [[-5.0, -5.0], [0.5, -5.0], [0.5, 5.0], [-5.0, 5.0]] +RIGHT_HALF = [[0.5, -5.0], [5.0, -5.0], [5.0, 5.0], [0.5, 5.0]] + + +def _mask(url, ident, cursors, min_photons=1.0, output=None): + body = {'cursors': cursors, 'min_photons': min_photons} + if output: + body['output'] = output + return _call(url, f'/v1/datasets/{ident}/phasor/mask', 'POST', body) + + +def test_a_polygon_covering_everything_selects_every_valid_pixel(dataset): + url, ident = dataset + points = _call(url, f'/v1/datasets/{ident}/phasor/points?bins=64&min_photons=1.0') + + payload = _mask(url, ident, [ + {'id': 'p1', 'type': 'polygon', 'vertices': FULL_PLANE}]) + + assert payload['cursors'][0]['n_pixels'] == points['n_valid'] + + +def test_two_polygons_partition_the_valid_pixels(dataset): + url, ident = dataset + points = _call(url, f'/v1/datasets/{ident}/phasor/points?bins=64&min_photons=1.0') + + payload = _mask(url, ident, [ + {'id': 'left', 'type': 'polygon', 'vertices': LEFT_HALF}, + {'id': 'right', 'type': 'polygon', 'vertices': RIGHT_HALF}]) + + counts = {entry['id']: entry['n_pixels'] for entry in payload['cursors']} + assert counts['left'] + counts['right'] == points['n_valid'] + + +def test_a_polygon_reports_a_lifetime(dataset): + url, ident = dataset + + payload = _mask(url, ident, [ + {'id': 'p1', 'type': 'polygon', 'vertices': FULL_PLANE}]) + + entry = payload['cursors'][0] + assert entry['n_pixels'] > 0 + assert entry['tau_phi_ns'] is not None + assert entry['photons'] > 0 + + +def test_a_polygon_becomes_a_label_image(dataset): + url, ident = dataset + + payload = _mask(url, ident, [ + {'id': 'p1', 'type': 'polygon', 'vertices': FULL_PLANE}], + output='labels') + + labels = np.frombuffer(base64.b64decode(payload['labels']), dtype=np.uint8) + assert labels.size == payload['width'] * payload['height'] + assert int((labels == 1).sum()) == payload['cursors'][0]['n_pixels'] + + +def test_a_polygon_and_an_ellipse_are_labelled_apart(dataset): + url, ident = dataset + + payload = _mask(url, ident, [ + {'id': 'e1', 'center_g': 0.5, 'center_s': 0.3, 'radius': 0.1}, + {'id': 'p1', 'type': 'polygon', 'vertices': FULL_PLANE}], + output='labels') + + labels = np.frombuffer(base64.b64decode(payload['labels']), dtype=np.uint8) + assert set(np.unique(labels)) <= {0, 1, 2} + assert int((labels == 1).sum()) == payload['cursors'][0]['n_pixels'] + + +def test_a_polygon_with_two_vertices_is_a_client_error(dataset): + url, ident = dataset + + with pytest.raises(HTTPError) as raised: + _mask(url, ident, [ + {'id': 'p1', 'type': 'polygon', 'vertices': [[0.2, 0.3], [0.4, 0.5]]}]) + + assert raised.value.code == 400