Skip to content

Factory functions for device initialization#32

Open
JorisVincent wants to merge 15 commits into
masterfrom
feat/factories
Open

Factory functions for device initialization#32
JorisVincent wants to merge 15 commits into
masterfrom
feat/factories

Conversation

@JorisVincent

@JorisVincent JorisVincent commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Adds factory functions to construct and setup graphics devices, input devices and photometers.
The public-facing API of the HRL-class does not change, making this change backwards compatible.

Status quo, problem

Currently, the HRL-class contains a lot of logic for selecting and setting up the correct device. This works through 'device aliases'; strings such as "gpu_grey", or "keyboard". The user specifies a device alias, and the HRL-class constructs the corresponding object.

        ## Load Graphics Device ##
        if graphics 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 in ("gpu_RGB", "RGB"):
            from .graphics.gpu import GPU_RGB

            self.graphics = GPU_RGB(
                ...
            )
        elif ....

This is an undesirable tight coupling between HRL-class and the devices. It requires a lot of this switching logic to be in the HRL-class. It also means that add more devices (or making changes to devices such as different parameters for initialization) requires updating of all this switching logic. In the end, it means that the HRL-object "needs to know" about the implementations of the graphics devices. This breaks the advantages of the Object-Oriented structure that we have. ( #11 )

Solution

This pull request moves all of that device intialization and switching logic, to dedicated factory functions, e.g., hrl.graphics.new_graphics(...), hrl.inputs.new_inputs(..). HRL-object then calls these factory functions.

        ## Setup screen and graphics ##
        self.graphics = hrl.graphics.new_graphics(
            graphics_alias=graphics,
            ...
        )

This moves all of the logic out of the HRL-class, decoupling it from the implementation classes. Thus, the HRL-class doesn't need to know which graphics object was created -- it can just use it.

The one coupling that does remain in the HRL-class is that it takes the .device from the graphics object (which may hold the binding to a *Pixx device), as HRL.device, and then passes that to new_input() where it can be used to initialize a DATAPixx device.

    @property
    def device(self):
        return self.graphics.device if self.graphics else None
        
    ...
    
    ## Setup Input Device ##
    self.inputs = hrl.inputs.new_input(
        input_alias=inputs,
        device=self.graphics.device,
    )

So, HRL coordinates between graphics, inputs, and photometer, rather than manage the individual devices.

Implementation details

The submodule graphics/__init__.py, inputs/__init__.py, photometer/__init__.py define the factory functions, so that they can be called as hrl.graphics.new_graphics(). The factory functions take the same device alias strings that HRL previously did, and return an instantiated object of the correct subclass.

my_graphics_obj = hrl.graphics.new_graphics('gpu_rgb', ...)
my_inputs_obj = hrl.inputs.new_input('keyboard', ...)

Inside these factory functions, the switching logic is set up so that potentially multiple aliases can refer to the same class to be constructed, e.g., 'gpu', 'gpu_grey', 'gpu_gray' all refer to the same device class.

NOTE: one can still bypass the new_graphics() (and HRL-class) entirely, and just construct a device specific object directly:

my_graphics_obj = hrl.graphics.gpu.GPU_grey(...)

The factory functions themselves also don't do any device specific initialization -- that's handled by the device-specific class. Thus, the new_graphics() does not need 'to know' about setting up, e.g., a ViewPixx vs. DataPixx. Only one device-specific module gets imported at a time, which also makes this more robust.

Future proofing

Adding a new device-class thus is simpler: write the device-class as normal (inhereting from the correct super-class, e.g., Graphics_grey or Graphics_GPU). Then, add it with one or more aliases to new_graphics():

    ...
    # 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 ["new_device"]:
        from .new_device import DEVICE_grey as graphics_class     
    ...

Screen setup

Additionally, there is some necessary setting up of screens on multimonitor setups, via setting environment variables. This used to be done by the HRL-object as well. Now, it is in a hrl.graphics.setup_screen() function. This function can be called separately, but is also automatically called by new_graphics().

@JorisVincent JorisVincent self-assigned this Feb 24, 2026
@JorisVincent
JorisVincent force-pushed the feat/factories branch 2 times, most recently from 065647e to e0d38e3 Compare March 12, 2026 16:01
`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.
Prevents importing all the possible devices; only import the branch we need
Leaves all the switching logic there, instead of in the `HRL`-class

@guillermoaguilar guillermoaguilar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small correction needed in the aliases mappings, and to add analogous for inputs and photometers

Comment thread hrl/hrl.py
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".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we here also give possible options, as in graphics?

"Valid options are defined in ..."

Comment thread hrl/hrl.py
inputs : str
alias for the desired input device, by default "keyboard".
photometer : str or None
alias for the desired photometer device, by default None.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Comment thread hrl/hrl.py
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JorisVincent

Copy link
Copy Markdown
Contributor Author

small correction needed in the aliases mappings, and to add analogous for inputs and photometers

Added a commit for this now: all three hardware types have the same setup in terms of aliases being defined in the module. These can be accessed through hrl.graphics.ALIASES, hrl.photometer.ALIASES, hrl.inputs.ALIASES.
The docstrings now refer to these dictionaries. That way, adding a new device just requires updating the ALIASES dictionary, not also the docstring -- fewer places that could get misaligned.

@guillermoaguilar

Copy link
Copy Markdown
Member

Ok changes look good.
BUT I found a bug, the changes break backward compatibility. When trying to run the example in examples/check_monitor_rate/check_monitor_rate.py, it fails with the error:

pygame 2.5.2 (SDL 2.28.2, Python 3.8.19)
Hello from the pygame community. https://www.pygame.org/contribute.html
Traceback (most recent call last):
  File "check_monitor_rate.py", line 249, in <module>
    main()
  File "check_monitor_rate.py", line 110, in main
    hrl = HRL(
  File "/home/guille/anaconda3/envs/hrl3-dev/lib/python3.8/site-packages/hrl/hrl.py", line 109, in __init__
    self.graphics = hrl.graphics.new_graphics(
  File "/home/guille/anaconda3/envs/hrl3-dev/lib/python3.8/site-packages/hrl/graphics/__init__.py", line 101, in new_graphics
    raise ValueError(
ValueError: Unknown graphics device 'gpu'. Valid options are: gpu_grey, grey, gray, gray8, viewpixx, viewpixx_grey, viewpixx_gray, viewpixx_gray8, datapixx, datapixx_grey, datapixx_gray, datapixx_gray8, gpu_RGB, RGB, viewpixx_RGB, viewpixx_color, viewpixx_colour

so 'gpu' does not default to 'gpu_gray'.

@JorisVincent

Copy link
Copy Markdown
Contributor Author

so 'gpu' does not default to 'gpu_gray'.

Added this alias now as well.

@JorisVincent

Copy link
Copy Markdown
Contributor Author

When trying to run the example in examples/check_monitor_rate/check_monitor_rate.py,

Should this example in some way become part of the test suite or documentation? I've ignored it (the examples directory entirely) so far...

My take would be that we shouldn't have an examples dir separate from documentation -- just more places that can get out of sync.
For timing specifically, I think we should have two things: 1) some documentation that highlights and explains timing control with HRL (also some example experiment with fixed display intervals); 2) some functionality in the library and/or CLI at least for this framerate check, but maybe also other timing-related tools?

If so, I'd probably make some changes as well -- mainly to use the Python standard library timing. Nowadays, that one is highly accurate across platforms. And, reading the vendorized PsychoPy clock, on Linux it already defaults to that anyway...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restructure HRL class to be more agnostic about device initalization

2 participants