Source code for qinfer.domains

#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# domains.py: module for domains of model outcomes
##
# © 2012 Chris Ferrie (csferrie@gmail.com) and
#        Christopher E. Granade (cgranade@gmail.com)
#
# This file is a part of the Qinfer project.
# Licensed under the AGPL version 3.
##
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
##

## IMPORTS ###################################################################

from __future__ import division
from __future__ import absolute_import

from builtins import range
from future.utils import with_metaclass

from scipy.special import binom
from math import factorial
from itertools import combinations_with_replacement
import numpy as np

import abc

import warnings

## EXPORTS ###################################################################

__all__ = [
    'Domain',
    'RealDomain',
    'IntegerDomain',
    'MultinomialDomain'
]

## FUNCTIONS #################################################################

## ABSTRACT CLASSES AND MIXINS ###############################################

[docs]class Domain(with_metaclass(abc.ABCMeta, object)): """ Abstract base class for domains of outcomes of models. """ ## ABSTRACT PROPERTIES ## @abc.abstractproperty def is_continuous(self): """ Whether or not the domain has an uncountable number of values. :type: `bool` """ pass @abc.abstractproperty def is_finite(self): """ Whether or not the domain contains a finite number of points. :type: `bool` """ pass @abc.abstractproperty def dtype(self): """ The numpy dtype of a single element of the domain. :type: `np.dtype` """ pass @abc.abstractproperty def n_members(self): """ Returns the number of members in the domain if it `is_finite`, otherwise, returns `None`. :type: ``int`` """ pass @abc.abstractproperty def example_point(self): """ Returns any single point guaranteed to be in the domain, but no other guarantees; useful for testing purposes. This is given as a size 1 ``np.array`` of type `dtype`. :type: ``np.ndarray`` """ pass @abc.abstractproperty def values(self): """ Returns an `np.array` of type `dtype` containing some values from the domain. For domains where `is_finite` is ``True``, all elements of the domain will be yielded exactly once. :rtype: `np.ndarray` """ pass ## CONCRETE PROPERTIES ## @property def is_discrete(self): """ Whether or not the domain has a countable number of values. :type: `bool` """ return not self.is_continuous ## ABSTRACT METHODS ## @abc.abstractmethod
[docs] def in_domain(self, points): """ Returns ``True`` if all of the given points are in the domain, ``False`` otherwise. :param np.ndarray points: An `np.ndarray` of type `self.dtype`. :rtype: `bool` """ pass
## CLASSES ###################################################################
[docs]class RealDomain(Domain): """ A domain specifying a contiguous (and possibly open ended) subset of the real numbers. :param float min: A number specifying the lowest possible value of the domain. If left as `None`, negative infinity is assumed. :param float max: A number specifying the largest possible value of the domain. If left as `None`, positive infinity is assumed. """ def __init__(self, min=None, max=None): self._min = min self._max = max ## PROPERTIES ## @property def min(self): """ Returns the minimum value of the domain. The outcome ``None`` is interpreted as negative infinity. :rtype: `float` """ return self._min @property def max(self): """ Returns the maximum value of the domain. The outcome ``None`` is interpreted as positive infinity. :rtype: `float` """ return self._max @property def is_continuous(self): """ Whether or not the domain has an uncountable number of values. :type: `bool` """ return True @property def is_finite(self): """ Whether or not the domain contains a finite number of points. :type: `bool` """ return False @property def dtype(self): """ The numpy dtype of a single element of the domain. :type: `np.dtype` """ return np.float @property def n_members(self): """ Returns the number of members in the domain if it `is_finite`, otherwise, returns `None`. :type: ``int`` """ return None @property def example_point(self): """ Returns any single point guaranteed to be in the domain, but no other guarantees; useful for testing purposes. This is given as a size 1 ``np.array`` of type ``dtype``. :type: ``np.ndarray`` """ if self.min is not None: return np.array([self.min], dtype=self.dtype) if self.max is not None: return np.array([self.max], dtype=self.dtype) else: return np.array([0], dtype=self.dtype) @property def values(self): """ Returns an `np.array` of type `self.dtype` containing some values from the domain. For domains where ``is_finite`` is ``True``, all elements of the domain will be yielded exactly once. :rtype: `np.ndarray` """ return self.example_point ## METHODS ##
[docs] def in_domain(self, points): """ Returns ``True`` if all of the given points are in the domain, ``False`` otherwise. :param np.ndarray points: An `np.ndarray` of type `self.dtype`. :rtype: `bool` """ return np.all(points >= self._min) and np.all(points <= self._max)
[docs]class IntegerDomain(Domain): """ A domain specifying a contiguous (and possibly open ended) subset of the integers. :param int min: A number specifying the lowest possible value of the domain. If `None`, negative infinity is assumed. :param int max: A number specifying the largest possible value of the domain. If left as `None`, positive infinity is assumed. Note: Yes, it is slightly unpythonic to specify `max` instead of `max`+1. """ def __init__(self, min=0, max=None): self._min = int(min) self._max = int(max) ## PROPERTIES ## @property def min(self): """ Returns the minimum value of the domain. The outcome ``None`` is interpreted as negative infinity. :rtype: `float` """ return self._min @property def max(self): """ Returns the maximum value of the domain. The outcome ``None`` is interpreted as positive infinity. :rtype: `float` """ return self._max @property def is_continuous(self): """ Whether or not the domain has an uncountable number of values. :type: `bool` """ return False @property def is_finite(self): """ Whether or not the domain contains a finite number of points. :type: `bool` """ return self.min is not None and self.max is not None @property def dtype(self): """ The numpy dtype of a single element of the domain. :type: `np.dtype` """ return np.int @property def n_members(self): """ Returns the number of members in the domain if it `is_finite`, otherwise, returns `None`. :type: ``int`` """ if self.is_finite: return int(self.max - self.min + 1) else: return None @property def example_point(self): """ Returns any single point guaranteed to be in the domain, but no other guarantees; useful for testing purposes. This is given as a size 1 ``np.array`` of type ``dtype``. :type: ``np.ndarray`` """ if self.min is not None: return np.array([self._min], dtype=self.dtype) if self.max is not None: return np.array([self._max], dtype=self.dtype) else: return np.array([0], dtype=self.dtype) @property def values(self): """ Returns an `np.array` of type `self.dtype` containing some values from the domain. For domains where ``is_finite`` is ``True``, all elements of the domain will be yielded exactly once. :rtype: `np.ndarray` """ if self.max is None or self.min is None: return self.example_point else: return np.arange(self.min, self.max + 1, dtype = self.dtype) ## METHODS ##
[docs] def in_domain(self, points): """ Returns ``True`` if all of the given points are in the domain, ``False`` otherwise. :param np.ndarray points: An `np.ndarray` of type `self.dtype`. :rtype: `bool` """ are_integer = np.all(np.mod(points,1) == 0) are_greater = True if self._min is None else np.all(points >= self._min) are_smaller = True if self._max is None else np.all(points <= self._max) return are_integer and are_greater and are_smaller
[docs]class MultinomialDomain(Domain): """ A domain specifying k-tuples of non-negative integers which sum to a specific value. :param int n_meas: The sum of any tuple in the domain. :param int n_elements: The number of elements in a tuple. """ def __init__(self, n_meas, n_elements=2): self._n_elements = n_elements self._n_meas = n_meas ## PROPERTIES ## @property def n_meas(self): """ Returns the sum of any tuple in the domain. :rtype: `int` """ return self._n_meas @property def n_elements(self): """ Returns the number of elements of a tuple in the domain. :rtype: `int` """ return self._n_elements @property def is_continuous(self): """ Whether or not the domain has an uncountable number of values. :type: `bool` """ return False @property def is_finite(self): """ Whether or not the domain contains a finite number of points. :type: `bool` """ return True @property def dtype(self): """ The numpy dtype of a single element of the domain. :type: `np.dtype` """ return np.dtype([('k', np.int, self.n_elements)]) @property def n_members(self): """ Returns the number of members in the domain if it `is_finite`, otherwise, returns `None`. :type: ``int`` """ return int(binom(self.n_meas + self.n_elements -1, self.n_elements - 1)) @property def example_point(self): """ Returns any single point guaranteed to be in the domain, but no other guarantees; useful for testing purposes. This is given as a size 1 ``np.array`` of type ``dtype``. :type: ``np.ndarray`` """ return np.array([([self.n_meas] + [0] * (self.n_elements-1))], dtype=self.dtype) @property def values(self): """ Returns an `np.array` of type `self.dtype` containing some values from the domain. For domains where ``is_finite`` is ``True``, all elements of the domain will be yielded exactly once. :rtype: `np.ndarray` """ # This code comes from Jared Goguen at http://stackoverflow.com/a/37712597/1082565 partition_array = np.empty((self.n_members, self.n_elements), dtype=int) masks = np.identity(self.n_elements, dtype=int) for i, c in enumerate(combinations_with_replacement(masks, self.n_meas)): partition_array[i,:] = sum(c) # Convert to dtype before returning return self.from_regular_array(partition_array) ## METHODS ##
[docs] def to_regular_array(self, A): """ Converts from an array of type `self.dtype` to an array of type `int` with an additional index labeling the tuple indeces. :param np.ndarray A: An `np.array` of type `self.dtype`. :rtype: `np.ndarray` """ # this could be a static method, but we choose to be consistent with # from_regular_array return A.view((int, len(A.dtype.names))).reshape(A.shape + (-1,))
[docs] def from_regular_array(self, A): """ Converts from an array of type `int` where the last index is assumed to have length `self.n_elements` to an array of type `self.d_type` with one fewer index. :param np.ndarray A: An `np.array` of type `int`. :rtype: `np.ndarray` """ dims = A.shape[:-1] return A.reshape((np.prod(dims),-1)).view(dtype=self.dtype).squeeze(-1).reshape(dims)
[docs] def in_domain(self, points): """ Returns ``True`` if all of the given points are in the domain, ``False`` otherwise. :param np.ndarray points: An `np.ndarray` of type `self.dtype`. :rtype: `bool` """ array_view = self.to_regular_array(points) return np.all(array_view >= 0) and np.all(np.sum(array_view, axis=-1) == self.n_meas)