Architecture¶
This page contains information about the package architecture.
prfmodel contains different submodules that contain different types of (sub-) models.
The main model classes that users interact with are in prfmodel.models, prfmodel.impulse,
and prfmodel.scaling. We distinguish between three user profiles:
Entry-level users will interact with high-level canonical model classes such as
prfmodel.models.prf.Gaussian2DPRFModel. These classes have good defaults and require very little adjustment.Advanced users will interact with submodel classes in
prfmodel.impulseandprfmodel.scaling. For example, they might customize the parameters of impulse submodel in a canonicalprfmodel.models.prf.Gaussian2DPRFModel.Expert uses will interact with all available public classes, potentially defining their own custom submodels or canonical models.
Inhertitance diagrams¶
To facilitate the development of the package and the creation of custom models, we provide an overview of the inhertiance structure of the three modules containing model classes.
Private helper base classes are omitted from the diagrams; classes inheriting from them are shown as inheriting from the nearest public base class.
The inheritance diagram for prfmodel.models:

The inheritance diagram for prfmodel.impulse:

The inheritance diagram for prfmodel.scaling:

Protocols and base classes¶
All (sub-) model classes inherit from prfmodel.protocols.ModelProtocol. This protocol requires subclasses to
implement methods for accessing and checking parameter names.
The modules prfmodel.models, prfmodel.impulse, prfmodel.scaling, and
prfmodel.regressors define abstract base classes (ABCs) that all subsequent models inherit from. These ABCs
define abstract methods and attributes that subclasses must implement. For making model predictions, they use a
“public facade” pattern: They implement a concrete user-facing __call__ method that accepts NumPy arrays and pandas
dataframe objects as arguments and performs input checks on these objects (e.g., by calling methods inherited from
prfmodel.protocols.ModelProtocol). To perform actual computations, __call__ converts all arguments to backend
specific tensor objects (i.e., prfmodel.typing.Tensor) and forwards them to an abstract call method that
each subclass must implement. Importantly, call must only use tensors as inputs and outputs and implement
tensor operations via keras.ops or backend-specific operations (e.g., tf.math or
jax.numpy).
The main reason behind this split is that Keras can only track gradients for tensors. Therefore, fitter classes that
require gradients, such as prfmodel.fitters.SGDFitter, interally only call each models call method.
However, because working with tensors is not very user-friendly (e.g., for data wrangling and plotting), users can
instead call the less restrictive __call__ method (e.g., to make a model prediction).
Another requirement is that call methods must be traceable to enable backend-specific compilation (see Backends).
This also requires inputs and outputs to be tensor objects, but also that the control flow inside call does not
depend on the values of input arguments (but depending on input shapes is allowed).
For an example, see Creating a custom model.
API¶
The API also uses a “public facade” design pattern where most complex model classes are defined in private submodules and exported as public in the next ancestor module in the hierarchy. This prevents the API docs from overflowing with submodules while keeping class definitions in separate files.