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:

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:

Inheritance diagram of prfmodel.models.base, prfmodel.models.prf, prfmodel.models.cf, prfmodel.models.csf, prfmodel.models.compression

The inheritance diagram for prfmodel.impulse:

Inheritance diagram of prfmodel.impulse.base

The inheritance diagram for prfmodel.scaling:

Inheritance diagram of prfmodel.scaling.base

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.