Source code for pycalphad.core.polytope

"""
This module provides functions to uniformly sample points subject to a system of linear
inequality constraints, :math:`Ax <= b` (convex polytope), and linear equality
constraints, :math:`Ax = b` (affine projection).

A comparison of MCMC algorithms to generate uniform samples over a convex polytope is
given in [Chen2018]_. Here, we use the Hit & Run algorithm described in [Smith1984]_.
The R-package `hitandrun`_ provides similar functionality to this module.

Based on https://github.com/DavidWalz/polytope-sampling
Used under the terms of the MIT license. License information can be found in the pycalphad LICENSE.txt.

References
----------
.. [Chen2018] Chen Y., Dwivedi, R., Wainwright, M., Yu B. (2018) Fast MCMC Sampling
    Algorithms on Polytopes. JMLR, 19(55):1−86
    https://arxiv.org/abs/1710.08165
.. [Smith1984] Smith, R. (1984). Efficient Monte Carlo Procedures for Generating
    Points Uniformly Distributed Over Bounded Regions. Operations Research,
    32(6), 1296-1308.
    www.jstor.org/stable/170949
.. _`hitandrun`: https://cran.r-project.org/web/packages/hitandrun/index.html
"""
import numpy as np
import scipy.linalg
import scipy.optimize


[docs] def check_Ab(A, b): """Check if matrix equation Ax=b is well defined. Parameters ---------- A : 2d-array of shape (n_constraints, dimension) Left-hand-side of Ax <= b. b : 1d-array of shape (n_constraints) Right-hand-side of Ax <= b. """ assert A.ndim == 2 assert b.ndim == 1 assert A.shape[0] == b.shape[0]
[docs] def chebyshev_center(A, b): """Find the center of the polytope Ax <= b. Parameters ---------- A : 2d-array of shape (n_constraints, dimension) Left-hand-side of Ax <= b. b : 1d-array of shape (n_constraints) Right-hand-side of Ax <= b. Returns ------- 1d-array of shape (dimension) Chebyshev center of the polytope """ res = scipy.optimize.linprog( np.r_[np.zeros(A.shape[1]), -1], A_ub=np.hstack([A, np.linalg.norm(A, axis=1, keepdims=True)]), b_ub=b, bounds=(None, None), ) if not res.success: raise Exception("Unable to find Chebyshev center") return res.x[:-1]
[docs] def constraints_from_bounds(lower, upper): """Construct the inequality constraints Ax <= b that correspond to the given lower and upper bounds. Parameters ---------- lower : array-like lower bound in each dimension upper : array-like upper bound in each dimension Returns ------- A: 2d-array of shape (2 * dimension, dimension) Left-hand-side of Ax <= b. b: 1d-array of shape (2 * dimension) Right-hand-side of Ax <= b. """ n = len(lower) A = np.vstack([-np.eye(n), np.eye(n)]) b = np.r_[-np.array(lower), np.array(upper)] return A, b
[docs] def affine_subspace(A, b): """Compute a basis of the nullspace of A, and a particular solution to Ax = b. This allows to to construct arbitrary solutions as the sum of any vector in the nullspace, plus the particular solution. Parameters ---------- A : 2d-array of shape (n_constraints, dimension) Left-hand-side of Ax <= b. b : 1d-array of shape (n_constraints) Right-hand-side of Ax <= b. Returns ------- N: 2d-array of shape (dimension, dimension) Orthonormal basis of the nullspace of A. xp: 1d-array of shape (dimension) Particular solution to Ax = b. """ N = scipy.linalg.null_space(A) xp = np.linalg.pinv(A) @ b return N, xp
[docs] def sample(n_points, lower, upper, A1=None, b1=None, A2=None, b2=None): """Sample a number of points from a convex polytope A1 x <= b1 using the Hit & Run algorithm. Lower and upper bounds need to be provided to ensure that the polytope is bounded. Equality constraints A2 x = b2 may be optionally provided. Parameters ---------- n_points : int Number of samples to generate. lower: 1d-array of shape (dimension) Lower bound in each dimension. If not wanted set to -np.inf. upper: 1d-array of shape (dimension) Upper bound in each dimension. If not wanted set to np.inf. A1 : 2d-array of shape (n_constraints, dimension) Left-hand-side of A1 x <= b1. b1 : 1d-array of shape (n_constraints) Right-hand-side of A1 x <= b1. A2 : 2d-array of shape (n_constraints, dimensions), optional Left-hand-side of A2 x = b2. b2 : 1d-array of shape (n_constraints), optional Right-hand-side of A2 x = b2. Returns ------- 2d-array of shape (n_points) Points sampled from the polytope. """ A, b = constraints_from_bounds(lower, upper) if (A1 is not None) and (b1 is not None): A1 = np.r_[A, A1] b1 = np.r_[b, b1] else: A1, b1 = A, b if (A2 is not None) and (b2 is not None): check_Ab(A2, b2) N, xp = affine_subspace(A2, b2) else: N = np.eye(A1.shape[1]) xp = np.zeros(A1.shape[1]) # Do not allow particular solutions to fall outside of bounds # This operation helps with numerical robustness xp = np.clip(xp, lower+1e-14, upper-1e-14) if N.shape[1] == 0: # zero-dimensional polytope, return unique solution # Use lstsq instead of solve, to allow for redundant constraints (non-square constraint matrix) solution = np.linalg.lstsq(A2, b2, rcond=None) X = np.atleast_2d(solution[0]) # Check residuals to ensure system was fully determined, or constraints were redundant if solution[1].size > 0: residual = float(solution[1]) if residual > 1e-10: # Starting point is not feasible return np.empty((0, A1.shape[1])) return X # project to the affine subspace of the equality constraints At = A1 @ N bt = b1 - A1 @ xp try: x0 = chebyshev_center(At, bt) except: # Unable to find center return np.empty((0, A1.shape[1])) test_point = x0[np.newaxis, :] @ N.T + xp if np.any(test_point < lower-1e-10) or np.any(test_point > upper+1e-10): # Starting point is not feasible return np.empty((0, A1.shape[1])) X = np.empty((n_points, At.shape[1])) x = x0 rng = np.random.RandomState(1769) with np.errstate(divide='ignore', invalid='ignore'): directions = rng.randn(n_points, At.shape[1]) directions /= np.linalg.norm(directions, axis=0) for i in range(n_points): # sample random direction from unit hypersphere direction = directions[i] # distances to each face from the current point in the sampled direction D = (bt - x @ At.T) / (direction @ At.T) # distance to the closest face in and opposite to direction lo = max(D[D < 1e-10]) hi = min(D[D > -1e-10]) if hi < lo: # Amount of 'wiggle room' is down in the numerical noise lo = 0.0 hi = 0.0 # make random step x += rng.uniform(lo, hi) * direction X[i] = x # project back X = X @ N.T + xp X = np.clip(X, lower, upper) return X