def test_recursive_stage_intensity(stage_factory: StageFactoryBase): root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages( stage_factory) from ceed.function.plugin import LinearFunc for i, stage in enumerate((s1, s2)): stage.stage.add_func(LinearFunc( function_factory=stage_factory.function_factory, b=0, m=.1, duration=(i + 1) * 5)) shape = CircleShapeP1( app=None, painter=stage_factory.shape_factory, show_in_gui=False, create_add_shape=True) shape2 = CircleShapeP1Internal( app=None, painter=stage_factory.shape_factory, show_in_gui=False, create_add_shape=True) s1.stage.add_shape(shape.shape) s2.stage.add_shape(shape2.shape) values, n = get_stage_time_intensity(stage_factory, root.name, 10) assert n == 10 * 10 assert len(values) == 2 colors = values[shape.name] colors2 = values[shape2.name] assert len(colors) == n for i, (r, g, b, a) in enumerate(colors): if i < 5 * 10: assert math.isclose(r, i / 10 * .1) if s2.color_r else r == 0 assert math.isclose(g, i / 10 * .1) if s2.color_g else g == 0 assert math.isclose(b, i / 10 * .1) if s2.color_b else b == 0 else: assert r == 0 assert g == 0 assert b == 0 for i, (r, g, b, a) in enumerate(colors2): assert math.isclose(r, i / 10 * .1) if s2.color_r else r == 0 assert math.isclose(g, i / 10 * .1) if s2.color_g else g == 0 assert math.isclose(b, i / 10 * .1) if s2.color_b else b == 0
def test_register_funcs(): from ceed.function.plugin import ConstFunc, LinearFunc function_factory = FunctionFactoryBase() count = 0 def count_changes(*largs): nonlocal count count += 1 function_factory.fbind('on_changed', count_changes) assert not function_factory.funcs_cls assert not function_factory.funcs_user assert not function_factory.funcs_inst assert not function_factory.funcs_inst_default assert not function_factory.get_classes() assert not function_factory.get_names() function_factory.register(ConstFunc) assert count assert function_factory.funcs_cls['ConstFunc'] is ConstFunc assert isinstance(function_factory.funcs_inst['Const'], ConstFunc) assert isinstance(function_factory.funcs_inst_default['Const'], ConstFunc) assert ConstFunc in function_factory.get_classes() assert 'ConstFunc' in function_factory.get_names() f = LinearFunc(function_factory=function_factory) count = 0 function_factory.register(LinearFunc, instance=f) assert count assert function_factory.funcs_cls['LinearFunc'] is LinearFunc assert function_factory.funcs_inst['Linear'] is f assert function_factory.funcs_inst_default['Linear'] is f assert LinearFunc in function_factory.get_classes() assert 'LinearFunc' in function_factory.get_names() assert not function_factory.funcs_user
def test_simple_stage_intensity(stage_factory: StageFactoryBase): from ceed.function.plugin import LinearFunc shape = EllipseShapeP1( app=None, painter=stage_factory.shape_factory, show_in_gui=False, create_add_shape=True) shape2 = EllipseShapeP2( app=None, painter=stage_factory.shape_factory, show_in_gui=False, create_add_shape=True) f: LinearFunc = LinearFunc( function_factory=stage_factory.function_factory, b=0, m=.1, duration=5) stage = make_stage( stage_factory, color_r=True, color_g=False, color_b=True) stage_factory.add_stage(stage) stage.add_func(f) stage.add_shape(shape.shape) stage.add_shape(shape2.shape) values, n = get_stage_time_intensity(stage_factory, stage.name, 10) assert n == 5 * 10 assert len(values) == 2 colors = values[shape.name] colors2 = values[shape2.name] assert len(colors) == 10 * 5 for i, (r, g, b, a) in enumerate(colors): assert math.isclose(r, i / 10 * .1) assert math.isclose(b, i / 10 * .1) assert g == 0 for i, (r, g, b, a) in enumerate(colors2): assert math.isclose(r, i / 10 * .1) assert math.isclose(b, i / 10 * .1) assert g == 0
async def run_data_experiment(stage_app: CeedTestApp): from ..test_stages import create_2_shape_stage from ceed.function.plugin import LinearFunc root, s1, s2, shape1, shape2 = create_2_shape_stage( stage_app.app.stage_factory, show_in_gui=True, app=stage_app) await stage_app.wait_clock_frames(2) # 30 frames f1 = LinearFunc(function_factory=stage_app.app.function_factory, duration=.25, loop=8, m=2.4) f2 = LinearFunc(function_factory=stage_app.app.function_factory, duration=.25, loop=8, m=2.4) s1.stage.add_func(f1) s2.stage.add_func(f2) await stage_app.wait_clock_frames(2) stage_app.app.view_controller.frame_rate = 120 # count frames stage_app.app.view_controller.use_software_frame_rate = False stage_app.app.view_controller.pad_to_stage_handshake = True for image, (b1, b2) in zip(stored_images, stored_b_values): f1.b = b1 f2.b = b2 # set background image stage_app.app.central_display.update_img(image) stage_app.app.player.last_image = image await stage_app.wait_clock_frames(2) stage_app.app.view_controller.request_stage_start(root.name) await wait_experiment_done(stage_app, timeout=10 * 60) for i, image in enumerate(stored_images[2:]): stage_app.app.ceed_data.add_image_to_file(image, f'image {i}') await stage_app.wait_clock_frames(2)
async def test_recursive_play_stage_intensity(stage_app: CeedTestApp, tmp_path, flip, skip, video_mode): """Checks that proper frame rendering happens in all these modes. In skip mode, some frames are skipped if GPU/CPU is too slow. """ from ..test_stages import create_recursive_stages from .examples.shapes import CircleShapeP1, CircleShapeP2 from kivy.clock import Clock from ceed.analysis import CeedDataReader root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages( stage_app.app.stage_factory, app=stage_app) from ceed.function.plugin import LinearFunc for i, stage in enumerate((s1, s2, s3, s4, s5, s6)): stage.stage.add_func( LinearFunc(function_factory=stage_app.app.function_factory, b=0, m=.5, duration=(i % 2 + 1) * 1)) shape = CircleShapeP1(app=None, painter=stage_app.app.shape_factory, show_in_gui=True) shape2 = CircleShapeP2(app=None, painter=stage_app.app.shape_factory, show_in_gui=True) s1.stage.add_shape(shape.shape) s4.stage.add_shape(shape.shape) s5.stage.add_shape(shape.shape) s2.stage.add_shape(shape2.shape) s3.stage.add_shape(shape2.shape) s6.stage.add_shape(shape2.shape) root.show_in_gui() await stage_app.wait_clock_frames(2) frame = 0 event = None # make GPU too slow to force skipping frames, when enabled fps = await measure_fps(stage_app) + 10 rate = stage_app.app.view_controller.frame_rate = fps stage_app.app.view_controller.use_software_frame_rate = False stage_app.app.view_controller.flip_projector = flip stage_app.app.view_controller.skip_estimated_missed_frames = skip stage_app.app.view_controller.video_mode = video_mode stage_app.app.view_controller.pad_to_stage_handshake = False n_sub_frames = 1 if video_mode == 'QUAD4X': n_sub_frames = 4 elif video_mode == 'QUAD12X': n_sub_frames = 12 centers = shape.center, shape2.center num_frames = rate * n_sub_frames * (2 + 1 + 2 + 1) shape_color = [ (False, False, False, 0.), ] * num_frames shape2_color = [ (False, False, False, 0.), ] * num_frames skipped_frame_indices = set() n_missed_frames = 0 for s, start, e in [(s1, 0, 1), (s4, 3, 5), (s5, 5, 6)]: for i in range(start * rate * n_sub_frames, e * rate * n_sub_frames): val = (i - start * rate * n_sub_frames) / (rate * n_sub_frames) * .5 shape_color[i] = s.color_r, s.color_g, s.color_b, val for s, start, e in [(s2, 0, 2), (s3, 2, 3), (s6, 5, 6)]: for i in range(start * rate * n_sub_frames, e * rate * n_sub_frames): val = (i - start * rate * n_sub_frames) / (rate * n_sub_frames) * .5 shape2_color[i] = s.color_r, s.color_g, s.color_b, val def verify_intensity(*largs): nonlocal frame, n_missed_frames # total frames is a multiple of n_sub_frames if not stage_app.app.view_controller.stage_active: assert stage_app.app.view_controller.count - 1 == num_frames if skip: # last frame could be passed actual frames assert frame - n_missed_frames * n_sub_frames <= num_frames else: assert frame == num_frames event.cancel() return # not yet started if not stage_app.app.view_controller.count: return # some frame may have been skipped, but num_frames is max frames # This callback happens after frame callback and after the frame flip. # This also means we record even the last skipped frames (if skipped) assert frame < num_frames frame = verify_color(stage_app, shape_color, shape2_color, frame, centers, flip, video_mode) assert stage_app.app.view_controller.count == frame if skip: # some frames may have been dropped for next frame n_missed_frames = stage_app.app.view_controller._n_missed_frames for k in range(n_missed_frames * n_sub_frames): # frame is next frame index, next frame is skipped skipped_frame_indices.add(frame) frame += 1 else: assert not stage_app.app.view_controller._n_missed_frames event = Clock.create_trigger(verify_intensity, timeout=0, interval=True) event() stage_app.app.view_controller.request_stage_start(root.name) await wait_experiment_done(stage_app, timeout=num_frames / rate * 50) filename = str(tmp_path / 'recursive_play_stage_intensity.h5') stage_app.app.ceed_data.save(filename=filename) f = CeedDataReader(filename) f.open_h5() assert f.experiments_in_file == ['0'] assert not f.num_images_in_file f.load_experiment(0) shape_data = f.shapes_intensity[shape.name] shape_data_rendered = f.shapes_intensity_rendered[shape.name] shape2_data = f.shapes_intensity[shape2.name] shape2_data_rendered = f.shapes_intensity_rendered[shape2.name] recorded_rendered_frames = f.rendered_frames # even when skipping, skipped frames are still logged but they are removed # in xxx_rendered arrays if skip: # because frame rate is high, we'll definitely drop frames assert skipped_frame_indices else: assert not skipped_frame_indices assert shape_data.shape[0] == num_frames assert shape2_data.shape[0] == num_frames n_skipped = len(skipped_frame_indices) if skip: # last frame may be recorded as skipped, but if stage is done frame is # not real. n_missed_frames is the n_missed_frames from last frame assert num_frames - n_skipped <= shape_data_rendered.shape[0] \ <= num_frames - n_skipped + n_sub_frames * n_missed_frames assert num_frames - n_skipped <= shape2_data_rendered.shape[0] \ <= num_frames - n_skipped + n_sub_frames * n_missed_frames else: assert shape_data_rendered.shape[0] == num_frames assert shape2_data_rendered.shape[0] == num_frames # in QUAD12X mode, all 3 channels have same value in the data (because we # show gray). But the projector outputs different values for each channel, # for each sub-frame gray = video_mode == 'QUAD12X' i = 0 k = 0 for (r, g, b, val), (r1, g1, b1, _) in zip(shape_color, shape_data): assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0 assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0 assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0 if skip: assert recorded_rendered_frames[k] \ == (k not in skipped_frame_indices) else: assert recorded_rendered_frames[k] if k not in skipped_frame_indices: r1, g1, b1, _ = shape_data_rendered[i, :] assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0 assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0 assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0 i += 1 k += 1 i = 0 k = 0 for (r, g, b, val), (r1, g1, b1, _) in zip(shape2_color, shape2_data): assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0 assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0 assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0 if skip: assert recorded_rendered_frames[k] \ == (k not in skipped_frame_indices) else: assert recorded_rendered_frames[k] if k not in skipped_frame_indices: r1, g1, b1, _ = shape2_data_rendered[i, :] assert isclose(val, r1, abs_tol=2 / 255) if r or gray else r1 == 0 assert isclose(val, g1, abs_tol=2 / 255) if g or gray else g1 == 0 assert isclose(val, b1, abs_tol=2 / 255) if b or gray else b1 == 0 i += 1 k += 1 f.close_h5()
async def test_short_stage(stage_app: CeedTestApp, tmp_path, quad, sub_frames, main_frames): from ceed.analysis import CeedDataReader from ceed.function.plugin import LinearFunc from kivy.clock import Clock num_frames = int(math.ceil(main_frames * sub_frames)) rate = main_frames root = SerialAllStage(stage_factory=stage_app.app.stage_factory, show_in_gui=False, app=stage_app, create_add_to_parent=True) shape = CircleShapeP1(app=None, painter=stage_app.app.shape_factory, show_in_gui=True) root.stage.add_shape(shape.shape) root.stage.add_func( LinearFunc(function_factory=stage_app.app.function_factory, b=0, m=1, duration=1)) root.show_in_gui() await stage_app.wait_clock_frames(2) # use a larger frame rate so we have to drop frames stage_app.app.view_controller.frame_rate = rate stage_app.app.view_controller.use_software_frame_rate = False stage_app.app.view_controller.video_mode = quad stage_app.app.view_controller.pad_to_stage_handshake = False stage_app.app.view_controller.flip_projector = False frame = 0 event = None cx, cy = shape.shape.centroid if sub_frames == 1: centers = [(cx, cy)] else: cx1, cy1 = cx // 2, cy // 2 corners = ((0, 540), (960, 540), (0, 0), (960, 0)) centers = [(cx1 + x, cy1 + y) for x, y in corners] intensity = [] total_rounded_frames = math.ceil(main_frames) * sub_frames def verify_intensity(*largs): nonlocal frame if not stage_app.app.view_controller.stage_active: event.cancel() return # not yet started if not stage_app.app.view_controller.count: return assert frame < num_frames rgb = stage_app.get_widget_pos_pixel(stage_app.app.shape_factory, centers) rgb = [[c / 255 for c in p] for p in rgb] if sub_frames == 12: for plane in range(3): for point in rgb: value = point[plane] intensity.append((value, value, value, 1)) else: intensity.extend(rgb) frame += sub_frames assert frame in (stage_app.app.view_controller.count, total_rounded_frames) assert not stage_app.app.view_controller._n_missed_frames event = Clock.create_trigger(verify_intensity, timeout=0, interval=True) event() stage_app.app.view_controller.request_stage_start(root.name) await wait_experiment_done(stage_app, timeout=50) assert stage_app.app.view_controller.count == num_frames + 1 # only counts whole frames assert frame == total_rounded_frames # have data for blank frames at end assert len(intensity) == total_rounded_frames assert total_rounded_frames >= num_frames filename = str(tmp_path / 'short_stage.h5') stage_app.app.ceed_data.save(filename=filename) with CeedDataReader(filename) as f: f.load_experiment(0) shape_data = f.shapes_intensity[shape.name] shape_data_rendered = f.shapes_intensity_rendered[shape.name] recorded_rendered_frames = f.rendered_frames assert shape_data.shape[0] == num_frames assert shape_data_rendered.shape[0] == num_frames assert len(recorded_rendered_frames) == num_frames # for each sub-frame gray = quad == 'QUAD12X' r, g, b = root.color_r, root.color_g, root.color_b for i, ((v1, v2, v3, _), (r1, g1, b1, _)) in enumerate(zip(intensity[:num_frames], shape_data)): # we saw the intensity we expect val = i / (main_frames * sub_frames) assert isclose(val, v1, abs_tol=2 / 255) if r or gray else v1 == 0 assert isclose(val, v2, abs_tol=2 / 255) if g or gray else v2 == 0 assert isclose(val, v3, abs_tol=2 / 255) if b or gray else v3 == 0 # what we saw is what is recorded assert isclose(v1, r1, abs_tol=2 / 255) assert isclose(v2, g1, abs_tol=2 / 255) assert isclose(v3, b1, abs_tol=2 / 255) assert recorded_rendered_frames[i] assert shape_data_rendered[i, 0] == r1 assert shape_data_rendered[i, 1] == g1 assert shape_data_rendered[i, 2] == b1 # remaining frames are blank in quad mode for r, g, b, _ in intensity[num_frames:]: assert not r assert not g assert not b
def test_recursive_full_stage_intensity(stage_factory: StageFactoryBase): root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages( stage_factory) from ceed.function.plugin import LinearFunc for i, stage in enumerate((s1, s2, s3, s4, s5, s6)): stage.stage.add_func(LinearFunc( function_factory=stage_factory.function_factory, b=0, m=.1, duration=(i % 2 + 1) * 5)) shape = CircleShapeP1( app=None, painter=stage_factory.shape_factory, show_in_gui=False, create_add_shape=True) shape2 = CircleShapeP1Internal( app=None, painter=stage_factory.shape_factory, show_in_gui=False, create_add_shape=True) s1.stage.add_shape(shape.shape) s4.stage.add_shape(shape.shape) s5.stage.add_shape(shape.shape) s2.stage.add_shape(shape2.shape) s3.stage.add_shape(shape2.shape) s6.stage.add_shape(shape2.shape) values, n = get_stage_time_intensity(stage_factory, root.name, 10) assert n == 10 * (10 + 5 + 10 + 5) assert len(values) == 2 colors = values[shape.name] colors2 = values[shape2.name] assert len(colors) == n for s, start, e in [(s1, 0, 5), (s4, 15, 25), (s5, 25, 30)]: for i in range(start * 10, e * 10): r, g, b, a = colors[i] i -= start * 10 assert math.isclose(r, i / 10 * .1) if s.color_r else r == 0 assert math.isclose(g, i / 10 * .1) if s.color_g else g == 0 assert math.isclose(b, i / 10 * .1) if s.color_b else b == 0 for start, e in [(5, 15), ]: for i in range(start * 10, e * 10): r, g, b, a = colors[i] assert r == 0 assert g == 0 assert b == 0 for s, start, e in [(s2, 0, 10), (s3, 10, 15), (s6, 25, 30)]: for i in range(start * 10, e * 10): r, g, b, a = colors2[i] i -= start * 10 assert math.isclose(r, i / 10 * .1) if s.color_r else r == 0 assert math.isclose(g, i / 10 * .1) if s.color_g else g == 0 assert math.isclose(b, i / 10 * .1) if s.color_b else b == 0 for start, e in [(15, 25), ]: for i in range(start * 10, e * 10): r, g, b, a = colors2[i] assert r == 0 assert g == 0 assert b == 0
async def test_recursive_play_stage_intensity(stage_app: CeedTestApp, tmp_path): from ..test_stages import create_recursive_stages from .examples.shapes import CircleShapeP1, CircleShapeP2 from kivy.clock import Clock from ceed.analysis import CeedDataReader root, g1, g2, s1, s2, s3, s4, s5, s6 = create_recursive_stages( stage_app.stage_factory, app=stage_app) from ceed.function.plugin import LinearFunc for i, stage in enumerate((s1, s2, s3, s4, s5, s6)): stage.stage.add_func( LinearFunc(function_factory=stage_app.function_factory, b=0, m=.1, duration=(i % 2 + 1) * 5)) shape = CircleShapeP1(app=None, painter=stage_app.shape_factory, show_in_gui=True) shape2 = CircleShapeP2(app=None, painter=stage_app.shape_factory, show_in_gui=True) s1.stage.add_shape(shape.shape) s4.stage.add_shape(shape.shape) s5.stage.add_shape(shape.shape) s2.stage.add_shape(shape2.shape) s3.stage.add_shape(shape2.shape) s6.stage.add_shape(shape2.shape) root.show_in_gui() await stage_app.wait_clock_frames(2) frame = 0 event = None trio_event = trio.Event() rate = stage_app.view_controller.frame_rate = 10 num_frames = rate * (10 + 5 + 10 + 5) initial_frames = Clock.frames_displayed stage_app.view_controller.use_software_frame_rate = False stage_app.view_controller.flip_projector = False shape_color = [ (0., 0., 0.), ] * num_frames shape2_color = [ (0., 0., 0.), ] * num_frames for s, start, e in [(s1, 0, 5), (s4, 15, 25), (s5, 25, 30)]: for i in range(start * rate, e * rate): val = (i - start * rate) / rate * .1 shape_color[i] = (int(s.color_r) * val, int(s.color_g) * val, int(s.color_b) * val) for s, start, e in [(s2, 0, 10), (s3, 10, 15), (s6, 25, 30)]: for i in range(start * rate, e * rate): val = (i - start * rate) / rate * .1 shape2_color[i] = (int(s.color_r) * val, int(s.color_g) * val, int(s.color_b) * val) def verify_intensity(*largs): nonlocal frame if Clock.frames_displayed <= initial_frames + 1: return if not stage_app.view_controller.stage_active: event.cancel() trio_event.set() assert frame == num_frames return assert frame < num_frames points = stage_app.get_widget_pos_pixel(stage_app.shape_factory, [shape.center, shape2.center]) points = [[c / 255 for c in p] for p in points] (r1, g1, b1, _), (r2, g2, b2, _) = points val = shape_color[frame] assert math.isclose(r1, val[0], abs_tol=2 / 255) if val[0] else r1 == 0 assert math.isclose(g1, val[1], abs_tol=2 / 255) if val[1] else g1 == 0 assert math.isclose(b1, val[2], abs_tol=2 / 255) if val[2] else b1 == 0 val = shape2_color[frame] assert math.isclose(r2, val[0], abs_tol=2 / 255) if val[0] else r2 == 0 assert math.isclose(g2, val[1], abs_tol=2 / 255) if val[1] else g2 == 0 assert math.isclose(b2, val[2], abs_tol=2 / 255) if val[2] else b2 == 0 frame += 1 event = Clock.create_trigger(verify_intensity, timeout=0, interval=True) event() stage_app.view_controller.request_stage_start(root.name) await trio_event.wait() stage_app.view_controller.request_stage_end() event.cancel() filename = str(tmp_path / 'recursive_play_stage_intensity.h5') stage_app.ceed_data.save(filename=filename) f = CeedDataReader(filename) f.open_h5() assert f.experiments_in_file == ['0'] assert not f.num_images_in_file f.load_experiment(0) shape_data = np.array(f.shapes_intensity[shape.name]) shape2_data = np.array(f.shapes_intensity[shape2.name]) assert len(shape_data) == num_frames assert len(shape2_data) == num_frames for (r, g, b), (r1, g1, b1, _) in zip(shape_color, shape_data): assert math.isclose(r, r1, abs_tol=2 / 255) if r else r1 == 0 assert math.isclose(g, g1, abs_tol=2 / 255) if g else g1 == 0 assert math.isclose(b, b1, abs_tol=2 / 255) if b else b1 == 0 for (r, g, b), (r1, g1, b1, _) in zip(shape2_color, shape2_data): assert math.isclose(r, r1, abs_tol=2 / 255) if r else r1 == 0 assert math.isclose(g, g1, abs_tol=2 / 255) if g else g1 == 0 assert math.isclose(b, b1, abs_tol=2 / 255) if b else b1 == 0 f.close_h5()