<commit_before># -*- coding: utf-8 -*-
# Author: Simone Marsili <simomarsili@gmail.com>
# License: BSD 3 clause
"""
Functions for entropy and information measures estimation.
"""
import logging
from itertools import combinations

import numpy

from ndd.data import DataArray
from ndd.divergence import JSDivergence
from ndd.estimators import NSB, Plugin, WolpertWolf
from ndd.exceptions import CombinationError, EstimatorInputError, PmfError

__all__ = [
    'entropy',
    'from_data',
    'jensen_shannon_divergence',
    'kullback_leibler_divergence',
    'interaction_information',
    'coinformation',
    'mutual_information',
    'conditional_entropy',
    'histogram',
]

logger = logging.getLogger(__name__)

def entropy(pk, k=None, alpha=None, plugin=False, return_std=False):
    """
    Entropy estimate from an array of counts.

    Return a Bayesian estimate for the entropy of an unknown discrete
    distribution from an input array of counts pk.

    Parameters
    ----------
    pk : array-like
        The number of occurrences of a set of bins.
    k : int or array-like, optional
        Total number of bins (including unobserved bins); k >= len(pk).
        A float is a valid input for whole numbers (e.g. k=1.e3).
        If an array, set k = numpy.prod(k). Defaults to len(pk).
    alpha : float, optional
        If not None: Wolpert-Wolf entropy estimator (fixed alpha).
        Use a single Dirichlet prior with concentration parameter alpha.
        alpha > 0.0.
    plugin : boolean, optional
        If True, return a 'plugin' estimate of the entropy. The discrete
        distribution is estimated from the empirical frequencies over bins
        and inserted into the entropy definition (plugin estimator).
        If alpha is passed in combination with plugin=True, add
        alpha pseudocounts to each frequency count (pseudocount estimator).
    return_std : boolean, optional
        If True, also return an approximation for the standard deviation
        over the entropy posterior.

    Returns
    -------
    entropy : float
        Entropy estimate.
    std : float, optional
        Uncertainty in the entropy estimate. Only if `return_std` is True.

    """

    estimator = select_estimator(alpha=alpha, plugin=plugin)

    estimator = estimator.fit(pk, k=k)
    S, err = estimator.estimate_, estimator.err_

    if numpy.isnan(S):
        logger.warning('nan value for entropy estimate')
        S = numpy.nan

    if return_std:
        if err is not None and numpy.isnan(err):
            err = numpy.nan
            logger.warning('nan value for entropy posterior std deviation')
        return S, err

    return S

def from_data(ar, ks=None, axis=0, r=None):
    """
    Entropy estimate from data matrix.

    Paramaters
    ----------
    ar : array-like, shape (n, p)
        2D array of n samples from p discrete variables.
    ks : int or 1D array of length p, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int, optional; ; 1<=r<=p.
        If passed, return a generator yielding estimates for the p-choose-r
        possible combinations of r variables.

    Returns
    -------
    float
        Entropy estimate

    """
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    # EntropyEstimator objects are callable and return the fitted estimate
    estimator = NSB()

    if r is not None:
        r = _check_r(r, ar)
        return (estimator(pk, k=k) for pk, k in ar.iter_counts(r=r))

    counts, k = ar.iter_counts()
    return estimator(counts, k=k)

def jensen_shannon_divergence(pk, k=None, alpha=None, plugin=False):
    """
    Return the Jensen-Shannon divergence from a m-by-p matrix of counts.

    Return an estimate of the Jensen-Shannon divergence between
    m unknown discrete distributions from a m-by-p input array of counts.
    The estimate (in nats) is computed as a combination of single Bayesian
    entropy estimates. If the total number of samples varies among the
    distributions, the function returns a weighted divergence with weights
    proportional to the total number of samples in each row
    (see the general definition of Jensen-Shannon divergence:
    https://en.wikipedia.org/wiki/Jensen-Shannon_divergence).

    Parameters
    ----------

    pk : array-like, shape (m, p)
        Matrix of frequency counts. Each row corresponds to the number of
        occurrences of a set of bins from a different distribution.
    k : int or array-like, optional
        Total number of bins (including unobserved bins); k >= p.
        A float is a valid input for whole numbers (e.g. k=1.e3).
        If an array, set k = numpy.prod(k). Defaults to p.
    alpha : float, optional
        If not None: Wolpert-Wolf entropy estimator (fixed alpha).
        Use a single Dirichlet prior with concentration parameter alpha.
        alpha > 0.0.
    plugin : boolean, optional
        If True, use a 'plugin' estimator for the entropy.
        If alpha is passed in combination with plugin == True, add alpha
        pseudoconts to the frequency counts in the plugin estimate.

    Returns
    -------
    float
        Jensen-Shannon divergence.

    """

    entropy_estimator = select_estimator(alpha, plugin)
    estimator = JSDivergence(entropy_estimator).fit(pk, k=k)
    js = estimator.estimate_

    if numpy.isnan(js):
        logger.warning('nan value for JS divergence')
        js = numpy.nan

    return js

def kullback_leibler_divergence(pk, qk, k=None, alpha=None, plugin=False):
    """
    Kullback-Leibler divergence given counts pk and a reference PMF qk.

    Return an estimate of the Kullback-Leibler given an array of counts pk and
    a reference PMF qk. The estimate (in nats) is computed as:
    - S_p - sum(pk * log(qk)) / sum(pk)
    where S_p is the entropy estimate from counts pk.

    Parameters
    ----------
    pk : array_like
        The number of occurrences of a set of bins.
    qk : array_like
        Reference PMF in sum(pk log(pk/qk). len(qk) = len(pk).
        Must be a valid PMF (non-negative, normalized).
    k : int or array-like, optional
        Total number of bins (including unobserved bins); k >= p.
        A float is a valid input for whole numbers (e.g. k=1.e3).
        If an array, set k = numpy.prod(k). Defaults to len(pk).
    alpha : float, optional
        If not None: Wolpert-Wolf entropy estimator (fixed alpha).
        Use a single Dirichlet prior with concentration parameter alpha.
        alpha > 0.0.
    plugin : boolean, optional
        If True, use a 'plugin' estimator for the entropy.
        If alpha is passed in combination with plugin == True, add alpha
        pseudoconts to the frequency counts in the plugin estimate.

    Returns
    -------
    float
        Kullback-Leibler divergence.

    """

    if is_pmf(qk):
        log_qk = numpy.log(qk)
    else:
        raise PmfError('qk must be a valid PMF')

    if sum(numpy.isinf(log_qk)) > 0:
        raise PmfError('qk must be positive')

    if len(log_qk) != len(pk):
        raise PmfError('qk and pk must have the same length.')

    if k == 1:  # single bin
        return 0.0
    if k is None:
        k = len(pk)

    estimator = select_estimator(alpha, plugin)
    estimate = estimator.fit(pk, k=k).estimate_
    kl = -(estimate + numpy.sum(pk * log_qk) / float(sum(pk)))
    if numpy.isnan(kl):
        logger.warning('nan value for KL divergence')
        kl = numpy.nan

    return kl

def interaction_information(ar, ks=None, axis=0, r=None):
    """Interaction information from data matrix.

    See Eq.10 in:
    Timme, Nicholas, et al.
    "Synergy, redundancy, and multivariate information measures:
    an experimentalist's perspective."
    Journal of computational neuroscience 36.2 (2014): 119-140.

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    ks : 1D array of length p, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int, optional; 1<=r<=p.
        If passed, return a generator yielding estimates for the p-choose-r
        possible combinations of r variables.
        Combinations are ordered as: list(itertools.combinations(range(p), r)).

    Returns
    -------
    float
        Interaction information estimate.

    Raises
    ------
    CardinalityError
        If len(ks) != p.

    """
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    if r is not None:
        r = _check_r(r, ar)
        return (iinfo(data, k) for data, k in ar.iter_data(r=r))

    data, k = ar.iter_data()
    return iinfo(data, k)

def coinformation(ar, ks=None, axis=0, r=None):
    """Coinformation from data matrix.

    See Eq.11 in:
    Timme, Nicholas, et al.
    "Synergy, redundancy, and multivariate information measures:
    an experimentalist's perspective."
    Journal of computational neuroscience 36.2 (2014): 119-140.

    The coinformation reduces to the entropy for a single variable and to the
    mutual information for a pair of variables.

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    ks : 1D array of length p, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int or None, optional; 1<=r<=p.
        If passed, return a generator yielding estimates for the p-choose-r
        possible combinations of r variables.
        If r == 1, return the entropy for each variable. If r == 2 return the
        mutual information for each possible pair. If r > 2 return the
        coinformation for each possible subset of length r.
        Combinations are ordered as: list(itertools.combinations(range(p), r)).

    Returns
    -------
    float
        Coinformation estimate.

    """

    # change sign for odd number of variables
    return (-1)**ar.shape[0] * interaction_information(
        ar=ar, ks=ks, axis=axis, r=r)

def mutual_information(ar, ks=None, axis=0):
    """Mutual information from p-by-n data matrix.

    If p > 2, return an estimate of the mutual information for each possible
    pair of variables, ordered as list(itertools.combinations(range(p), r=2)).

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    ks : 1D p-dimensional array, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.

    Returns
    -------
    float
        Coinformation estimate.

    Raises
    ------
    CardinalityError
        If len(ks) != p.

    """
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    p = ar.shape[0]

    if p > 2:
        h1 = list(from_data(ar, r=1))
        return (h1[i1] + h1[i2] - from_data(ar[i1, i2])
                for i1, i2 in combinations(range(p), 2))

    return numpy.sum(from_data(ar, r=1)) - from_data(ar)

def conditional_entropy(ar, c, ks=None, axis=0, r=None):
    """
    Coditional entropy estimate from data matrix.

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    c : int or array-like
        The variables on which entropy is conditioned (as column indices).
    ks : 1D p-dimensional array, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int or None, optional; 1<=r<=p-len(c).
        If passed, return a generator yielding estimates for all possible
        combinations of r variables conditioning on the `c` variables.
        Indices are sorted as:
        list(x for x in collections.combinations(range(p), r=r+len(c))
             if set(c) <= set(x))

    Returns
    -------
    float
        Conditional entropy estimate

    """
    # check data shape
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    p = ar.shape[0]

    try:
        c = list(c)
    except TypeError:
        c = [c]
    if not set(c) <= set(range(p)):
        return EstimatorInputError('The indices of conditioning variables'
                                   ' are not valid')

    # EntropyEstimator objects are callable and return the fitted estimate
    estimator = NSB()

    # Entropy of features on which we are conditioning
    counts, k = ar[c].iter_counts()
    hc = estimator(counts, k=k)

    if r is not None:

        r = _check_r(r, p - len(c))

        # include the c variables in the set
        r = r + len(c)

        indices = combinations(range(p), r=r)

        return (estimator(counts, k=k) - hc
                for ids, (counts, k) in zip(indices, ar.iter_counts(r=r))
                if set(c) <= set(ids))

    counts, k = ar.iter_counts()
    return estimator(counts, k=k) - hc

def histogram(data, axis=0, r=None):
    """Compute an histogram from a data matrix. Wrapper to numpy.unique.

    Parameters
    ----------
    data : array-like, shape (p, n)
        A n-by-p array of n samples from p variables.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int, optional
        For r values in the interval [1, p],
        return a generator yielding bin counts for each of the p-choose-r
        combinations of r variables.

    Returns
    -------
    counts : ndarray
        Bin counts.

    """
    if not isinstance(data, DataArray):
        data = DataArray(data, axis=axis)

    if r is not None:
        r = _check_r(r, data)
        return (histogram(d) for d in combinations(data, r=r))

    # statistics for the p-dimensional variable
    _, counts = numpy.unique(data, return_counts=True, axis=1)
    return counts

def _check_r(r, ar):
    """
    Raises
    ------
    CombinationError
        For r values out of the interval [1, p].
    """
    if ar.shape:
        p = ar.shape[0]
    else:
        p = ar
    if r < 1 or r > p:
        raise CombinationError('r values must be in the interval [1, %s]' % p)
    return r

def iinfo(X, ks):
    """Helper function for interaction information definition.

    Ref: timme2014synergy
    """
    info = 0.0
    S = len(X)
    for T in range(1, S + 1):
        sgn = (-1)**(S - T)
        info += sgn * numpy.sum(from_data(X, ks=ks, r=T))
    return -info

def coinfo(X, ks):
    """Helper function for coinformation definition.

    Ref: timme2014synergy
    """
    info = 0.0
    S = len(X)
    for T in range(1, S + 1):
        sgn = (-1)**T
        info += sgn * numpy.sum(from_data(X, ks=ks, r=T))
    return -info

def is_pmf(a):
    """If a is a valid probability mass function."""
    a = numpy.float64(a)
    not_negative = numpy.all(a >= 0)
    normalized = numpy.isclose(sum(a), 1.0)
    return not_negative and normalized

def select_estimator(alpha, plugin):
    """Select the appropriate estimator."""
    if plugin:
        estimator = Plugin(alpha)
    else:
        if alpha is None:
            estimator = NSB()
        else:
            estimator = WolpertWolf(alpha)
    return estimator
<commit_msg>remove _check_r function<commit_after># -*- coding: utf-8 -*-
# Author: Simone Marsili <simomarsili@gmail.com>
# License: BSD 3 clause
"""
Functions for entropy and information measures estimation.
"""
import logging
from itertools import combinations

import numpy

from ndd.data import DataArray
from ndd.divergence import JSDivergence
from ndd.estimators import NSB, Plugin, WolpertWolf
from ndd.exceptions import EstimatorInputError, PmfError

__all__ = [
    'entropy',
    'from_data',
    'jensen_shannon_divergence',
    'kullback_leibler_divergence',
    'interaction_information',
    'coinformation',
    'mutual_information',
    'conditional_entropy',
    'histogram',
]

logger = logging.getLogger(__name__)

def entropy(pk, k=None, alpha=None, plugin=False, return_std=False):
    """
    Entropy estimate from an array of counts.

    Return a Bayesian estimate for the entropy of an unknown discrete
    distribution from an input array of counts pk.

    Parameters
    ----------
    pk : array-like
        The number of occurrences of a set of bins.
    k : int or array-like, optional
        Total number of bins (including unobserved bins); k >= len(pk).
        A float is a valid input for whole numbers (e.g. k=1.e3).
        If an array, set k = numpy.prod(k). Defaults to len(pk).
    alpha : float, optional
        If not None: Wolpert-Wolf entropy estimator (fixed alpha).
        Use a single Dirichlet prior with concentration parameter alpha.
        alpha > 0.0.
    plugin : boolean, optional
        If True, return a 'plugin' estimate of the entropy. The discrete
        distribution is estimated from the empirical frequencies over bins
        and inserted into the entropy definition (plugin estimator).
        If alpha is passed in combination with plugin=True, add
        alpha pseudocounts to each frequency count (pseudocount estimator).
    return_std : boolean, optional
        If True, also return an approximation for the standard deviation
        over the entropy posterior.

    Returns
    -------
    entropy : float
        Entropy estimate.
    std : float, optional
        Uncertainty in the entropy estimate. Only if `return_std` is True.

    """

    estimator = select_estimator(alpha=alpha, plugin=plugin)

    estimator = estimator.fit(pk, k=k)
    S, err = estimator.estimate_, estimator.err_

    if numpy.isnan(S):
        logger.warning('nan value for entropy estimate')
        S = numpy.nan

    if return_std:
        if err is not None and numpy.isnan(err):
            err = numpy.nan
            logger.warning('nan value for entropy posterior std deviation')
        return S, err

    return S

def from_data(ar, ks=None, axis=0, r=None):
    """
    Entropy estimate from data matrix.

    Paramaters
    ----------
    ar : array-like, shape (n, p)
        2D array of n samples from p discrete variables.
    ks : int or 1D array of length p, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int, optional; ; 1<=r<=p.
        If passed, return a generator yielding estimates for the p-choose-r
        possible combinations of r variables.

    Returns
    -------
    float
        Entropy estimate

    """
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    # EntropyEstimator objects are callable and return the fitted estimate
    estimator = NSB()

    if r is not None:
        return (estimator(pk, k=k) for pk, k in ar.iter_counts(r=r))

    counts, k = ar.iter_counts()
    return estimator(counts, k=k)

def jensen_shannon_divergence(pk, k=None, alpha=None, plugin=False):
    """
    Return the Jensen-Shannon divergence from a m-by-p matrix of counts.

    Return an estimate of the Jensen-Shannon divergence between
    m unknown discrete distributions from a m-by-p input array of counts.
    The estimate (in nats) is computed as a combination of single Bayesian
    entropy estimates. If the total number of samples varies among the
    distributions, the function returns a weighted divergence with weights
    proportional to the total number of samples in each row
    (see the general definition of Jensen-Shannon divergence:
    https://en.wikipedia.org/wiki/Jensen-Shannon_divergence).

    Parameters
    ----------

    pk : array-like, shape (m, p)
        Matrix of frequency counts. Each row corresponds to the number of
        occurrences of a set of bins from a different distribution.
    k : int or array-like, optional
        Total number of bins (including unobserved bins); k >= p.
        A float is a valid input for whole numbers (e.g. k=1.e3).
        If an array, set k = numpy.prod(k). Defaults to p.
    alpha : float, optional
        If not None: Wolpert-Wolf entropy estimator (fixed alpha).
        Use a single Dirichlet prior with concentration parameter alpha.
        alpha > 0.0.
    plugin : boolean, optional
        If True, use a 'plugin' estimator for the entropy.
        If alpha is passed in combination with plugin == True, add alpha
        pseudoconts to the frequency counts in the plugin estimate.

    Returns
    -------
    float
        Jensen-Shannon divergence.

    """

    entropy_estimator = select_estimator(alpha, plugin)
    estimator = JSDivergence(entropy_estimator).fit(pk, k=k)
    js = estimator.estimate_

    if numpy.isnan(js):
        logger.warning('nan value for JS divergence')
        js = numpy.nan

    return js

def kullback_leibler_divergence(pk, qk, k=None, alpha=None, plugin=False):
    """
    Kullback-Leibler divergence given counts pk and a reference PMF qk.

    Return an estimate of the Kullback-Leibler given an array of counts pk and
    a reference PMF qk. The estimate (in nats) is computed as:
    - S_p - sum(pk * log(qk)) / sum(pk)
    where S_p is the entropy estimate from counts pk.

    Parameters
    ----------
    pk : array_like
        The number of occurrences of a set of bins.
    qk : array_like
        Reference PMF in sum(pk log(pk/qk). len(qk) = len(pk).
        Must be a valid PMF (non-negative, normalized).
    k : int or array-like, optional
        Total number of bins (including unobserved bins); k >= p.
        A float is a valid input for whole numbers (e.g. k=1.e3).
        If an array, set k = numpy.prod(k). Defaults to len(pk).
    alpha : float, optional
        If not None: Wolpert-Wolf entropy estimator (fixed alpha).
        Use a single Dirichlet prior with concentration parameter alpha.
        alpha > 0.0.
    plugin : boolean, optional
        If True, use a 'plugin' estimator for the entropy.
        If alpha is passed in combination with plugin == True, add alpha
        pseudoconts to the frequency counts in the plugin estimate.

    Returns
    -------
    float
        Kullback-Leibler divergence.

    """

    if is_pmf(qk):
        log_qk = numpy.log(qk)
    else:
        raise PmfError('qk must be a valid PMF')

    if sum(numpy.isinf(log_qk)) > 0:
        raise PmfError('qk must be positive')

    if len(log_qk) != len(pk):
        raise PmfError('qk and pk must have the same length.')

    if k == 1:  # single bin
        return 0.0
    if k is None:
        k = len(pk)

    estimator = select_estimator(alpha, plugin)
    estimate = estimator.fit(pk, k=k).estimate_
    kl = -(estimate + numpy.sum(pk * log_qk) / float(sum(pk)))
    if numpy.isnan(kl):
        logger.warning('nan value for KL divergence')
        kl = numpy.nan

    return kl

def interaction_information(ar, ks=None, axis=0, r=None):
    """Interaction information from data matrix.

    See Eq.10 in:
    Timme, Nicholas, et al.
    "Synergy, redundancy, and multivariate information measures:
    an experimentalist's perspective."
    Journal of computational neuroscience 36.2 (2014): 119-140.

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    ks : 1D array of length p, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int, optional; 1<=r<=p.
        If passed, return a generator yielding estimates for the p-choose-r
        possible combinations of r variables.
        Combinations are ordered as: list(itertools.combinations(range(p), r)).

    Returns
    -------
    float
        Interaction information estimate.

    Raises
    ------
    CardinalityError
        If len(ks) != p.

    """
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    if r is not None:
        return (iinfo(data, k) for data, k in ar.iter_data(r=r))

    data, k = ar.iter_data()
    return iinfo(data, k)

def coinformation(ar, ks=None, axis=0, r=None):
    """Coinformation from data matrix.

    See Eq.11 in:
    Timme, Nicholas, et al.
    "Synergy, redundancy, and multivariate information measures:
    an experimentalist's perspective."
    Journal of computational neuroscience 36.2 (2014): 119-140.

    The coinformation reduces to the entropy for a single variable and to the
    mutual information for a pair of variables.

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    ks : 1D array of length p, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int or None, optional; 1<=r<=p.
        If passed, return a generator yielding estimates for the p-choose-r
        possible combinations of r variables.
        If r == 1, return the entropy for each variable. If r == 2 return the
        mutual information for each possible pair. If r > 2 return the
        coinformation for each possible subset of length r.
        Combinations are ordered as: list(itertools.combinations(range(p), r)).

    Returns
    -------
    float
        Coinformation estimate.

    """

    # change sign for odd number of variables
    return (-1)**ar.shape[0] * interaction_information(
        ar=ar, ks=ks, axis=axis, r=r)

def mutual_information(ar, ks=None, axis=0):
    """Mutual information from p-by-n data matrix.

    If p > 2, return an estimate of the mutual information for each possible
    pair of variables, ordered as list(itertools.combinations(range(p), r=2)).

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    ks : 1D p-dimensional array, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.

    Returns
    -------
    float
        Coinformation estimate.

    Raises
    ------
    CardinalityError
        If len(ks) != p.

    """
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    p = ar.shape[0]

    if p > 2:
        h1 = list(from_data(ar, r=1))
        return (h1[i1] + h1[i2] - from_data(ar[i1, i2])
                for i1, i2 in combinations(range(p), 2))

    return numpy.sum(from_data(ar, r=1)) - from_data(ar)

def conditional_entropy(ar, c, ks=None, axis=0, r=None):
    """
    Coditional entropy estimate from data matrix.

    Paramaters
    ----------
    ar : array-like
        n-by-p array of n samples from p discrete variables.
    c : int or array-like
        The variables on which entropy is conditioned (as column indices).
    ks : 1D p-dimensional array, optional
        Alphabet size for each variable.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int or None, optional; 1<=r<=p-len(c).
        If passed, return a generator yielding estimates for all possible
        combinations of r variables conditioning on the `c` variables.
        Indices are sorted as:
        list(x for x in collections.combinations(range(p), r=r+len(c))
             if set(c) <= set(x))

    Returns
    -------
    float
        Conditional entropy estimate

    """
    # check data shape
    if not isinstance(ar, DataArray):
        ar = DataArray(ar, k=ks, axis=axis)

    p = ar.shape[0]

    try:
        c = list(c)
    except TypeError:
        c = [c]
    if not set(c) <= set(range(p)):
        return EstimatorInputError('The indices of conditioning variables'
                                   ' are not valid')

    # EntropyEstimator objects are callable and return the fitted estimate
    estimator = NSB()

    # Entropy of features on which we are conditioning
    counts, k = ar[c].iter_counts()
    hc = estimator(counts, k=k)

    if r is not None:

        # r should be >= p - len(c)

        # include the c variables in the set
        r = r + len(c)

        indices = combinations(range(p), r=r)

        return (estimator(counts, k=k) - hc
                for ids, (counts, k) in zip(indices, ar.iter_counts(r=r))
                if set(c) <= set(ids))

    counts, k = ar.iter_counts()
    return estimator(counts, k=k) - hc

def histogram(data, axis=0, r=None):
    """Compute an histogram from a data matrix. Wrapper to numpy.unique.

    Parameters
    ----------
    data : array-like, shape (p, n)
        A n-by-p array of n samples from p variables.
    axis : int, optional
        The sample-indexing axis. Defaults to 0.
    r : int, optional
        For r values in the interval [1, p],
        return a generator yielding bin counts for each of the p-choose-r
        combinations of r variables.

    Returns
    -------
    counts : ndarray
        Bin counts.

    """
    if not isinstance(data, DataArray):
        data = DataArray(data, axis=axis)

    if r is not None:
        return (histogram(d) for d in combinations(data, r=r))

    # statistics for the p-dimensional variable
    _, counts = numpy.unique(data, return_counts=True, axis=1)
    return counts

def iinfo(X, ks):
    """Helper function for interaction information definition.

    Ref: timme2014synergy
    """
    info = 0.0
    S = len(X)
    for T in range(1, S + 1):
        sgn = (-1)**(S - T)
        info += sgn * numpy.sum(from_data(X, ks=ks, r=T))
    return -info

def coinfo(X, ks):
    """Helper function for coinformation definition.

    Ref: timme2014synergy
    """
    info = 0.0
    S = len(X)
    for T in range(1, S + 1):
        sgn = (-1)**T
        info += sgn * numpy.sum(from_data(X, ks=ks, r=T))
    return -info

def is_pmf(a):
    """If a is a valid probability mass function."""
    a = numpy.float64(a)
    not_negative = numpy.all(a >= 0)
    normalized = numpy.isclose(sum(a), 1.0)
    return not_negative and normalized

def select_estimator(alpha, plugin):
    """Select the appropriate estimator."""
    if plugin:
        estimator = Plugin(alpha)
    else:
        if alpha is None:
            estimator = NSB()
        else:
            estimator = WolpertWolf(alpha)
    return estimator
<|endoftext|>


<commit_before>#!/usr/bin/python3
import logging
from gi.repository import Gst
from enum import Enum

from lib.config import Config

class CompositeModes(Enum):
	fullscreen = 0
	side_by_side_equal = 1
	side_by_side_preview = 2
	picture_in_picture = 3

class PadState(object):
	noScaleCaps = Gst.Caps.from_string('video/x-raw')

	def __init__(self):
		self.reset()

	def reset(self):
		self.alpha = 1.0
		self.xpos = 0
		self.ypos = 0
		self.zorder = 1
		self.scaleCaps = PadState.noScaleCaps

class VideoMix(object):
	log = logging.getLogger('VideoMix')

	def __init__(self):
		self.caps = Config.get('mix', 'videocaps')

		self.names = Config.getlist('mix', 'sources')
		self.log.info('Configuring Mixer for %u Sources', len(self.names))

		pipeline = """
			videomixer name=mix !
			{caps} !
			identity name=sig !
			queue !
			tee name=tee

			intervideosrc channel=video_background !
			{caps} !
			mix.

			tee. ! queue ! intervideosink channel=video_mix_out
		""".format(
			caps=self.caps
		)

		if Config.getboolean('previews', 'enabled'):
			pipeline += """
				tee. ! queue ! intervideosink channel=video_mix_preview
			"""

		for idx, name in enumerate(self.names):
			pipeline += """
				intervideosrc channel=video_{name}_mixer !
				{caps} !
				videoscale !
				capsfilter name=caps_{idx} !
				mix.
			""".format(
				name=name,
				caps=self.caps,
				idx=idx
			)

		self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline)
		self.mixingPipeline = Gst.parse_launch(pipeline)

		self.log.debug('Binding Error & End-of-Stream-Signal on Mixing-Pipeline')
		self.mixingPipeline.bus.add_signal_watch()
		self.mixingPipeline.bus.connect("message::eos", self.on_eos)
		self.mixingPipeline.bus.connect("message::error", self.on_error)

		self.log.debug('Binding Handoff-Handler for Synchronus mixer manipulation')
		sig = self.mixingPipeline.get_by_name('sig')
		sig.connect('handoff', self.on_handoff)

		self.padStateDirty = False
		self.padState = list()
		for idx, name in enumerate(self.names):
			self.padState.append(PadState())

		self.log.debug('Initializing Mixer-State')
		self.compositeMode = CompositeModes.fullscreen
		self.sourceA = 0
		self.sourceB = 1
		self.recalculateMixerState()
		self.applyMixerState()

		bgMixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_0')
		bgMixerpad.set_property('zorder', 0)

		self.log.debug('Launching Mixing-Pipeline')
		self.mixingPipeline.set_state(Gst.State.PLAYING)

	def getInputVideoSize(self):
		caps = Gst.Caps.from_string(self.caps)
		struct = caps.get_structure(0)
		_, width = struct.get_int('width')
		_, height = struct.get_int('height')

		return width, height

	def recalculateMixerState(self):
		if self.compositeMode == CompositeModes.fullscreen:
			self.recalculateMixerStateFullscreen()

		elif self.compositeMode == CompositeModes.side_by_side_equal:
			self.recalculateMixerStateSideBySideEqual()

		elif self.compositeMode == CompositeModes.side_by_side_preview:
			self.recalculateMixerStateSideBySidePreview()

		elif self.compositeMode == CompositeModes.picture_in_picture:
			self.recalculateMixerStatePictureInPicture()

		self.log.debug('Marking Pad-State as Dirty')
		self.padStateDirty = True

	def recalculateMixerStateFullscreen(self):
		self.log.info('Updating Mixer-State for Fullscreen-Composition')

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]

			pad.reset()
			pad.alpha = float(idx == self.sourceA)

	def recalculateMixerStateSideBySideEqual(self):
		self.log.info('Updating Mixer-State for Side-by-side-Equal-Composition')

		width, height = self.getInputVideoSize()
		self.log.debug('Video-Size parsed as %ux%u', width, height)

		try:
			gutter = Config.getint('side-by-side-equal', 'gutter')
			self.log.debug('Gutter configured to %u', gutter)
		except:
			gutter = int(width / 100)
			self.log.debug('Gutter calculated to %u', gutter)

		targetWidth = int((width - gutter) / 2)
		targetHeight = int(targetWidth / width * height)

		try:
			y = Config.getint('side-by-side-equal', 'ypos')
			self.log.debug('Y-Pos configured to %u', y)
		except:
			y = (height - targetHeight) / 2
			self.log.debug('Y-Pos calculated to %u', y)

		xa = 0
		xb = width - targetWidth

		scaleCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % (targetWidth, targetHeight))

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]
			pad.reset()

			if idx == self.sourceA:
				pad.xpos = xa
				pad.ypos = y
				pad.zorder = 1
				pad.scaleCaps = scaleCaps

			elif idx == self.sourceB:
				pad.xpos = xb
				pad.ypos = y
				pad.zorder = 2
				pad.scaleCaps = scaleCaps

			else:
				pad.alpha = 0

	def recalculateMixerStateSideBySidePreview(self):
		self.log.info('Updating Mixer-State for Side-by-side-Preview-Composition')

		width, height = self.getInputVideoSize()
		self.log.debug('Video-Size parsed as %ux%u', width, height)

		try:
			asize = [int(i) for i in Config.get('side-by-side-preview', 'asize').split('x', 1)]
			self.log.debug('A-Video-Size configured to %ux%u', asize[0], asize[1])
		except:
			asize = [
				int(width / 1.25), # 80%
				int(height / 1.25) # 80%
			]
			self.log.debug('A-Video-Size calculated to %ux%u', asize[0], asize[1])

		try:
			apos = [int(i) for i in Config.get('side-by-side-preview', 'apos').split('/', 1)]
			self.log.debug('B-Video-Position configured to %u/%u', apos[0], apos[1])
		except:
			apos = [
				int(width / 100), # 1%
				int(width / 100)  # 1%
			]
			self.log.debug('B-Video-Position calculated to %u/%u', apos[0], apos[1])

		try:
			bsize = [int(i) for i in Config.get('side-by-side-preview', 'bsize').split('x', 1)]
			self.log.debug('B-Video-Size configured to %ux%u', bsize[0], bsize[1])
		except:
			bsize = [
				int(width / 4), # 25%
				int(height / 4) # 25%
			]
			self.log.debug('B-Video-Size calculated to %ux%u', bsize[0], bsize[1])

		try:
			bpos = [int(i) for i in Config.get('side-by-side-preview', 'bpos').split('/', 1)]
			self.log.debug('B-Video-Position configured to %u/%u', bpos[0], bpos[1])
		except:
			bpos = [
				width - int(width / 100) - bsize[0],
				height - int(width / 100) - bsize[1]  # 1%
			]
			self.log.debug('B-Video-Position calculated to %u/%u', bpos[0], bpos[1])

		aCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(asize))
		bCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(bsize))

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]
			pad.reset()

			if idx == self.sourceA:
				pad.xpos, pad.ypos = apos
				pad.zorder = 1
				pad.scaleCaps = aCaps

			elif idx == self.sourceB:
				pad.xpos, pad.ypos = bpos
				pad.zorder = 2
				pad.scaleCaps = bCaps

			else:
				pad.alpha = 0

	def recalculateMixerStatePictureInPicture(self):
		self.log.info('Updating Mixer-State for Picture-in-Picture-Composition')

		width, height = self.getInputVideoSize()
		self.log.debug('Video-Size parsed as %ux%u', width, height)

		try:
			pipsize = [int(i) for i in Config.get('picture-in-picture', 'pipsize').split('x', 1)]
			self.log.debug('PIP-Size configured to %ux%u', pipsize[0], pipsize[1])
		except:
			pipsize = [
				int(width / 4), # 25%
				int(height / 4) # 25%
			]
			self.log.debug('PIP-Size calculated to %ux%u', pipsize[0], pipsize[1])

		try:
			pippos = [int(i) for i in Config.get('picture-in-picture', 'pippos').split('/', 1)]
			self.log.debug('PIP-Position configured to %u/%u', pippos[0], pippos[1])
		except:
			pippos = [
				width - pipsize[0] - int(width / 100), # 1%
				height - pipsize[1] -int(width / 100)  # 1%
			]
			self.log.debug('PIP-Position calculated to %u/%u', pippos[0], pippos[1])

		scaleCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(pipsize))
		noScaleCaps = Gst.Caps.from_string('video/x-raw')

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]
			pad.reset()

			if idx == self.sourceA:
				pass
			elif idx == self.sourceB:
				pad.xpos, pad.ypos = pippos
				pad.zorder = 2
				pad.scaleCaps = scaleCaps

			else:
				pad.alpha = 0

	def getMixerpadAndCapsfilter(self, idx):
		# mixerpad 0 = background
		mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_%u' % (idx+1))
		capsfilter = self.mixingPipeline.get_by_name('caps_%u' % idx)
		return mixerpad, capsfilter

	def applyMixerState(self):
		for idx, state in enumerate(self.padState):
			mixerpad, capsfilter = self.getMixerpadAndCapsfilter(idx)
			self.log.debug('Reconfiguring Mixerpad %u to x/y=%u/%u, alpha=%0.2f, zorder=%u', idx, state.xpos, state.ypos, state.alpha, state.zorder)
			self.log.debug('Reconfiguring Capsfilter %u to %s', idx, state.scaleCaps.to_string())
			mixerpad.set_property('xpos', state.xpos)
			mixerpad.set_property('ypos', state.ypos)
			mixerpad.set_property('alpha', state.alpha)
			mixerpad.set_property('zorder', state.zorder)
			capsfilter.set_property('caps', state.scaleCaps)

	def on_handoff(self, object, buffer):
		if self.padStateDirty:
			self.padStateDirty = False
			self.log.debug('[Streaming-Thread]: Pad-State is Dirty, applying new Mixer-State')
			self.applyMixerState()

	def on_eos(self, bus, message):
		self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline')

	def on_error(self, bus, message):
		self.log.debug('Received Error-Signal on Mixing-Pipeline')
		(error, debug) = message.parse_error()
		self.log.debug('Error-Details: #%u: %s', error.code, debug)

	def setVideoSourceA(self, source):
		# swap if required
		if self.sourceB == source:
			self.sourceB = self.sourceA

		self.sourceA = source
		self.recalculateMixerState()

	def setVideoSourceB(self, source):
		# swap if required
		if self.sourceA == source:
			self.sourceA = self.sourceB

		self.sourceB = source
		self.recalculateMixerState()

	def setCompositeMode(self, mode):
		self.compositeMode = mode
		self.recalculateMixerState()
<commit_msg>use compositor, the new videomixer!<commit_after>#!/usr/bin/python3
import logging
from gi.repository import Gst
from enum import Enum

from lib.config import Config

class CompositeModes(Enum):
	fullscreen = 0
	side_by_side_equal = 1
	side_by_side_preview = 2
	picture_in_picture = 3

class PadState(object):
	noScaleCaps = Gst.Caps.from_string('video/x-raw')

	def __init__(self):
		self.reset()

	def reset(self):
		self.alpha = 1.0
		self.xpos = 0
		self.ypos = 0
		self.zorder = 1
		self.scaleCaps = PadState.noScaleCaps

class VideoMix(object):
	log = logging.getLogger('VideoMix')

	def __init__(self):
		self.caps = Config.get('mix', 'videocaps')

		self.names = Config.getlist('mix', 'sources')
		self.log.info('Configuring Mixer for %u Sources', len(self.names))

		pipeline = """
			compositor name=mix !
			{caps} !
			identity name=sig !
			queue !
			tee name=tee

			intervideosrc channel=video_background !
			{caps} !
			mix.

			tee. ! queue ! intervideosink channel=video_mix_out
		""".format(
			caps=self.caps
		)

		if Config.getboolean('previews', 'enabled'):
			pipeline += """
				tee. ! queue ! intervideosink channel=video_mix_preview
			"""

		for idx, name in enumerate(self.names):
			pipeline += """
				intervideosrc channel=video_{name}_mixer !
				{caps} !
				videoscale !
				capsfilter name=caps_{idx} !
				mix.
			""".format(
				name=name,
				caps=self.caps,
				idx=idx
			)

		self.log.debug('Creating Mixing-Pipeline:\n%s', pipeline)
		self.mixingPipeline = Gst.parse_launch(pipeline)

		self.log.debug('Binding Error & End-of-Stream-Signal on Mixing-Pipeline')
		self.mixingPipeline.bus.add_signal_watch()
		self.mixingPipeline.bus.connect("message::eos", self.on_eos)
		self.mixingPipeline.bus.connect("message::error", self.on_error)

		self.log.debug('Binding Handoff-Handler for Synchronus mixer manipulation')
		sig = self.mixingPipeline.get_by_name('sig')
		sig.connect('handoff', self.on_handoff)

		self.padStateDirty = False
		self.padState = list()
		for idx, name in enumerate(self.names):
			self.padState.append(PadState())

		self.log.debug('Initializing Mixer-State')
		self.compositeMode = CompositeModes.fullscreen
		self.sourceA = 0
		self.sourceB = 1
		self.recalculateMixerState()
		self.applyMixerState()

		bgMixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_0')
		bgMixerpad.set_property('zorder', 0)

		self.log.debug('Launching Mixing-Pipeline')
		self.mixingPipeline.set_state(Gst.State.PLAYING)

	def getInputVideoSize(self):
		caps = Gst.Caps.from_string(self.caps)
		struct = caps.get_structure(0)
		_, width = struct.get_int('width')
		_, height = struct.get_int('height')

		return width, height

	def recalculateMixerState(self):
		if self.compositeMode == CompositeModes.fullscreen:
			self.recalculateMixerStateFullscreen()

		elif self.compositeMode == CompositeModes.side_by_side_equal:
			self.recalculateMixerStateSideBySideEqual()

		elif self.compositeMode == CompositeModes.side_by_side_preview:
			self.recalculateMixerStateSideBySidePreview()

		elif self.compositeMode == CompositeModes.picture_in_picture:
			self.recalculateMixerStatePictureInPicture()

		self.log.debug('Marking Pad-State as Dirty')
		self.padStateDirty = True

	def recalculateMixerStateFullscreen(self):
		self.log.info('Updating Mixer-State for Fullscreen-Composition')

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]

			pad.reset()
			pad.alpha = float(idx == self.sourceA)

	def recalculateMixerStateSideBySideEqual(self):
		self.log.info('Updating Mixer-State for Side-by-side-Equal-Composition')

		width, height = self.getInputVideoSize()
		self.log.debug('Video-Size parsed as %ux%u', width, height)

		try:
			gutter = Config.getint('side-by-side-equal', 'gutter')
			self.log.debug('Gutter configured to %u', gutter)
		except:
			gutter = int(width / 100)
			self.log.debug('Gutter calculated to %u', gutter)

		targetWidth = int((width - gutter) / 2)
		targetHeight = int(targetWidth / width * height)

		try:
			y = Config.getint('side-by-side-equal', 'ypos')
			self.log.debug('Y-Pos configured to %u', y)
		except:
			y = (height - targetHeight) / 2
			self.log.debug('Y-Pos calculated to %u', y)

		xa = 0
		xb = width - targetWidth

		scaleCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % (targetWidth, targetHeight))

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]
			pad.reset()

			if idx == self.sourceA:
				pad.xpos = xa
				pad.ypos = y
				pad.zorder = 1
				pad.scaleCaps = scaleCaps

			elif idx == self.sourceB:
				pad.xpos = xb
				pad.ypos = y
				pad.zorder = 2
				pad.scaleCaps = scaleCaps

			else:
				pad.alpha = 0

	def recalculateMixerStateSideBySidePreview(self):
		self.log.info('Updating Mixer-State for Side-by-side-Preview-Composition')

		width, height = self.getInputVideoSize()
		self.log.debug('Video-Size parsed as %ux%u', width, height)

		try:
			asize = [int(i) for i in Config.get('side-by-side-preview', 'asize').split('x', 1)]
			self.log.debug('A-Video-Size configured to %ux%u', asize[0], asize[1])
		except:
			asize = [
				int(width / 1.25), # 80%
				int(height / 1.25) # 80%
			]
			self.log.debug('A-Video-Size calculated to %ux%u', asize[0], asize[1])

		try:
			apos = [int(i) for i in Config.get('side-by-side-preview', 'apos').split('/', 1)]
			self.log.debug('B-Video-Position configured to %u/%u', apos[0], apos[1])
		except:
			apos = [
				int(width / 100), # 1%
				int(width / 100)  # 1%
			]
			self.log.debug('B-Video-Position calculated to %u/%u', apos[0], apos[1])

		try:
			bsize = [int(i) for i in Config.get('side-by-side-preview', 'bsize').split('x', 1)]
			self.log.debug('B-Video-Size configured to %ux%u', bsize[0], bsize[1])
		except:
			bsize = [
				int(width / 4), # 25%
				int(height / 4) # 25%
			]
			self.log.debug('B-Video-Size calculated to %ux%u', bsize[0], bsize[1])

		try:
			bpos = [int(i) for i in Config.get('side-by-side-preview', 'bpos').split('/', 1)]
			self.log.debug('B-Video-Position configured to %u/%u', bpos[0], bpos[1])
		except:
			bpos = [
				width - int(width / 100) - bsize[0],
				height - int(width / 100) - bsize[1]  # 1%
			]
			self.log.debug('B-Video-Position calculated to %u/%u', bpos[0], bpos[1])

		aCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(asize))
		bCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(bsize))

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]
			pad.reset()

			if idx == self.sourceA:
				pad.xpos, pad.ypos = apos
				pad.zorder = 1
				pad.scaleCaps = aCaps

			elif idx == self.sourceB:
				pad.xpos, pad.ypos = bpos
				pad.zorder = 2
				pad.scaleCaps = bCaps

			else:
				pad.alpha = 0

	def recalculateMixerStatePictureInPicture(self):
		self.log.info('Updating Mixer-State for Picture-in-Picture-Composition')

		width, height = self.getInputVideoSize()
		self.log.debug('Video-Size parsed as %ux%u', width, height)

		try:
			pipsize = [int(i) for i in Config.get('picture-in-picture', 'pipsize').split('x', 1)]
			self.log.debug('PIP-Size configured to %ux%u', pipsize[0], pipsize[1])
		except:
			pipsize = [
				int(width / 4), # 25%
				int(height / 4) # 25%
			]
			self.log.debug('PIP-Size calculated to %ux%u', pipsize[0], pipsize[1])

		try:
			pippos = [int(i) for i in Config.get('picture-in-picture', 'pippos').split('/', 1)]
			self.log.debug('PIP-Position configured to %u/%u', pippos[0], pippos[1])
		except:
			pippos = [
				width - pipsize[0] - int(width / 100), # 1%
				height - pipsize[1] -int(width / 100)  # 1%
			]
			self.log.debug('PIP-Position calculated to %u/%u', pippos[0], pippos[1])

		scaleCaps = Gst.Caps.from_string('video/x-raw,width=%u,height=%u' % tuple(pipsize))
		noScaleCaps = Gst.Caps.from_string('video/x-raw')

		for idx, name in enumerate(self.names):
			pad = self.padState[idx]
			pad.reset()

			if idx == self.sourceA:
				pass
			elif idx == self.sourceB:
				pad.xpos, pad.ypos = pippos
				pad.zorder = 2
				pad.scaleCaps = scaleCaps

			else:
				pad.alpha = 0

	def getMixerpadAndCapsfilter(self, idx):
		# mixerpad 0 = background
		mixerpad = self.mixingPipeline.get_by_name('mix').get_static_pad('sink_%u' % (idx+1))
		capsfilter = self.mixingPipeline.get_by_name('caps_%u' % idx)
		return mixerpad, capsfilter

	def applyMixerState(self):
		for idx, state in enumerate(self.padState):
			mixerpad, capsfilter = self.getMixerpadAndCapsfilter(idx)
			self.log.debug('Reconfiguring Mixerpad %u to x/y=%u/%u, alpha=%0.2f, zorder=%u', idx, state.xpos, state.ypos, state.alpha, state.zorder)
			self.log.debug('Reconfiguring Capsfilter %u to %s', idx, state.scaleCaps.to_string())
			mixerpad.set_property('xpos', state.xpos)
			mixerpad.set_property('ypos', state.ypos)
			mixerpad.set_property('alpha', state.alpha)
			mixerpad.set_property('zorder', state.zorder)
			capsfilter.set_property('caps', state.scaleCaps)

	def on_handoff(self, object, buffer):
		if self.padStateDirty:
			self.padStateDirty = False
			self.log.debug('[Streaming-Thread]: Pad-State is Dirty, applying new Mixer-State')
			self.applyMixerState()

	def on_eos(self, bus, message):
		self.log.debug('Received End-of-Stream-Signal on Mixing-Pipeline')

	def on_error(self, bus, message):
		self.log.debug('Received Error-Signal on Mixing-Pipeline')
		(error, debug) = message.parse_error()
		self.log.debug('Error-Details: #%u: %s', error.code, debug)

	def setVideoSourceA(self, source):
		# swap if required
		if self.sourceB == source:
			self.sourceB = self.sourceA

		self.sourceA = source
		self.recalculateMixerState()

	def setVideoSourceB(self, source):
		# swap if required
		if self.sourceA == source:
			self.sourceA = self.sourceB

		self.sourceB = source
		self.recalculateMixerState()

	def setCompositeMode(self, mode):
		self.compositeMode = mode
		self.recalculateMixerState()
<|endoftext|>


<commit_before>#!/usr/bin/env python3

class AutoMKVChapters:
    class Template:
        def __init__(self):
            from random import randint
            self.uid = randint(10**4,10**6)
            self.num_editions = 1
            self.lang = ['eng']
            self.country = ['us']
            self.fps = '30'
            self.ofps = '24'
            self.qpf = '0'
            self.trims = None
            self.kframes = None

        def toxml(self,chapfile):

            chf = open(chapfile+'.xml','w',encoding='utf-8')
            head = '<?xml version="1.0" encoding="UTF-8"?>\n<!-- <!DOCTYPE Tags SYSTEM "matroskatags.dtd"> -->\n'
            chf.write(head+'<Chapters>\n')

            if self.num_editions > 1:
                tagf = open(chapfile+'tags.xml','w')
                tagf.write(head+'<Tags>\n')
            else:
                tagf = False

            for ed in self.editions:
                chf.write('\t<EditionEntry>\n')
                chf.write('\t\t<EditionFlagHidden>{0:d}</EditionFlagHidden>\n'.format(ed.hidden) if ed.hidden else '')
                chf.write('\t\t<EditionFlagDefault>{0:d}</EditionFlagDefault>\n'.format(ed.default) if ed.default else '')
                chf.write('\t\t<EditionFlagOrdered>{0:d}</EditionFlagOrdered>\n'.format(ed.ordered) if ed.ordered else '')
                chf.write('\t\t<EditionUID>{0:d}</EditionUID>\n'.format(ed.uid))

                if tagf:
                    tagf.write('\t<Tag>\n\t\t<Targets>\n')
                    tagf.write('\t\t\t<EditionUID>{0:d}</EditionUID>\n'.format(ed.uid))
                    tagf.write('\t\t\t<TargetTypeValue>50</TargetTypeValue>\n\t\t</Targets>\n')
                    num_names = len(ed.name) if len(ed.name) < len(self.lang) else len(self.lang)
                    for i in range(num_names):
                        tagf.write('\t\t<Simple>\n\t\t\t<Name>TITLE</Name>\n')
                        tagf.write('\t\t\t<String>{0}</String>\n'.format(ed.name[i] if ed.name[i] != '' else ed.name[i-1]))
                        tagf.write('\t\t\t<TagLanguage>{0}</TagLanguage>\n'.format(self.lang[i]))
                        tagf.write('\t\t\t<DefaultLanguage>{0:d}</DefaultLanguage>\n'.format(1 if i == 0 else 0))
                        tagf.write('\t\t</Simple>\n')
                    tagf.write('\t</Tag>\n')

                for ch in ed.chapters:
                    chf.write('\t\t<ChapterAtom>\n')
                    num_names = len(ch.name) if len(ch.name) < len(self.lang) else len(self.lang)
                    for i in range(num_names):
                        chf.write('\t\t\t<ChapterDisplay>\n')
                        chf.write('\t\t\t\t<ChapterString>{0}</ChapterString>\n'.format(ch.name[i] if ch.name[i] != '' else ch.name[i-1]))
                        chf.write('\t\t\t\t<ChapterLanguage>{0}</ChapterLanguage>\n'.format(self.lang[i]) if self.lang[i] != 'eng' else '')
                        chf.write('\t\t\t\t<ChapterCountry>{0}</ChapterCountry>\n'.format(self.country[i]) if i < len(self.country) else '')
                        chf.write('\t\t\t</ChapterDisplay>\n')
                    chf.write('\t\t\t<ChapterUID>{0:d}</ChapterUID>\n'.format(ch.uid))
                    chf.write('\t\t\t<ChapterTimeStart>{0}</ChapterTimeStart>\n'.format(ch.start))
                    chf.write('\t\t\t<ChapterTimeEnd>{0}</ChapterTimeEnd>\n'.format(ch.end) if ch.end else '')
                    chf.write('\t\t\t<ChapterFlagHidden>{0:d}</ChapterFlagHidden>\n'.format(ch.hidden) if ch.hidden != 0 else '')
                    chf.write('\t\t\t<ChapterFlagEnabled>{0:d}</ChapterFlagEnabled>\n'.format(ch.enabled) if ch.enabled != 1 else '')
                    chf.write('\t\t\t<ChapterSegmentUID format="hex">{0}</ChapterSegmentUID>\n'.format(ch.suid) if ch.suid else '')
                    chf.write('\t\t</ChapterAtom>\n')

                chf.write('\t</EditionEntry>\n')

            chf.write('</Chapters>\n')

            if tagf:
                tagf.write('</Tags>\n')
                tagf.close()

            if self.qpf != '0' and self.kframes:
                from vfr import write_qpfile
                if self.qpf != '1':
                    qpfile = self.qpf
                else:
                    qpfile = chapfile+'.qpfile'
                write_qpfile(qpfile,self.kframes)

        def connect_with_vfr(self,avs,label=None):
            """
            Connects templates.py with vfr.py, enabling its use outside of vfr.py.

            Uses the same quirks as AMkvC but only for 24 and 30 fps.
            Ex: inputfps=30 is understood as being '30*1000/1001'

            """

            from vfr import parse_trims, fmt_time

            # compensate for amkvc's fps assumption
            if self.fps in ('24','30'):
                fps = self.fps + '/1.001'
            else:
                self.fps = str(fps)
            if self.ofps and self.ofps in ('24','30'):
                ofps = self.ofps + '/1.001'
            else:
                ofps = str(self.ofps)

            Trims2, Trims2ts = parse_trims(avs, fps, ofps, label=label)[2:4]
            Trims2ts = [(fmt_time(i[0]),fmt_time(i[1])) for i in Trims2ts]

            self.trims = Trims2ts
            self.kframes = Trims2

        class Edition:
            def __init__(self):
                self.default = 0
                self.name = 'Default'
                self.hidden = 0
                self.ordered = 0
                self.num_chapters = 1
                self.uid = 0

        class Chapter:
            def __init__(self):
                self.name = 'Chapter'
                self.chapter = False
                self.start = False
                self.end = False
                self.suid = False
                self.hidden = 0
                self.uid = 0
                self.enabled = 1

    def __init__(self, templatefile, output=None, avs=None, trims=None, kframes=None, uid=None, label=None, ifps=None):
        import configparser

        # Init config
        config = configparser.ConfigParser()
        template = open(templatefile,encoding='utf-8')

        # Read template
        config.readfp(template)
        template.close()

        # Template defaults
        self = self.Template()
        self.editions = []
        self.uid = uid if uid else self.uid

        # Set mkvinfo path
        from vfr import mkvmerge
        from os.path import dirname, join
        mkvinfo_path = join(dirname(mkvmerge), 'mkvinfo')

        # Set placeholder for mkvinfo output
        mkv_globbed = False
        mkvinfo = {}

        for k, v in config.items('info'):
            if k == 'lang':
                self.lang = v.split(',')
            elif k == 'country':
                self.country = v.split(',')
            elif k == 'inputfps':
                self.fps = v
            elif k == 'outputfps':
                self.ofps = v
            elif k == 'createqpfile':
                self.qpf = v
            elif k == 'uid':
                self.uid = int(v)
            elif k == 'editions':
                self.num_editions = int(v)

        if avs and not ifps:
            self.connect_with_vfr(avs, label)
        elif trims:
            self.trims = trims
            self.kframes = kframes
        else:
            self.trims = False

        for i in range(self.num_editions):
            from re import compile
            ed = self.Edition()
            ed.uid = self.uid * 100
            self.uid += 1
            cuid = ed.uid
            ed.num = i+1
            ed.chapters = []
            stuff = {}

            for k, v in config.items('edition{0:d}'.format(ed.num)):
                if k == 'default':
                    ed.default = int(v)
                elif k == 'name':
                    ed.name = v.split(',')
                elif k == 'ordered':
                    ed.ordered = int(v)
                elif k == 'hidden':
                    ed.hidden = int(v)
                elif k == 'chapters':
                    ed.num_chapters = int(v)
                    for i in range(ed.num_chapters):
                        stuff[i+1] = []
                elif k == 'uid':
                    ed.uid = int(v)
                else:
                    opt_re = compile('(\d+)(\w+)')
                    ret = opt_re.search(k)
                    if ret:
                        stuff[int(ret.group(1))].append((ret.group(2),v))

            for j in range(ed.num_chapters):
                ch = self.Chapter()
                cuid += 1
                ch.uid = cuid
                ch.num = j+1

                for k, v in stuff[j+1]:
                    if k == 'name':
                        ch.name = v.split(',')
                    elif k == 'chapter':
                        ch.chapter = int(v)
                    elif k == 'start':
                        ch.start = v
                    elif k == 'end':
                        ch.end = v
                    elif k == 'suid':
                        ch.suid = v.strip() if ret else 0
                    elif k == 'hidden':
                        ch.hidden = int(v)
                    elif k == 'enabled':
                        ch.enabled = int(v)

                from os.path import isfile
                if ch.suid and not isfile(ch.suid):
                    ch.suid = ch.suid.replace('0x','').lower().replace(' ','')

                if ch.chapter and not (ch.start and ch.end):
                    ch.start, ch.end = self.trims[ch.chapter-1] if self.trims else (ch.start, ch.end)
                elif ch.suid:
                    from subprocess import check_output

                    suid_re = compile('^\| \+ Segment UID:(.*)(?m)')
                    duration_re = compile('^\| \+ Duration: \d+\.\d*s \((\d+:\d+:\d+.\d+)\)(?m)')
                    suid = None
                    mkvfiles = []
                    if isfile(ch.suid):
                        mkvfiles = [ch.suid]
                    elif not mkv_globbed:
                        from glob import glob
                        mkvfiles = glob('*.mkv') + glob(join(dirname(avs),'*.mkv'))
                        mkv_globbed = True
                    if mkvfiles:
                        for file in mkvfiles:
                            info = check_output([mkvinfo_path, '--ui-language', 'en', '--output-charset', 'utf-8', file]).decode('utf-8')
                            ret = suid_re.search(info)
                            ch.suid = ret.group(1).lower().strip().replace('0x','').replace(' ','') if ret else 0
                            ret = duration_re.search(info)
                            duration = ret.group(1) if ret else 0
                            mkvinfo[ch.suid] = {'file': file, 'duration': duration}
                    if not (ch.start or ch.end):
                        ch.start = '00:00:00.000' if not ch.start else ch.start
                        ch.end = mkvinfo[ch.suid]['duration'] if not ch.end and (ch.suid in mkvinfo) else ch.end

                ed.chapters.append(ch)
            self.editions.append(ed)
        if output:
            self.toxml(output)

def main(args):

    template = args[0]
    output = args[1]
    avs = args[2] if len(args) == 3 else None

    chaps = AutoMKVChapters(template,output,avs)

if __name__ == '__main__':
    from sys import argv, exit
    if len(argv) > 1:
        main(argv[1:])
    else:
        exit("templates.py <template file> <output filenames w/o extension> [<avisynth file>]")
<commit_msg>Correct minor bug in templates.py default names<commit_after>#!/usr/bin/env python3

class AutoMKVChapters:
    class Template:
        def __init__(self):
            from random import randint
            self.uid = randint(10**4,10**6)
            self.num_editions = 1
            self.lang = ['eng']
            self.country = ['us']
            self.fps = '30'
            self.ofps = '24'
            self.qpf = '0'
            self.trims = None
            self.kframes = None

        def toxml(self,chapfile):

            chf = open(chapfile+'.xml','w',encoding='utf-8')
            head = '<?xml version="1.0" encoding="UTF-8"?>\n<!-- <!DOCTYPE Tags SYSTEM "matroskatags.dtd"> -->\n'
            chf.write(head+'<Chapters>\n')

            if self.num_editions > 1:
                tagf = open(chapfile+'tags.xml','w')
                tagf.write(head+'<Tags>\n')
            else:
                tagf = False

            for ed in self.editions:
                chf.write('\t<EditionEntry>\n')
                chf.write('\t\t<EditionFlagHidden>{0:d}</EditionFlagHidden>\n'.format(ed.hidden) if ed.hidden else '')
                chf.write('\t\t<EditionFlagDefault>{0:d}</EditionFlagDefault>\n'.format(ed.default) if ed.default else '')
                chf.write('\t\t<EditionFlagOrdered>{0:d}</EditionFlagOrdered>\n'.format(ed.ordered) if ed.ordered else '')
                chf.write('\t\t<EditionUID>{0:d}</EditionUID>\n'.format(ed.uid))

                if tagf:
                    tagf.write('\t<Tag>\n\t\t<Targets>\n')
                    tagf.write('\t\t\t<EditionUID>{0:d}</EditionUID>\n'.format(ed.uid))
                    tagf.write('\t\t\t<TargetTypeValue>50</TargetTypeValue>\n\t\t</Targets>\n')
                    num_names = len(ed.name) if len(ed.name) < len(self.lang) else len(self.lang)
                    for i in range(num_names):
                        tagf.write('\t\t<Simple>\n\t\t\t<Name>TITLE</Name>\n')
                        tagf.write('\t\t\t<String>{0}</String>\n'.format(ed.name[i] if ed.name[i] != '' else ed.name[i-1]))
                        tagf.write('\t\t\t<TagLanguage>{0}</TagLanguage>\n'.format(self.lang[i]))
                        tagf.write('\t\t\t<DefaultLanguage>{0:d}</DefaultLanguage>\n'.format(1 if i == 0 else 0))
                        tagf.write('\t\t</Simple>\n')
                    tagf.write('\t</Tag>\n')

                for ch in ed.chapters:
                    chf.write('\t\t<ChapterAtom>\n')
                    num_names = len(ch.name) if len(ch.name) < len(self.lang) else len(self.lang)
                    for i in range(num_names):
                        chf.write('\t\t\t<ChapterDisplay>\n')
                        chf.write('\t\t\t\t<ChapterString>{0}</ChapterString>\n'.format(ch.name[i] if ch.name[i] != '' else ch.name[i-1]))
                        chf.write('\t\t\t\t<ChapterLanguage>{0}</ChapterLanguage>\n'.format(self.lang[i]) if self.lang[i] != 'eng' else '')
                        chf.write('\t\t\t\t<ChapterCountry>{0}</ChapterCountry>\n'.format(self.country[i]) if i < len(self.country) else '')
                        chf.write('\t\t\t</ChapterDisplay>\n')
                    chf.write('\t\t\t<ChapterUID>{0:d}</ChapterUID>\n'.format(ch.uid))
                    chf.write('\t\t\t<ChapterTimeStart>{0}</ChapterTimeStart>\n'.format(ch.start))
                    chf.write('\t\t\t<ChapterTimeEnd>{0}</ChapterTimeEnd>\n'.format(ch.end) if ch.end else '')
                    chf.write('\t\t\t<ChapterFlagHidden>{0:d}</ChapterFlagHidden>\n'.format(ch.hidden) if ch.hidden != 0 else '')
                    chf.write('\t\t\t<ChapterFlagEnabled>{0:d}</ChapterFlagEnabled>\n'.format(ch.enabled) if ch.enabled != 1 else '')
                    chf.write('\t\t\t<ChapterSegmentUID format="hex">{0}</ChapterSegmentUID>\n'.format(ch.suid) if ch.suid else '')
                    chf.write('\t\t</ChapterAtom>\n')

                chf.write('\t</EditionEntry>\n')

            chf.write('</Chapters>\n')

            if tagf:
                tagf.write('</Tags>\n')
                tagf.close()

            if self.qpf != '0' and self.kframes:
                from vfr import write_qpfile
                if self.qpf != '1':
                    qpfile = self.qpf
                else:
                    qpfile = chapfile+'.qpfile'
                write_qpfile(qpfile,self.kframes)

        def connect_with_vfr(self,avs,label=None):
            """
            Connects templates.py with vfr.py, enabling its use outside of vfr.py.

            Uses the same quirks as AMkvC but only for 24 and 30 fps.
            Ex: inputfps=30 is understood as being '30*1000/1001'

            """

            from vfr import parse_trims, fmt_time

            # compensate for amkvc's fps assumption
            if self.fps in ('24','30'):
                fps = self.fps + '/1.001'
            else:
                self.fps = str(fps)
            if self.ofps and self.ofps in ('24','30'):
                ofps = self.ofps + '/1.001'
            else:
                ofps = str(self.ofps)

            Trims2, Trims2ts = parse_trims(avs, fps, ofps, label=label)[2:4]
            Trims2ts = [(fmt_time(i[0]),fmt_time(i[1])) for i in Trims2ts]

            self.trims = Trims2ts
            self.kframes = Trims2

        class Edition:
            def __init__(self):
                self.default = 0
                self.name = ['Default']
                self.hidden = 0
                self.ordered = 0
                self.num_chapters = 1
                self.uid = 0

        class Chapter:
            def __init__(self):
                self.name = ['Chapter']
                self.chapter = False
                self.start = False
                self.end = False
                self.suid = False
                self.hidden = 0
                self.uid = 0
                self.enabled = 1

    def __init__(self, templatefile, output=None, avs=None, trims=None, kframes=None, uid=None, label=None, ifps=None):
        import configparser

        # Init config
        config = configparser.ConfigParser()
        template = open(templatefile,encoding='utf-8')

        # Read template
        config.readfp(template)
        template.close()

        # Template defaults
        self = self.Template()
        self.editions = []
        self.uid = uid if uid else self.uid

        # Set mkvinfo path
        from vfr import mkvmerge
        from os.path import dirname, join
        mkvinfo_path = join(dirname(mkvmerge), 'mkvinfo')

        # Set placeholder for mkvinfo output
        mkv_globbed = False
        mkvinfo = {}

        for k, v in config.items('info'):
            if k == 'lang':
                self.lang = v.split(',')
            elif k == 'country':
                self.country = v.split(',')
            elif k == 'inputfps':
                self.fps = v
            elif k == 'outputfps':
                self.ofps = v
            elif k == 'createqpfile':
                self.qpf = v
            elif k == 'uid':
                self.uid = int(v)
            elif k == 'editions':
                self.num_editions = int(v)

        if avs and not ifps:
            self.connect_with_vfr(avs, label)
        elif trims:
            self.trims = trims
            self.kframes = kframes
        else:
            self.trims = False

        for i in range(self.num_editions):
            from re import compile
            ed = self.Edition()
            ed.uid = self.uid * 100
            self.uid += 1
            cuid = ed.uid
            ed.num = i+1
            ed.chapters = []
            stuff = {}

            for k, v in config.items('edition{0:d}'.format(ed.num)):
                if k == 'default':
                    ed.default = int(v)
                elif k == 'name':
                    ed.name = v.split(',')
                elif k == 'ordered':
                    ed.ordered = int(v)
                elif k == 'hidden':
                    ed.hidden = int(v)
                elif k == 'chapters':
                    ed.num_chapters = int(v)
                    for i in range(ed.num_chapters):
                        stuff[i+1] = []
                elif k == 'uid':
                    ed.uid = int(v)
                else:
                    opt_re = compile('(\d+)(\w+)')
                    ret = opt_re.search(k)
                    if ret:
                        stuff[int(ret.group(1))].append((ret.group(2),v))

            for j in range(ed.num_chapters):
                ch = self.Chapter()
                cuid += 1
                ch.uid = cuid
                ch.num = j+1

                for k, v in stuff[j+1]:
                    if k == 'name':
                        ch.name = v.split(',')
                    elif k == 'chapter':
                        ch.chapter = int(v)
                    elif k == 'start':
                        ch.start = v
                    elif k == 'end':
                        ch.end = v
                    elif k == 'suid':
                        ch.suid = v.strip() if ret else 0
                    elif k == 'hidden':
                        ch.hidden = int(v)
                    elif k == 'enabled':
                        ch.enabled = int(v)

                from os.path import isfile
                if ch.suid and not isfile(ch.suid):
                    ch.suid = ch.suid.replace('0x','').lower().replace(' ','')

                if ch.chapter and not (ch.start and ch.end):
                    ch.start, ch.end = self.trims[ch.chapter-1] if self.trims else (ch.start, ch.end)
                elif ch.suid:
                    from subprocess import check_output

                    suid_re = compile('^\| \+ Segment UID:(.*)(?m)')
                    duration_re = compile('^\| \+ Duration: \d+\.\d*s \((\d+:\d+:\d+.\d+)\)(?m)')
                    suid = None
                    mkvfiles = []
                    if isfile(ch.suid):
                        mkvfiles = [ch.suid]
                    elif not mkv_globbed:
                        from glob import glob
                        mkvfiles = glob('*.mkv') + glob(join(dirname(avs),'*.mkv'))
                        mkv_globbed = True
                    if mkvfiles:
                        for file in mkvfiles:
                            info = check_output([mkvinfo_path, '--ui-language', 'en', '--output-charset', 'utf-8', file]).decode('utf-8')
                            ret = suid_re.search(info)
                            ch.suid = ret.group(1).lower().strip().replace('0x','').replace(' ','') if ret else 0
                            ret = duration_re.search(info)
                            duration = ret.group(1) if ret else 0
                            mkvinfo[ch.suid] = {'file': file, 'duration': duration}
                    if not (ch.start or ch.end):
                        ch.start = '00:00:00.000' if not ch.start else ch.start
                        ch.end = mkvinfo[ch.suid]['duration'] if not ch.end and (ch.suid in mkvinfo) else ch.end

                ed.chapters.append(ch)
            self.editions.append(ed)
        if output:
            self.toxml(output)

def main(args):

    template = args[0]
    output = args[1]
    avs = args[2] if len(args) == 3 else None

    chaps = AutoMKVChapters(template,output,avs)

if __name__ == '__main__':
    from sys import argv, exit
    if len(argv) > 1:
        main(argv[1:])
    else:
        exit("templates.py <template file> <output filenames w/o extension> [<avisynth file>]")
<|endoftext|>


<commit_before># Copyright (c) 2019-2021, Manfred Moitzi
# License: MIT License
from typing import List, Sequence, TYPE_CHECKING, Iterable, Tuple
import math
from ezdxf.math import Vec2
from .bbox import BoundingBox2d
from .line import ConstructionLine
from .construct2d import point_to_line_relation

if TYPE_CHECKING:
    from ezdxf.eztypes import Vertex

__all__ = ["ConstructionBox"]

class ConstructionBox:
    """
    Helper class to create rectangles.

    Args:
        center: center of rectangle
        width: width of rectangle
        height: height of rectangle
        angle: angle of rectangle in degrees

    """

    def __init__(
        self,
        center: "Vertex" = (0, 0),
        width: float = 1,
        height: float = 1,
        angle: float = 0,
    ):
        self._center = Vec2(center)
        self._width = abs(width)  # type: float
        self._height = abs(height)  # type: float
        self._angle = angle  # type: float  # in degrees
        self._corners = None  # type: Tuple[Vec2, Vec2, Vec2, Vec2]
        self._tainted = True

    @classmethod
    def from_points(cls, p1: "Vertex", p2: "Vertex") -> "ConstructionBox":
        """Creates a box from two opposite corners, box sides are parallel to x-
        and y-axis.

        Args:
            p1: first corner as :class:`Vec2` compatible object
            p2: second corner as :class:`Vec2` compatible object

        """
        p1 = Vec2(p1)
        p2 = Vec2(p2)
        width = abs(p2.x - p1.x)
        height = abs(p2.y - p1.y)
        center = p1.lerp(p2)
        return cls(center=center, width=width, height=height)

    def update(self) -> None:
        if not self._tainted:
            return
        center = self.center
        w2 = Vec2.from_deg_angle(self._angle, self._width / 2.0)
        h2 = Vec2.from_deg_angle(self._angle + 90, self._height / 2.0)
        self._corners = (
            center - w2 - h2,  # lower left
            center + w2 - h2,  # lower right
            center + w2 + h2,  # upper right
            center - w2 + h2,  # upper left
        )
        self._tainted = False

    @property
    def bounding_box(self) -> BoundingBox2d:
        """:class:`BoundingBox2d`"""
        return BoundingBox2d(self.corners)

    @property
    def center(self) -> Vec2:
        """box center"""
        return self._center

    @center.setter
    def center(self, c: "Vertex") -> None:
        self._center = Vec2(c)
        self._tainted = True

    @property
    def width(self) -> float:
        """box width"""
        return self._width

    @width.setter
    def width(self, w: float) -> None:
        self._width = abs(w)
        self._tainted = True

    @property
    def height(self) -> float:
        """box height"""
        return self._height

    @height.setter
    def height(self, h: float) -> None:
        self._height = abs(h)
        self._tainted = True

    @property
    def incircle_radius(self) -> float:
        """incircle radius"""
        return min(self._width, self._height) * 0.5

    @property
    def circumcircle_radius(self) -> float:
        """circum circle radius"""
        return math.hypot(self._width, self._height) * 0.5

    @property
    def angle(self) -> float:
        """rotation angle in degrees"""
        return self._angle

    @angle.setter
    def angle(self, a: float) -> None:
        self._angle = a
        self._tainted = True

    @property
    def corners(self) -> Sequence[Vec2]:
        """box corners as sequence of :class:`Vec2` objects."""
        self.update()
        return self._corners

    def __iter__(self) -> Iterable[Vec2]:
        """Iterable of box corners as :class:`Vec2` objects."""
        return iter(self.corners)

    def __getitem__(self, corner) -> Vec2:
        """Get corner by index `corner`, ``list`` like slicing is supported."""
        return self.corners[corner]

    def __repr__(self) -> str:
        """Returns string representation of box as
        ``ConstructionBox(center, width, height, angle)``"""
        return "ConstructionBox({0.center}, {0.width}, {0.height}, {0.angle})".format(
            self
        )

    def translate(self, dx: float, dy: float) -> None:
        """
        Move box about `dx` in x-axis and about `dy` in y-axis.

        Args:
            dx: translation in x-axis
            dy: translation in y-axis

        """
        self.center += Vec2((dx, dy))

    def expand(self, dw: float, dh: float) -> None:
        """Expand box: `dw` expand width, `dh` expand height."""
        self.width += dw
        self.height += dh

    def scale(self, sw: float, sh: float) -> None:
        """Scale box: `sw` scales width, `sh` scales height."""
        self.width *= sw
        self.height *= sh

    def rotate(self, angle: float) -> None:
        """Rotate box by `angle` in degrees."""
        self.angle += angle

    def is_inside(self, point: "Vertex") -> bool:
        """Returns ``True`` if `point` is inside of box."""
        point = Vec2(point)
        delta = self.center - point
        if math.isclose(self.angle, 0.0):  # fast path for horizontal rectangles
            return abs(delta.x) <= (self._width / 2.0) and abs(delta.y) <= (
                self._height / 2.0
            )
        else:
            distance = delta.magnitude
            if distance > self.circumcircle_radius:
                return False
            elif distance <= self.incircle_radius:
                return True
            else:
                # inside if point is "left of line" of all border lines.
                p1, p2, p3, p4 = self.corners
                return all(
                    (
                        point_to_line_relation(point, a, b) < 1
                        for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)]
                    )
                )

    def is_any_corner_inside(self, other: "ConstructionBox") -> bool:
        """Returns ``True`` if any corner of `other` box is inside this box."""
        return any(self.is_inside(p) for p in other.corners)

    def is_overlapping(self, other: "ConstructionBox") -> bool:
        """Returns ``True`` if this box and `other` box do overlap."""
        distance = (self.center - other.center).magnitude
        max_distance = self.circumcircle_radius + other.circumcircle_radius
        if distance > max_distance:
            return False
        min_distance = self.incircle_radius + other.incircle_radius
        if distance <= min_distance:
            return True

        if self.is_any_corner_inside(other):
            return True
        if other.is_any_corner_inside(self):
            return True
        # no corner inside of any box, maybe crossing boxes?
        # check intersection of diagonals
        c1, c2, c3, c4 = self.corners
        diag1 = ConstructionLine(c1, c3)
        diag2 = ConstructionLine(c2, c4)

        t1, t2, t3, t4 = other.corners
        test_diag = ConstructionLine(t1, t3)
        if test_diag.has_intersection(diag1) or test_diag.has_intersection(
            diag2
        ):
            return True
        test_diag = ConstructionLine(t2, t4)
        if test_diag.has_intersection(diag1) or test_diag.has_intersection(
            diag2
        ):
            return True

        return False

    def border_lines(self) -> Sequence[ConstructionLine]:
        """Returns border lines of box as sequence of :class:`ConstructionLine`.
        """
        p1, p2, p3, p4 = self.corners
        return (
            ConstructionLine(p1, p2),
            ConstructionLine(p2, p3),
            ConstructionLine(p3, p4),
            ConstructionLine(p4, p1),
        )

    def intersect(self, line: ConstructionLine) -> List[Vec2]:
        """Returns 0, 1 or 2 intersection points between `line` and box border
        lines.

        Args:
            line: line to intersect with border lines

        Returns:
            list of intersection points

            =========== ==================================
            list size   Description
            =========== ==================================
            0           no intersection
            1           line touches box at one corner
            2           line intersects with box
            =========== ==================================

        """
        result = set()
        for border_line in self.border_lines():
            p = line.intersect(border_line)
            if p is not None:
                result.add(p)
        return sorted(result)
<commit_msg>improve type annotations<commit_after># Copyright (c) 2019-2021, Manfred Moitzi
# License: MIT License
from typing import List, Sequence, TYPE_CHECKING, Iterable
import math
from ezdxf.math import Vec2
from .bbox import BoundingBox2d
from .line import ConstructionLine
from .construct2d import point_to_line_relation

if TYPE_CHECKING:
    from ezdxf.math import Vertex

ABS_TOL = 1e-12

__all__ = ["ConstructionBox"]

class ConstructionBox:
    """
    Helper class to create rectangles.

    Args:
        center: center of rectangle
        width: width of rectangle
        height: height of rectangle
        angle: angle of rectangle in degrees

    """

    def __init__(
        self,
        center: "Vertex" = (0, 0),
        width: float = 1,
        height: float = 1,
        angle: float = 0,
    ):
        self._center = Vec2(center)
        self._width: float = abs(width)
        self._height: float = abs(height)
        self._angle: float = angle  # in degrees
        self._corners: Sequence[Vec2] = tuple()
        self._tainted: bool = True

    @classmethod
    def from_points(cls, p1: "Vertex", p2: "Vertex") -> "ConstructionBox":
        """Creates a box from two opposite corners, box sides are parallel to x-
        and y-axis.

        Args:
            p1: first corner as :class:`Vec2` compatible object
            p2: second corner as :class:`Vec2` compatible object

        """
        p1 = Vec2(p1)
        p2 = Vec2(p2)
        width: float = abs(p2.x - p1.x)
        height: float = abs(p2.y - p1.y)
        center: Vec2 = p1.lerp(p2)
        return cls(center=center, width=width, height=height)

    def update(self) -> None:
        if not self._tainted:
            return
        center = self.center
        w2 = Vec2.from_deg_angle(self._angle, self._width / 2.0)
        h2 = Vec2.from_deg_angle(self._angle + 90, self._height / 2.0)
        self._corners = (
            center - w2 - h2,  # lower left
            center + w2 - h2,  # lower right
            center + w2 + h2,  # upper right
            center - w2 + h2,  # upper left
        )
        self._tainted = False

    @property
    def bounding_box(self) -> BoundingBox2d:
        """:class:`BoundingBox2d`"""
        return BoundingBox2d(self.corners)

    @property
    def center(self) -> Vec2:
        """box center"""
        return self._center

    @center.setter
    def center(self, c: "Vertex") -> None:
        self._center = Vec2(c)
        self._tainted = True

    @property
    def width(self) -> float:
        """box width"""
        return self._width

    @width.setter
    def width(self, w: float) -> None:
        self._width = abs(w)
        self._tainted = True

    @property
    def height(self) -> float:
        """box height"""
        return self._height

    @height.setter
    def height(self, h: float) -> None:
        self._height = abs(h)
        self._tainted = True

    @property
    def incircle_radius(self) -> float:
        """incircle radius"""
        return min(self._width, self._height) * 0.5

    @property
    def circumcircle_radius(self) -> float:
        """circum circle radius"""
        return math.hypot(self._width, self._height) * 0.5

    @property
    def angle(self) -> float:
        """rotation angle in degrees"""
        return self._angle

    @angle.setter
    def angle(self, a: float) -> None:
        self._angle = a
        self._tainted = True

    @property
    def corners(self) -> Sequence[Vec2]:
        """box corners as sequence of :class:`Vec2` objects."""
        self.update()
        return self._corners

    def __iter__(self) -> Iterable[Vec2]:
        """Iterable of box corners as :class:`Vec2` objects."""
        return iter(self.corners)

    def __getitem__(self, corner) -> Vec2:
        """Get corner by index `corner`, ``list`` like slicing is supported."""
        return self.corners[corner]

    def __repr__(self) -> str:
        """Returns string representation of box as
        ``ConstructionBox(center, width, height, angle)``"""
        return "ConstructionBox({0.center}, {0.width}, {0.height}, {0.angle})".format(
            self
        )

    def translate(self, dx: float, dy: float) -> None:
        """
        Move box about `dx` in x-axis and about `dy` in y-axis.

        Args:
            dx: translation in x-axis
            dy: translation in y-axis

        """
        self.center += Vec2((dx, dy))

    def expand(self, dw: float, dh: float) -> None:
        """Expand box: `dw` expand width, `dh` expand height."""
        self.width += dw
        self.height += dh

    def scale(self, sw: float, sh: float) -> None:
        """Scale box: `sw` scales width, `sh` scales height."""
        self.width *= sw
        self.height *= sh

    def rotate(self, angle: float) -> None:
        """Rotate box by `angle` in degrees."""
        self.angle += angle

    def is_inside(self, point: "Vertex") -> bool:
        """Returns ``True`` if `point` is inside of box."""
        point = Vec2(point)
        delta = self.center - point
        if abs(self.angle) < ABS_TOL:  # fast path for horizontal rectangles
            return abs(delta.x) <= (self._width / 2.0) and abs(delta.y) <= (
                self._height / 2.0
            )
        else:
            distance = delta.magnitude
            if distance > self.circumcircle_radius:
                return False
            elif distance <= self.incircle_radius:
                return True
            else:
                # inside if point is "left of line" of all border lines.
                p1, p2, p3, p4 = self.corners
                return all(
                    (
                        point_to_line_relation(point, a, b) < 1
                        for a, b in [(p1, p2), (p2, p3), (p3, p4), (p4, p1)]
                    )
                )

    def is_any_corner_inside(self, other: "ConstructionBox") -> bool:
        """Returns ``True`` if any corner of `other` box is inside this box."""
        return any(self.is_inside(p) for p in other.corners)

    def is_overlapping(self, other: "ConstructionBox") -> bool:
        """Returns ``True`` if this box and `other` box do overlap."""
        distance = (self.center - other.center).magnitude
        max_distance = self.circumcircle_radius + other.circumcircle_radius
        if distance > max_distance:
            return False
        min_distance = self.incircle_radius + other.incircle_radius
        if distance <= min_distance:
            return True

        if self.is_any_corner_inside(other):
            return True
        if other.is_any_corner_inside(self):
            return True
        # no corner inside of any box, maybe crossing boxes?
        # check intersection of diagonals
        c1, c2, c3, c4 = self.corners
        diag1 = ConstructionLine(c1, c3)
        diag2 = ConstructionLine(c2, c4)

        t1, t2, t3, t4 = other.corners
        test_diag = ConstructionLine(t1, t3)
        if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
            return True
        test_diag = ConstructionLine(t2, t4)
        if test_diag.has_intersection(diag1) or test_diag.has_intersection(diag2):
            return True

        return False

    def border_lines(self) -> Sequence[ConstructionLine]:
        """Returns border lines of box as sequence of :class:`ConstructionLine`.
        """
        p1, p2, p3, p4 = self.corners
        return (
            ConstructionLine(p1, p2),
            ConstructionLine(p2, p3),
            ConstructionLine(p3, p4),
            ConstructionLine(p4, p1),
        )

    def intersect(self, line: ConstructionLine) -> List[Vec2]:
        """Returns 0, 1 or 2 intersection points between `line` and box border
        lines.

        Args:
            line: line to intersect with border lines

        Returns:
            list of intersection points

            =========== ==================================
            list size   Description
            =========== ==================================
            0           no intersection
            1           line touches box at one corner
            2           line intersects with box
            =========== ==================================

        """
        result = set()
        for border_line in self.border_lines():
            p = line.intersect(border_line)
            if p is not None:
                result.add(p)
        return sorted(result)
<|endoftext|>