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)
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
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.
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
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)
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
is the matrix given by bilinear_form, and is a diagonal matrix with the same signature as , then this function returns a matrix such that .
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.
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.
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).
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).
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 froma
tob
is shorter than the counterclockwise arc fromb
toa
.
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 ofb
is at most the cosine ofa
.
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 thatreference_theta
lies in the counterclockwise angle betweentheta_p[..., 0]
andtheta_p[..., 1]
.
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
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.
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.
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
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
ontov2
.
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 ifform
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
).
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 firstj
rows ofresult
is the same as the subspace spanned by the firstj
rows ofmatrices
.
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.
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.
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. IfA
is an orientation-reversing matrix inmatrices
, then the corresponding matrix inresult
has its last row negated.
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 eithernew_axes - unit_axes
, or 0 if this is negative.
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 lengthunit_axes
, and[excess axes]
is a tuple of lengthother_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 withunit_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]
.
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).
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
andarray2
is viewed as an array withunit_axis_1
andunit_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
andarray2
have shapes([outer ndims 1], [inner ndims 1])
and([outer ndims 2], [inner ndims 2])
(where[inner ndims]
are tuples with lengthunit_axis_1
andunit_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])
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).
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)
, wherecenter
is an ndarray containing Euclidean coordinates of the center of the determined circle, andradius
is either a float or an ndarray containing the radius of the determined circle.
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.
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)
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))
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))
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
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
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
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)
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
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)
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
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)
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
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
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
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
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)