geometry_tools.utils.core

   1import warnings
   2import functools
   3
   4import numpy as np
   5from scipy.optimize import linprog
   6
   7from . import numerical
   8from . import types
   9
  10try:
  11    import sage.all
  12    from . import sagewrap
  13    SAGE_AVAILABLE = True
  14except ModuleNotFoundError:
  15    SAGE_AVAILABLE = False
  16
  17def rotation_matrix(angle, like=None, **kwargs):
  18    r"""Get a 2x2 rotation matrix rotating counterclockwise by the
  19    specified angle.
  20
  21    Parameters
  22    ----------
  23    angle : float
  24        angle to rotate by
  25
  26    Returns
  27    -------
  28    ndarray
  29        numpy array of shape (2,2) of the form
  30        $\begin{pmatrix}\cos \theta & -\sin \theta\\\sin\theta & \cos \theta
  31        \end{pmatrix}$
  32
  33    """
  34    if like is None:
  35        like = angle
  36    return array_like([[cos(angle), -1*sin(angle)],
  37                       [sin(angle), cos(angle)]],
  38                      like=like, **kwargs)
  39
  40def permutation_matrix(permutation, inverse=False, **kwargs):
  41    """Return a permutation matrix representing the given permutation.
  42
  43    Parameters
  44    ----------
  45    permutation: iterable
  46        a sequence of n numbers (indices), specifying a permutation of
  47        (1, ... n).
  48    inverse : bool
  49        if True, return the matrix for the inverse of the
  50        permutation specified.
  51
  52    Returns
  53    -------
  54    ndarray
  55        square 2-dimensional array, giving a permutation matrix.
  56
  57    """
  58    n = len(permutation)
  59    p_mat = zeros((n, n), **kwargs)
  60    one = number(1, **kwargs)
  61
  62    for i,j in enumerate(permutation):
  63        if inverse:
  64            p_mat[j,i] = one
  65        else:
  66            p_mat[i,j] = one
  67
  68    return p_mat
  69
  70def conjugate_by_permutation(matrix, permutation, inverse=False):
  71    result = zeros(matrix.shape, like=matrix)
  72
  73    for i, si in enumerate(np.moveaxis(permutation, -1, 0)):
  74        res_col = zeros(matrix.shape[:-1], like=matrix)
  75        i_take = np.expand_dims(si, (-2, -1))
  76        mat_col = np.take_along_axis(matrix, i_take, axis=-1)
  77        for j, sj in enumerate(np.moveaxis(permutation, -1, 0)):
  78            j_take = np.expand_dims(sj, (-2, -1))
  79            val = np.take_along_axis(mat_col, j_take, axis=-2)
  80            res_col[..., j] = np.squeeze(val, (-1, -2))
  81        result[..., i] = res_col
  82
  83    return result
  84
  85def permute_along_axis(matrix, permutation, axis, inverse=False):
  86    result = zeros(matrix.shape, like=matrix).swapaxes(-1, axis)
  87    for i, si in enumerate(np.moveaxis(permutation, -1, 0)):
  88        p_ind = np.expand_dims(si, (-2, -1))
  89        if inverse:
  90            values = np.take_along_axis(matrix.swapaxes(-1, axis), p_ind, axis=-1)
  91            result[..., i] = values.squeeze(axis=-1)
  92        else:
  93            values = np.expand_dims(matrix.swapaxes(-1, axis)[..., i], -1)
  94            np.put_along_axis(result, p_ind, values, axis=-1)
  95
  96    return result.swapaxes(-1, axis)
  97
  98def construct_diagonal(diags):
  99    n = diags.shape[-1]
 100    result = zeros(diags.shape[:-1] + (n * n,), like=diags)
 101    result[..., ::n+1] = diags
 102    return result.reshape(diags.shape[:-1] + (n, n))
 103
 104def diagonalize_form(bilinear_form,
 105                     order_eigenvalues="signed",
 106                     reverse=False,
 107                     with_inverse=True):
 108    r"""Return a matrix conjugating a symmetric real bilinear form to a
 109    diagonal form.
 110
 111    Parameters
 112    ----------
 113    bilinear_form: ndarray
 114        numpy array of shape (n, n) representing, a symmetric bilinear
 115        form in standard coordinates
 116    order_eigenvalues: {'signed', 'minkowski'}
 117        If "signed" (the default), conjugate to a diagonal bilinear
 118        form on R^n where the basis vectors are ordered in order of
 119        increasing eigenvalue.
 120
 121        If "minkowski", and the bilinear form has signature (p, q, r)
 122        (meaning that a maximal positive definite subspace has
 123        dimension p, a maximal negative definite subspace has
 124        dimension q, and r = n - p - q), then conjugate to a bilinear
 125        form where the basis vectors are ordered so that spacelike
 126        (positive) vectors come first if p <= q and timelike
 127        (negative) basis vectors come first if q < p.
 128
 129    reverse : bool
 130        if True, reverse the order of the basis vectors for the
 131        diagonal bilinear form (from the order specified by
 132        `order_eigenvalues`).
 133
 134    Returns
 135    -------
 136    ndarray
 137        numpy array of shape (n, n), representing a coordinate change
 138        taking the given bilinear form to a diagonal form. If $B$ is
 139        the matrix given by bilinear_form, and $D$ is a diagonal
 140        matrix with the same signature as $B$, then this function
 141        returns a matrix $M$ such that $M^TDM = B$.
 142
 143    """
 144
 145    n = bilinear_form.shape[-1]
 146
 147    eigs, U = eigh(bilinear_form)
 148    Uinv = invert(U)
 149
 150    Dinv = construct_diagonal(np.sqrt(np.abs(eigs)))
 151    D = zeros(Dinv.shape, like=Dinv)
 152
 153    close_to_zero = np.isclose(Dinv.astype('float64'), 0.)
 154    np.divide(1, Dinv, out=D, where=~close_to_zero)
 155
 156    n_eigs = eigs.astype('float64')
 157
 158    W = U @ D
 159
 160    if order_eigenvalues:
 161        if order_eigenvalues == "signed":
 162            order = np.argsort(n_eigs, axis=-1)
 163        if order_eigenvalues == "minkowski":
 164            sort_indices = np.zeros_like(n_eigs)
 165            num_positive = np.count_nonzero(n_eigs < 0, axis=-1)
 166            num_negative = np.count_nonzero(n_eigs > 0, axis=-1)
 167
 168            flip = np.ones_like(n_eigs)
 169            flip[num_negative < num_positive] = -1
 170
 171            sort_indices[n_eigs < 0] = -1 * flip[n_eigs < 0]
 172            sort_indices[n_eigs > 0] = flip[n_eigs > 0]
 173            sort_indices[n_eigs == 0] == 2
 174
 175            order = np.argsort(sort_indices, axis=-1)
 176        if reverse:
 177            order = np.flip(order)
 178
 179        W = permute_along_axis(W, order, axis=-1, inverse=True)
 180
 181    if not with_inverse:
 182        return W
 183
 184    Winv = Dinv @ Uinv
 185    if order_eigenvalues:
 186        Winv = permute_along_axis(Winv, order, axis=-2)
 187
 188    return (W, Winv)
 189
 190def circle_angles(center, coords):
 191    """Return angles relative to the center of a circle.
 192
 193    Parameters
 194    ----------
 195    center: ndarray
 196        numpy array with shape (..., 2) representing x,y coordinates
 197        the centers of some circles.
 198    coords: ndarray
 199        numpy array with shape (..., 2) representing x,y coordinates
 200        of some points.
 201
 202    Returns
 203    -------
 204    ndarray
 205        angles (relative to x-axis) of each of the pair of points
 206        specified by coords.
 207
 208    """
 209    xs = (coords - np.expand_dims(center, axis=-2))[..., 0]
 210    ys = (coords - np.expand_dims(center, axis=-2))[..., 1]
 211
 212    return np.arctan2(ys, xs)
 213
 214def apply_bilinear(v1, v2, bilinear_form=None,
 215                   broadcast="elementwise"):
 216    """Apply a bilinear form to a pair of arrays of vectors.
 217
 218    Parameters
 219    ----------
 220    v1, v2: ndarray
 221        ndarrays of shape (..., n) giving arrays of vectors.
 222    bilinear_form: ndarray
 223        ndarray of shape (n, n) specifying a matrix representing a
 224        symmetric bilinear form to use to pair v1 and v2. If
 225        `None`, use the standard (Euclidean) bilinear form on R^n.
 226
 227    Returns
 228    -------
 229    ndarray
 230        ndarray representing the result of pairing the vectors in v1
 231        with v2.
 232
 233    """
 234
 235    intermed = np.expand_dims(v1, -2)
 236
 237    if bilinear_form is not None:
 238        intermed = matrix_product(
 239            np.expand_dims(v1, -2), bilinear_form,
 240            broadcast=broadcast
 241        )
 242    prod = matrix_product(
 243        intermed, np.expand_dims(v2, -1)
 244    )
 245
 246    return prod.squeeze((-1, -2))
 247
 248    #return np.array(((v1 @ bilinear_form) * v2).sum(-1))
 249
 250def normsq(vectors, bilinear_form=None):
 251    """Evaluate the norm squared of an array of vectors, with respect to a
 252       bilinear form.
 253
 254    Equivalent to a call of apply_bilinear(vectors, vectors, bilinear_form).
 255
 256    """
 257    return apply_bilinear(vectors, vectors, bilinear_form)
 258
 259def normalize(vectors, bilinear_form=None):
 260    """Normalize an array of vectors, with respect to a bilinear form.
 261
 262    Parameters
 263    ----------
 264    vectors: ndarray
 265        ndarray of shape (..., n)
 266    bilinear_form: ndarray or None
 267        bilinear form to use to evaluate the norms in vectors. If
 268        None, use the standard (Euclidean) bilinear form on R^n.
 269
 270    Returns
 271    -------
 272    ndarray
 273        ndarray with the same shape as vectors. Each vector in this
 274        array has norm either +1 or -1, depending on whether the
 275        original vector had positive or negative square-norm (with
 276        respect to the given bilinear form).
 277
 278    """
 279    sq_norms = normsq(vectors, bilinear_form)
 280
 281    abs_norms = np.sqrt(np.abs(np.expand_dims(sq_norms, axis=-1)))
 282
 283    return np.divide(vectors, abs_norms, out=vectors,
 284                     where=(abs_norms.astype('float64') != 0))
 285
 286def short_arc(thetas):
 287    """Reorder angles so that the counterclockwise arc between them is
 288    shorter than the clockwise angle.
 289
 290    Parameters
 291    ----------
 292    thetas: ndarray
 293        numpy array of shape (..., 2), giving ordered pairs of angles
 294        in the range (-2pi, 2pi)
 295
 296    Returns
 297    -------
 298    ndarray
 299        numpy array with the same shape as thetas. The ordered pairs
 300        in this array are arranged so that for each pair `(a, b)`, the
 301        counterclockwise arc from `a` to `b` is shorter than the
 302        counterclockwise arc from `b` to `a`.
 303
 304    """
 305    shifted_thetas = np.copy(thetas)
 306
 307    shifted_thetas[shifted_thetas < 0] += 2 * np.pi
 308    shifted_thetas.sort(axis=-1)
 309
 310    to_flip = shifted_thetas[...,1] - shifted_thetas[...,0] > np.pi
 311    shifted_thetas[to_flip] = np.flip(shifted_thetas[to_flip], axis=-1)
 312
 313    return shifted_thetas
 314
 315def right_to_left(thetas):
 316    """Reorder angles so that the counterclockwise arc goes right to left.
 317
 318    Parameters
 319    ----------
 320    thetas: ndarray
 321        numpy array of shape (..., 2), giving ordered pairs of angles.
 322
 323    Returns
 324    -------
 325    ndarray
 326        numpy array with the same shape as `thetas`. The ordered pairs
 327        in this array are arranged so that for each pair `(a, b)`, the
 328        cosine of `b` is at most the cosine of `a`.
 329
 330    """
 331    flipped_thetas = np.copy(thetas)
 332
 333    to_flip = np.cos(thetas[..., 0]) < np.cos(thetas[..., 1])
 334    flipped_thetas[to_flip] = np.flip(thetas[to_flip], axis=-1)
 335
 336    return flipped_thetas
 337
 338
 339def arc_include(thetas, reference_theta):
 340    """Reorder angles so that the counterclockwise arc between them always
 341    includes some reference point on the circle.
 342
 343    Parameters
 344    ----------
 345    thetas : ndarray(float)
 346        pairs of angles in the range (-2pi, 2pi).
 347    reference_theta: ndarray(float)
 348         angles in the range (-2pi, 2pi)
 349
 350    Returns
 351    -------
 352    theta_p : ndarray(float)
 353        pairs of angles of the same shape as `thetas`, so that
 354        `reference_theta` lies in the counterclockwise angle between
 355        `theta_p[..., 0]` and `theta_p[..., 1]`.
 356
 357    """
 358
 359    s_thetas = np.copy(thetas)
 360    s_theta1 = thetas[..., 1] - thetas[..., 0]
 361    s_reference = np.expand_dims(reference_theta - thetas[..., 0],
 362                                 axis=-1)
 363
 364    s_theta1[s_theta1 < 0] += 2 * np.pi
 365    s_reference[s_reference < 0] += 2 * np.pi
 366
 367    to_swap = (s_theta1 < s_reference[..., 0])
 368
 369    s_thetas[to_swap] = np.flip(s_thetas[to_swap], axis=-1)
 370    return s_thetas
 371
 372def sphere_inversion(points):
 373    r"""Apply unit sphere inversion to points in R^n.
 374
 375    This realizes the map $v \mapsto v / ||v||^2$.
 376
 377    Parameters
 378    ----------
 379    points : ndarray
 380        Array of shape `(..., n)` giving a set of points in R^n
 381
 382    Returns
 383    -------
 384    ndarray
 385        The image of the `points` array under sphere inversion.
 386
 387    """
 388    with np.errstate(divide="ignore", invalid="ignore"):
 389        return (points.T / (normsq(points)).T).T
 390
 391def swap_matrix(i, j, n):
 392    """Return a permutation matrix representing a single transposition.
 393
 394    Parameters
 395    ----------
 396    i, j : int
 397        Indices swapped by a transposition.
 398    n : int
 399        dimension of the permutation matrix.
 400
 401    Returns
 402    -------
 403    ndarray
 404        Array of shape `(n, n)` giving a transposition matrix.
 405
 406    """
 407    permutation = list(range(n))
 408    permutation[i] = j
 409    permutation[j] = i
 410    return permutation_matrix(permutation)
 411
 412def projection(v1, v2, bilinear_form):
 413    r"""Return the projection of `v1` onto `v2` with respect to some
 414    bilinear form.
 415
 416    The returned vector `w` is parallel to `v2`, and `v1 - w` is
 417    orthogonal to `v2` with respect to the given bilinear form.
 418    `w` is determined by the formula
 419    $$
 420    v_2 \cdot \langle v_1, v_2 \rangle / \langle v_2, v_2 \rangle,
 421    $$
 422    where $\langle \cdot, \cdot \rangle$ is the pairing determined by
 423    `bilinear_form`.
 424
 425    Parameters
 426    ----------
 427    v1 : ndarray
 428        vector in R^n to project
 429    v2 : ndarray
 430        vector in R^n to project onto
 431    bilinear_form : ndarray
 432        array of shape `(n, n)` giving a bilinear form to use for the
 433        projection.
 434
 435    Returns
 436    -------
 437    w : ndarray
 438        vector in R^n giving the projection of `v1` onto `v2`.
 439
 440    """
 441
 442    return (v2.T *
 443            apply_bilinear(v1, v2, bilinear_form).T /
 444            normsq(v2, bilinear_form).T).T
 445
 446def orthogonal_complement(vectors, form=None, normalize="form"):
 447    """Find an orthogonal complement with respect to a nondegenerate (but
 448       possibly indefinite) bilinear form.
 449
 450    Parameters
 451    ----------
 452    vectors : ndarray of shape (..., k, n)
 453        array of k row vectors for which to find a complement. Each
 454        set of k row vectors should be linearly independent.
 455
 456    form : ndarray of shape `(n, n)`
 457        bilinear form to use to find complements. If None (the
 458        default), use the standard Euclidean form on R^n.
 459
 460    normalize : One of {'form', 'euclidean'} or None
 461        How to normalize the vectors spanning the orthogonal
 462        complement. If 'form' (the default), attempt to return vectors
 463        which have unit length with respect to `form.` (Note this may
 464        fail if `form` is indefinite, i.e. there are nonzero null
 465        vectors. If 'euclidean', then use the standard Euclidean form
 466        on R^n to normalize the vectors.
 467
 468    Returns
 469    -------
 470    result : ndarray with shape (..., n - k, n)
 471        array of n-k row vectors, each of which is orthogonal to all
 472        of the k row vectors provided (with respect to `form`).
 473
 474    """
 475    if form is None:
 476        form = np.identity(vectors.shape[-1])
 477
 478    kernel_basis = kernel(vectors @ form).swapaxes(-1, -2)
 479
 480    if normalize == 'form':
 481        return indefinite_orthogonalize(form, kernel_basis)
 482
 483    return kernel_basis
 484
 485def indefinite_orthogonalize(form, matrices):
 486    """Apply the Gram-Schmidt algorithm, but for a possibly indefinite
 487    bilinear form.
 488
 489    Parameters
 490    ----------
 491    form : ndarray of shape `(n,n)`
 492        bilinear form to orthogonalize with respect to
 493
 494    matrices : ndarray of shape `(..., k, n)`
 495        array of k row vectors to orthogonalize.
 496
 497    Returns
 498    -------
 499    result : ndarray
 500        array with the same shape as `matrices`. The last two
 501        dimensions of this array give matrices with mutually
 502        orthogonal rows, with respect to the given bilinear form.
 503
 504        For all `j <= k`, the subspace spanned by the first `j` rows
 505        of `result` is the same as the subspace spanned by the first
 506        `j` rows of `matrices`.
 507
 508    """
 509    if len(matrices.shape) < 2:
 510        return normalize(matrices, form)
 511
 512    n, m = matrices.shape[-2:]
 513
 514    result = zeros(matrices.shape,
 515                   like=matrices,
 516                   integer_type=False)
 517
 518    #we're using a python for loop, but only over dimension^2 which
 519    #is probably small
 520
 521    for i in range(n):
 522        row = matrices[..., i, :]
 523        for j in range(i):
 524            row -= projection(row, result[..., j, :], form)
 525        result[..., i, :] = row
 526
 527    return normalize(result, form)
 528
 529def find_isometry(form, partial_map, force_oriented=False):
 530    """find a form-preserving matrix agreeing with a specified map on
 531    the flag defined by the standard basis.
 532
 533    Parameters
 534    ----------
 535    form : ndarray of shape `(n, n)`
 536        the bilinear map to preserve
 537
 538    partial_map : ndarray of shape `(..., k, n)`
 539        array representing the images of the first k standard basis
 540        vectors (row vectors)
 541
 542    force_oriented : boolean
 543        whether we should apply a reflection to force the resulting
 544        map to be orientation-preserving.
 545
 546    Returns
 547    -------
 548    ndarray
 549        array of shape `(..., n, n)` representing an array of matrices
 550        whose rows and columns are "orthonormal" with respect to the
 551        bilinear form (since the form may be indefinite, "normal"
 552        vectors may have norm -1).
 553
 554        For all `j <= k`, the subspace spanned by the first `j`
 555        standard basis vectors is sent to the subspace spanned by the
 556        first `j` rows of the result.
 557
 558    """
 559
 560    orth_partial = indefinite_orthogonalize(form, partial_map)
 561
 562    if len(orth_partial.shape) < 2:
 563        orth_partial = np.expand_dims(orth_partial, axis=0)
 564
 565    kernel_basis = kernel(orth_partial @ form).swapaxes(-1, -2)
 566
 567    orth_kernel = indefinite_orthogonalize(form, kernel_basis)
 568
 569    iso = np.concatenate([orth_partial, orth_kernel], axis=-2)
 570
 571    if force_oriented:
 572        iso = make_orientation_preserving(iso)
 573
 574    return iso
 575
 576def find_definite_isometry(partial_map, force_oriented=False):
 577    """find an orthogonal matrix agreeing with a specified map on
 578    the flag defined by the standard basis.
 579
 580    Parameters
 581    ----------
 582    partial_map : ndarray of shape `(..., k, n)`
 583        array representing the images of the first k standard basis
 584        vectors (row vectors)
 585
 586    force_oriented : boolean
 587        whether we should apply a reflection to force the resulting
 588        map to be orientation-preserving.
 589
 590    Returns
 591    -------
 592    ndarray
 593        array of shape `(..., n, n)` representing an array of matrices
 594        with orthonormal rows and columns.
 595
 596        For all `j <= k`, the subspace spanned by the first `j`
 597        standard basis vectors is sent to the subspace spanned by the
 598        first `j` rows of the result.
 599
 600    """
 601    pmap = np.array(partial_map)
 602    if len(pmap.shape) < 2:
 603        pmap = pmap.reshape((len(pmap), 1))
 604    h, w = pmap.shape[-2:]
 605    n = max(h, w)
 606    if w > h:
 607        mat = np.concatenate([pmap.swapaxes(-1, -2),
 608                             np.identity(n)], axis=-1)
 609    else:
 610        mat = np.concatenate([pmap, np.identity(n)], axis=-1)
 611
 612    q, r = np.linalg.qr(mat)
 613
 614    iso = np.sign(r[..., 0,0]) * q
 615
 616    if force_oriented:
 617        iso = make_orientation_preserving(iso)
 618
 619    return iso
 620
 621def make_orientation_preserving(matrix):
 622    """apply a reflection to the last row of matrix to make it orientation
 623    preserving.
 624
 625    if matrix is already orientation preserving, do nothing.
 626
 627    Parameters
 628    ----------
 629    matrix : ndarray of shape `(..., n, n)`
 630        ndarray of linear maps
 631
 632    Returns
 633    -------
 634    result : ndarray
 635        array with the same shape as `matrices`, representating an
 636        ndarray of linear maps. If `A` is an orientation-reversing
 637        matrix in `matrices`, then the corresponding matrix in
 638        `result` has its last row negated.
 639    """
 640    preserved = matrix.copy()
 641    preserved[det(preserved) < 0, -1, :] *= -1
 642    return preserved
 643
 644def expand_unit_axes(array, unit_axes, new_axes):
 645    """expand the last axes of an array to make it suitable for ndarray
 646    pairwise multiplication.
 647
 648    `array` is an ndarray, viewed as an array of arrays, each of which
 649    has `unit_axes` axes. That is, its shape decomposes into a pair of
 650    tuples `([axes 1], [axes 2])`, where `[axes 2]` is a tuple of
 651    length `unit_axes`.
 652
 653    Parameters
 654    ----------
 655    array : ndarray
 656        ndarray to expand
 657    unit_axes : int
 658        number of axes of `array` to treat as a "unit"
 659    new_axes : int
 660        number of axes to add to the array
 661
 662    Returns
 663    -------
 664    ndarray
 665        ndarray of shape `([object axes], 1, ..., 1, [unit axes])`,
 666        where the number of 1's is either `new_axes - unit_axes`, or 0
 667        if this is negative.
 668
 669    """
 670    if new_axes <= unit_axes:
 671        return array
 672
 673    return np.expand_dims(array.T, axis=tuple(range(unit_axes, new_axes))).T
 674
 675def squeeze_excess(array, unit_axes, other_unit_axes):
 676    """Squeeze all excess axes from an ndarray of arrays with unit_axes axes.
 677
 678    This undoes expand_unit_axes.
 679
 680    Parameters
 681    ----------
 682    array : ndarray
 683        ndarray of shape
 684        `([object axes], [excess axes], [unit axes])`, where `[unit axes]`
 685        is a tuple of length `unit_axes`, and `[excess axes]` is a
 686        tuple of length `other_unit_axes - unit_axes`.
 687    unit_axes : int
 688        number of axes to view as "units" in `array`. That is, `array`
 689        is viewed as an ndarray of arrays each with `unit_axes` axes.
 690    other_unit_axes : int
 691        axes to avoid squeezing when we reshape the array.
 692
 693    Returns
 694    -------
 695    ndarray
 696        Reshaped array with certain length-1 axes removed. If the
 697        input array has shape
 698        `([object axes], [excess axes], [unit axes])`,
 699        squeeze out all the ones in `[excess axes]`.
 700
 701    """
 702    squeezable = np.array(array.T.shape[unit_axes:other_unit_axes])
 703    (to_squeeze,) = np.nonzero(squeezable == 1)
 704    to_squeeze += unit_axes
 705
 706    return np.squeeze(array.T, axis=tuple(to_squeeze)).T
 707
 708def broadcast_match(a1, a2, unit_axes):
 709    """Tile a pair of arrays so that they can be broadcast against each
 710    other, treating the last ndims as a unit.
 711
 712    It is often not necessary to call this function, since numpy
 713    broadcasting will handle this implicitly for many vectorized
 714    functions.
 715
 716    Parameters
 717    ----------
 718    a1, a2 : ndarray
 719        arrays to tile
 720    unit_axes : int
 721        number of axes to treat as a unit when tiling
 722
 723    Returns
 724    -------
 725    (u1, u2) : (ndarray, ndarray)
 726        Tiled versions of a1 and a2, respectively.
 727
 728        If unit_axes is k, and a1 has shape (N1, ..., Ni, L1, ...,
 729        Lk), and a2 has shape (M1, ..., Mj, P1, ..., Pk), then u1 has
 730        shape (N1, ..., Ni, M1, ..., Mj, L1, ..., Lk) and u2 has shape
 731        (N1, ..., Ni, M1, ..., Mj, P1, ..., Pk).
 732    """
 733    c1_ndims = len(a1.shape) - unit_axes
 734    c2_ndims = len(a2.shape) - unit_axes
 735
 736    exp1 = np.expand_dims(a1, tuple(range(c1_ndims, c1_ndims + c2_ndims)))
 737    exp2 = np.expand_dims(a2, tuple(range(c1_ndims)))
 738
 739    tile1 = np.tile(exp1, (1,) * c1_ndims + a2.shape[:c2_ndims] + (1,) * unit_axes)
 740    tile2 = np.tile(exp2, a1.shape[:c1_ndims] + (1,) * (c2_ndims + unit_axes))
 741
 742    return (tile1, tile2)
 743
 744def matrix_product(array1, array2, unit_axis_1=2, unit_axis_2=2,
 745                   broadcast="elementwise"):
 746    """Multiply two ndarrays of ndarrays together.
 747
 748    Each array in the input is viewed as an ndarray of smaller
 749    ndarrays with a specified number of axes. The shapes of these
 750    smaller arrays must broadcast against each other (i.e. be
 751    compatible with the numpy `@` operator).
 752
 753    For the dimensions of the outer ndarray, the behavior of this
 754    function depends on the broadcast rule provided by the `broadcast`
 755    keyword.
 756
 757    Parameters
 758    ----------
 759    array1, array2 : ndarray
 760        ndarrays of ndarrays to multiply together
 761    unit_axis_1, unit_axis_2 : int
 762        Each of `array1` and `array2` is viewed as an array with
 763        `unit_axis_1` and `unit_axis_2` ndims, respectively. By
 764        default both are set to 2, meaning this function multiplies a
 765        pair of ndarrays of matrices (2-dim arrays).
 766    broadcast : {'elementwise', 'pairwise', 'pairwise_reversed'}
 767        broadcast rule to use when multiplying arrays.
 768
 769        If the broadcast rule is 'elementwise' (the default), assume that
 770        the shape of one outer array broadcasts against the shape of
 771        the other outer array, and multiply with the same rules as the
 772        numpy `@` operator.
 773
 774        If 'pairwise', multiply every element in the first outer array
 775        by every element in the second outer array, and return a new
 776        array of arrays with expanded axes. If 'pairwise_reversed', do
 777        the same thing, but use the axes of the second array first in
 778        the result.
 779
 780    Returns
 781    -------
 782    result : ndarray
 783
 784        result of matrix multiplication.
 785
 786        If `array1` and `array2` have shapes
 787        `([outer ndims 1], [inner ndims 1])` and
 788        `([outer ndims 2], [inner ndims 2])` (where
 789        `[inner ndims]` are tuples with length `unit_axis_1` and
 790        `unit_axis_2`, respectively) then:
 791
 792        - if `broadcast` is 'elementwise', `result` has shape
 793          `([result ndims], [product ndims])`, where `[result ndims]`
 794          is the result of broadcasting the outer ndims against each
 795          other, and `[product ndims]` is the result of broadcasting
 796          the inner ndims against each other.
 797
 798        - if `broadcast` is 'pairwise', result has shape `([outer
 799          ndims 1], [outer ndims 2], [product ndims])`.
 800
 801        - if `broadcast` is 'pairwise_reversed', result has shape
 802          `([outer ndims 2], [outer ndims 1], [product ndims])`
 803
 804    """
 805
 806    reshape1 = expand_unit_axes(array1, unit_axis_1, unit_axis_2)
 807    reshape2 = expand_unit_axes(array2, unit_axis_2, unit_axis_1)
 808
 809    if broadcast == "pairwise" or broadcast == "pairwise_reversed":
 810        large_axes = max(unit_axis_1, unit_axis_2)
 811
 812        excess1 = reshape1.ndim - large_axes
 813        excess2 = reshape2.ndim - large_axes
 814
 815        if excess1 > 0:
 816            if broadcast == "pairwise_reversed":
 817                reshape1 = np.expand_dims(reshape1,
 818                                          axis=tuple(range(excess1, excess1 + excess2)))
 819            else:
 820                reshape1 = np.expand_dims(reshape1,
 821                                          axis=tuple(range(excess2)))
 822
 823        if excess2 > 0:
 824            if broadcast == "pairwise_reversed":
 825                reshape2 = np.expand_dims(reshape2, axis=tuple(range(excess1)))
 826            else:
 827                reshape2 = np.expand_dims(reshape2,
 828                                          axis=tuple(range(excess2, excess1 + excess2)))
 829
 830    product = reshape1 @ reshape2
 831
 832    if unit_axis_1 < unit_axis_2:
 833        product = squeeze_excess(product, unit_axis_1, unit_axis_2)
 834
 835    return product
 836
 837def find_positive_functional(positive_points):
 838    """Find a dual vector which evaluates to a positive real on a given
 839       array of vectors, using scipy's linear programming routines.
 840
 841    Parameters
 842    ----------
 843    positive_points : ndarray
 844        array of vectors to find a dual vector for. If this is a
 845        2-dimensional array, then find a single dual vector which is
 846        positive when paired with every row of the array.
 847
 848        If the array has shape (..., k, n), then find an array of dual
 849        vectors such that the vectors in the array pair positively
 850        with the corresponding k vectors in positive_points.
 851
 852    Returns
 853    -------
 854    duals : ndarray
 855        array of dual vectors. If positive_points has shape (d1, ...,
 856        dj, k, n), then `duals` has shape (d1, ..., dj, n).
 857
 858    """
 859    dim = positive_points.shape[-1]
 860    codim = positive_points.shape[-2]
 861
 862    functionals = np.zeros(positive_points.shape[:-2] +
 863                           (positive_points.shape[-1],))
 864
 865    for ind in np.ndindex(positive_points.shape[:-2]):
 866        res = linprog(
 867            np.zeros(dim),
 868            -1 * positive_points[ind],
 869            -1 * np.ones(codim),
 870            bounds=(None, None))
 871
 872        if not res.success:
 873            return None
 874
 875        functionals[ind] = res.x
 876
 877    return normalize(functionals)
 878
 879def first_sign_switch(array):
 880    signs = np.sign(array)
 881    row_i, col_i = np.nonzero(signs != np.expand_dims(signs[..., 0], axis=-1))
 882    _, init_inds = np.unique(row_i, return_index=True)
 883    return col_i[init_inds]
 884
 885def circle_through(p1, p2, p3):
 886    """Get the unique circle passing through three points in the plane.
 887
 888    This does NOT check for colinearity and will just return nans in
 889    that case.
 890
 891    Parameters
 892    ----------
 893    p1, p2, p3 : ndarray of shape `(..., 2)`
 894        Euclidean coordinates of three points in the plane (or three
 895        arrays of points)
 896
 897    Returns
 898    -------
 899    tuple
 900        tuple of the form `(center, radius)`, where `center` is an
 901        ndarray containing Euclidean coordinates of the center of the
 902        determined circle, and `radius` is either a float or an
 903        ndarray containing the radius of the determined circle.
 904
 905    """
 906    t_p1 = p1 - p3
 907    t_p2 = p2 - p3
 908
 909    x1 = t_p1[..., 0]
 910    y1 = t_p1[..., 1]
 911
 912    x2 = t_p2[..., 0]
 913    y2 = t_p2[..., 1]
 914
 915    r_sq = np.stack([x1**2 + y1**2, x2**2 + y2**2], axis=-1)
 916    r_sq = np.expand_dims(r_sq, axis=-2)
 917
 918    mats = np.stack([t_p1, t_p2], axis=-1)
 919
 920    # we'll act on the right
 921    t_ctrs = np.squeeze(r_sq @ invert(mats), axis=-2)/2.
 922
 923    radii = np.linalg.norm(t_ctrs, axis=-1)
 924
 925    return (t_ctrs + p3, radii)
 926
 927def r_to_c(real_coords):
 928    return real_coords[..., 0] + real_coords[..., 1]*1.0j
 929
 930def c_to_r(cx_array):
 931    return cx_array.astype('complex').view('(2,)float')
 932
 933def order_eigs(eigenvalues, eigenvectors):
 934    """Sort eigenvalue/eigenvector tuples by increasing modulus.
 935
 936    This function accepts the eigenvalue/eigenvector data returned by
 937    the `np.linalg.eig` function.
 938
 939    Parameters
 940    ----------
 941    eigenvalues : ndarray
 942        Array of shape (..., n) representing eigenvalues of some
 943        matrices
 944    eigenvectors : ndarray
 945        Array of shape (..., n, n) representing eigenvectors of some
 946        matrices (as column vectors).
 947
 948    Returns
 949    -------
 950    tuple
 951        Tuple of the form `(eigvals, eigvecs)`, where both eigvals and
 952        eigvecs have the same data as the input arrays, but arranged
 953        so that eigenvalues and eigenvectors are in increasing order
 954        of modulus.
 955
 956    """
 957    eigorder = np.argsort(np.abs(eigenvalues), axis=-1)
 958    eigvecs = np.take_along_axis(eigenvectors, np.expand_dims(eigorder, axis=-2), axis=-1)
 959    eigvals = np.take_along_axis(eigenvalues, eigorder, axis=-1)
 960
 961    return eigvals, eigvecs
 962
 963def affine_disks_contain(cout, rout, cin, rin,
 964                         broadcast="elementwise"):
 965    if broadcast == "pairwise":
 966        pairwise_dists = np.linalg.norm(
 967            np.expand_dims(cout, 0) - np.expand_dims(cin, 1),
 968            axis=-1
 969        )
 970        radial_diffs = np.expand_dims(rout, 0) - np.expand_dims(rin, 1)
 971        return pairwise_dists < radial_diffs
 972
 973    return np.linalg.norm(cout - cin, axis=-1) < (rout - rin)
 974
 975
 976def disk_containments(c1, r1, c2, r2,
 977                      broadcast="elementwise"):
 978    if broadcast == "pairwise":
 979        pairwise_dists = np.linalg.norm(
 980            np.expand_dims(cout, 0) - np.expand_dims(cin, 1),
 981            axis=-1
 982        )
 983        radial_diff1 = np.expand_dims(rout, 0) - np.expand_dims(rin, 1)
 984        return (pairwise_dists < radial_diffs,
 985                pairwise_dists < -radial_diffs)
 986
 987    dists = np.linalg.norm(cout - cin, axis=-1)
 988    return (dists < (rout - rin),
 989            dists < (rin - rout))
 990
 991def disk_interactions(c1, r1, c2, r2,
 992                      broadcast="elementwise"):
 993    # WARNING: this will only work for Nx2 arrays
 994    if broadcast == "pairwise":
 995        pairwise_dists = np.linalg.norm(
 996            np.expand_dims(c1, 1) - np.expand_dims(c2, 0),
 997            axis=-1
 998        )
 999        radial_diff = np.expand_dims(r1, 1) - np.expand_dims(r2, 0)
1000        radial_sum = np.expand_dims(r1, 1) + np.expand_dims(r2, 0)
1001        return (pairwise_dists < radial_diff,
1002                pairwise_dists < -radial_diff,
1003                pairwise_dists < radial_sum)
1004
1005    dists = np.linalg.norm(c1 - c2, axis=-1)
1006
1007    return (dists < (r1 - r2),
1008            dists < (r2 - r1),
1009            dists < (r2 + r1))
1010
1011def indefinite_form(p, q, neg_first=True, **kwargs):
1012    n = p + q
1013
1014    mult = 1
1015    if neg_first:
1016        mult = -1
1017
1018    form_mat = zeros((n,n), **kwargs)
1019    form_mat[:p, :p] = mult * identity(p, **kwargs)
1020    form_mat[p:, p:] = -mult * identity(q, **kwargs)
1021
1022    return form_mat
1023
1024def symplectic_form(n, **kwargs):
1025    if n != n % 2:
1026        raise ValueError("Cannot construct a symplectic form in odd dimensions.")
1027
1028    m = n / 2
1029
1030    form_mat = zeros((n, n), **kwargs)
1031    form_mat[:m, m:] = -identity(m, **kwargs)
1032    form_mat[m:, :m] = identity(m, **kwargs)
1033
1034    return form_mat
1035
1036def symmetric_part(bilinear_form):
1037    return (bilinear_form + bilinear_form.swapaxes(-1, -2)) / 2
1038
1039def antisymmetric_part(bilinear_form):
1040    return (bilinear_form - bilinear_form.swapaxes(-1, -2)) / 2
1041
1042def matrix_func(func):
1043
1044    # get sage_func now, so if there's a name issue we'll throw an
1045    # error when the wrapped function is defined (rather than when
1046    # it's called)
1047    if SAGE_AVAILABLE:
1048        sage_func = getattr(sagewrap, func.__name__)
1049
1050    @functools.wraps(func)
1051    def wrapped(*args, **kwargs):
1052        if not SAGE_AVAILABLE:
1053            return func(*args, **kwargs)
1054
1055        return sage_func(*args, **kwargs)
1056
1057    return wrapped
1058
1059@matrix_func
1060def invert(mat):
1061    return np.linalg.inv(mat)
1062
1063@matrix_func
1064def kernel(mat):
1065    return numerical.svd_kernel(mat)
1066
1067@matrix_func
1068def eig(mat):
1069    return np.linalg.eig(mat)
1070
1071@matrix_func
1072def eigh(mat):
1073    return np.linalg.eigh(mat)
1074
1075@matrix_func
1076def det(mat):
1077    return np.linalg.det(mat)
1078
1079def check_type(base_ring=None, dtype=None, like=None,
1080               default_dtype='float64', integer_type=True):
1081
1082    default_ring = None
1083    if SAGE_AVAILABLE:
1084        default_ring = sagewrap.Integer
1085        if not integer_type:
1086            default_ring = sagewrap.SR
1087
1088    if like is not None:
1089        try:
1090            if dtype is None:
1091                dtype = like.dtype
1092        except AttributeError:
1093            if not types.is_linalg_type(like):
1094                dtype = np.dtype('O')
1095
1096    if base_ring is None and dtype == np.dtype('O') and SAGE_AVAILABLE:
1097        base_ring = default_ring
1098
1099    if base_ring is not None:
1100        if not SAGE_AVAILABLE:
1101            raise EnvironmentError(
1102                "Cannot specify base_ring unless running within sage"
1103            )
1104        if dtype is not None and dtype != np.dtype('object'):
1105            raise TypeError(
1106                "Cannot specify base_ring and dtype unless dtype is dtype('object')"
1107            )
1108        dtype = np.dtype('object')
1109    elif dtype is None:
1110        dtype = default_dtype
1111
1112    if not integer_type and np.can_cast(dtype, int):
1113        dtype = np.dtype('float64')
1114
1115    return (base_ring, dtype)
1116
1117def complex_type(**kwargs):
1118    base_ring, dtype = check_type(integer_type=False, **kwargs)
1119
1120    if np.can_cast(dtype, 'float'):
1121        return base_ring, np.dtype('complex')
1122
1123    return base_ring, dtype
1124
1125def guess_literal_ring(data):
1126    if not SAGE_AVAILABLE:
1127        return None
1128
1129    try:
1130        if data.dtype == np.dtype('O'):
1131            return sage.all.QQ
1132    except AttributeError:
1133        if not types.is_linalg_type(data):
1134            return sage.all.QQ
1135
1136    # this is maybe not Pythonic, but it's also not a mistake.
1137    return None
1138
1139def astype(val, dtype=None):
1140    if dtype is None:
1141        return val
1142
1143    return np.array(val).astype(dtype)
1144
1145def number(val, like=None, dtype=None, base_ring=None):
1146    if like is not None and dtype is not None:
1147        raise UserWarning(
1148            "Passing both 'like' and 'dtype' when specifying a number;"
1149            " 'like' parameter is ignored."
1150        )
1151
1152    if base_ring is not None:
1153        if dtype is not None:
1154            raise UserWarning(
1155                "Passing both 'base_ring' and 'dtype' when specifying a number;"
1156                " 'dtype' is ignored (assumed to be dtype('O'))"
1157            )
1158        dtype = np.dtype('O')
1159
1160        if not SAGE_AVAILABLE:
1161            raise UserWarning( "Specifying base_ring when sage is not"
1162            "available has no effect."  )
1163
1164    if not SAGE_AVAILABLE:
1165        return astype(val, dtype)
1166
1167    if dtype is None and like is not None:
1168        try:
1169            dtype = like.dtype
1170        except AttributeError:
1171            if not types.is_linalg_type(like):
1172                dtype = np.dtype('O')
1173
1174    if dtype == np.dtype('O'):
1175        if isinstance(val, int):
1176            # we use SR instead of Integer here, because numpy
1177            # sometimes silently converts sage Integers
1178            return sage.all.SR(val)
1179        if isinstance(val, float):
1180            return sage.all.SR(sage.all.QQ(val))
1181
1182    return astype(val, dtype)
1183
1184def wrap_elementary(func):
1185    @functools.wraps(func)
1186    def wrapped(*args, like=None, base_ring=None, dtype=None, **kwargs):
1187        if not SAGE_AVAILABLE:
1188            return func(*args, **kwargs)
1189
1190        else:
1191            packaged_args = []
1192            arg_ring = None
1193            if base_ring is not None:
1194                arg_ring = sagewrap.SR
1195
1196            for arg in args:
1197                arg_like = like
1198                if arg_like is None:
1199                    arg_like = arg
1200
1201                packaged_args.append(
1202                    array_like(arg, like=arg_like,
1203                               base_ring=arg_ring,
1204                               dtype=dtype,
1205                               integer_type=False)
1206                )
1207
1208            return sagewrap.change_base_ring(
1209                func(*packaged_args, **kwargs),
1210                base_ring
1211            )
1212
1213    return wrapped
1214
1215@wrap_elementary
1216def power(base, exp):
1217    return np.power(base, exp)
1218
1219@wrap_elementary
1220def cos(arg):
1221    return np.cos(arg)
1222
1223@wrap_elementary
1224def sin(arg):
1225    return np.sin(arg)
1226
1227@wrap_elementary
1228def conjugate(arg):
1229    return np.conjugate(arg)
1230
1231def real(arg):
1232    if not SAGE_AVAILABLE or types.is_linalg_type(arg):
1233        return np.real(arg)
1234
1235    # np.real doesn't work well with sage since it expects the real
1236    # attribute to be a property, not a callable. So we need to wrap
1237    # sage's real function explicitly.
1238    return sagewrap.real(arg)
1239
1240def imag(arg):
1241    if not SAGE_AVAILABLE or types.is_linalg_type(arg):
1242        return np.imag(arg)
1243
1244    return sagewrap.imag(arg)
1245
1246def array_like(array, like=None, base_ring=None, dtype=None,
1247               integer_type=False, **kwargs):
1248
1249    if like is None:
1250        like = array
1251
1252    base_ring, dtype = check_type(base_ring, dtype, like,
1253                                  integer_type=integer_type)
1254
1255    arr = np.array(array, dtype=dtype, **kwargs)
1256
1257    if base_ring is not None:
1258        return sagewrap.change_base_ring(arr, base_ring)
1259    return arr
1260
1261def pi(**kwargs):
1262    if not SAGE_AVAILABLE:
1263        return np.pi
1264
1265    base_ring, dtype = check_type(**kwargs)
1266
1267    if base_ring is not None:
1268        return sagewrap.pi
1269
1270    return np.pi
1271
1272def unit_imag(**kwargs):
1273    if not SAGE_AVAILABLE:
1274        return 1j
1275
1276    base_ring, dtype = check_type(**kwargs)
1277
1278    if base_ring is not None:
1279        return sagewrap.I
1280    return 1j
1281
1282
1283def zeros(shape, base_ring=None, dtype=None, like=None,
1284          integer_type=True, **kwargs):
1285
1286    base_ring, dtype = check_type(base_ring, dtype, like,
1287                                  integer_type=integer_type)
1288
1289    zero_arr = np.zeros(shape, dtype=dtype, **kwargs)
1290    if base_ring is not None:
1291        return sagewrap.change_base_ring(zero_arr, base_ring)
1292    return zero_arr
1293
1294def ones(shape, base_ring=None, dtype=None, like=None,
1295         integer_type=True, **kwargs):
1296    base_ring, dtype = check_type(base_ring, dtype, like,
1297                                  integer_type=integer_type)
1298
1299    ones_arr = np.ones(shape, dtype=dtype, **kwargs)
1300    if base_ring is not None:
1301        return sagewrap.change_base_ring(ones_arr, base_ring)
1302    return ones_arr
1303
1304def identity(n, base_ring=None, dtype=None, like=None,
1305             integer_type=True, **kwargs):
1306    base_ring, dtype = check_type(base_ring, dtype, like,
1307                                  integer_type=integer_type)
1308
1309    identity_arr = np.identity(n, dtype=dtype, **kwargs)
1310
1311    if base_ring is not None:
1312        return sagewrap.change_base_ring(identity_arr, base_ring)
1313    return identity_arr
1314
1315def change_base_ring(array, base_ring=None,
1316                     rational_approx=False):
1317    if base_ring is None:
1318        return array
1319
1320    if not SAGE_AVAILABLE:
1321        raise EnvironmentError(
1322            "Cannot change base ring unless sage is available."
1323        )
1324
1325    return sagewrap.change_base_ring(array, base_ring,
1326                                     rational_approx=rational_approx)
def rotation_matrix(angle, like=None, **kwargs):
18def rotation_matrix(angle, like=None, **kwargs):
19    r"""Get a 2x2 rotation matrix rotating counterclockwise by the
20    specified angle.
21
22    Parameters
23    ----------
24    angle : float
25        angle to rotate by
26
27    Returns
28    -------
29    ndarray
30        numpy array of shape (2,2) of the form
31        $\begin{pmatrix}\cos \theta & -\sin \theta\\\sin\theta & \cos \theta
32        \end{pmatrix}$
33
34    """
35    if like is None:
36        like = angle
37    return array_like([[cos(angle), -1*sin(angle)],
38                       [sin(angle), cos(angle)]],
39                      like=like, **kwargs)

Get a 2x2 rotation matrix rotating counterclockwise by the specified angle.

Parameters
  • angle (float): angle to rotate by
Returns
  • ndarray: numpy array of shape (2,2) of the form (cosθsinθsinθcosθ)
def permutation_matrix(permutation, inverse=False, **kwargs):
41def permutation_matrix(permutation, inverse=False, **kwargs):
42    """Return a permutation matrix representing the given permutation.
43
44    Parameters
45    ----------
46    permutation: iterable
47        a sequence of n numbers (indices), specifying a permutation of
48        (1, ... n).
49    inverse : bool
50        if True, return the matrix for the inverse of the
51        permutation specified.
52
53    Returns
54    -------
55    ndarray
56        square 2-dimensional array, giving a permutation matrix.
57
58    """
59    n = len(permutation)
60    p_mat = zeros((n, n), **kwargs)
61    one = number(1, **kwargs)
62
63    for i,j in enumerate(permutation):
64        if inverse:
65            p_mat[j,i] = one
66        else:
67            p_mat[i,j] = one
68
69    return p_mat

Return a permutation matrix representing the given permutation.

Parameters
  • permutation (iterable): a sequence of n numbers (indices), specifying a permutation of (1, ... n).
  • inverse (bool): if True, return the matrix for the inverse of the permutation specified.
Returns
  • ndarray: square 2-dimensional array, giving a permutation matrix.
def conjugate_by_permutation(matrix, permutation, inverse=False):
71def conjugate_by_permutation(matrix, permutation, inverse=False):
72    result = zeros(matrix.shape, like=matrix)
73
74    for i, si in enumerate(np.moveaxis(permutation, -1, 0)):
75        res_col = zeros(matrix.shape[:-1], like=matrix)
76        i_take = np.expand_dims(si, (-2, -1))
77        mat_col = np.take_along_axis(matrix, i_take, axis=-1)
78        for j, sj in enumerate(np.moveaxis(permutation, -1, 0)):
79            j_take = np.expand_dims(sj, (-2, -1))
80            val = np.take_along_axis(mat_col, j_take, axis=-2)
81            res_col[..., j] = np.squeeze(val, (-1, -2))
82        result[..., i] = res_col
83
84    return result
def permute_along_axis(matrix, permutation, axis, inverse=False):
86def permute_along_axis(matrix, permutation, axis, inverse=False):
87    result = zeros(matrix.shape, like=matrix).swapaxes(-1, axis)
88    for i, si in enumerate(np.moveaxis(permutation, -1, 0)):
89        p_ind = np.expand_dims(si, (-2, -1))
90        if inverse:
91            values = np.take_along_axis(matrix.swapaxes(-1, axis), p_ind, axis=-1)
92            result[..., i] = values.squeeze(axis=-1)
93        else:
94            values = np.expand_dims(matrix.swapaxes(-1, axis)[..., i], -1)
95            np.put_along_axis(result, p_ind, values, axis=-1)
96
97    return result.swapaxes(-1, axis)
def construct_diagonal(diags):
 99def construct_diagonal(diags):
100    n = diags.shape[-1]
101    result = zeros(diags.shape[:-1] + (n * n,), like=diags)
102    result[..., ::n+1] = diags
103    return result.reshape(diags.shape[:-1] + (n, n))
def diagonalize_form( bilinear_form, order_eigenvalues='signed', reverse=False, with_inverse=True):
105def diagonalize_form(bilinear_form,
106                     order_eigenvalues="signed",
107                     reverse=False,
108                     with_inverse=True):
109    r"""Return a matrix conjugating a symmetric real bilinear form to a
110    diagonal form.
111
112    Parameters
113    ----------
114    bilinear_form: ndarray
115        numpy array of shape (n, n) representing, a symmetric bilinear
116        form in standard coordinates
117    order_eigenvalues: {'signed', 'minkowski'}
118        If "signed" (the default), conjugate to a diagonal bilinear
119        form on R^n where the basis vectors are ordered in order of
120        increasing eigenvalue.
121
122        If "minkowski", and the bilinear form has signature (p, q, r)
123        (meaning that a maximal positive definite subspace has
124        dimension p, a maximal negative definite subspace has
125        dimension q, and r = n - p - q), then conjugate to a bilinear
126        form where the basis vectors are ordered so that spacelike
127        (positive) vectors come first if p <= q and timelike
128        (negative) basis vectors come first if q < p.
129
130    reverse : bool
131        if True, reverse the order of the basis vectors for the
132        diagonal bilinear form (from the order specified by
133        `order_eigenvalues`).
134
135    Returns
136    -------
137    ndarray
138        numpy array of shape (n, n), representing a coordinate change
139        taking the given bilinear form to a diagonal form. If $B$ is
140        the matrix given by bilinear_form, and $D$ is a diagonal
141        matrix with the same signature as $B$, then this function
142        returns a matrix $M$ such that $M^TDM = B$.
143
144    """
145
146    n = bilinear_form.shape[-1]
147
148    eigs, U = eigh(bilinear_form)
149    Uinv = invert(U)
150
151    Dinv = construct_diagonal(np.sqrt(np.abs(eigs)))
152    D = zeros(Dinv.shape, like=Dinv)
153
154    close_to_zero = np.isclose(Dinv.astype('float64'), 0.)
155    np.divide(1, Dinv, out=D, where=~close_to_zero)
156
157    n_eigs = eigs.astype('float64')
158
159    W = U @ D
160
161    if order_eigenvalues:
162        if order_eigenvalues == "signed":
163            order = np.argsort(n_eigs, axis=-1)
164        if order_eigenvalues == "minkowski":
165            sort_indices = np.zeros_like(n_eigs)
166            num_positive = np.count_nonzero(n_eigs < 0, axis=-1)
167            num_negative = np.count_nonzero(n_eigs > 0, axis=-1)
168
169            flip = np.ones_like(n_eigs)
170            flip[num_negative < num_positive] = -1
171
172            sort_indices[n_eigs < 0] = -1 * flip[n_eigs < 0]
173            sort_indices[n_eigs > 0] = flip[n_eigs > 0]
174            sort_indices[n_eigs == 0] == 2
175
176            order = np.argsort(sort_indices, axis=-1)
177        if reverse:
178            order = np.flip(order)
179
180        W = permute_along_axis(W, order, axis=-1, inverse=True)
181
182    if not with_inverse:
183        return W
184
185    Winv = Dinv @ Uinv
186    if order_eigenvalues:
187        Winv = permute_along_axis(Winv, order, axis=-2)
188
189    return (W, Winv)

Return a matrix conjugating a symmetric real bilinear form to a diagonal form.

Parameters
  • bilinear_form (ndarray): numpy array of shape (n, n) representing, a symmetric bilinear form in standard coordinates
  • order_eigenvalues ({'signed', 'minkowski'}): If "signed" (the default), conjugate to a diagonal bilinear form on R^n where the basis vectors are ordered in order of increasing eigenvalue.

    If "minkowski", and the bilinear form has signature (p, q, r) (meaning that a maximal positive definite subspace has dimension p, a maximal negative definite subspace has dimension q, and r = n - p - q), then conjugate to a bilinear form where the basis vectors are ordered so that spacelike (positive) vectors come first if p <= q and timelike (negative) basis vectors come first if q < p.

  • reverse (bool): if True, reverse the order of the basis vectors for the diagonal bilinear form (from the order specified by order_eigenvalues).
Returns
  • ndarray: numpy array of shape (n, n), representing a coordinate change taking the given bilinear form to a diagonal form. If B is the matrix given by bilinear_form, and D is a diagonal matrix with the same signature as B, then this function returns a matrix M such that MTDM=B.
def circle_angles(center, coords):
191def circle_angles(center, coords):
192    """Return angles relative to the center of a circle.
193
194    Parameters
195    ----------
196    center: ndarray
197        numpy array with shape (..., 2) representing x,y coordinates
198        the centers of some circles.
199    coords: ndarray
200        numpy array with shape (..., 2) representing x,y coordinates
201        of some points.
202
203    Returns
204    -------
205    ndarray
206        angles (relative to x-axis) of each of the pair of points
207        specified by coords.
208
209    """
210    xs = (coords - np.expand_dims(center, axis=-2))[..., 0]
211    ys = (coords - np.expand_dims(center, axis=-2))[..., 1]
212
213    return np.arctan2(ys, xs)

Return angles relative to the center of a circle.

Parameters
  • center (ndarray): numpy array with shape (..., 2) representing x,y coordinates the centers of some circles.
  • coords (ndarray): numpy array with shape (..., 2) representing x,y coordinates of some points.
Returns
  • ndarray: angles (relative to x-axis) of each of the pair of points specified by coords.
def apply_bilinear(v1, v2, bilinear_form=None, broadcast='elementwise'):
215def apply_bilinear(v1, v2, bilinear_form=None,
216                   broadcast="elementwise"):
217    """Apply a bilinear form to a pair of arrays of vectors.
218
219    Parameters
220    ----------
221    v1, v2: ndarray
222        ndarrays of shape (..., n) giving arrays of vectors.
223    bilinear_form: ndarray
224        ndarray of shape (n, n) specifying a matrix representing a
225        symmetric bilinear form to use to pair v1 and v2. If
226        `None`, use the standard (Euclidean) bilinear form on R^n.
227
228    Returns
229    -------
230    ndarray
231        ndarray representing the result of pairing the vectors in v1
232        with v2.
233
234    """
235
236    intermed = np.expand_dims(v1, -2)
237
238    if bilinear_form is not None:
239        intermed = matrix_product(
240            np.expand_dims(v1, -2), bilinear_form,
241            broadcast=broadcast
242        )
243    prod = matrix_product(
244        intermed, np.expand_dims(v2, -1)
245    )
246
247    return prod.squeeze((-1, -2))
248
249    #return np.array(((v1 @ bilinear_form) * v2).sum(-1))

Apply a bilinear form to a pair of arrays of vectors.

Parameters
  • v1, v2 (ndarray): ndarrays of shape (..., n) giving arrays of vectors.
  • bilinear_form (ndarray): ndarray of shape (n, n) specifying a matrix representing a symmetric bilinear form to use to pair v1 and v2. If None, use the standard (Euclidean) bilinear form on R^n.
Returns
  • ndarray: ndarray representing the result of pairing the vectors in v1 with v2.
def normsq(vectors, bilinear_form=None):
251def normsq(vectors, bilinear_form=None):
252    """Evaluate the norm squared of an array of vectors, with respect to a
253       bilinear form.
254
255    Equivalent to a call of apply_bilinear(vectors, vectors, bilinear_form).
256
257    """
258    return apply_bilinear(vectors, vectors, bilinear_form)

Evaluate the norm squared of an array of vectors, with respect to a bilinear form.

Equivalent to a call of apply_bilinear(vectors, vectors, bilinear_form).

def normalize(vectors, bilinear_form=None):
260def normalize(vectors, bilinear_form=None):
261    """Normalize an array of vectors, with respect to a bilinear form.
262
263    Parameters
264    ----------
265    vectors: ndarray
266        ndarray of shape (..., n)
267    bilinear_form: ndarray or None
268        bilinear form to use to evaluate the norms in vectors. If
269        None, use the standard (Euclidean) bilinear form on R^n.
270
271    Returns
272    -------
273    ndarray
274        ndarray with the same shape as vectors. Each vector in this
275        array has norm either +1 or -1, depending on whether the
276        original vector had positive or negative square-norm (with
277        respect to the given bilinear form).
278
279    """
280    sq_norms = normsq(vectors, bilinear_form)
281
282    abs_norms = np.sqrt(np.abs(np.expand_dims(sq_norms, axis=-1)))
283
284    return np.divide(vectors, abs_norms, out=vectors,
285                     where=(abs_norms.astype('float64') != 0))

Normalize an array of vectors, with respect to a bilinear form.

Parameters
  • vectors (ndarray): ndarray of shape (..., n)
  • bilinear_form (ndarray or None): bilinear form to use to evaluate the norms in vectors. If None, use the standard (Euclidean) bilinear form on R^n.
Returns
  • ndarray: ndarray with the same shape as vectors. Each vector in this array has norm either +1 or -1, depending on whether the original vector had positive or negative square-norm (with respect to the given bilinear form).
def short_arc(thetas):
287def short_arc(thetas):
288    """Reorder angles so that the counterclockwise arc between them is
289    shorter than the clockwise angle.
290
291    Parameters
292    ----------
293    thetas: ndarray
294        numpy array of shape (..., 2), giving ordered pairs of angles
295        in the range (-2pi, 2pi)
296
297    Returns
298    -------
299    ndarray
300        numpy array with the same shape as thetas. The ordered pairs
301        in this array are arranged so that for each pair `(a, b)`, the
302        counterclockwise arc from `a` to `b` is shorter than the
303        counterclockwise arc from `b` to `a`.
304
305    """
306    shifted_thetas = np.copy(thetas)
307
308    shifted_thetas[shifted_thetas < 0] += 2 * np.pi
309    shifted_thetas.sort(axis=-1)
310
311    to_flip = shifted_thetas[...,1] - shifted_thetas[...,0] > np.pi
312    shifted_thetas[to_flip] = np.flip(shifted_thetas[to_flip], axis=-1)
313
314    return shifted_thetas

Reorder angles so that the counterclockwise arc between them is shorter than the clockwise angle.

Parameters
  • thetas (ndarray): numpy array of shape (..., 2), giving ordered pairs of angles in the range (-2pi, 2pi)
Returns
  • ndarray: numpy array with the same shape as thetas. The ordered pairs in this array are arranged so that for each pair (a, b), the counterclockwise arc from a to b is shorter than the counterclockwise arc from b to a.
def right_to_left(thetas):
316def right_to_left(thetas):
317    """Reorder angles so that the counterclockwise arc goes right to left.
318
319    Parameters
320    ----------
321    thetas: ndarray
322        numpy array of shape (..., 2), giving ordered pairs of angles.
323
324    Returns
325    -------
326    ndarray
327        numpy array with the same shape as `thetas`. The ordered pairs
328        in this array are arranged so that for each pair `(a, b)`, the
329        cosine of `b` is at most the cosine of `a`.
330
331    """
332    flipped_thetas = np.copy(thetas)
333
334    to_flip = np.cos(thetas[..., 0]) < np.cos(thetas[..., 1])
335    flipped_thetas[to_flip] = np.flip(thetas[to_flip], axis=-1)
336
337    return flipped_thetas

Reorder angles so that the counterclockwise arc goes right to left.

Parameters
  • thetas (ndarray): numpy array of shape (..., 2), giving ordered pairs of angles.
Returns
  • ndarray: numpy array with the same shape as thetas. The ordered pairs in this array are arranged so that for each pair (a, b), the cosine of b is at most the cosine of a.
def arc_include(thetas, reference_theta):
340def arc_include(thetas, reference_theta):
341    """Reorder angles so that the counterclockwise arc between them always
342    includes some reference point on the circle.
343
344    Parameters
345    ----------
346    thetas : ndarray(float)
347        pairs of angles in the range (-2pi, 2pi).
348    reference_theta: ndarray(float)
349         angles in the range (-2pi, 2pi)
350
351    Returns
352    -------
353    theta_p : ndarray(float)
354        pairs of angles of the same shape as `thetas`, so that
355        `reference_theta` lies in the counterclockwise angle between
356        `theta_p[..., 0]` and `theta_p[..., 1]`.
357
358    """
359
360    s_thetas = np.copy(thetas)
361    s_theta1 = thetas[..., 1] - thetas[..., 0]
362    s_reference = np.expand_dims(reference_theta - thetas[..., 0],
363                                 axis=-1)
364
365    s_theta1[s_theta1 < 0] += 2 * np.pi
366    s_reference[s_reference < 0] += 2 * np.pi
367
368    to_swap = (s_theta1 < s_reference[..., 0])
369
370    s_thetas[to_swap] = np.flip(s_thetas[to_swap], axis=-1)
371    return s_thetas

Reorder angles so that the counterclockwise arc between them always includes some reference point on the circle.

Parameters
  • thetas (ndarray(float)): pairs of angles in the range (-2pi, 2pi).
  • reference_theta (ndarray(float)): angles in the range (-2pi, 2pi)
Returns
  • theta_p (ndarray(float)): pairs of angles of the same shape as thetas, so that reference_theta lies in the counterclockwise angle between theta_p[..., 0] and theta_p[..., 1].
def sphere_inversion(points):
373def sphere_inversion(points):
374    r"""Apply unit sphere inversion to points in R^n.
375
376    This realizes the map $v \mapsto v / ||v||^2$.
377
378    Parameters
379    ----------
380    points : ndarray
381        Array of shape `(..., n)` giving a set of points in R^n
382
383    Returns
384    -------
385    ndarray
386        The image of the `points` array under sphere inversion.
387
388    """
389    with np.errstate(divide="ignore", invalid="ignore"):
390        return (points.T / (normsq(points)).T).T

Apply unit sphere inversion to points in R^n.

This realizes the map vv/||v||2.

Parameters
  • points (ndarray): Array of shape (..., n) giving a set of points in R^n
Returns
  • ndarray: The image of the points array under sphere inversion.
def swap_matrix(i, j, n):
392def swap_matrix(i, j, n):
393    """Return a permutation matrix representing a single transposition.
394
395    Parameters
396    ----------
397    i, j : int
398        Indices swapped by a transposition.
399    n : int
400        dimension of the permutation matrix.
401
402    Returns
403    -------
404    ndarray
405        Array of shape `(n, n)` giving a transposition matrix.
406
407    """
408    permutation = list(range(n))
409    permutation[i] = j
410    permutation[j] = i
411    return permutation_matrix(permutation)

Return a permutation matrix representing a single transposition.

Parameters
  • i, j (int): Indices swapped by a transposition.
  • n (int): dimension of the permutation matrix.
Returns
  • ndarray: Array of shape (n, n) giving a transposition matrix.
def projection(v1, v2, bilinear_form):
413def projection(v1, v2, bilinear_form):
414    r"""Return the projection of `v1` onto `v2` with respect to some
415    bilinear form.
416
417    The returned vector `w` is parallel to `v2`, and `v1 - w` is
418    orthogonal to `v2` with respect to the given bilinear form.
419    `w` is determined by the formula
420    $$
421    v_2 \cdot \langle v_1, v_2 \rangle / \langle v_2, v_2 \rangle,
422    $$
423    where $\langle \cdot, \cdot \rangle$ is the pairing determined by
424    `bilinear_form`.
425
426    Parameters
427    ----------
428    v1 : ndarray
429        vector in R^n to project
430    v2 : ndarray
431        vector in R^n to project onto
432    bilinear_form : ndarray
433        array of shape `(n, n)` giving a bilinear form to use for the
434        projection.
435
436    Returns
437    -------
438    w : ndarray
439        vector in R^n giving the projection of `v1` onto `v2`.
440
441    """
442
443    return (v2.T *
444            apply_bilinear(v1, v2, bilinear_form).T /
445            normsq(v2, bilinear_form).T).T

Return the projection of v1 onto v2 with respect to some bilinear form.

The returned vector w is parallel to v2, and v1 - w is orthogonal to v2 with respect to the given bilinear form. w is determined by the formula v2v1,v2/v2,v2, where , is the pairing determined by bilinear_form.

Parameters
  • v1 (ndarray): vector in R^n to project
  • v2 (ndarray): vector in R^n to project onto
  • bilinear_form (ndarray): array of shape (n, n) giving a bilinear form to use for the projection.
Returns
  • w (ndarray): vector in R^n giving the projection of v1 onto v2.
def orthogonal_complement(vectors, form=None, normalize='form'):
447def orthogonal_complement(vectors, form=None, normalize="form"):
448    """Find an orthogonal complement with respect to a nondegenerate (but
449       possibly indefinite) bilinear form.
450
451    Parameters
452    ----------
453    vectors : ndarray of shape (..., k, n)
454        array of k row vectors for which to find a complement. Each
455        set of k row vectors should be linearly independent.
456
457    form : ndarray of shape `(n, n)`
458        bilinear form to use to find complements. If None (the
459        default), use the standard Euclidean form on R^n.
460
461    normalize : One of {'form', 'euclidean'} or None
462        How to normalize the vectors spanning the orthogonal
463        complement. If 'form' (the default), attempt to return vectors
464        which have unit length with respect to `form.` (Note this may
465        fail if `form` is indefinite, i.e. there are nonzero null
466        vectors. If 'euclidean', then use the standard Euclidean form
467        on R^n to normalize the vectors.
468
469    Returns
470    -------
471    result : ndarray with shape (..., n - k, n)
472        array of n-k row vectors, each of which is orthogonal to all
473        of the k row vectors provided (with respect to `form`).
474
475    """
476    if form is None:
477        form = np.identity(vectors.shape[-1])
478
479    kernel_basis = kernel(vectors @ form).swapaxes(-1, -2)
480
481    if normalize == 'form':
482        return indefinite_orthogonalize(form, kernel_basis)
483
484    return kernel_basis

Find an orthogonal complement with respect to a nondegenerate (but possibly indefinite) bilinear form.

Parameters
  • vectors (ndarray of shape (..., k, n)): array of k row vectors for which to find a complement. Each set of k row vectors should be linearly independent.
  • form (ndarray of shape (n, n)): bilinear form to use to find complements. If None (the default), use the standard Euclidean form on R^n.
  • normalize (One of {'form', 'euclidean'} or None): How to normalize the vectors spanning the orthogonal complement. If 'form' (the default), attempt to return vectors which have unit length with respect to form. (Note this may fail if form is indefinite, i.e. there are nonzero null vectors. If 'euclidean', then use the standard Euclidean form on R^n to normalize the vectors.
Returns
  • result (ndarray with shape (..., n - k, n)): array of n-k row vectors, each of which is orthogonal to all of the k row vectors provided (with respect to form).
def indefinite_orthogonalize(form, matrices):
486def indefinite_orthogonalize(form, matrices):
487    """Apply the Gram-Schmidt algorithm, but for a possibly indefinite
488    bilinear form.
489
490    Parameters
491    ----------
492    form : ndarray of shape `(n,n)`
493        bilinear form to orthogonalize with respect to
494
495    matrices : ndarray of shape `(..., k, n)`
496        array of k row vectors to orthogonalize.
497
498    Returns
499    -------
500    result : ndarray
501        array with the same shape as `matrices`. The last two
502        dimensions of this array give matrices with mutually
503        orthogonal rows, with respect to the given bilinear form.
504
505        For all `j <= k`, the subspace spanned by the first `j` rows
506        of `result` is the same as the subspace spanned by the first
507        `j` rows of `matrices`.
508
509    """
510    if len(matrices.shape) < 2:
511        return normalize(matrices, form)
512
513    n, m = matrices.shape[-2:]
514
515    result = zeros(matrices.shape,
516                   like=matrices,
517                   integer_type=False)
518
519    #we're using a python for loop, but only over dimension^2 which
520    #is probably small
521
522    for i in range(n):
523        row = matrices[..., i, :]
524        for j in range(i):
525            row -= projection(row, result[..., j, :], form)
526        result[..., i, :] = row
527
528    return normalize(result, form)

Apply the Gram-Schmidt algorithm, but for a possibly indefinite bilinear form.

Parameters
  • form (ndarray of shape (n,n)): bilinear form to orthogonalize with respect to
  • matrices (ndarray of shape (..., k, n)): array of k row vectors to orthogonalize.
Returns
  • result (ndarray): array with the same shape as matrices. The last two dimensions of this array give matrices with mutually orthogonal rows, with respect to the given bilinear form.

    For all j <= k, the subspace spanned by the first j rows of result is the same as the subspace spanned by the first j rows of matrices.

def find_isometry(form, partial_map, force_oriented=False):
530def find_isometry(form, partial_map, force_oriented=False):
531    """find a form-preserving matrix agreeing with a specified map on
532    the flag defined by the standard basis.
533
534    Parameters
535    ----------
536    form : ndarray of shape `(n, n)`
537        the bilinear map to preserve
538
539    partial_map : ndarray of shape `(..., k, n)`
540        array representing the images of the first k standard basis
541        vectors (row vectors)
542
543    force_oriented : boolean
544        whether we should apply a reflection to force the resulting
545        map to be orientation-preserving.
546
547    Returns
548    -------
549    ndarray
550        array of shape `(..., n, n)` representing an array of matrices
551        whose rows and columns are "orthonormal" with respect to the
552        bilinear form (since the form may be indefinite, "normal"
553        vectors may have norm -1).
554
555        For all `j <= k`, the subspace spanned by the first `j`
556        standard basis vectors is sent to the subspace spanned by the
557        first `j` rows of the result.
558
559    """
560
561    orth_partial = indefinite_orthogonalize(form, partial_map)
562
563    if len(orth_partial.shape) < 2:
564        orth_partial = np.expand_dims(orth_partial, axis=0)
565
566    kernel_basis = kernel(orth_partial @ form).swapaxes(-1, -2)
567
568    orth_kernel = indefinite_orthogonalize(form, kernel_basis)
569
570    iso = np.concatenate([orth_partial, orth_kernel], axis=-2)
571
572    if force_oriented:
573        iso = make_orientation_preserving(iso)
574
575    return iso

find a form-preserving matrix agreeing with a specified map on the flag defined by the standard basis.

Parameters
  • form (ndarray of shape (n, n)): the bilinear map to preserve
  • partial_map (ndarray of shape (..., k, n)): array representing the images of the first k standard basis vectors (row vectors)
  • force_oriented (boolean): whether we should apply a reflection to force the resulting map to be orientation-preserving.
Returns
  • ndarray: array of shape (..., n, n) representing an array of matrices whose rows and columns are "orthonormal" with respect to the bilinear form (since the form may be indefinite, "normal" vectors may have norm -1).

For all j <= k, the subspace spanned by the first j standard basis vectors is sent to the subspace spanned by the first j rows of the result.

def find_definite_isometry(partial_map, force_oriented=False):
577def find_definite_isometry(partial_map, force_oriented=False):
578    """find an orthogonal matrix agreeing with a specified map on
579    the flag defined by the standard basis.
580
581    Parameters
582    ----------
583    partial_map : ndarray of shape `(..., k, n)`
584        array representing the images of the first k standard basis
585        vectors (row vectors)
586
587    force_oriented : boolean
588        whether we should apply a reflection to force the resulting
589        map to be orientation-preserving.
590
591    Returns
592    -------
593    ndarray
594        array of shape `(..., n, n)` representing an array of matrices
595        with orthonormal rows and columns.
596
597        For all `j <= k`, the subspace spanned by the first `j`
598        standard basis vectors is sent to the subspace spanned by the
599        first `j` rows of the result.
600
601    """
602    pmap = np.array(partial_map)
603    if len(pmap.shape) < 2:
604        pmap = pmap.reshape((len(pmap), 1))
605    h, w = pmap.shape[-2:]
606    n = max(h, w)
607    if w > h:
608        mat = np.concatenate([pmap.swapaxes(-1, -2),
609                             np.identity(n)], axis=-1)
610    else:
611        mat = np.concatenate([pmap, np.identity(n)], axis=-1)
612
613    q, r = np.linalg.qr(mat)
614
615    iso = np.sign(r[..., 0,0]) * q
616
617    if force_oriented:
618        iso = make_orientation_preserving(iso)
619
620    return iso

find an orthogonal matrix agreeing with a specified map on the flag defined by the standard basis.

Parameters
  • partial_map (ndarray of shape (..., k, n)): array representing the images of the first k standard basis vectors (row vectors)
  • force_oriented (boolean): whether we should apply a reflection to force the resulting map to be orientation-preserving.
Returns
  • ndarray: array of shape (..., n, n) representing an array of matrices with orthonormal rows and columns.

For all j <= k, the subspace spanned by the first j standard basis vectors is sent to the subspace spanned by the first j rows of the result.

def make_orientation_preserving(matrix):
622def make_orientation_preserving(matrix):
623    """apply a reflection to the last row of matrix to make it orientation
624    preserving.
625
626    if matrix is already orientation preserving, do nothing.
627
628    Parameters
629    ----------
630    matrix : ndarray of shape `(..., n, n)`
631        ndarray of linear maps
632
633    Returns
634    -------
635    result : ndarray
636        array with the same shape as `matrices`, representating an
637        ndarray of linear maps. If `A` is an orientation-reversing
638        matrix in `matrices`, then the corresponding matrix in
639        `result` has its last row negated.
640    """
641    preserved = matrix.copy()
642    preserved[det(preserved) < 0, -1, :] *= -1
643    return preserved

apply a reflection to the last row of matrix to make it orientation preserving.

if matrix is already orientation preserving, do nothing.

Parameters
  • matrix (ndarray of shape (..., n, n)): ndarray of linear maps
Returns
  • result (ndarray): array with the same shape as matrices, representating an ndarray of linear maps. If A is an orientation-reversing matrix in matrices, then the corresponding matrix in result has its last row negated.
def expand_unit_axes(array, unit_axes, new_axes):
645def expand_unit_axes(array, unit_axes, new_axes):
646    """expand the last axes of an array to make it suitable for ndarray
647    pairwise multiplication.
648
649    `array` is an ndarray, viewed as an array of arrays, each of which
650    has `unit_axes` axes. That is, its shape decomposes into a pair of
651    tuples `([axes 1], [axes 2])`, where `[axes 2]` is a tuple of
652    length `unit_axes`.
653
654    Parameters
655    ----------
656    array : ndarray
657        ndarray to expand
658    unit_axes : int
659        number of axes of `array` to treat as a "unit"
660    new_axes : int
661        number of axes to add to the array
662
663    Returns
664    -------
665    ndarray
666        ndarray of shape `([object axes], 1, ..., 1, [unit axes])`,
667        where the number of 1's is either `new_axes - unit_axes`, or 0
668        if this is negative.
669
670    """
671    if new_axes <= unit_axes:
672        return array
673
674    return np.expand_dims(array.T, axis=tuple(range(unit_axes, new_axes))).T

expand the last axes of an array to make it suitable for ndarray pairwise multiplication.

array is an ndarray, viewed as an array of arrays, each of which has unit_axes axes. That is, its shape decomposes into a pair of tuples ([axes 1], [axes 2]), where [axes 2] is a tuple of length unit_axes.

Parameters
  • array (ndarray): ndarray to expand
  • unit_axes (int): number of axes of array to treat as a "unit"
  • new_axes (int): number of axes to add to the array
Returns
  • ndarray: ndarray of shape ([object axes], 1, ..., 1, [unit axes]), where the number of 1's is either new_axes - unit_axes, or 0 if this is negative.
def squeeze_excess(array, unit_axes, other_unit_axes):
676def squeeze_excess(array, unit_axes, other_unit_axes):
677    """Squeeze all excess axes from an ndarray of arrays with unit_axes axes.
678
679    This undoes expand_unit_axes.
680
681    Parameters
682    ----------
683    array : ndarray
684        ndarray of shape
685        `([object axes], [excess axes], [unit axes])`, where `[unit axes]`
686        is a tuple of length `unit_axes`, and `[excess axes]` is a
687        tuple of length `other_unit_axes - unit_axes`.
688    unit_axes : int
689        number of axes to view as "units" in `array`. That is, `array`
690        is viewed as an ndarray of arrays each with `unit_axes` axes.
691    other_unit_axes : int
692        axes to avoid squeezing when we reshape the array.
693
694    Returns
695    -------
696    ndarray
697        Reshaped array with certain length-1 axes removed. If the
698        input array has shape
699        `([object axes], [excess axes], [unit axes])`,
700        squeeze out all the ones in `[excess axes]`.
701
702    """
703    squeezable = np.array(array.T.shape[unit_axes:other_unit_axes])
704    (to_squeeze,) = np.nonzero(squeezable == 1)
705    to_squeeze += unit_axes
706
707    return np.squeeze(array.T, axis=tuple(to_squeeze)).T

Squeeze all excess axes from an ndarray of arrays with unit_axes axes.

This undoes expand_unit_axes.

Parameters
  • array (ndarray): ndarray of shape ([object axes], [excess axes], [unit axes]), where [unit axes] is a tuple of length unit_axes, and [excess axes] is a tuple of length other_unit_axes - unit_axes.
  • unit_axes (int): number of axes to view as "units" in array. That is, array is viewed as an ndarray of arrays each with unit_axes axes.
  • other_unit_axes (int): axes to avoid squeezing when we reshape the array.
Returns
  • ndarray: Reshaped array with certain length-1 axes removed. If the input array has shape ([object axes], [excess axes], [unit axes]), squeeze out all the ones in [excess axes].
def broadcast_match(a1, a2, unit_axes):
709def broadcast_match(a1, a2, unit_axes):
710    """Tile a pair of arrays so that they can be broadcast against each
711    other, treating the last ndims as a unit.
712
713    It is often not necessary to call this function, since numpy
714    broadcasting will handle this implicitly for many vectorized
715    functions.
716
717    Parameters
718    ----------
719    a1, a2 : ndarray
720        arrays to tile
721    unit_axes : int
722        number of axes to treat as a unit when tiling
723
724    Returns
725    -------
726    (u1, u2) : (ndarray, ndarray)
727        Tiled versions of a1 and a2, respectively.
728
729        If unit_axes is k, and a1 has shape (N1, ..., Ni, L1, ...,
730        Lk), and a2 has shape (M1, ..., Mj, P1, ..., Pk), then u1 has
731        shape (N1, ..., Ni, M1, ..., Mj, L1, ..., Lk) and u2 has shape
732        (N1, ..., Ni, M1, ..., Mj, P1, ..., Pk).
733    """
734    c1_ndims = len(a1.shape) - unit_axes
735    c2_ndims = len(a2.shape) - unit_axes
736
737    exp1 = np.expand_dims(a1, tuple(range(c1_ndims, c1_ndims + c2_ndims)))
738    exp2 = np.expand_dims(a2, tuple(range(c1_ndims)))
739
740    tile1 = np.tile(exp1, (1,) * c1_ndims + a2.shape[:c2_ndims] + (1,) * unit_axes)
741    tile2 = np.tile(exp2, a1.shape[:c1_ndims] + (1,) * (c2_ndims + unit_axes))
742
743    return (tile1, tile2)

Tile a pair of arrays so that they can be broadcast against each other, treating the last ndims as a unit.

It is often not necessary to call this function, since numpy broadcasting will handle this implicitly for many vectorized functions.

Parameters
  • a1, a2 (ndarray): arrays to tile
  • unit_axes (int): number of axes to treat as a unit when tiling
Returns
  • (u1, u2) ((ndarray, ndarray)): Tiled versions of a1 and a2, respectively.

    If unit_axes is k, and a1 has shape (N1, ..., Ni, L1, ..., Lk), and a2 has shape (M1, ..., Mj, P1, ..., Pk), then u1 has shape (N1, ..., Ni, M1, ..., Mj, L1, ..., Lk) and u2 has shape (N1, ..., Ni, M1, ..., Mj, P1, ..., Pk).

def matrix_product( array1, array2, unit_axis_1=2, unit_axis_2=2, broadcast='elementwise'):
745def matrix_product(array1, array2, unit_axis_1=2, unit_axis_2=2,
746                   broadcast="elementwise"):
747    """Multiply two ndarrays of ndarrays together.
748
749    Each array in the input is viewed as an ndarray of smaller
750    ndarrays with a specified number of axes. The shapes of these
751    smaller arrays must broadcast against each other (i.e. be
752    compatible with the numpy `@` operator).
753
754    For the dimensions of the outer ndarray, the behavior of this
755    function depends on the broadcast rule provided by the `broadcast`
756    keyword.
757
758    Parameters
759    ----------
760    array1, array2 : ndarray
761        ndarrays of ndarrays to multiply together
762    unit_axis_1, unit_axis_2 : int
763        Each of `array1` and `array2` is viewed as an array with
764        `unit_axis_1` and `unit_axis_2` ndims, respectively. By
765        default both are set to 2, meaning this function multiplies a
766        pair of ndarrays of matrices (2-dim arrays).
767    broadcast : {'elementwise', 'pairwise', 'pairwise_reversed'}
768        broadcast rule to use when multiplying arrays.
769
770        If the broadcast rule is 'elementwise' (the default), assume that
771        the shape of one outer array broadcasts against the shape of
772        the other outer array, and multiply with the same rules as the
773        numpy `@` operator.
774
775        If 'pairwise', multiply every element in the first outer array
776        by every element in the second outer array, and return a new
777        array of arrays with expanded axes. If 'pairwise_reversed', do
778        the same thing, but use the axes of the second array first in
779        the result.
780
781    Returns
782    -------
783    result : ndarray
784
785        result of matrix multiplication.
786
787        If `array1` and `array2` have shapes
788        `([outer ndims 1], [inner ndims 1])` and
789        `([outer ndims 2], [inner ndims 2])` (where
790        `[inner ndims]` are tuples with length `unit_axis_1` and
791        `unit_axis_2`, respectively) then:
792
793        - if `broadcast` is 'elementwise', `result` has shape
794          `([result ndims], [product ndims])`, where `[result ndims]`
795          is the result of broadcasting the outer ndims against each
796          other, and `[product ndims]` is the result of broadcasting
797          the inner ndims against each other.
798
799        - if `broadcast` is 'pairwise', result has shape `([outer
800          ndims 1], [outer ndims 2], [product ndims])`.
801
802        - if `broadcast` is 'pairwise_reversed', result has shape
803          `([outer ndims 2], [outer ndims 1], [product ndims])`
804
805    """
806
807    reshape1 = expand_unit_axes(array1, unit_axis_1, unit_axis_2)
808    reshape2 = expand_unit_axes(array2, unit_axis_2, unit_axis_1)
809
810    if broadcast == "pairwise" or broadcast == "pairwise_reversed":
811        large_axes = max(unit_axis_1, unit_axis_2)
812
813        excess1 = reshape1.ndim - large_axes
814        excess2 = reshape2.ndim - large_axes
815
816        if excess1 > 0:
817            if broadcast == "pairwise_reversed":
818                reshape1 = np.expand_dims(reshape1,
819                                          axis=tuple(range(excess1, excess1 + excess2)))
820            else:
821                reshape1 = np.expand_dims(reshape1,
822                                          axis=tuple(range(excess2)))
823
824        if excess2 > 0:
825            if broadcast == "pairwise_reversed":
826                reshape2 = np.expand_dims(reshape2, axis=tuple(range(excess1)))
827            else:
828                reshape2 = np.expand_dims(reshape2,
829                                          axis=tuple(range(excess2, excess1 + excess2)))
830
831    product = reshape1 @ reshape2
832
833    if unit_axis_1 < unit_axis_2:
834        product = squeeze_excess(product, unit_axis_1, unit_axis_2)
835
836    return product

Multiply two ndarrays of ndarrays together.

Each array in the input is viewed as an ndarray of smaller ndarrays with a specified number of axes. The shapes of these smaller arrays must broadcast against each other (i.e. be compatible with the numpy @ operator).

For the dimensions of the outer ndarray, the behavior of this function depends on the broadcast rule provided by the broadcast keyword.

Parameters
  • array1, array2 (ndarray): ndarrays of ndarrays to multiply together
  • unit_axis_1, unit_axis_2 (int): Each of array1 and array2 is viewed as an array with unit_axis_1 and unit_axis_2 ndims, respectively. By default both are set to 2, meaning this function multiplies a pair of ndarrays of matrices (2-dim arrays).
  • broadcast ({'elementwise', 'pairwise', 'pairwise_reversed'}): broadcast rule to use when multiplying arrays.

    If the broadcast rule is 'elementwise' (the default), assume that the shape of one outer array broadcasts against the shape of the other outer array, and multiply with the same rules as the numpy @ operator.

    If 'pairwise', multiply every element in the first outer array by every element in the second outer array, and return a new array of arrays with expanded axes. If 'pairwise_reversed', do the same thing, but use the axes of the second array first in the result.

Returns
  • result (ndarray): result of matrix multiplication.

    If array1 and array2 have shapes ([outer ndims 1], [inner ndims 1]) and ([outer ndims 2], [inner ndims 2]) (where [inner ndims] are tuples with length unit_axis_1 and unit_axis_2, respectively) then:

    • if broadcast is 'elementwise', result has shape ([result ndims], [product ndims]), where [result ndims] is the result of broadcasting the outer ndims against each other, and [product ndims] is the result of broadcasting the inner ndims against each other.

    • if broadcast is 'pairwise', result has shape ([outer ndims 1], [outer ndims 2], [product ndims]).

    • if broadcast is 'pairwise_reversed', result has shape ([outer ndims 2], [outer ndims 1], [product ndims])

def find_positive_functional(positive_points):
838def find_positive_functional(positive_points):
839    """Find a dual vector which evaluates to a positive real on a given
840       array of vectors, using scipy's linear programming routines.
841
842    Parameters
843    ----------
844    positive_points : ndarray
845        array of vectors to find a dual vector for. If this is a
846        2-dimensional array, then find a single dual vector which is
847        positive when paired with every row of the array.
848
849        If the array has shape (..., k, n), then find an array of dual
850        vectors such that the vectors in the array pair positively
851        with the corresponding k vectors in positive_points.
852
853    Returns
854    -------
855    duals : ndarray
856        array of dual vectors. If positive_points has shape (d1, ...,
857        dj, k, n), then `duals` has shape (d1, ..., dj, n).
858
859    """
860    dim = positive_points.shape[-1]
861    codim = positive_points.shape[-2]
862
863    functionals = np.zeros(positive_points.shape[:-2] +
864                           (positive_points.shape[-1],))
865
866    for ind in np.ndindex(positive_points.shape[:-2]):
867        res = linprog(
868            np.zeros(dim),
869            -1 * positive_points[ind],
870            -1 * np.ones(codim),
871            bounds=(None, None))
872
873        if not res.success:
874            return None
875
876        functionals[ind] = res.x
877
878    return normalize(functionals)

Find a dual vector which evaluates to a positive real on a given array of vectors, using scipy's linear programming routines.

Parameters
  • positive_points (ndarray): array of vectors to find a dual vector for. If this is a 2-dimensional array, then find a single dual vector which is positive when paired with every row of the array.

    If the array has shape (..., k, n), then find an array of dual vectors such that the vectors in the array pair positively with the corresponding k vectors in positive_points.

Returns
  • duals (ndarray): array of dual vectors. If positive_points has shape (d1, ..., dj, k, n), then duals has shape (d1, ..., dj, n).
def first_sign_switch(array):
880def first_sign_switch(array):
881    signs = np.sign(array)
882    row_i, col_i = np.nonzero(signs != np.expand_dims(signs[..., 0], axis=-1))
883    _, init_inds = np.unique(row_i, return_index=True)
884    return col_i[init_inds]
def circle_through(p1, p2, p3):
886def circle_through(p1, p2, p3):
887    """Get the unique circle passing through three points in the plane.
888
889    This does NOT check for colinearity and will just return nans in
890    that case.
891
892    Parameters
893    ----------
894    p1, p2, p3 : ndarray of shape `(..., 2)`
895        Euclidean coordinates of three points in the plane (or three
896        arrays of points)
897
898    Returns
899    -------
900    tuple
901        tuple of the form `(center, radius)`, where `center` is an
902        ndarray containing Euclidean coordinates of the center of the
903        determined circle, and `radius` is either a float or an
904        ndarray containing the radius of the determined circle.
905
906    """
907    t_p1 = p1 - p3
908    t_p2 = p2 - p3
909
910    x1 = t_p1[..., 0]
911    y1 = t_p1[..., 1]
912
913    x2 = t_p2[..., 0]
914    y2 = t_p2[..., 1]
915
916    r_sq = np.stack([x1**2 + y1**2, x2**2 + y2**2], axis=-1)
917    r_sq = np.expand_dims(r_sq, axis=-2)
918
919    mats = np.stack([t_p1, t_p2], axis=-1)
920
921    # we'll act on the right
922    t_ctrs = np.squeeze(r_sq @ invert(mats), axis=-2)/2.
923
924    radii = np.linalg.norm(t_ctrs, axis=-1)
925
926    return (t_ctrs + p3, radii)

Get the unique circle passing through three points in the plane.

This does NOT check for colinearity and will just return nans in that case.

Parameters
  • p1, p2, p3 (ndarray of shape (..., 2)): Euclidean coordinates of three points in the plane (or three arrays of points)
Returns
  • tuple: tuple of the form (center, radius), where center is an ndarray containing Euclidean coordinates of the center of the determined circle, and radius is either a float or an ndarray containing the radius of the determined circle.
def r_to_c(real_coords):
928def r_to_c(real_coords):
929    return real_coords[..., 0] + real_coords[..., 1]*1.0j
def c_to_r(cx_array):
931def c_to_r(cx_array):
932    return cx_array.astype('complex').view('(2,)float')
def order_eigs(eigenvalues, eigenvectors):
934def order_eigs(eigenvalues, eigenvectors):
935    """Sort eigenvalue/eigenvector tuples by increasing modulus.
936
937    This function accepts the eigenvalue/eigenvector data returned by
938    the `np.linalg.eig` function.
939
940    Parameters
941    ----------
942    eigenvalues : ndarray
943        Array of shape (..., n) representing eigenvalues of some
944        matrices
945    eigenvectors : ndarray
946        Array of shape (..., n, n) representing eigenvectors of some
947        matrices (as column vectors).
948
949    Returns
950    -------
951    tuple
952        Tuple of the form `(eigvals, eigvecs)`, where both eigvals and
953        eigvecs have the same data as the input arrays, but arranged
954        so that eigenvalues and eigenvectors are in increasing order
955        of modulus.
956
957    """
958    eigorder = np.argsort(np.abs(eigenvalues), axis=-1)
959    eigvecs = np.take_along_axis(eigenvectors, np.expand_dims(eigorder, axis=-2), axis=-1)
960    eigvals = np.take_along_axis(eigenvalues, eigorder, axis=-1)
961
962    return eigvals, eigvecs

Sort eigenvalue/eigenvector tuples by increasing modulus.

This function accepts the eigenvalue/eigenvector data returned by the np.linalg.eig function.

Parameters
  • eigenvalues (ndarray): Array of shape (..., n) representing eigenvalues of some matrices
  • eigenvectors (ndarray): Array of shape (..., n, n) representing eigenvectors of some matrices (as column vectors).
Returns
  • tuple: Tuple of the form (eigvals, eigvecs), where both eigvals and eigvecs have the same data as the input arrays, but arranged so that eigenvalues and eigenvectors are in increasing order of modulus.
def affine_disks_contain(cout, rout, cin, rin, broadcast='elementwise'):
964def affine_disks_contain(cout, rout, cin, rin,
965                         broadcast="elementwise"):
966    if broadcast == "pairwise":
967        pairwise_dists = np.linalg.norm(
968            np.expand_dims(cout, 0) - np.expand_dims(cin, 1),
969            axis=-1
970        )
971        radial_diffs = np.expand_dims(rout, 0) - np.expand_dims(rin, 1)
972        return pairwise_dists < radial_diffs
973
974    return np.linalg.norm(cout - cin, axis=-1) < (rout - rin)
def disk_containments(c1, r1, c2, r2, broadcast='elementwise'):
977def disk_containments(c1, r1, c2, r2,
978                      broadcast="elementwise"):
979    if broadcast == "pairwise":
980        pairwise_dists = np.linalg.norm(
981            np.expand_dims(cout, 0) - np.expand_dims(cin, 1),
982            axis=-1
983        )
984        radial_diff1 = np.expand_dims(rout, 0) - np.expand_dims(rin, 1)
985        return (pairwise_dists < radial_diffs,
986                pairwise_dists < -radial_diffs)
987
988    dists = np.linalg.norm(cout - cin, axis=-1)
989    return (dists < (rout - rin),
990            dists < (rin - rout))
def disk_interactions(c1, r1, c2, r2, broadcast='elementwise'):
 992def disk_interactions(c1, r1, c2, r2,
 993                      broadcast="elementwise"):
 994    # WARNING: this will only work for Nx2 arrays
 995    if broadcast == "pairwise":
 996        pairwise_dists = np.linalg.norm(
 997            np.expand_dims(c1, 1) - np.expand_dims(c2, 0),
 998            axis=-1
 999        )
1000        radial_diff = np.expand_dims(r1, 1) - np.expand_dims(r2, 0)
1001        radial_sum = np.expand_dims(r1, 1) + np.expand_dims(r2, 0)
1002        return (pairwise_dists < radial_diff,
1003                pairwise_dists < -radial_diff,
1004                pairwise_dists < radial_sum)
1005
1006    dists = np.linalg.norm(c1 - c2, axis=-1)
1007
1008    return (dists < (r1 - r2),
1009            dists < (r2 - r1),
1010            dists < (r2 + r1))
def indefinite_form(p, q, neg_first=True, **kwargs):
1012def indefinite_form(p, q, neg_first=True, **kwargs):
1013    n = p + q
1014
1015    mult = 1
1016    if neg_first:
1017        mult = -1
1018
1019    form_mat = zeros((n,n), **kwargs)
1020    form_mat[:p, :p] = mult * identity(p, **kwargs)
1021    form_mat[p:, p:] = -mult * identity(q, **kwargs)
1022
1023    return form_mat
def symplectic_form(n, **kwargs):
1025def symplectic_form(n, **kwargs):
1026    if n != n % 2:
1027        raise ValueError("Cannot construct a symplectic form in odd dimensions.")
1028
1029    m = n / 2
1030
1031    form_mat = zeros((n, n), **kwargs)
1032    form_mat[:m, m:] = -identity(m, **kwargs)
1033    form_mat[m:, :m] = identity(m, **kwargs)
1034
1035    return form_mat
def symmetric_part(bilinear_form):
1037def symmetric_part(bilinear_form):
1038    return (bilinear_form + bilinear_form.swapaxes(-1, -2)) / 2
def antisymmetric_part(bilinear_form):
1040def antisymmetric_part(bilinear_form):
1041    return (bilinear_form - bilinear_form.swapaxes(-1, -2)) / 2
def matrix_func(func):
1043def matrix_func(func):
1044
1045    # get sage_func now, so if there's a name issue we'll throw an
1046    # error when the wrapped function is defined (rather than when
1047    # it's called)
1048    if SAGE_AVAILABLE:
1049        sage_func = getattr(sagewrap, func.__name__)
1050
1051    @functools.wraps(func)
1052    def wrapped(*args, **kwargs):
1053        if not SAGE_AVAILABLE:
1054            return func(*args, **kwargs)
1055
1056        return sage_func(*args, **kwargs)
1057
1058    return wrapped
@matrix_func
def invert(mat):
1060@matrix_func
1061def invert(mat):
1062    return np.linalg.inv(mat)
@matrix_func
def kernel(mat):
1064@matrix_func
1065def kernel(mat):
1066    return numerical.svd_kernel(mat)
@matrix_func
def eig(mat):
1068@matrix_func
1069def eig(mat):
1070    return np.linalg.eig(mat)
@matrix_func
def eigh(mat):
1072@matrix_func
1073def eigh(mat):
1074    return np.linalg.eigh(mat)
@matrix_func
def det(mat):
1076@matrix_func
1077def det(mat):
1078    return np.linalg.det(mat)
def check_type( base_ring=None, dtype=None, like=None, default_dtype='float64', integer_type=True):
1080def check_type(base_ring=None, dtype=None, like=None,
1081               default_dtype='float64', integer_type=True):
1082
1083    default_ring = None
1084    if SAGE_AVAILABLE:
1085        default_ring = sagewrap.Integer
1086        if not integer_type:
1087            default_ring = sagewrap.SR
1088
1089    if like is not None:
1090        try:
1091            if dtype is None:
1092                dtype = like.dtype
1093        except AttributeError:
1094            if not types.is_linalg_type(like):
1095                dtype = np.dtype('O')
1096
1097    if base_ring is None and dtype == np.dtype('O') and SAGE_AVAILABLE:
1098        base_ring = default_ring
1099
1100    if base_ring is not None:
1101        if not SAGE_AVAILABLE:
1102            raise EnvironmentError(
1103                "Cannot specify base_ring unless running within sage"
1104            )
1105        if dtype is not None and dtype != np.dtype('object'):
1106            raise TypeError(
1107                "Cannot specify base_ring and dtype unless dtype is dtype('object')"
1108            )
1109        dtype = np.dtype('object')
1110    elif dtype is None:
1111        dtype = default_dtype
1112
1113    if not integer_type and np.can_cast(dtype, int):
1114        dtype = np.dtype('float64')
1115
1116    return (base_ring, dtype)
def complex_type(**kwargs):
1118def complex_type(**kwargs):
1119    base_ring, dtype = check_type(integer_type=False, **kwargs)
1120
1121    if np.can_cast(dtype, 'float'):
1122        return base_ring, np.dtype('complex')
1123
1124    return base_ring, dtype
def guess_literal_ring(data):
1126def guess_literal_ring(data):
1127    if not SAGE_AVAILABLE:
1128        return None
1129
1130    try:
1131        if data.dtype == np.dtype('O'):
1132            return sage.all.QQ
1133    except AttributeError:
1134        if not types.is_linalg_type(data):
1135            return sage.all.QQ
1136
1137    # this is maybe not Pythonic, but it's also not a mistake.
1138    return None
def astype(val, dtype=None):
1140def astype(val, dtype=None):
1141    if dtype is None:
1142        return val
1143
1144    return np.array(val).astype(dtype)
def number(val, like=None, dtype=None, base_ring=None):
1146def number(val, like=None, dtype=None, base_ring=None):
1147    if like is not None and dtype is not None:
1148        raise UserWarning(
1149            "Passing both 'like' and 'dtype' when specifying a number;"
1150            " 'like' parameter is ignored."
1151        )
1152
1153    if base_ring is not None:
1154        if dtype is not None:
1155            raise UserWarning(
1156                "Passing both 'base_ring' and 'dtype' when specifying a number;"
1157                " 'dtype' is ignored (assumed to be dtype('O'))"
1158            )
1159        dtype = np.dtype('O')
1160
1161        if not SAGE_AVAILABLE:
1162            raise UserWarning( "Specifying base_ring when sage is not"
1163            "available has no effect."  )
1164
1165    if not SAGE_AVAILABLE:
1166        return astype(val, dtype)
1167
1168    if dtype is None and like is not None:
1169        try:
1170            dtype = like.dtype
1171        except AttributeError:
1172            if not types.is_linalg_type(like):
1173                dtype = np.dtype('O')
1174
1175    if dtype == np.dtype('O'):
1176        if isinstance(val, int):
1177            # we use SR instead of Integer here, because numpy
1178            # sometimes silently converts sage Integers
1179            return sage.all.SR(val)
1180        if isinstance(val, float):
1181            return sage.all.SR(sage.all.QQ(val))
1182
1183    return astype(val, dtype)
def wrap_elementary(func):
1185def wrap_elementary(func):
1186    @functools.wraps(func)
1187    def wrapped(*args, like=None, base_ring=None, dtype=None, **kwargs):
1188        if not SAGE_AVAILABLE:
1189            return func(*args, **kwargs)
1190
1191        else:
1192            packaged_args = []
1193            arg_ring = None
1194            if base_ring is not None:
1195                arg_ring = sagewrap.SR
1196
1197            for arg in args:
1198                arg_like = like
1199                if arg_like is None:
1200                    arg_like = arg
1201
1202                packaged_args.append(
1203                    array_like(arg, like=arg_like,
1204                               base_ring=arg_ring,
1205                               dtype=dtype,
1206                               integer_type=False)
1207                )
1208
1209            return sagewrap.change_base_ring(
1210                func(*packaged_args, **kwargs),
1211                base_ring
1212            )
1213
1214    return wrapped
@wrap_elementary
def power(base, exp):
1216@wrap_elementary
1217def power(base, exp):
1218    return np.power(base, exp)
@wrap_elementary
def cos(arg):
1220@wrap_elementary
1221def cos(arg):
1222    return np.cos(arg)
@wrap_elementary
def sin(arg):
1224@wrap_elementary
1225def sin(arg):
1226    return np.sin(arg)
@wrap_elementary
def conjugate(arg):
1228@wrap_elementary
1229def conjugate(arg):
1230    return np.conjugate(arg)
def real(arg):
1232def real(arg):
1233    if not SAGE_AVAILABLE or types.is_linalg_type(arg):
1234        return np.real(arg)
1235
1236    # np.real doesn't work well with sage since it expects the real
1237    # attribute to be a property, not a callable. So we need to wrap
1238    # sage's real function explicitly.
1239    return sagewrap.real(arg)
def imag(arg):
1241def imag(arg):
1242    if not SAGE_AVAILABLE or types.is_linalg_type(arg):
1243        return np.imag(arg)
1244
1245    return sagewrap.imag(arg)
def array_like( array, like=None, base_ring=None, dtype=None, integer_type=False, **kwargs):
1247def array_like(array, like=None, base_ring=None, dtype=None,
1248               integer_type=False, **kwargs):
1249
1250    if like is None:
1251        like = array
1252
1253    base_ring, dtype = check_type(base_ring, dtype, like,
1254                                  integer_type=integer_type)
1255
1256    arr = np.array(array, dtype=dtype, **kwargs)
1257
1258    if base_ring is not None:
1259        return sagewrap.change_base_ring(arr, base_ring)
1260    return arr
def pi(**kwargs):
1262def pi(**kwargs):
1263    if not SAGE_AVAILABLE:
1264        return np.pi
1265
1266    base_ring, dtype = check_type(**kwargs)
1267
1268    if base_ring is not None:
1269        return sagewrap.pi
1270
1271    return np.pi
def unit_imag(**kwargs):
1273def unit_imag(**kwargs):
1274    if not SAGE_AVAILABLE:
1275        return 1j
1276
1277    base_ring, dtype = check_type(**kwargs)
1278
1279    if base_ring is not None:
1280        return sagewrap.I
1281    return 1j
def zeros( shape, base_ring=None, dtype=None, like=None, integer_type=True, **kwargs):
1284def zeros(shape, base_ring=None, dtype=None, like=None,
1285          integer_type=True, **kwargs):
1286
1287    base_ring, dtype = check_type(base_ring, dtype, like,
1288                                  integer_type=integer_type)
1289
1290    zero_arr = np.zeros(shape, dtype=dtype, **kwargs)
1291    if base_ring is not None:
1292        return sagewrap.change_base_ring(zero_arr, base_ring)
1293    return zero_arr
def ones( shape, base_ring=None, dtype=None, like=None, integer_type=True, **kwargs):
1295def ones(shape, base_ring=None, dtype=None, like=None,
1296         integer_type=True, **kwargs):
1297    base_ring, dtype = check_type(base_ring, dtype, like,
1298                                  integer_type=integer_type)
1299
1300    ones_arr = np.ones(shape, dtype=dtype, **kwargs)
1301    if base_ring is not None:
1302        return sagewrap.change_base_ring(ones_arr, base_ring)
1303    return ones_arr
def identity( n, base_ring=None, dtype=None, like=None, integer_type=True, **kwargs):
1305def identity(n, base_ring=None, dtype=None, like=None,
1306             integer_type=True, **kwargs):
1307    base_ring, dtype = check_type(base_ring, dtype, like,
1308                                  integer_type=integer_type)
1309
1310    identity_arr = np.identity(n, dtype=dtype, **kwargs)
1311
1312    if base_ring is not None:
1313        return sagewrap.change_base_ring(identity_arr, base_ring)
1314    return identity_arr
def change_base_ring(array, base_ring=None, rational_approx=False):
1316def change_base_ring(array, base_ring=None,
1317                     rational_approx=False):
1318    if base_ring is None:
1319        return array
1320
1321    if not SAGE_AVAILABLE:
1322        raise EnvironmentError(
1323            "Cannot change base ring unless sage is available."
1324        )
1325
1326    return sagewrap.change_base_ring(array, base_ring,
1327                                     rational_approx=rational_approx)