Source code for qinfer.domains

#!/usr/bin/python
# -*- coding: utf-8 -*-
##
# domains.py: module for domains of model outcomes
##
# © 2017, Chris Ferrie (csferrie@gmail.com) and
#         Christopher Granade (cgranade@cgranade.com).
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#     1. Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#
#     2. Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#
#     3. Neither the name of the copyright holder nor the names of its
#        contributors may be used to endorse or promote products derived from
#        this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
##

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

from __future__ import division
from __future__ import absolute_import

from builtins import range
from future.utils import with_metaclass
from functools import reduce

from operator import mul
from scipy.special import binom
from math import factorial
from itertools import combinations_with_replacement, product
import numpy as np
from .utils import join_struct_arrays, separate_struct_array

import abc

import warnings

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

__all__ = [
    'Domain',
    'ProductDomain',
    '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 `np.inf`. :type: ``int`` or ``np.inf`` """ 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 ##
[docs] @abc.abstractmethod 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
[docs]class ProductDomain(Domain): """ A domain made from the cartesian product of other domains. :param Domain domains: ``Domain`` instances as separate arguments, or as a singe list of ``Domain`` instances. """ def __init__(self, *domains): if len(domains) == 1: try: self._domains = list(domains[0]) except: self._domains = domains else: self._domains = domains self._domains = domains self._dtypes = [domain.example_point.dtype for domain in self._domains] self._example_point = join_struct_arrays( [np.array(domain.example_point) for domain in self._domains] ) self._dtype = self._example_point.dtype @property def is_continuous(self): """ Whether or not the domain has an uncountable number of values. :type: `bool` """ return any([domain.is_continuous for domain in self._domains]) @property def is_finite(self): """ Whether or not the domain contains a finite number of points. :type: `bool` """ return all([domain.is_finite for domain in self._domains]) @property def dtype(self): """ The numpy dtype of a single element of the domain. :type: `np.dtype` """ return self._dtype @property def n_members(self): """ Returns the number of members in the domain if it `is_finite`, otherwise, returns `np.inf`. :type: ``int`` or ``np.inf`` """ if self.is_finite: return reduce(mul, [domain.n_members for domain in self._domains], 1) else: return np.inf @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 self._example_point @property 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` """ separate_values = [domain.values for domain in self._domains] return np.concatenate([ join_struct_arrays(list(map(np.array, value))) for value in product(*separate_values) ]) ## METHODS ## def _mytype(self, array): # astype does weird stuff with struct names, and possibly # depends on numpy version; hopefully # the following is a bit more predictable since it passes through # uint8 return separate_struct_array(array, self.dtype)[0]
[docs] def to_regular_arrays(self, array): """ Expands from an array of type `self.dtype` into a list of arrays with dtypes corresponding to the factor domains. :param np.ndarray array: An `np.array` of type `self.dtype`. :rtype: ``list`` """ return separate_struct_array(self._mytype(array), self._dtypes)
[docs] def from_regular_arrays(self, arrays): """ Merges a list of arrays (of the same shape) of dtypes corresponding to the factor domains into a single array with the dtype of the ``ProductDomain``. :param list array: A list with each element of type ``np.ndarray`` :rtype: `np.ndarray` """ return self._mytype(join_struct_arrays([ array.astype(dtype) for dtype, array in zip(self._dtypes, arrays) ]))
[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 all([ domain.in_domain(array) for domain, array in zip(self._domains, separate_struct_array(points, self._dtypes)) ])
## 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. :param float max: A number specifying the largest possible value of the domain. """ def __init__(self, min=-np.inf, max=np.inf): self._min = min self._max = max ## PROPERTIES ## @property def min(self): """ Returns the minimum value of the domain. :rtype: `float` """ return self._min @property def max(self): """ Returns the maximum value of the domain. :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: ``np.inf`` """ return np.inf @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 not np.isinf(self.min): return np.array([self.min], dtype=self.dtype) if not np.isinf(self.max): 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` """ if np.all(np.isreal(points)): are_greater = np.all(np.greater_equal(points, self._min)) are_smaller = np.all(np.less_equal(points, self._max)) return are_greater and are_smaller else: return False
[docs]class IntegerDomain(Domain): """ A domain specifying a contiguous (and possibly open ended) subset of the integers. Internally minimum and maximum are represented as floats in order to handle the case of infinite maximum, and minimums. The integer conversion function will be applied to the min and max values. :param int min: A number specifying the lowest possible value of the domain. :param int max: A number specifying the largest possible value of the domain. Note: Yes, it is slightly unpythonic to specify `max` instead of `max`+1. """ def __init__(self, min=0, max=np.inf): self._min = int(min) if not np.isinf(min) else min self._max = int(max) if not np.isinf(max) else max ## PROPERTIES ## @property def min(self): """ Returns the minimum value of the domain. :rtype: `float` or `np.inf` """ return int(self._min) if not np.isinf(self._min) else self._min @property def max(self): """ Returns the maximum value of the domain. :rtype: `float` or `np.inf` """ return int(self._max) if not np.isinf(self._max) else 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 not np.isinf(self.min) and not np.isinf(self.max) @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 `np.inf`. :type: ``int`` or ``np.inf`` """ if self.is_finite: return int(self.max - self.min + 1) else: return np.inf @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 not np.isinf(self.min): return np.array([self._min], dtype=self.dtype) if not np.isinf(self.max): 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.is_finite: return np.arange(self.min, self.max + 1, dtype = self.dtype) else: 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` """ if np.all(np.isreal(points)): try: are_integer = np.all(np.mod(points, 1) == 0) except TypeError: are_integer = False are_greater = np.all(np.greater_equal(points, self._min)) are_smaller = np.all(np.less_equal(points, self._max)) return are_integer and are_greater and are_smaller else: return False
[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) non_negative = np.all(np.greater_equal(array_view, 0)) correct_sum = np.all(np.sum(array_view, axis=-1) == self.n_meas) return non_negative and correct_sum