From 7c26790d2fae6b3773046249960ef9823a1976fd Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Mon, 11 May 2026 17:28:18 -0500 Subject: [PATCH 1/5] display the pixel --- examples/sync/sync_test.py | 5 ++- examples/sync/sync_test_dualmon.py | 6 +--- expyfun/_experiment_controller.py | 51 ++++++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/examples/sync/sync_test.py b/examples/sync/sync_test.py index f59ef418..cd136f7d 100644 --- a/examples/sync/sync_test.py +++ b/examples/sync/sync_test.py @@ -64,9 +64,12 @@ circle = Circle(ec, 1, units="deg", fill_color="k", line_color="w") # Make a rectangle that is the standard credit card size rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") + # set the vpixx trigger pixel + ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red + print(f"{ec.vpixx_color=}") while pressed != "8": # enable a clean quit if required ec.set_background_color("white") - t1 = ec.start_stimulus(start_of_trial=False) # skip checks + t1 = ec.start_stimulus(start_of_trial=False, vpixx=True) # skip checks ec.set_background_color("black") t2 = ec.flip() diff = round(1000 * (t2 - t1), 2) diff --git a/examples/sync/sync_test_dualmon.py b/examples/sync/sync_test_dualmon.py index 42cbda96..b8b8796d 100644 --- a/examples/sync/sync_test_dualmon.py +++ b/examples/sync/sync_test_dualmon.py @@ -39,8 +39,6 @@ from expyfun import ExperimentController, building_doc from expyfun.visual import Circle, Rectangle -print(__doc__) - SCREEN = 0 FULL = False WIN_SIZE = (1000, 1000) # or None for full screen if size matches config @@ -73,7 +71,7 @@ rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") while pressed != "8": # enable a clean quit if required ec.set_background_color("white") - t1 = ec.start_stimulus(start_of_trial=False) # skip checks + t1 = ec.start_stimulus(start_of_trial=False, vpixx=True) # skip checks ec.set_background_color("black") t2 = ec.flip() diff = round(1000 * (t2 - t1), 2) @@ -86,5 +84,3 @@ ec.refocus() pressed = ec.wait_one_press(0.5)[0] if not building_doc else "8" ec.stop() - -# ea.plot_screen(screenshot) diff --git a/expyfun/_experiment_controller.py b/expyfun/_experiment_controller.py index 42c8a0bb..e2e44841 100644 --- a/expyfun/_experiment_controller.py +++ b/expyfun/_experiment_controller.py @@ -581,6 +581,7 @@ def __init__( car = sum([np.sin(2 * np.pi * f * t) for f in [800, 1000, 1200]]) self._beep = None self._beep_data = np.tile(car * np.exp(-t * 10) / 4, (2, 3)) + self.vpixx_color = () # finish initialization logger.info("Expyfun: Initialization complete") @@ -794,7 +795,28 @@ def set_background_color(self, color="black"): self._bgcolor = _convert_color(color) gl.glClearColor(*[c / 255.0 for c in self._bgcolor]) - def start_stimulus(self, start_of_trial=True, flip=True, when=None): + def set_vpixx_color(self, bits=()): + """Calculate color for vpixx "pixel mode" triggering, from the desired channels. + + Parameters + ---------- + bits: array-like of int + The bits to be set high (0-indexed). Values between 0 and 23 are valid. + Note that these are the RGB bits *not the DSUB channels*; pins 1-4 and 14-17 + control the red value, pins 5-8 and 18-21 control the green value, and pins + 9-12 and 22-25 control the blue value (13 is ground). See + https://docs.vpixx.com/vocal/sending-triggers-with-pixel-mode for details. + """ + bits = np.array(bits, dtype=int) + assert bits.min() >= 0 and bits.max() < 24, ( + "Vpixx color bits must be between 0 and 23" + ) + trig_out = np.exp2(bits).sum(dtype=int).item() + col = (trig_out % 256, (trig_out >> 8) % 256, (trig_out >> 16) % 256) + # this gets (unavoidably) passed to _convert_color later; must be in range [0,1] + self.vpixx_color = np.array(col) / 255 + + def start_stimulus(self, start_of_trial=True, flip=True, when=None, vpixx=False): """Play audio, (optionally) flip screen, run any "on_flip" functions. Parameters @@ -812,6 +834,9 @@ def start_stimulus(self, start_of_trial=True, flip=True, when=None): flip completes (if `flip` is ``True``). As a result, in some cases `when` should be set to a value smaller than your true intended flip time. + vpixx : bool + Whether to display the vpixx trigger pixel on flip. See + :meth:`ExperimentController.flip` for details. Ignored if ``flip`` is False. Returns ------- @@ -844,7 +869,7 @@ def start_stimulus(self, start_of_trial=True, flip=True, when=None): self._on_next_flip = ( [self._play] + self._ofp_critical_funs + self._on_next_flip ) - stimulus_time = self.flip(when) + stimulus_time = self.flip(when, vpixx=vpixx) else: if when is not None: self.wait_until(when) @@ -1150,7 +1175,7 @@ def _setup_window(self, window_size, exp_name, full_screen, screen): % (window_size, screen, self.dpi) ) - def flip(self, when=None): + def flip(self, when=None, vpixx=False): """Flip screen, then run any "on-flip" functions. Parameters @@ -1161,6 +1186,11 @@ def flip(self, when=None): absolute) wait time before the flip completes. As a result, in some cases `when` should be set to a value smaller than your true intended flip time. + vpixx : bool + Whether to display the vpixx trigger pixel on this flip. Useful only with + Vpixx projectors set in "pixel mode". Will use the value of + ``ExperimentController.vpixx_color``, which can be computed from the desired + output trigger values with :meth:`~ExperimentController.set_vpixx_color`. Returns ------- @@ -1191,6 +1221,21 @@ def flip(self, when=None): if self.safe_flipping: # On NVIDIA Linux these calls cause a 2x delay (33ms instead of 16) gl.glFinish() + if vpixx: + if not len(self.vpixx_color): + raise RuntimeError( + "Vpixx pixel color not set; did you forget to call " + "ec.set_vpixx_color() ?" + ) + rect = Rectangle( + ec=self, + pos=(0.5, self.window_size_pix[1] - 0.5, 1, 1), + units="pix", + fill_color=self.vpixx_color, + line_color=None, + line_width=0.0, + ) + rect.draw() self._win.flip() # this waits until everything is called, including last draw self._clear_rect.draw() From d6e48ab78915e094c61911a3f16f204eea65bef4 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 12 May 2026 13:04:01 -0500 Subject: [PATCH 2/5] cruft --- examples/sync/sync_test.py | 1 - expyfun/_experiment_controller.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/sync/sync_test.py b/examples/sync/sync_test.py index cd136f7d..3f2f79b2 100644 --- a/examples/sync/sync_test.py +++ b/examples/sync/sync_test.py @@ -66,7 +66,6 @@ rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") # set the vpixx trigger pixel ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red - print(f"{ec.vpixx_color=}") while pressed != "8": # enable a clean quit if required ec.set_background_color("white") t1 = ec.start_stimulus(start_of_trial=False, vpixx=True) # skip checks diff --git a/expyfun/_experiment_controller.py b/expyfun/_experiment_controller.py index e2e44841..3ca5a72d 100644 --- a/expyfun/_experiment_controller.py +++ b/expyfun/_experiment_controller.py @@ -796,13 +796,13 @@ def set_background_color(self, color="black"): gl.glClearColor(*[c / 255.0 for c in self._bgcolor]) def set_vpixx_color(self, bits=()): - """Calculate color for vpixx "pixel mode" triggering, from the desired channels. + """Calculate color for vpixx "pixel mode" triggering, from the desired bits. Parameters ---------- bits: array-like of int The bits to be set high (0-indexed). Values between 0 and 23 are valid. - Note that these are the RGB bits *not the DSUB channels*; pins 1-4 and 14-17 + Note that these are the RGB bits *not the DSUB pins*; pins 1-4 and 14-17 control the red value, pins 5-8 and 18-21 control the green value, and pins 9-12 and 22-25 control the blue value (13 is ground). See https://docs.vpixx.com/vocal/sending-triggers-with-pixel-mode for details. From 41ea832a86775915293cc3b254847d342e40feda Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 12 May 2026 14:53:09 -0500 Subject: [PATCH 3/5] oops --- examples/sync/sync_test_dualmon.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/sync/sync_test_dualmon.py b/examples/sync/sync_test_dualmon.py index b8b8796d..28d6aad4 100644 --- a/examples/sync/sync_test_dualmon.py +++ b/examples/sync/sync_test_dualmon.py @@ -69,6 +69,8 @@ circle = Circle(ec, 1, units="deg", fill_color="k", line_color="w") # Make a rectangle that is the standard credit card size rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") + # set the vpixx trigger pixel + ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red while pressed != "8": # enable a clean quit if required ec.set_background_color("white") t1 = ec.start_stimulus(start_of_trial=False, vpixx=True) # skip checks From cc69b92dc6d93d82551dd6e1d857cde06e24f5df Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 26 May 2026 16:37:15 -0500 Subject: [PATCH 4/5] address comments --- examples/sync/sync_test.py | 6 ++++-- examples/sync/sync_test_dualmon.py | 6 ++++-- expyfun/_experiment_controller.py | 3 +++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/sync/sync_test.py b/examples/sync/sync_test.py index 3f2f79b2..e9b78108 100644 --- a/examples/sync/sync_test.py +++ b/examples/sync/sync_test.py @@ -40,6 +40,7 @@ from expyfun import ExperimentController, building_doc from expyfun.visual import Circle, Rectangle +USE_VPIXX = False n_channels = 2 click_idx = [0] with ExperimentController( @@ -65,10 +66,11 @@ # Make a rectangle that is the standard credit card size rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") # set the vpixx trigger pixel - ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red + if USE_VPIXX: + ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red while pressed != "8": # enable a clean quit if required ec.set_background_color("white") - t1 = ec.start_stimulus(start_of_trial=False, vpixx=True) # skip checks + t1 = ec.start_stimulus(start_of_trial=False, vpixx=USE_VPIXX) # skip checks ec.set_background_color("black") t2 = ec.flip() diff = round(1000 * (t2 - t1), 2) diff --git a/examples/sync/sync_test_dualmon.py b/examples/sync/sync_test_dualmon.py index 28d6aad4..a8b2074a 100644 --- a/examples/sync/sync_test_dualmon.py +++ b/examples/sync/sync_test_dualmon.py @@ -42,6 +42,7 @@ SCREEN = 0 FULL = False WIN_SIZE = (1000, 1000) # or None for full screen if size matches config +USE_VPIXX = True n_channels = 2 click_idx = [0] @@ -70,10 +71,11 @@ # Make a rectangle that is the standard credit card size rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") # set the vpixx trigger pixel - ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red + if USE_VPIXX: + ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red while pressed != "8": # enable a clean quit if required ec.set_background_color("white") - t1 = ec.start_stimulus(start_of_trial=False, vpixx=True) # skip checks + t1 = ec.start_stimulus(start_of_trial=False, vpixx=USE_VPIXX) # skip checks ec.set_background_color("black") t2 = ec.flip() diff = round(1000 * (t2 - t1), 2) diff --git a/expyfun/_experiment_controller.py b/expyfun/_experiment_controller.py index 3ca5a72d..5e12c0b0 100644 --- a/expyfun/_experiment_controller.py +++ b/expyfun/_experiment_controller.py @@ -807,6 +807,9 @@ def set_vpixx_color(self, bits=()): 9-12 and 22-25 control the blue value (13 is ground). See https://docs.vpixx.com/vocal/sending-triggers-with-pixel-mode for details. """ + if not len(bits): + self.vpixx_color = () + return bits = np.array(bits, dtype=int) assert bits.min() >= 0 and bits.max() < 24, ( "Vpixx color bits must be between 0 and 23" From 25bb7325a1a8a64febb4030fa427f672a3c11392 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Tue, 26 May 2026 16:39:00 -0500 Subject: [PATCH 5/5] make vpixx color a script-level variable --- examples/sync/sync_test.py | 3 ++- examples/sync/sync_test_dualmon.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/sync/sync_test.py b/examples/sync/sync_test.py index e9b78108..dcab80c6 100644 --- a/examples/sync/sync_test.py +++ b/examples/sync/sync_test.py @@ -41,6 +41,7 @@ from expyfun.visual import Circle, Rectangle USE_VPIXX = False +VPIXX_COLOR = [0, 1, 2, 3, 4, 5, 6, 7] # full red n_channels = 2 click_idx = [0] with ExperimentController( @@ -67,7 +68,7 @@ rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") # set the vpixx trigger pixel if USE_VPIXX: - ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red + ec.set_vpixx_color(VPIXX_COLOR) while pressed != "8": # enable a clean quit if required ec.set_background_color("white") t1 = ec.start_stimulus(start_of_trial=False, vpixx=USE_VPIXX) # skip checks diff --git a/examples/sync/sync_test_dualmon.py b/examples/sync/sync_test_dualmon.py index a8b2074a..f2689dcd 100644 --- a/examples/sync/sync_test_dualmon.py +++ b/examples/sync/sync_test_dualmon.py @@ -43,6 +43,7 @@ FULL = False WIN_SIZE = (1000, 1000) # or None for full screen if size matches config USE_VPIXX = True +VPIXX_COLOR = [0, 1, 2, 3, 4, 5, 6, 7] # full red n_channels = 2 click_idx = [0] @@ -72,7 +73,7 @@ rect = Rectangle(ec, [0, 0, 8.56, 5.398], "cm", None, "#AA3377") # set the vpixx trigger pixel if USE_VPIXX: - ec.set_vpixx_color([0, 1, 2, 3, 4, 5, 6, 7]) # full red + ec.set_vpixx_color(VPIXX_COLOR) while pressed != "8": # enable a clean quit if required ec.set_background_color("white") t1 = ec.start_stimulus(start_of_trial=False, vpixx=USE_VPIXX) # skip checks