From 8bff51fd87196b44bee67206263e50d37fdebd1b Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 11:27:53 +0100 Subject: [PATCH 01/15] feat(graphics): graphics factory function `hrl.graphics.new_graphics(...)`creates the correct graphics object based on a specified alias. These aliases are defined in a map (`hrl.graphics.ALIAS_MAP`). Thus, adding a new device would only require adding it to this map and the factory function. --- hrl/graphics/__init__.py | 89 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/hrl/graphics/__init__.py b/hrl/graphics/__init__.py index a496313..88e5ba6 100644 --- a/hrl/graphics/__init__.py +++ b/hrl/graphics/__init__.py @@ -17,3 +17,92 @@ "VIEWPixx_RGB", "Texture", ] + + +ALIAS_MAP = { + "GPU_grey": ["gpu", "gpu_grey", "grey", "gray", "gray8"], + "GPU_RGB": ["gpu_RGB", "RGB"], + "VIEWPixx_grey": ["viewpixx", "viewpixx_grey", "viewpixx_gray", "viewpixx_gray8"], + "VIEWPixx_RGB": ["viewpixx_RGB", "viewpixx_color", "viewpixx_colour"], + "DataPixx": ["datapixx", "datapixx_grey", "datapixx_gray", "datapixx_gray8"], +} + + +def new_graphics( + graphics_alias, + width, + height, + background=0.5, + fullscreen=False, + double_buffer=True, + lut=None, + mouse=False, +): + """Factory function to create appropriate Graphics subclass based on configuration. + + This function can be extended to read from a configuration file or environment + variables to determine which graphics device to instantiate (e.g., GPU, DataPixx, ViewPixx). + + Parameters + ---------- + graphics_alias : str + alias for the desired graphics device. Valid options are defined in ALIAS_MAP. + width : int + width of the screen in pixels. + height : int + height of the screen in pixels. + background : float or tuple, optional + background intensity value (0.0, 1.0), by default 0.5. + For grayscale devices, this is a gray level; + for color devices, can specify an RGB triplet. + fullscreen : bool, optional + run in Fullscreen, by default False + double_buffer : bool, optional + use double buffering, by default True. + lut : Path or str, optional + Path to Look-up Table, by default None. + mouse : bool, optional + enable mouse cursor, by default False. + screen : int or str, optional + which monitor to use, by default None (use the environment). + Given as an integer (e.g., 0, 1), + or as a string containing the X11 screen specification string (e.g., ":0.1" or ":1"). + width_offset : int, optional + horizontal offset of the window, in pixels, by default 0. + Useful for setups with multiple monitors but a single Xscreen session. + + Returns + ------- + Graphics + an instance of a concrete Graphics subclass appropriate for the specified hardware setup + + Raises + ------ + ValueError + if the provided graphics_alias does not match any known device aliases + (see ALIAS_MAP for valid options) + """ + if graphics_alias in ALIAS_MAP["GPU_grey"]: + graphics_class = GPU_grey + elif graphics_alias in ALIAS_MAP["GPU_RGB"]: + graphics_class = GPU_grey + elif graphics_alias in ALIAS_MAP["DataPixx"]: + graphics_class = GPU_grey + elif graphics_alias in ALIAS_MAP["VIEWPixx_grey"]: + graphics_class = GPU_grey + elif graphics_alias in ALIAS_MAP["VIEWPixx_RGB"]: + graphics_class = GPU_grey + else: + raise ValueError( + f"Unknown graphics device '{graphics_alias}'. Valid options are: {ALIAS_MAP.items()}" + ) + + return graphics_class( + width=width, + height=height, + background=background, + fullscreen=fullscreen, + double_buffer=double_buffer, + lut=lut, + mouse=mouse, + ) From f046492d9771230cd46cdbcf880904fded02bd6c Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 11:28:25 +0100 Subject: [PATCH 02/15] refactor(HRL): HRL calls `new_graphics()`, is itself agnostic to devices Closes #11 --- hrl/hrl.py | 77 +++++++++--------------------------------------------- 1 file changed, 12 insertions(+), 65 deletions(-) diff --git a/hrl/hrl.py b/hrl/hrl.py index 09fe871..c2827b5 100644 --- a/hrl/hrl.py +++ b/hrl/hrl.py @@ -7,6 +7,8 @@ import numpy as np import pygame +import hrl.graphics + ### HRL Class ### @@ -118,71 +120,16 @@ def __init__( fs = False ## Load Graphics Device ## - if graphics.lower() in ("gpu", "gpu_grey", "grey", "gray", "gray8"): - from .graphics.gpu import GPU_grey - - self.graphics = GPU_grey( - width=wdth, - height=hght, - background=bg, - fullscreen=fs, - double_buffer=db, - lut=lut, - mouse=mouse, - ) - elif graphics.lower() in ("gpu_rgb", "rgb"): - from .graphics.gpu import GPU_RGB - - self.graphics = GPU_RGB( - width=wdth, - height=hght, - background=[bg, bg, bg], - fullscreen=fs, - double_buffer=db, - lut=lut, - mouse=mouse, - ) - - elif graphics.lower() == "datapixx": - from .graphics.datapixx import DATAPixx - - self.graphics = DATAPixx( - width=wdth, - height=hght, - background=bg, - fullscreen=fs, - double_buffer=db, - lut=lut, - mouse=mouse, - ) - elif graphics.lower() in ("viewpixx", "viewpixx_grey", "viewpixx_gray", "viewpixx_gray8"): - from .graphics.viewpixx import VIEWPixx_grey - - self.graphics = VIEWPixx_grey( - width=wdth, - height=hght, - background=bg, - fullscreen=fs, - double_buffer=db, - lut=lut, - mouse=mouse, - ) - - elif graphics.lower() in ("viewpixx_rgb", "viewpixx_color", "viewpixx_colour"): - from .graphics.viewpixx import VIEWPixx_RGB - - self.graphics = VIEWPixx_RGB( - width=wdth, - height=hght, - background=[bg, bg, bg], - fullscreen=fs, - double_buffer=db, - lut=lut, - mouse=mouse, - ) - - else: - self.graphics = None + self.graphics = hrl.graphics.new_graphics( + graphics_alias=graphics.lower(), + width=wdth, + height=hght, + background=bg, + fullscreen=fs, + double_buffer=db, + lut=lut, + mouse=mouse, + ) ## Load Input Device ## From 1cf212052f5a9bad4092fef17311ee5cda51de0d Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 11:55:35 +0100 Subject: [PATCH 03/15] refactor(graphics): map to subclasses lazily Prevents importing all the possible devices; only import the branch we need --- hrl/graphics/__init__.py | 58 +++++++++++++++------------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/hrl/graphics/__init__.py b/hrl/graphics/__init__.py index 88e5ba6..a702865 100644 --- a/hrl/graphics/__init__.py +++ b/hrl/graphics/__init__.py @@ -1,33 +1,11 @@ """HRL Graphics module - display device interfaces.""" -from .datapixx import DATAPixx -from .gpu import GPU_RGB, GPU_grey -from .graphics import Graphics, Graphics_grey, Graphics_RGB -from .texture import Texture -from .viewpixx import VIEWPixx_grey, VIEWPixx_RGB - __all__ = [ - "Graphics", - "Graphics_grey", - "Graphics_RGB", - "GPU_grey", - "GPU_RGB", - "DATAPixx", - "VIEWPixx_grey", - "VIEWPixx_RGB", + "new_graphics", "Texture", ] -ALIAS_MAP = { - "GPU_grey": ["gpu", "gpu_grey", "grey", "gray", "gray8"], - "GPU_RGB": ["gpu_RGB", "RGB"], - "VIEWPixx_grey": ["viewpixx", "viewpixx_grey", "viewpixx_gray", "viewpixx_gray8"], - "VIEWPixx_RGB": ["viewpixx_RGB", "viewpixx_color", "viewpixx_colour"], - "DataPixx": ["datapixx", "datapixx_grey", "datapixx_gray", "datapixx_gray8"], -} - - def new_graphics( graphics_alias, width, @@ -46,7 +24,11 @@ def new_graphics( Parameters ---------- graphics_alias : str - alias for the desired graphics device. Valid options are defined in ALIAS_MAP. + alias for the desired graphics device. Valid options include: + 'gpu', 'gpu_grey', 'grey', 'gray', 'gray8', 'gpu_RGB', 'RGB', + 'viewpixx', 'viewpixx_grey', 'viewpixx_gray', 'viewpixx_gray8', + 'viewpixx_RGB', 'viewpixx_color', 'viewpixx_colour', + 'datapixx', 'datapixx_grey', 'datapixx_gray', 'datapixx_gray8'. width : int width of the screen in pixels. height : int @@ -80,21 +62,25 @@ def new_graphics( ------ ValueError if the provided graphics_alias does not match any known device aliases - (see ALIAS_MAP for valid options) """ - if graphics_alias in ALIAS_MAP["GPU_grey"]: - graphics_class = GPU_grey - elif graphics_alias in ALIAS_MAP["GPU_RGB"]: - graphics_class = GPU_grey - elif graphics_alias in ALIAS_MAP["DataPixx"]: - graphics_class = GPU_grey - elif graphics_alias in ALIAS_MAP["VIEWPixx_grey"]: - graphics_class = GPU_grey - elif graphics_alias in ALIAS_MAP["VIEWPixx_RGB"]: - graphics_class = GPU_grey + # Lazy import the graphics class based on alias + if graphics_alias in ["gpu", "gpu_grey", "grey", "gray", "gray8"]: + from .gpu import GPU_grey as graphics_class + elif graphics_alias in ["gpu_RGB", "RGB"]: + from .gpu import GPU_RGB as graphics_class + elif graphics_alias in ["viewpixx", "viewpixx_grey", "viewpixx_gray", "viewpixx_gray8"]: + from .viewpixx import VIEWPixx_grey as graphics_class + elif graphics_alias in ["viewpixx_RGB", "viewpixx_color", "viewpixx_colour"]: + from .viewpixx import VIEWPixx_RGB as graphics_class + elif graphics_alias in ["datapixx", "datapixx_grey", "datapixx_gray", "datapixx_gray8"]: + from .datapixx import DATAPixx as graphics_class else: raise ValueError( - f"Unknown graphics device '{graphics_alias}'. Valid options are: {ALIAS_MAP.items()}" + f"Unknown graphics device '{graphics_alias}'. Valid options are: " + f"'gpu', 'gpu_grey', 'grey', 'gray', 'gray8', 'gpu_RGB', 'RGB', " + f"'viewpixx', 'viewpixx_grey', 'viewpixx_gray', 'viewpixx_gray8', " + f"'viewpixx_RGB', 'viewpixx_color', 'viewpixx_colour', " + f"'datapixx', 'datapixx_grey', 'datapixx_gray', 'datapixx_gray8'" ) return graphics_class( From ec5c6adcd3551ce16d9633b9a557640a57cc9fd7 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 13:33:51 +0100 Subject: [PATCH 04/15] refactor(graphics): let graphics factory handle screen setup --- hrl/graphics/__init__.py | 35 ++++++++++++++++++++++++++++++++++- hrl/hrl.py | 34 +++------------------------------- 2 files changed, 37 insertions(+), 32 deletions(-) diff --git a/hrl/graphics/__init__.py b/hrl/graphics/__init__.py index a702865..6dff911 100644 --- a/hrl/graphics/__init__.py +++ b/hrl/graphics/__init__.py @@ -1,5 +1,8 @@ """HRL Graphics module - display device interfaces.""" +import os +import platform + __all__ = [ "new_graphics", "Texture", @@ -15,6 +18,8 @@ def new_graphics( double_buffer=True, lut=None, mouse=False, + screen=None, + width_offset=0, ): """Factory function to create appropriate Graphics subclass based on configuration. @@ -83,12 +88,40 @@ def new_graphics( f"'datapixx', 'datapixx_grey', 'datapixx_gray', 'datapixx_gray8'" ) + # Run screen setup for multiple monitor support + screen_setup(screen=screen, window_width_offset=width_offset) + return graphics_class( width=width, height=height, background=background, - fullscreen=fullscreen, + fullscreen=False, double_buffer=double_buffer, lut=lut, mouse=mouse, ) + + +def screen_setup(screen, window_width_offset): + ####### Setting up on which monitor to use + # In older systems or systems with separate Xscreens, the naming is still :0.0 or :0.1. + # For systems with only one screen, it is :1. + if platform.system() == "Linux": + print(f"Default screen number used by the OS: {os.environ['DISPLAY']}") + + if screen is not None: + # legacy option for older configs or separate Xscreens + if os.environ["DISPLAY"] == ":0": + os.environ["DISPLAY"] = ":0." + str(screen) + else: + if isinstance(screen, str): + os.environ["DISPLAY"] = screen + elif isinstance(screen, int): + os.environ["DISPLAY"] = ":" + str(screen) + + print(f"Display number changed to: {os.environ['DISPLAY']}") + + ## 11. Aug 2021 + # we add a wdth_offset to be able to run HRL in setups with a + # single Xscreen but multiple monitors (a config with Xinerama enabled) + os.environ["SDL_VIDEO_WINDOW_POS"] = f"{window_width_offset},0" diff --git a/hrl/hrl.py b/hrl/hrl.py index c2827b5..5560979 100644 --- a/hrl/hrl.py +++ b/hrl/hrl.py @@ -2,7 +2,6 @@ import csv import os -import platform import numpy as np import pygame @@ -90,36 +89,7 @@ def __init__( experiment. """ - ####### Setting up on which monitor to use - # In older systems or systems with separate Xscreens, the naming is still :0.0 or :0.1. - # For systems with only one screen, it is :1. - if platform.system() == "Linux": - print("default screen number used by the OS: %s" % os.environ["DISPLAY"]) - - if scrn!=None: - # legacy option for older configs or separate Xscreens - if (os.environ["DISPLAY"] == ":0"): - os.environ["DISPLAY"] = ":0." + str(scrn) - else: - if isinstance(scrn, str): - os.environ["DISPLAY"] = scrn - elif isinstance(scrn, int): - os.environ["DISPLAY"] = ":" + str(scrn) - - print("display number changed to: %s" % os.environ["DISPLAY"]) - - ## 11. Aug 2021 - # we add a wdth_offset to be able to run HRL in setups with a - # single Xscreen but multiple monitors (a config with Xinerama enabled) - os.environ["SDL_VIDEO_WINDOW_POS"] = "%d,0" % wdth_offset - - # we force not fullscreen, even in experimental computer, - # because in later versions of pygame - # fullscreen on a second screen gives very weird behavior on - # recording of keyboard events, effectively hanging the computer. - fs = False - - ## Load Graphics Device ## + ## Setup screen and graphics ## self.graphics = hrl.graphics.new_graphics( graphics_alias=graphics.lower(), width=wdth, @@ -129,6 +99,8 @@ def __init__( double_buffer=db, lut=lut, mouse=mouse, + screen=scrn, + width_offset=wdth_offset, ) ## Load Input Device ## From a11f14400e8d2669da031e4d783deac139700431 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 12:23:07 +0100 Subject: [PATCH 05/15] refactor(inputs): all `inputs` devices hold a `.device` bindings attribute --- hrl/inputs/inputs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hrl/inputs/inputs.py b/hrl/inputs/inputs.py index ffe9c89..d92e712 100644 --- a/hrl/inputs/inputs.py +++ b/hrl/inputs/inputs.py @@ -22,6 +22,8 @@ class Input(ABC): a clear explanation of the keymap if it deviates from this typical set. """ + device = None + @abstractmethod def readButton(self, btns, to): """ From 784832225568b1ab6311682f382d4b3ea3a677f2 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 12:23:39 +0100 Subject: [PATCH 06/15] feat(inputs): factory function for inputs devices Leaves all the switching logic there, instead of in the `HRL`-class --- hrl/inputs/__init__.py | 28 ++++++++++++++++++++++++++++ hrl/inputs/responsepixx.py | 4 ---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/hrl/inputs/__init__.py b/hrl/inputs/__init__.py index e69de29..a5f9996 100644 --- a/hrl/inputs/__init__.py +++ b/hrl/inputs/__init__.py @@ -0,0 +1,28 @@ +def new_input(input_alias, device=None): + """Factory function to create appropriate Input subclass based on configuration. + + This function can be extended to read from a configuration file or environment + variables to determine which input device to instantiate (e.g., Keyboard, RESPONSEPixx). + + Parameters + ---------- + input_alias : str + alias for the desired input device. Valid options: 'keyboard', 'responsepixx'. + + Returns + ------- + Input + an instance of the appropriate Input subclass. + """ + if input_alias == "keyboard": + from .keyboard import Keyboard as input_class + elif input_alias == "responsepixx": + from .responsepixx import RESPONSEPixx as input_class + else: + raise ValueError( + f"Unknown input device '{input_alias}'. Valid options are: 'keyboard', 'responsepixx'" + ) + + input_device = input_class() + input_device.device = device + return input_device diff --git a/hrl/inputs/responsepixx.py b/hrl/inputs/responsepixx.py index 24b7d9c..80ad3e1 100644 --- a/hrl/inputs/responsepixx.py +++ b/hrl/inputs/responsepixx.py @@ -17,10 +17,6 @@ class RESPONSEPixx(Input): implementation. """ - def __init__(self, device): - super(RESPONSEPixx, self).__init__() - self.device = device - ## new function in HRL3. waitButton() function implemented in python ## in previous version of HRL waitButton() was a function of the ## datapixx.so python wrapper From 4ed8420d838151e0b2c2ff1b25b5bcabf3dc3203 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 12:29:18 +0100 Subject: [PATCH 07/15] feat(photometer): factory function for photometer devices --- hrl/photometer/__init__.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/hrl/photometer/__init__.py b/hrl/photometer/__init__.py index e69de29..4924e4a 100644 --- a/hrl/photometer/__init__.py +++ b/hrl/photometer/__init__.py @@ -0,0 +1,33 @@ +def new_photometer(photometer_alias, device="/dev/ttyUSB0", timeout=10): + """Factory function to create a new photometer instance based on the provided name. + + Parameters + ---------- + photometer_alias : str + name of the photometer to create. Valid options: 'optical', 'minolta'. + device : str, optional + device path to the photometer, by default "/dev/ttyUSB0". + timeout : int, optional + timeout in seconds for communication with the photometer, by default 10. + + Returns + ------- + Photometer + instance of the photometer corresponding to the provided alias. + + Raises + ------ + ValueError + If the provided photometer_alias does not match any known photometer. + + """ + if photometer_alias == "optical": + from .optical import OptiCAL as photometer_class + elif photometer_alias == "minolta": + from .minolta import Minolta as photometer_class + else: + raise ValueError( + f"Unknown photometer: {photometer_alias}. Valid options are: 'optical', 'minolta'" + ) + + return photometer_class(device, timeout=timeout) From ea45d6d9d1e940d853b1dfbc3dcd3555072f4a53 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 12:24:24 +0100 Subject: [PATCH 08/15] refactor(HRL): use `inputs` factory function --- hrl/hrl.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/hrl/hrl.py b/hrl/hrl.py index 5560979..09f3b5c 100644 --- a/hrl/hrl.py +++ b/hrl/hrl.py @@ -7,6 +7,7 @@ import pygame import hrl.graphics +import hrl.inputs ### HRL Class ### @@ -103,21 +104,11 @@ def __init__( width_offset=wdth_offset, ) - ## Load Input Device ## - - if inputs == "keyboard": - from .inputs.keyboard import Keyboard - - self.inputs = Keyboard() - - elif inputs == "responsepixx": - from .inputs.responsepixx import RESPONSEPixx - - # Assume hardware connection already established by graphics - self.inputs = RESPONSEPixx(self.graphics.device) - - else: - self.inputs = None + ## Setup Input Device ## + self.inputs = hrl.inputs.new_input( + input_alias=inputs, + device=self.graphics.device, + ) ## Load Photometer ## From ff99e5e6f795f4ab4d503be9c441394717e66aa9 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 12:29:31 +0100 Subject: [PATCH 09/15] refactor(HRL): use `photometer` factory function --- hrl/hrl.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/hrl/hrl.py b/hrl/hrl.py index 09f3b5c..7265e34 100644 --- a/hrl/hrl.py +++ b/hrl/hrl.py @@ -8,6 +8,7 @@ import hrl.graphics import hrl.inputs +import hrl.photometer ### HRL Class ### @@ -110,18 +111,13 @@ def __init__( device=self.graphics.device, ) - ## Load Photometer ## - - if photometer == "optical": - from .photometer.optical import OptiCAL - self.photometer = OptiCAL("/dev/ttyUSB0", timeout=10) - - elif photometer == "minolta": - from .photometer.minolta import Minolta - self.photometer = Minolta("/dev/ttyUSB0") - - else: - self.photometer = None + ## Setup photometer ## + if photometer is not None: + self.photometer = hrl.photometer.new_photometer( + photometer_alias=photometer, + device="/dev/ttyUSB0", + timeout=10, + ) ## Results file ## self._rfl = None From 57d4a8cf27ef6c27cd1f56f714f7516b075de850 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 10 Feb 2026 12:12:00 +0100 Subject: [PATCH 10/15] docs(hrl): align docstrings --- hrl/hrl.py | 74 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/hrl/hrl.py b/hrl/hrl.py index 7265e34..73633d4 100644 --- a/hrl/hrl.py +++ b/hrl/hrl.py @@ -53,42 +53,56 @@ def __init__( lut=None, mouse=False, ): - """ - Initialize an HRL object. + """Initialize an HRL object. Parameters ---------- - - graphics : The graphics device to use. Available: 'gpu','datapixx' - (to be used with DataPixx 1) or 'viewpixx' (for ViewPixx 3D). - Default: 'gpu' - inputs : The input device to use. Available: 'keyboard', 'responsepixx'. - Default: 'keyboard' - photometer : The graphics device to use. Available: 'optical', 'minolta', None. - Default: None - wdth : The desired width of the screen. Default: 1024 - hght : The desired height of the screen. Default: 768 - bg : The background luminance on a scale from 0 to 1. Default: 0.0 - fs : Whether or not to run in Fullscreen. Default: False - wdth_offset: The desired horizontal offset of the window. Useful for setups - with multiple monitors but a single Xscreen session. Default: 0 - db : Whether or not to use double buffering. Default: True - scrn: Which monitor to use, given as an integer (e.g. 0, 1), or as a string containing the - X11 screen specification string (e.g. ":0.1" or ":1"). - Default: None (uses the default settings) - dfl : The read location of the design matrix. Default: None - rfl : The write location of the result matrix. Default: None - rhds : A list of the string names of the headers for the Result - Matrix. The string names should be without spaces, e.g. - 'InputLuminance'. If rfl != None, rhds must be provided. - Default: None - lut : The lookup table. Default: None - mouse: enables or disables the mouse cursor. Default: False + graphics : str + alias for the desired graphics device, by default "gpu". + Valid options are defined in hrl.graphics.ALIAS_MAP. + inputs : str + alias for the desired input device, by default "keyboard". + photometer : str or None + alias for the desired photometer device, by default None. + wdth : int + width of the screen in pixels, by default 1024. + hght : int + height of the screen in pixels, by default 768. + bg : float or tuple, optional + background intensity value (0.0, 1.0), by default 0.0. + For grayscale devices, this is a gray level; + for color devices, can specify an RGB triplet. + fs : bool, optional + run in Fullscreen, currently fixed to False -- because in later versions of pygame + fullscreen on a second screen gives very weird behavior on recording of keyboard events, + effectively hanging the computer. + wdth_offset: int, optional + horizontal offset of the window, in pixels, by default 0. + Useful for setups with multiple monitors but a single Xscreen session. + db : bool, optional + use double buffering, by default True. + scrn : int or str, optional + which monitor to use, by default None (use the environment). + Given as an integer (e.g., 0, 1), + or as a string containing the X11 screen specification string (e.g., ":0.1" or ":1"). + dfl : Path or str or None, optional + path to design file, by default None. + rfl : Path or str or None, optional + path to results file, by default None. + rhds : List(str) or None, optional + headers for the Result Matrix, by default None. + The string names should be without spaces, e.g. 'InputLuminance'. + If rfl != None, rhds must be provided. + lut : Path or str or None, optional + Path to Look-up Table, by default None. + mouse: bool, optional + enables or disables the mouse cursor, by default False. Returns ------- - hrl instance. Comes with a number of methods required to run an - experiment. + HRL + an initialized hrl instance. + Comes with a number of methods required to run an experiment. """ ## Setup screen and graphics ## From 01138431fd78ad405c0a26c7defc22d1b0805632 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Fri, 27 Mar 2026 14:23:16 +0100 Subject: [PATCH 11/15] refactor(graphics): define graphics aliases more explicitly --- hrl/graphics/__init__.py | 43 +++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/hrl/graphics/__init__.py b/hrl/graphics/__init__.py index 6dff911..48f43f3 100644 --- a/hrl/graphics/__init__.py +++ b/hrl/graphics/__init__.py @@ -1,5 +1,6 @@ """HRL Graphics module - display device interfaces.""" +import importlib import os import platform @@ -8,6 +9,29 @@ "Texture", ] +GRAPHICS_GREY_ALIASES = { + "gpu_grey": "gpu.GPU_grey", + "grey": "gpu.GPU_grey", + "gray": "gpu.GPU_grey", + "gray8": "gpu.GPU_grey", + "viewpixx": "viewpixx.VIEWPixx_grey", + "viewpixx_grey": "viewpixx.VIEWPixx_grey", + "viewpixx_gray": "viewpixx.VIEWPixx_grey", + "viewpixx_gray8": "viewpixx.VIEWPixx_grey", + "datapixx": "datapixx.DATAPixx", + "datapixx_grey": "datapixx.DATAPixx", + "datapixx_gray": "datapixx.DATAPixx", + "datapixx_gray8": "datapixx.DATAPixx", +} +GRAPHICS_RGB_ALIASES = { + "gpu_RGB": "gpu.GPU_RGB", + "RGB": "gpu.GPU_RGB", + "viewpixx_RGB": "viewpixx.VIEWPixx_RGB", + "viewpixx_color": "viewpixx.VIEWPixx_RGB", + "viewpixx_colour": "viewpixx.VIEWPixx_RGB", +} +GRAPHICS_ALIASES = {**GRAPHICS_GREY_ALIASES, **GRAPHICS_RGB_ALIASES} + def new_graphics( graphics_alias, @@ -69,23 +93,14 @@ def new_graphics( if the provided graphics_alias does not match any known device aliases """ # Lazy import the graphics class based on alias - if graphics_alias in ["gpu", "gpu_grey", "grey", "gray", "gray8"]: - from .gpu import GPU_grey as graphics_class - elif graphics_alias in ["gpu_RGB", "RGB"]: - from .gpu import GPU_RGB as graphics_class - elif graphics_alias in ["viewpixx", "viewpixx_grey", "viewpixx_gray", "viewpixx_gray8"]: - from .viewpixx import VIEWPixx_grey as graphics_class - elif graphics_alias in ["viewpixx_RGB", "viewpixx_color", "viewpixx_colour"]: - from .viewpixx import VIEWPixx_RGB as graphics_class - elif graphics_alias in ["datapixx", "datapixx_grey", "datapixx_gray", "datapixx_gray8"]: - from .datapixx import DATAPixx as graphics_class + if graphics_alias in GRAPHICS_ALIASES: + module_name, class_name = GRAPHICS_ALIASES[graphics_alias].rsplit(".", 1) + module = importlib.import_module(f".{module_name}", package=__name__) + graphics_class = getattr(module, class_name) else: raise ValueError( f"Unknown graphics device '{graphics_alias}'. Valid options are: " - f"'gpu', 'gpu_grey', 'grey', 'gray', 'gray8', 'gpu_RGB', 'RGB', " - f"'viewpixx', 'viewpixx_grey', 'viewpixx_gray', 'viewpixx_gray8', " - f"'viewpixx_RGB', 'viewpixx_color', 'viewpixx_colour', " - f"'datapixx', 'datapixx_grey', 'datapixx_gray', 'datapixx_gray8'" + f"{', '.join(list(GRAPHICS_GREY_ALIASES.keys()) + list(GRAPHICS_RGB_ALIASES.keys()))}" ) # Run screen setup for multiple monitor support From 71207a0e82a0a2b3df62700aef33a607c0fa5631 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Fri, 27 Mar 2026 14:23:35 +0100 Subject: [PATCH 12/15] refactor(util): update to use graphics aliases --- hrl/util/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/hrl/util/__init__.py b/hrl/util/__init__.py index 0ddd6d9..e341cf0 100644 --- a/hrl/util/__init__.py +++ b/hrl/util/__init__.py @@ -1,7 +1,6 @@ """HRL utility package for calibration and testing.""" import argparse -import inspect import hrl.graphics @@ -10,12 +9,7 @@ graphics_arggroup.add_argument( "-gr", "--graphics", - choices=[ - alias - for alias in hrl.graphics.__all__ - if not inspect.isabstract(device := getattr(hrl.graphics, alias)) - and issubclass(device, hrl.graphics.Graphics_grey) - ], + choices=[hrl.graphics.GRAPHICS_GREY_ALIASES.keys()], default="datapixx", help="Graphics device (default: datapixx)", ) From 4d27b704732a161df854c7910dd5ba991f1ed26b Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 14 Jul 2026 17:51:31 +0200 Subject: [PATCH 13/15] refactor: align `aliases` system & docstring for all three hardware types --- hrl/graphics/__init__.py | 28 +++++++++++++------------- hrl/inputs/__init__.py | 41 ++++++++++++++++++++++++++++---------- hrl/photometer/__init__.py | 32 +++++++++++++++++++++-------- hrl/util/__init__.py | 2 +- 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/hrl/graphics/__init__.py b/hrl/graphics/__init__.py index 48f43f3..6870849 100644 --- a/hrl/graphics/__init__.py +++ b/hrl/graphics/__init__.py @@ -6,10 +6,13 @@ __all__ = [ "new_graphics", + "ALIASES", + "GREY_ALIASES", + "RGB_ALIASES", "Texture", ] -GRAPHICS_GREY_ALIASES = { +GREY_ALIASES = { "gpu_grey": "gpu.GPU_grey", "grey": "gpu.GPU_grey", "gray": "gpu.GPU_grey", @@ -23,14 +26,14 @@ "datapixx_gray": "datapixx.DATAPixx", "datapixx_gray8": "datapixx.DATAPixx", } -GRAPHICS_RGB_ALIASES = { +RGB_ALIASES = { "gpu_RGB": "gpu.GPU_RGB", "RGB": "gpu.GPU_RGB", "viewpixx_RGB": "viewpixx.VIEWPixx_RGB", "viewpixx_color": "viewpixx.VIEWPixx_RGB", "viewpixx_colour": "viewpixx.VIEWPixx_RGB", } -GRAPHICS_ALIASES = {**GRAPHICS_GREY_ALIASES, **GRAPHICS_RGB_ALIASES} +ALIASES = {**GREY_ALIASES, **RGB_ALIASES} def new_graphics( @@ -45,7 +48,7 @@ def new_graphics( screen=None, width_offset=0, ): - """Factory function to create appropriate Graphics subclass based on configuration. + """Factory function to create appropriate Graphics subclass based on provided alias. This function can be extended to read from a configuration file or environment variables to determine which graphics device to instantiate (e.g., GPU, DataPixx, ViewPixx). @@ -53,11 +56,8 @@ def new_graphics( Parameters ---------- graphics_alias : str - alias for the desired graphics device. Valid options include: - 'gpu', 'gpu_grey', 'grey', 'gray', 'gray8', 'gpu_RGB', 'RGB', - 'viewpixx', 'viewpixx_grey', 'viewpixx_gray', 'viewpixx_gray8', - 'viewpixx_RGB', 'viewpixx_color', 'viewpixx_colour', - 'datapixx', 'datapixx_grey', 'datapixx_gray', 'datapixx_gray8'. + alias for the desired graphics device. Valid options can be found in the + hrl.graphics.ALIASES.keys(). width : int width of the screen in pixels. height : int @@ -85,22 +85,22 @@ def new_graphics( Returns ------- Graphics - an instance of a concrete Graphics subclass appropriate for the specified hardware setup + an instance of a concrete Graphics subclass corresponding to the provided alias Raises ------ ValueError - if the provided graphics_alias does not match any known device aliases + if the provided graphics_alias does not match any known device """ # Lazy import the graphics class based on alias - if graphics_alias in GRAPHICS_ALIASES: - module_name, class_name = GRAPHICS_ALIASES[graphics_alias].rsplit(".", 1) + if graphics_alias in ALIASES: + module_name, class_name = ALIASES[graphics_alias].rsplit(".", 1) module = importlib.import_module(f".{module_name}", package=__name__) graphics_class = getattr(module, class_name) else: raise ValueError( f"Unknown graphics device '{graphics_alias}'. Valid options are: " - f"{', '.join(list(GRAPHICS_GREY_ALIASES.keys()) + list(GRAPHICS_RGB_ALIASES.keys()))}" + f"{', '.join(list(ALIASES.keys()))}" ) # Run screen setup for multiple monitor support diff --git a/hrl/inputs/__init__.py b/hrl/inputs/__init__.py index a5f9996..a5ffcf9 100644 --- a/hrl/inputs/__init__.py +++ b/hrl/inputs/__init__.py @@ -1,26 +1,45 @@ -def new_input(input_alias, device=None): - """Factory function to create appropriate Input subclass based on configuration. +import importlib + +__all__ = [ + "new_input", + "ALIASES", +] - This function can be extended to read from a configuration file or environment - variables to determine which input device to instantiate (e.g., Keyboard, RESPONSEPixx). +ALIASES = { + "keyboard": "keyboard.Keyboard", + "responsepixx": "responsepixx.RESPONSEPixx", +} + + +def new_input(input_alias, device=None): + """Factory function to create appropriate Input subclass based on provided alias. Parameters ---------- input_alias : str - alias for the desired input device. Valid options: 'keyboard', 'responsepixx'. + alias for the desired input device. Valid options can be found in the + hrl.inputs.ALIASES.keys(). Returns ------- Input - an instance of the appropriate Input subclass. + an instance of the Input subclass corresponding to the provided alias + + Raises + ------ + ValueError + if the provided input_alias does not match any known input device + """ - if input_alias == "keyboard": - from .keyboard import Keyboard as input_class - elif input_alias == "responsepixx": - from .responsepixx import RESPONSEPixx as input_class + # Lazy import the input class based on alias + if input_alias in ALIASES: + module_name, class_name = ALIASES[input_alias].rsplit(".", 1) + module = importlib.import_module(f".{module_name}", package=__name__) + input_class = getattr(module, class_name) else: raise ValueError( - f"Unknown input device '{input_alias}'. Valid options are: 'keyboard', 'responsepixx'" + f"Unknown input device '{input_alias}'. Valid options are: " + f"{', '.join(list(ALIASES.keys()))}" ) input_device = input_class() diff --git a/hrl/photometer/__init__.py b/hrl/photometer/__init__.py index 4924e4a..2a3b3f9 100644 --- a/hrl/photometer/__init__.py +++ b/hrl/photometer/__init__.py @@ -1,10 +1,24 @@ +import importlib + +__all__ = [ + "new_photometer", + "ALIASES", +] + +ALIASES = { + "optical": "optical.OptiCAL", + "minolta": "minolta.Minolta", +} + + def new_photometer(photometer_alias, device="/dev/ttyUSB0", timeout=10): """Factory function to create a new photometer instance based on the provided name. Parameters ---------- photometer_alias : str - name of the photometer to create. Valid options: 'optical', 'minolta'. + alias for the desired photometer. Valid options can be found in the + hrl.photometer.ALIASES.keys(). device : str, optional device path to the photometer, by default "/dev/ttyUSB0". timeout : int, optional @@ -13,21 +27,23 @@ def new_photometer(photometer_alias, device="/dev/ttyUSB0", timeout=10): Returns ------- Photometer - instance of the photometer corresponding to the provided alias. + instance of the photometer subclass corresponding to the provided alias Raises ------ ValueError - If the provided photometer_alias does not match any known photometer. + if the provided photometer_alias does not match any known photometer device """ - if photometer_alias == "optical": - from .optical import OptiCAL as photometer_class - elif photometer_alias == "minolta": - from .minolta import Minolta as photometer_class + # Lazy import the photometer class based on alias + if photometer_alias in ALIASES: + module_name, class_name = ALIASES[photometer_alias].rsplit(".", 1) + module = importlib.import_module(f".{module_name}", package=__name__) + photometer_class = getattr(module, class_name) else: raise ValueError( - f"Unknown photometer: {photometer_alias}. Valid options are: 'optical', 'minolta'" + f"Unknown photometer device '{photometer_alias}'. Valid options are: " + f"{', '.join(list(ALIASES.keys()))}" ) return photometer_class(device, timeout=timeout) diff --git a/hrl/util/__init__.py b/hrl/util/__init__.py index e341cf0..b49d703 100644 --- a/hrl/util/__init__.py +++ b/hrl/util/__init__.py @@ -9,7 +9,7 @@ graphics_arggroup.add_argument( "-gr", "--graphics", - choices=[hrl.graphics.GRAPHICS_GREY_ALIASES.keys()], + choices=[hrl.graphics.GREY_ALIASES.keys()], default="datapixx", help="Graphics device (default: datapixx)", ) From 5aaf3ba3189260b891d1cede1b7734adc529ede9 Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Tue, 14 Jul 2026 17:51:39 +0200 Subject: [PATCH 14/15] test: fix test --- tests/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index fc1aca7..8f976bc 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -57,7 +57,7 @@ def test_hrl_class(): from hrl import HRL ihrl = HRL( - graphics="gpu", + graphics="gpu_grey", inputs="keyboard", wdth=200, hght=200, From 0ea39e9a01f4bbfb99078a2a8ae8923227e8ff8e Mon Sep 17 00:00:00 2001 From: Joris Vincent Date: Thu, 16 Jul 2026 18:46:30 +0200 Subject: [PATCH 15/15] fix: add alias `'gpu': 'gpu_grey'` --- hrl/graphics/__init__.py | 1 + tests/test_basic.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hrl/graphics/__init__.py b/hrl/graphics/__init__.py index 6870849..f9a2cbb 100644 --- a/hrl/graphics/__init__.py +++ b/hrl/graphics/__init__.py @@ -14,6 +14,7 @@ GREY_ALIASES = { "gpu_grey": "gpu.GPU_grey", + "gpu": "gpu.GPU_grey", "grey": "gpu.GPU_grey", "gray": "gpu.GPU_grey", "gray8": "gpu.GPU_grey", diff --git a/tests/test_basic.py b/tests/test_basic.py index 8f976bc..fc1aca7 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -57,7 +57,7 @@ def test_hrl_class(): from hrl import HRL ihrl = HRL( - graphics="gpu_grey", + graphics="gpu", inputs="keyboard", wdth=200, hght=200,