Pyre Basics

AlTar’s architecture is based on the pyre framework. Before the official documentation for pyre is released, we offer here a brief introduction to pyre, its programming design and its configuration file format which are necessary for AlTar users.

Protocols and Components

Python offers a modular programming design with modules and classes while pyre extends the functionalities of python classes to facilitate their integrations and configurations, through components.

A prescribed functionality is defined as a protocol (similar to an abstract class). For example, various distributions are used in AlTar, mainly serving as prior distributions. We first define a protocol,

# the protocol
class Distribution(altar.protocol, family="altar.distributions"):
    """
    The protocol that all AlTar probability distributions must satisfy
    """

    # required behaviors
    @altar.provides
    def initialize(self, **kwds):
        """
        Initialize with the given random number generator
        """

    # model support
    @altar.provides
    def initializeSample(self, theta):
        """
        Fill my portion of {theta} with initial random values from my distribution.
        """

    @altar.provides
    def priorLikelihood(self, theta, prior):
        """
        Fill my portion of {prior} with the likelihoods of the samples in {theta}
        """

    @altar.provides
    def verify(self, theta, mask):
        """
        Check whether my portion of the samples in {theta} are consistent with my constraints, and
        update {mask}, a vector with zeroes for valid samples and non-zero for invalid ones
        """

    ... ...

    # framework hooks
    @classmethod
    def pyre_default(cls):
        """
        Supply a default implementation
        """
        # use the uniform distribution
        from .Uniform import Uniform as default
        # and return it
        return default

where @altar.provides decorator specifies the behaviors (methods) that its implementations must define. An implementation, for example, the Uniform distribution, is defined as a component,

class Uniform(altar.component, family="altar.distributions.uniform"):
    """
    The uniform probability distribution
    """

    # user configurable state
    parameters = altar.properties.int()
    parameters.doc = "the number of model parameters that i take care of"

    offset = altar.properties.int(default=0)
    offset.doc = "the starting point of my parameters in the overall model state"

    # user configurable state
    support = altar.properties.array(default=(0,1))
    support.doc = "the support interval of the prior distribution"


    # protocol obligations
    @altar.export
    def initialize(self, rng):
        """
        Initialize with the given random number generator
        """
        # set up my pdf
        self.pdf = altar.pdf.uniform(rng=rng.rng, support=self.support)
        # all done
        return self

    @altar.export
    def initializeSample(self, theta):
        """
        Fill my portion of {theta} with initial random values from my distribution.
        """
        # grab the portion of the sample that's mine
        θ = self.restrict(theta=theta)
        # fill it with random numbers from my initializer
        self.pdf.matrix(matrix=θ)
        # and return
        return self


    @altar.export
    def verify(self, theta, mask):
        """
        Check whether my portion of the samples in {theta} are consistent with my constraints, and
        update {mask}, a vector with zeroes for valid samples and non-zero for invalid ones
        """

        ... ...

        # all done; return the rejection map
        return mask

    ... ...

    # private data
    pdf = None # the pdf implementation

where the required behaviors specific to the Uniform distribution are defined. Besides behaviors, a component may also include attributes such as

  • Properties, configurable parameters in terms of basic Python data type, such as an integer [defined as altar.properties.int();

  • Sub-Components, configurable attributes in terms of components;

  • Non-configurable attributes, regular Python objects such as static properties or objects determined at runtime, e.g., the pdf function in Distribution.

Components are building blocks of AlTar. For example, Distribution can be used as the prior distribution in a Bayesian model,

class Bayesian(altar.component, family="altar.models.bayesian", implements=model):
"""
The base class of AlTar models that are compatible with Bayesian explorations
"""

    prior = altar.distributions.distribution()
    prior.doc = "the prior distribution"

    ... ...

Here, prior is a configurable component, for which users can specify at runtime by model.prior=uniform or any other distributions implementing the Distribution-protocol. Since the protocol defines the uniform distribution as its default implementation, if none is specified at runtime, the uniform distribution is used by default.

Note also that components are abstract methods and can be only be instantiated by an AlTar application instance. If you create a component instance in a Python shell, it will not behave as a regular Python class.

Pyre Config Format (.pfg)

Configurations of properties and components can be passed to the program as command line arguments, or more conveniently, by a configuration file. Three types of configuration files are supported by pyre/AlTar: .pml (XML-style), .cfg (an INI-style format used in AlTar 1.1), and .pfg (YAML/JSON-style). We recommend .pfg for its human-readable data serialization format.

An example of .pfg file is provided in QuickStart, for the linear model.

Some basic rules of .pfg format are

  • Whitespace indentation is used for denoting structure, or hierarchy; however, tab characters are not allowed.

  • Hierarchy of components can be specified by indentation, or by explicit full path, or by a combination of partial path with indentation. For example, these three configurations are equivalent:

    ; method 1: all by indentation
    linear:
        job:
            tasks = 1
            gpus = 0
            chains = 2**10
    
    ; method 2: all by full path
    linear.job.tasks = 1
    linear.job.gpus = 0
    linear.job.chains = 2**10
    
    ; method 3: combination with partial path and indentation
    linear:
        job.tasks = 1
        job.gpus = 0
        job.chains = 2**10
    
  • If a component is not specified or listed in the configuration file, a default value/implementation specified in the Python program will be used instead.

  • Strings such as paths, names, don’t need quotation marks.