Source code for ddg.geometry.signatures

from dataclasses import dataclass
from typing import Literal, Optional

import numpy as np

import ddg.math.projective as pmath
import ddg.math.symmetric_matrices as sm
from ddg import nonexact
from ddg.math import linalg

###################
# Signature classes
###################


[docs]@dataclass(frozen=True, eq=False) class Signature: """Projective signature class. Two projective signatures are equal if and only if quadrics with those signatures are related by a projective transformation. We want to identify: * Signatures * Quadrics up to projective transformation * "Normalized matrices" up to nonzero scalar multiplication (-1 in particular), which will be the result of e.g. :py:func:`projective_normalization`. To achieve this, we define a normalized matrix as a diagonal matrix :: I_n | | -----|------|--- | -I_m | -----|------|--- | | 0 or :: -I_n | | ------|-----|--- | I_m | ------|-----|--- | | 0 where ``I_n`` is the (n x n)-identity matrix, 0 means a 0 matrix and ``n >= m``. Parameters ---------- plus : int minus : int zero : int (default=0) These three arguments mean the number of 1, -1, 0 on the diagonal. Notes ----- This class implements the equals relation ``==``, with the meaning explained above. It is also hashable. Examples -------- >>> from ddg.geometry.signatures import Signature >>> sgn1 = Signature(2, 1, 0) >>> sgn1 Signature(plus=2, minus=1, zero=0) >>> sgn2 = Signature(1, 2, 0) >>> sgn2 Signature(plus=1, minus=2, zero=0) >>> sgn1 == sgn2 True >>> sgn1.matrix array([[ 1, 0, 0], [ 0, 1, 0], [ 0, 0, -1]]) >>> sgn2.matrix array([[-1, 0, 0], [ 0, -1, 0], [ 0, 0, 1]]) >>> sgn1.is_degenerate False >>> sgn1.rank 3 >>> sgn1.is_positive_definite False >>> sgn3 = Signature(2, 1, 1) >>> sgn3 Signature(plus=2, minus=1, zero=1) >>> sgn3.matrix array([[ 1, 0, 0, 0], [ 0, 1, 0, 0], [ 0, 0, -1, 0], [ 0, 0, 0, 0]]) >>> sgn1 == sgn3 False """ plus: int minus: int zero: int = 0 @property def matrix(self): """Returns the normalized matrix corresponding to the signature. See the class docstring for how this is defined. This method respects the global sign the signature is initialized with, even though the equality operator does not. Returns ------- numpy.ndarray of shape (n, n) """ p, q, z = self.plus, self.minus, self.zero diag = max([p, q]) * [1] + min([p, q]) * [-1] + z * [0] # respect the actual counts if p < q: diag = -np.array(diag) return sm.symmetric_matrix_from_diagonal(diag) @property def is_degenerate(self): """Whether this is the signature of a degenerate quadric. Returns ------- bool """ return self.zero > 0 @property def rank(self): """Rank of associated matrix. Returns ------- int """ return self.plus + self.minus @property def is_positive_definite(self): """Whether the signature is positive definite. Meaning, if ``q`` is a quadratic form with this signature, ``q(p,p) > 0`` for all ``p != 0``. Returns ------- bool """ return self.plus > 0 and self.minus == 0 and self.zero == 0 @property def is_negative_definite(self): """Whether the signature is negative definite. Meaning, if ``q`` is a quadratic form with this signature, ``q(p,p) < 0`` for all ``p != 0``. Returns ------- bool """ return self.minus > 0 and self.plus == 0 and self.zero == 0 @property def is_positive_semi_definite(self): """Whether the signature is positive semi-definite. Meaning, if ``q`` is a quadratic form with this signature, ``q(p,p) >= 0`` for all ``p``. Returns ------- bool """ return self.minus == 0 @property def is_negative_semi_definite(self): """Whether the signature is negative semi-definite. Meaning, if ``q`` is a quadratic form with this signature, ``q(p,p) <= 0`` for all ``p``. Returns ------- bool """ return self.plus == 0 @property def is_indefinite(self): """Whether the signature is indefinite. Meaning, that a quadratic form with this signature, can have both positive and negative values. Returns ------- bool """ return self.plus > 0 and self.minus > 0 @property def is_definite(self): """Whether the signature is either positive or negative definite. Returns ------- bool """ return self.is_positive_definite or self.is_negative_definite @property def is_semi_definite(self): """Whether the signature is either positive or negative semi-definite. Returns ------- bool """ return self.is_positive_semi_definite or self.is_negative_semi_definite @property def _counts_up_to_sign(self): """Returns counts of 1, -1 and 0, ignoring overall sign. Ignoring overall sign means that we want the signatures ``(p, q, z)`` and ``(q, p, z)`` to be treated as equal. This method encodes the counts in such a way that this is the case. Returns ------- tuple(frozenset(int, int), int) """ return (frozenset([self.plus, self.minus]), self.zero) def __eq__(self, other): if not isinstance(other, Signature): return NotImplemented return self._counts_up_to_sign == other._counts_up_to_sign def __str__(self): result = (self.plus, self.minus) if self.is_degenerate: result += (self.zero,) return str(result) def __hash__(self): return hash(self._counts_up_to_sign)
[docs]@dataclass(frozen=True, eq=False) class AffineSignature(Signature): """Affine signature class. Two affine signatures are equal if and only if quadrics with those signatures are related by an affine transformation. We want to identify: * Affine signatures * Quadrics up to affine transformation * "Normalized matrices" up to nonzero scalar multiplication (-1 in particular), which will be the result of e.g. :py:func:`affine_normalization`. To achieve this, we define what a normalized matrix is as follows: In the non-parabolic case: A diagonal matrix with only 1, -1 and 0 on the diagonal. The diagonal looks as follows (where ``s`` is either 1, -1 or 0): * If the last entry on the diagonal is 0: ``(s,...,s, -s,...,-s, 0,...,0)``, and the number of ``s``'s is greater than or equal to the number of ``-s``'s. * If the last entry on the diagonal is ``s != 0``: ``(s,...,s, -s,...,-s, 0,...,0, s)``. In the parabolic case: a matrix of the form :: D | --+----- | 0 s | s 0 where ``D`` is a diagonal matrix with only 1, -1 and 0 on the diagonal. The diagonal is sorted in the order ``s``, ``-s``, ``0`` and the number of ``s``'s is greater than or equal to the number of ``-s``'s. Note that for parabolic cases, flipping the sign of the diagonal is the same as flipping the sign of the off-diagonal entries, which as an affine transformation is a reflection about the plane ``x[-2] = 0``. Parameters ---------- plus : int minus : int zero : int (default=0) These three arguments mean the number of 1, -1 or 0 on the diagonal of the **diagonalized** matrix. Note that for parabolic cases, these will not be the counts on the diagonal: A 1 and a -1 will be replaced by a 0 and entries on the off-diagonal, see above. last_entry : 1, -1, 0 or "parabolic" (default=None) The last entry on the diagonal. A value of "parabolic" means the matrix :: 0 1 1 0 See above for details. This can be omitted (left as ``None``) if only one of `plus`, `minus` or `zero` is nonzero. Raises ------ ValueError * If `last_entry` is "parabolic" and `plus` or `minus` are 0. * If `last_entry` was not given and could not be inferred uniquely. * If `last_entry` was given and is not 1, -1, 0 or 'parabolic'. Notes ----- This class implements the equals relation ``==``, with the meaning explained above. It is also hashable. Examples -------- >>> from ddg.geometry.signatures import AffineSignature >>> sgn1 = AffineSignature(2, 1, 0, last_entry=-1) >>> sgn1.matrix array([[ 1, 0, 0], [ 0, 1, 0], [ 0, 0, -1]]) >>> sgn2 = AffineSignature(1, 2, 0, last_entry=1) >>> sgn2 AffineSignature(plus=1, minus=2, zero=0, last_entry=1) >>> sgn2.matrix array([[-1, 0, 0], [ 0, -1, 0], [ 0, 0, 1]]) >>> sgn1 == sgn2 True >>> sgn3 = AffineSignature(1, 2, 0, last_entry=-1) >>> sgn3.matrix array([[-1, 0, 0], [ 0, 1, 0], [ 0, 0, -1]]) >>> sgn3 == sgn2 False >>> sgn4 = AffineSignature(1, 2, 0, last_entry="parabolic") >>> sgn4 AffineSignature(plus=1, minus=2, zero=0, last_entry='parabolic') >>> sgn4.matrix array([[-1, 0, 0], [ 0, 0, -1], [ 0, -1, 0]]) >>> sgn3 == sgn4 False """ plus: int minus: int zero: int = 0 last_entry: Optional[Literal[0, 1, -1, "parabolic"]] = None def __post_init__(self): # Just consistency checking p, q, r = self.plus, self.minus, self.zero s = self.last_entry # We can only check if the entry can be inferred, but can not set # self.last_entry due to immutability. if s is None: entries = set(p * [1] + q * [-1] + r * [0]) if len(entries) != 1: raise ValueError( "Sign of last entry could not be inferred uniquely. Please specify " "1, -1, 0 or 'parabolic'." ) elif s not in [1, -1, 0, "parabolic"]: raise ValueError("Sign of last entry must be 1, -1, 0 or 'parabolic'.") elif s == "parabolic" and (p == 0 or q == 0): raise ValueError("Parabolic signatures must have a 1 and a -1.") elif (s == 1 and p == 0) or (s == -1 and q == 0) or (s == 0 and r == 0): raise ValueError("The last sign is not contained in the diagonal.") @property def matrix(self): """Return normalized matrix corresponding to the signature. See the class docstring for how this is defined. Returns ------- numpy.ndarray of shape (n, n) Notes ----- This method respects the sign the signature is initialized with, even though the equality operator does not. """ p, q, z = self.plus, self.minus, self.zero # We already checked in __post_init__ that only one is nonzero. if self.last_entry is None: for count, s in zip([p, q, z], [1, -1, 0]): if count > 0: break else: s = self.last_entry parabolic = s == "parabolic" # We need this to change the sign of the off-diagonal entries in the # parabolic case sign = 1 # build the diagonal without last_entry if parabolic: diag_ex = (max([p, q]) - 1) * [1] + (min([p, q]) - 1) * [-1] + z * [0] # respect the actual counts if p < q: sign = -1 elif s == 0: diag_ex = max([p, q]) * [1] + min([p, q]) * [-1] + (z - 1) * [0] # respect the actual counts if p < q: sign = -1 elif s == 1: diag_ex = (p - 1) * [1] + q * [-1] + z * [0] elif s == -1: diag_ex = (q - 1) * [-1] + p * [1] + z * [0] # build the actual diagonal # dtype int for consistency with Signature.matrix diag = np.empty(p + q + z, dtype=int) if parabolic: diag[-2:] = 0 diag[:-2] = diag_ex else: diag[-1] = s diag[:-1] = diag_ex return sign * sm.symmetric_matrix_from_diagonal(diag, parabolic=parabolic) def __eq__(self, other): if not isinstance(other, AffineSignature): return NotImplemented return np.array_equal(self.matrix, other.matrix) or np.array_equal( self.matrix, -other.matrix ) def __str__(self): lines = [super().__str__()] if self.last_entry is not None: lines.append(f"last entry: {self.last_entry}") return "\n".join(lines) def __hash__(self): """Hash function computed from self.matrix().""" M = self.matrix.flatten() for m in M: if m != 0: M *= m break return hash(tuple(M))
[docs]def signature_from_diagonal(diag): """Create signature from diagonal. Parameters ---------- diag : array_like of shape (n,) 1D array containing only 1, -1 and 0. Returns ------- Signature Raises ------ ValueError If `diag` contains entries other than 1, -1 or 0. Examples -------- >>> from ddg.geometry.signatures import signature_from_diagonal >>> signature_from_diagonal((1, 0, 1)) Signature(plus=2, minus=0, zero=1) >>> signature_from_diagonal((0, -1, -1)) Signature(plus=0, minus=2, zero=1) >>> signature_from_diagonal((1, 1, 0, 0, -1, 0)) Signature(plus=2, minus=1, zero=3) """ diag = np.array(diag) p = np.count_nonzero(diag == 1) q = np.count_nonzero(diag == -1) z = np.count_nonzero(diag == 0) if p + q + z != len(diag): raise ValueError("Diagonal can only contain 1, -1 and 0") return Signature(p, q, z)
[docs]def affine_signature_from_diagonal(diag, parabolic=False): """Create affine signature from diagonal. Parameters ---------- diag : array_like of shape (n,) or list 1D array containing only 1, -1 and 0. If `parabolic` is True, this must have 0 at entries -1 and -2. parabolic : bool (default=False) Returns ------- AffineSignature Raises ------ ValueError * If `diag` contains entries other than 1, -1 or 0. * If `parabolic` is True and `diag` does not have 0 at positions -1 and -2. Examples -------- >>> from ddg.geometry.signatures import affine_signature_from_diagonal >>> affine_signature_from_diagonal((1, 0, 1)) AffineSignature(plus=2, minus=0, zero=1, last_entry=1) >>> affine_signature_from_diagonal((0, -1, -1)) AffineSignature(plus=0, minus=2, zero=1, last_entry=-1) >>> affine_signature_from_diagonal((1, 1, 0, 0, -1, 0)) AffineSignature(plus=2, minus=1, zero=3, last_entry=0) """ diag = np.array(diag) p = np.count_nonzero(diag == 1) q = np.count_nonzero(diag == -1) z = np.count_nonzero(diag == 0) if p + q + z != len(diag): raise ValueError("Diagonal can only contain 1, -1 and 0") if parabolic and (diag[-1] != 0 or diag[-2] != 0): raise ValueError("parabolic diagonals must have 0 at positions -1 and -2.") if parabolic: last_entry = "parabolic" p += 1 q += 1 z -= 2 else: last_entry = diag[-1] return AffineSignature(p, q, z, last_entry=last_entry)
################################### # Normalization and diagonalization ###################################
[docs]def projective_normalization(Q, atol=None, rtol=None): """Bring quadric to normal form by projective transformation. Returns signature `sgn` and transformation matrix `A` such that :: A.T @ Q @ A == sgn.matrix See :py:class:`Signature` for more information. Note that this means that the point transformation defined by A transforms ``sgn.matrix`` into Q. Parameters ---------- Q : numpy.ndarray Symmetric matrix. atol, rtol : float (default=None) This function uses the global tolerance defaults if `atol` or `rtol` are set to None. See :py:mod:`ddg.nonexact` for details. Returns ------- sgn : Signature A : numpy.ndarray Transformation matrix. Raises ------ ValueError If `Q` is not symmetric. Examples -------- >>> import numpy as np >>> from ddg.geometry.signatures import projective_normalization >>> Q = np.array([[10, 0, 0], [0, 5, 0], [0, 0, 3]]) >>> Q array([[10, 0, 0], [ 0, 5, 0], [ 0, 0, 3]]) >>> sgn, A = projective_normalization(Q) >>> sgn Signature(plus=3, minus=0, zero=0) >>> A array([[0.31622777, 0. , 0. ], [0. , 0.4472136 , 0. ], [0. , 0. , 0.57735027]]) >>> B = np.array([[1, 0, 2], [0, 5, 0], [2, 0, 3]]) >>> B array([[1, 0, 2], [0, 5, 0], [2, 0, 3]]) >>> sgn, A = projective_normalization(B) >>> sgn Signature(plus=2, minus=1, zero=0) >>> A array([[ 0. , -0.25543607, -1.75078485], [-0.4472136 , 0. , 0. ], [ 0. , -0.41330424, 1.08204454]]) """ if not sm.is_symmetric(Q, atol=atol, rtol=rtol): raise ValueError("The matrix is not symmetric: " + str(Q)) ev, A = sm.diagonalize(Q, atol=atol, rtol=rtol) # Add affine scaling to the transformation B = np.diag([1.0 if x == 0 else 1 / np.sqrt(np.abs(x)) for x in ev]) A = np.dot(A, B) # compute signature sgn = np.array([int(np.sign(x)) for x in ev]) # permutation step to bring the matrix to normal form as defined in the # Signature docstring. # We might have to swap the first two blocks of diag, i.e. go from # (1,...,1, -1,...,-1, 0,...,0) to (-1,...,-1, 1,...,1, 0,...,0). B = np.eye(len(sgn)) p = np.count_nonzero(sgn == 1) q = np.count_nonzero(sgn == -1) z = np.count_nonzero(sgn == 0) if q > p: permutation = np.concatenate( (np.arange(q) + p, np.arange(p), np.arange(z) + p + q) ) B = B[:, permutation] A = A @ B sgn = signature_from_diagonal(sgn) return sgn, A
[docs]def affine_normalization(Q, atol=None, rtol=None): """Bring quadric to normal form by affine transformation. Returns affine signature `sgn` and affine transformation `A` such that :: A.T @ Q @ A == sgn.matrix For non-parabolic cases, this means diagonalizing `Q` with an affine transformation such that there is only 1, -1 or 0 on the diagonal. For parabolic cases, normalizes to a matrix of the form :: D1 | (+/-) ---+----- | 0 1 | 1 0 Where ``D1`` is a diagonal matrix with only 1, -1 or 0 on the diagonal. For more information about the order of entries on the diagonal etc, see :py:class:`.AffineSignature`. Parameters ---------- Q : numpy.ndarray of shape (n + 1, n + 1) Symmetric matrix. Can be interpreted as a quadric in n-dim. projective space. atol, rtol : float (default=None) This function uses the global tolerance defaults if `atol` or `rtol` are set to None. See :py:mod:`ddg.nonexact` for details. Returns ------- sgn : AffineSignature A : numpy.ndarray of shape (n + 1, n + 1) Affine transformation. Raises ------ ValueError If `Q` is not symmetric. Notes ----- For a detailed explanation of the implementation, see :ref:`supplemental material <affine_normalization>`. See also -------- ddg.math.symmetric_matrices.AffineSignature Examples -------- >>> import numpy as np >>> from ddg.geometry.signatures import affine_normalization >>> Q = np.array([[10, 0, 0], [0, 5, 0], [0, 0, 3]]) >>> Q array([[10, 0, 0], [ 0, 5, 0], [ 0, 0, 3]]) >>> sgn, A = affine_normalization(Q) >>> sgn AffineSignature(plus=3, minus=0, zero=0, last_entry=1) >>> A array([[0.31622777, 0. , 0. ], [0. , 0.4472136 , 0. ], [0. , 0. , 0.57735027]]) >>> B = np.array([[1, 0, 2], [0, 5, 0], [2, 0, 3]]) >>> B array([[1, 0, 2], [0, 5, 0], [2, 0, 3]]) >>> sgn, A = affine_normalization(B) >>> sgn AffineSignature(plus=2, minus=1, zero=0, last_entry=-1) >>> A array([[ 0. , 1. , -2. ], [ 0.4472136, 0. , 0. ], [ 0. , 0. , 1. ]]) """ if not sm.is_symmetric(Q, atol=atol, rtol=rtol): raise ValueError("The matrix is not symmetric: " + str(Q)) # We treat this as a special case because the decomposition will produce empty # arrays Q1, q which many functions can't deal with. if Q.shape[0] == 1: r = Q[0, 0] if nonexact.isclose(r, 0, atol=atol, rtol=rtol): return AffineSignature(0, 0, 1), np.array([[1]]) else: return AffineSignature(1, 0), np.array([[1 / np.sqrt(np.abs(r))]]) # List of affine transformations that must be applied to normalize Q. trafos = [] Q1, q, r = sm._decompose_matrix(Q) rk = linalg.rank(Q1, atol=atol, rtol=rtol) b = sm._compute_translation(Q, rk) parabolic = sm._check_parabolic(Q, b, atol, rtol) ev_, A1 = sm.diagonalize(Q1, atol=atol, rtol=rtol) trafos.append(pmath.affine_transformation(A1, b)) if parabolic: trafos.append(sm._parabolic_reduce_q(A1, Q, b, rk, atol, rtol)) trafos.append(sm._parabolic_eliminate_r(Q, b)) ev = np.append(ev_, 0.0) else: r2 = np.dot(b, q) + r r2 = 0.0 if nonexact.isclose(r2, 0, atol=atol, rtol=rtol) else r2 ev = np.append(ev_, r2) # We now normalize the diagonal entries to 1, -1 or 0. # diagonalize already sets eigenvalues close to 0 according to tolerances to 0 and # we did the same for the last eigenvalue manually, so an exact check is fine here. trafos.append(np.diag([1.0 if x == 0 else 1 / np.sqrt(np.abs(x)) for x in ev])) # compute signature # This is sorted in the order 1, -1, 0, except for the last entry (or last two, in # the parabolic case). sgn_diag = np.sign(ev).astype(int) # Final permutation step to achieve normal form as described in AffineSignature. trafos.append(sm._normal_form_permutation(sgn_diag, parabolic)) return ( affine_signature_from_diagonal(sgn_diag, parabolic=parabolic), np.linalg.multi_dot(trafos), )
########### # Sequences ###########
[docs]class SignatureSequence: """Signature sequence class Signature sequences can be used to classify non-degenerate pencils (ddg.geometry.Pencil) of real quadrics (ddg.geometry.Quadric). If the pencil is represented as:: P(Q1,Q2) = lambda Q2 + Q1 with ``A, B`` real symmetric matrices representing the quadrics spanning the pencil, and we consider the roots ``lambda_l`` of:: det(lambda Q2 + Q1) = 0 then the signature sequence is calculated as:: [v_1, (...(i_1, j_1)...), v_2, ...., v_k-1, (...(i_k-1, j_k-1)...), v_k], where ``v_l`` are the index of the nonsingular matrices ``lambda Q2 + Q1`` between two roots and ``(...(i_l, j_l)...)`` are the signatures of the singular matrices ``lambda_l Q2 + Q1``, with the multiplicity of the root being represented in the number of brackets A number of operations can be performed on the attributes of a signature sequence to receive an equivalent sequence: - rotate_left(indices, signatures, multiplicities) - rotate_right(indices, signatures, multiplicities) - inverse_order(indices, signatures, multiplicities) - find_complement(indices, signatures, multiplicities) If the dimension of the sequence is 4 (``RP³``), the sequence can be brought into normalform for classification via equivalence operations. Normalform is reached by the following rules in descending order of importance: - multiple roots to the front - smallest values to the front - smallest sum of values possible for indices - more positive than negative entries in signatures Parameters ---------- indices: tuple or list of integers Indexes of the non-degenerate quadrics in the pencil, given in an arbitrary order (but fitting for the other tuples) signatures: tuple or list of Signature Signatures of the degenerate quadrics in the pencil multiplicities: tuple or list of integers Multiplicities of the roots in the characteristic function ``det(lambda Q2 + Q1)=0`` where ``lambda Q2 + Q1`` describes the pencil Attributes ---------- dimension: int indices: tuple signatures: tuple multiplicities: tuple Raises ------ ValueError * If the tuples are not of the correct lengths ``len(indices)-1=len(signatures)=len(multiplicities)`` * If the tuples are not filled with the right type of entries: - indices: integers - signatures: signatures - multiplicities: integers Notes ----- This class supports the following operators: - ``==``, meaning equivalency of the signature sequences, reached through a projective transformation on lambda See also -------- ddg.geometry.signatures.IndexSequence ddg.geometry.signatures.SegreSymbol ddg.geometry.Pencil """ def __init__(self, indices, signatures, multiplicities): indices: tuple signatures: tuple multiplicities: tuple # if instead entries are lists, turn them into tuples if isinstance(indices, list): indices = tuple(indices) if isinstance(signatures, list): signatures = tuple(signatures) if isinstance(multiplicities, list): multiplicities = tuple(multiplicities) # check whether tuple lengths match to build the sequence if ( not isinstance(indices, tuple) or not isinstance(signatures, tuple) or not isinstance(multiplicities, tuple) or len(indices) - 1 != len(signatures) or len(signatures) != len(multiplicities) ): raise ValueError( "Tuples must have matching lengths with tuples of indices one entry " + "longer" ) for k in range(len(signatures)): # check whether we have entries in the tuples of the right type if not isinstance(indices[k], int) or not isinstance( multiplicities[k], int ): raise ValueError( "Tuple of indices and multiplicities need to have integer entries" ) elif not isinstance(signatures[k], Signature): raise ValueError( "Tuple of signatures need to have signatures as entries" ) # check the last entry in the indices tuple (one longer) if not isinstance(indices[-1], int): raise ValueError("Tuple of indices needs have integer entries") # dimension of sequence for an n-dim pencil is n+1 self.dimension = indices[0] + indices[-1] self.indices = indices self.signatures = signatures self.multiplicities = multiplicities return def __str__(self): result = "(" for k in range(len(self.signatures)): # indices are plainly put result += str(self.indices[k]) + ", " # signatures are put in as many brackets as # the multiplicity of the root of the characteristic function here elem = self.signatures[k] entry = str((elem.plus, elem.minus)) for l in range(self.multiplicities[k] - 1): entry = "(" + entry + ")" result += entry + ", " result += str(self.indices[-1]) + ")" return str(result) def __repr__(self): return ( "Signature Sequence with tuples of\n indices: " + str(self.indices) + ",\n signatures: " + str(self.signatures) + ",\n multiplicities: " + str(self.multiplicities) )
[docs] def normalform(self): """Calculates the canonical form of the Signature sequence if the sequence is of dimension 4 (3 dimensional projective space) Normalform is reached by the following rules in descending order of importance: - multiple roots to the front - smallest values to the front - smallest sum of values possible for indices - more positive than negative entries in signatures The function returns an equivalent signature sequence. Returns ------- ddg.geometry.signatures.SignatureSequence in canonical form Raises ------ NotImplementedError if the signature sequence is not of dimension 4 See Also -------- normalform """ # check the dimension if self.dimension == 4: indices, signatures, multiplicities = normalform( self.indices, self.signatures, self.multiplicities ) else: raise NotImplementedError( "Normalform is only implemented for sequences of dimension 4" ) return SignatureSequence(indices, signatures, multiplicities)
def __eq__(self, other): # check whether other is a signature sequence if isinstance(other, self.__class__): # calculate the normalform of the sequences: normal1 = self.normalform() normal2 = other.normalform() # take the two normalforms as tuples norm1 = (normal1.indices, normal1.signatures, normal1.multiplicities) norm2 = (normal2.indices, normal2.signatures, normal2.multiplicities) # and check tuple equivalence return norm1 == norm2 else: return False
[docs]class JordanBlock: """JordanBlock class Jordanblocks ``J`` are quadratic blocks in the Jordannormalform of a matrix ``M``. They have the form:: | u 1 | | . . | J = | . . | | . 1 | | u | where ``u`` is an eigenvalue of ``M``. Parameters ---------- size: int Size of the jordanblock. If ``J`` is of size ``nxn``, size = ``n`` epsilon: 1, -1 denotes whether the jordanblock has a positive or negative factor epsilon in the canonical form of a pencil. Attributes ---------- size epsilon Raises ------ ValueError If the Parameters are not of the correct type """ def __init__(self, size, epsilon): if not isinstance(size, int) or epsilon not in [1, -1]: raise ValueError( "Please provide an integer for the size of Jordan block and" " either 1 or -1 for the sign." ) else: self.size = size self.epsilon = epsilon def __str__(self): result = "(" + str(self.size) + ", " if self.epsilon == 1: result += "+)" else: result += "-)" return result def __repr__(self): return ( "JordanBlock object of size = " + str(self.size) + " and sign = " + str(self.epsilon) ) def __eq__(self, other): if isinstance(other, self.__class__): # take the two signature sequences as tuples norm1 = (self.size, self.epsilon) norm2 = (other.size, other.epsilon) # and check tuple equivalence return norm1 == norm2 else: return False
[docs]class IndexSequence: """Index sequence class Index sequences can be used to classify non-degenerate pencils (ddg.geometry.Pencil) of real quadrics (ddg.geometry.Quadric). If the pencil is represented as:: P(Q1,Q2) = lambda Q2 + Q1 with ``A, B`` real symmetric matrices representing the quadrics spanning the pencil, and we consider the roots lambda_l of:: det(lambda Q2 + Q1) = 0 then the signature sequence is calculated as:: [v_1 | v_2, ...., v_k-1 | v_k] where ``v_l`` are the index of the nonsingular matrices ``lambda Q2 + Q1`` between two roots and ``|`` is a placeholder for an array of symbols denoting the sizes and numbers of Jordan blocks in the quadric pair canonical form of the singular matrices:: lambda_l Q2 + Q1 where - ““|““ JordanBlock of size 1 - ““≀...ā‰€ā‚ŠĀ“Ā“ JordanBlock of size n (number of wavy lines) (with positive sign in the quadric pair canonical form) - ““≀...≀₋““ JordanBlock of size n (number of wavy lines) (with negative sign in the quadric pair canonical form) (The quadric pair canonical form is calculated as follows: If ``Q2`` is non-singular and ``Q2⁻¹Q1`` has Jordannormalform ``J=diag(J_1,..., J_n)``, then ``Q2, Q1`` are simultaneously congruent to:: diag(±E_1, ..., ±E_n), diag(±E_1J_1, ..., ±E_nJ_n), where ``E_i`` is a matrix of the form:: | 1 | | . | | . | | . | | 1 |) A number of operations can be performed on the attributes of an index sequence to receive an equivalent sequence: - rotate_left(indices, signatures, multiplicities) - rotate_right(indices, signatures, multiplicities) - inverse_order(indices, signatures, multiplicities) - find_complement(indices, signatures, multiplicities) If the dimension of the sequence is 4 (``RP³``), the sequence can be brought into normalform for classification via equivalence operations. Normalform is reached by the following rules in descending order of importance: - multiple roots to the front - smallest values to the front - smallest sum of values possible for indices - more negative than positive signs for the Jordanblocks Parameters ---------- indices: tuple or list of integers Indexes of the non-degenerate quadrics in the pencil given in an arbitrary order (but fitting for the other tuples) jordanblocks: tuple or list of tuples or lists of JordanBlock Sizes of the Jordanblocks in the canonical form of matrices in the pencil corresponding to roots in the characteristic function ``det(lambda Q2 + Q1)=0`` where ``lambda Q2 + Q1`` describes the pencil Attributes ---------- indices: tuple jordanblocks: tuple dimension: int calculated by sum of first and last index dim = indices[0]+ indices[-1] multiplicities: tuple calculated from the tuple of jordanblocks Raises ------ ValueError * If the tuples are not of the correct lengths (len(indices)-1=len(jordanblocks)) * If the tuples are not filled with the right type of entries: - indices: integers - jordanblocks: lists - elements of jordanblocks: JordanBlock Notes ----- This class supports the following operators: -``==``, meaning equivalency of the index sequences, reached through a projective transformation on lambda See also -------- SignatureSequence SegreSymbol ddg.geometry.Pencil """ def __init__(self, indices, jordanblocks): # if indices is a list, turn it into a tuple if isinstance(indices, list): indices = tuple(indices) # check that input types are tuples of matching lengths # list allowed for jordanblocks if ( not isinstance(indices, tuple) or ( not isinstance(jordanblocks, tuple) and not isinstance(jordanblocks, list) ) or len(indices) - 1 != len(jordanblocks) ): raise ValueError( "Lists must have matching lengths with list of indices one longer" ) multiplicities = () build_jordanblocks = () for k in range(len(jordanblocks)): # check that index is an integer and jordanblock is a tuple or list if not isinstance(indices[k], int): raise ValueError("Tuple of indices needs to have integer entries") if not isinstance(jordanblocks[k], tuple) and not isinstance( jordanblocks[k], list ): raise ValueError( "Tuple of jordanblocks needs to have tuples as entries" ) multiplicity = 0 block = () for l in range(len(jordanblocks[k])): # check that entries in jordan block tuples are jordanblocks if not isinstance(jordanblocks[k][l], JordanBlock): raise ValueError( "Entries in list of jordanblocks need to be Jordan blocks." ) else: # calculate multiplicity of root as sum of length of jordanblocks multiplicity += jordanblocks[k][l].size block += (jordanblocks[k][l],) multiplicities += (multiplicity,) # sort the tuple for every root first by descending size of jordanblocks # and then among those to have the negative epsilon in the front block = tuple(sorted(block, key=lambda block: block.epsilon)) block = tuple(sorted(block, reverse=True, key=lambda block: block.size)) build_jordanblocks += (block,) # also last index needs to be an integer if not isinstance(indices[-1], int): raise ValueError("List of indices needs to have integer entries") # dimension of sequence for an n-dim pencil is n+1 self.dimension = indices[0] + indices[-1] self.indices = indices self.jordanblocks = build_jordanblocks self.multiplicities = multiplicities return def __str__(self): sub = str.maketrans("+-", "ā‚Šā‚‹") result = "\u2329" for k in range(len(self.jordanblocks)): result += str(self.indices[k]) + " " entry = "" for block in self.jordanblocks[k]: # vertical stroke for singular root if block.size == 1: entry += "|" else: # wavy lign for a multiple root ≀ for l in range(block.size): entry += "\u2240" if block.epsilon == 1: blocksign = "+" else: blocksign = "-" entry += blocksign.translate(sub) result += entry + " " result += str(self.indices[-1]) + "\u232a" return result def __repr__(self): return ( "Index Sequence with lists of\n indices: " + str(self.indices) + ",\n jordanblocks: " + str(self.jordanblocks) )
[docs] def normalform(self): """Calculates the canonical form of the index sequence if the sequence is of dimension 4 (3 dimensional projective space) Normalform is reached by the following rules in descending order of importance: - multiple roots to the front - smallest values to the front - smallest sum of values possible for indices - more positive than negative entries in signatures The function returns an equivalent signature sequence. Returns ------- ddg.geometry.signatures.IndexSequence in canonical form Raises ------ NotImplementedError if the signature sequence is not of dimension 4 See Also -------- normalform """ # check the dimension if self.dimension == 4: indices, jordanblocks, multiplicities = normalform( self.indices, self.jordanblocks, self.multiplicities ) else: raise NotImplementedError( "Normalform is only implemented for sequences of dimension 4" ) return IndexSequence(indices, jordanblocks)
def __eq__(self, other): # check whether other is a index sequence if isinstance(other, self.__class__): # calculate the normalform of the sequences: normal1 = self.normalform() normal2 = other.normalform() # take the two normalforms as tuples norm1 = (normal1.indices, normal1.jordanblocks, normal1.multiplicities) norm2 = (normal2.indices, normal2.jordanblocks, normal2.multiplicities) # and check tuple equivalence return norm1 == norm2 else: return False
[docs] def signature_sequence(self): """Derive corresponding signature sequence to index sequence Signatures can be calculated directly from tuples of ddg.geometry.signatures.JordanBlocks Returns ------- ddg.geometry.SignatureSequence """ signatures = [] for k in range(len(self.jordanblocks)): block = self.jordanblocks[k] # signature before the jordanblock is the index (+) and dim - index (-) signat = np.array([self.indices[k], self.dimension - self.indices[k]]) # the type of jordanblocks then subtract values from either side # depending on what sign the block has and whether the size of the block # is even for elem in block: if (elem.size % 2 == 1 and elem.epsilon == 1) or ( elem.size % 2 == 0 and elem.epsilon == -1 ): signat += np.array([0, -1]) else: signat += np.array([-1, 0]) signatures += [ Signature( signat[0], signat[1], self.dimension - (signat[0] + signat[1]) ) ] signatures = tuple(signatures) # the tuples of indices and multiplicities can directly be taken # from the index sequence return SignatureSequence(self.indices, signatures, self.multiplicities)
[docs]class SegreSymbol: """Segre symbol class The Segre symbol is defined for representative matrices for a pencil of quadrics (ddg.geometry.Pencil). Two projectively equivalent pencils have matrices with the same Segre Symbol. As such, the Segre symbol can be calculated from the Smith normal form (SNF) of blocks of type:: | 1 | | a | | . | | . 1 | Q2= | . | , Q1= | . . | | . | | . . | | 1 | | a 1 | in the polynomial matrix ```lambda Q2 + Q1`` representing the pencil (see Jordannormalform for IndexSequence). SNF of block ``lambda Q2_i + Q1_i(lambda_i)``:: | 1 | | . | | . | | 1 | | (lambda-lambda_i)^(v_ij) | The Segree symbol is then the combination of the information of all blocks in the form:: [(v_11, ..., v_1r1), ..., (v_n1, ..., v_nrn)]_k with the ``v_ij`` the exponents in the SNF and ``k`` the number of real ``lambda_i`` in the symbol (counted with multiplicity). The ``v_ij`` also denote the sizes of Jordanblocks in the Jordannormalform and hence the multiplicity of the roots of the characteristic equation of the pencil:: det (lambda Q2 + Q1)=0 Parameters ---------- roots: list of integers Multiplicities of the roots of the characteristic equation of the pencil (``det (lambda Q2 + Q1)=0``) , given in an arbitrary order reals: int Number of real roots among those Attributes ---------- roots reals Raises ------ ValueError * If we don't get the right type of parameter: list and integer * If the list is not filled with the right type of entries: lists of lists of integers See also -------- ddg.geometry.signatures.SignatureSequence ddg.geometry.signatures.IndexSequence ddg.geometry.Pencil """ def __init__(self, roots, reals): # check we have a list of multiplicities of roots and one integer for the reals roots: list reals: int if not isinstance(roots, list) or not isinstance(reals, int): raise ValueError( "The type of the parameter roots needs to be a list " + "and the type of the parameter reals an integer." ) for root in roots: if not isinstance(root, list): raise ValueError("List of roots needs to have lists as entries") for elem in root: if not isinstance(elem, int): raise ValueError( "List of roots needs have lists of integers as entries" ) self.roots = roots self.reals = reals def __str__(self): # create an array of small subscript numbers # to represent the number of reals sub = str.maketrans("0123456789", "ā‚€ā‚ā‚‚ā‚ƒā‚„ā‚…ā‚†ā‚‡ā‚ˆā‚‰") subscript = str(self.reals).translate(sub) result = "[" for elem in self.roots: if len(elem) == 1: result += str(elem[0]) + " " else: result += "(" for root in elem: result += str(root) + " " result = result[:-1] + ") " result = result[:-1] result += "]" + subscript return result def __repr__(self): return ( "Segree symbol with lists of\n roots: " + str(self.roots) + ",\n number of real roots: " + str(self.reals) )
[docs] def normalform(self): """Calculates the canonical form of the segre symbol Normalform is reached by the following rules: * lists of roots sorted in descending order * longest lists of roots to the front * lists of same length sorted by their entries, highest in the front Returns ------- ddg.geometry.signatures.SegreSymbol in canonical form """ roots = self.roots for root in roots: # sort the lists in the list of roots in descending order of # size of jordanblocks root.sort(reverse=True) # sort the lists in the list by size of entries roots.sort(reverse=True) # sort the list of roots by length of list of integers roots.sort(reverse=True, key=len) return SegreSymbol(roots, self.reals)
def __eq__(self, other): # check whether other is a segre symbol if isinstance(other, self.__class__): # calculate the normalform of the sequences: normal1 = self.normalform() normal2 = other.normalform() # take the two normalforms as lists norm1 = (normal1.roots, normal1.reals) norm2 = (normal2.roots, normal2.reals) # and check list equivalence return norm1 == norm2 else: return False
[docs]def rotate_left(indices, signatures_jordanblocks, multiplicities): """Rotate Signature and Index sequences one step to the left to create a projectively equivalent sequence The second index goes to the first spot, the first index and first signature/jordanblock are thrown out. As new last signature/jordanblock, the inverse of the thrown out signature/jordanblock gets used. As new last index the ambient dimension minus the new first entry gets used. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple signatures for signature sequence jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple """ if len(indices) <= 1: return indices, signatures_jordanblocks, multiplicities # find new index to put in the front index = indices[0] + indices[-1] - indices[1] indices = indices[1:] + (index,) elem = signatures_jordanblocks[0] # inverse first signature because of crossing infinity if isinstance(elem, Signature): element = Signature(elem.minus, elem.plus, elem.zero) # or inverse direction of first jordanblock elif isinstance(elem, tuple): element = () for tupl in elem: new_tuple = JordanBlock(tupl.size, -1 * tupl.epsilon) element += (new_tuple,) element = tuple(sorted(element, key=lambda block: block.epsilon)) element = tuple(sorted(element, reverse=True, key=lambda block: block.size)) else: raise NotImplementedError # rotate the tuples to the left signatures_jordanblocks = signatures_jordanblocks[1:] + (element,) multiplicities = multiplicities[1:] + (multiplicities[0],) return indices, signatures_jordanblocks, multiplicities
[docs]def rotate_right(indices, signatures_jordanblocks, multiplicities): """Rotate Signature and Index sequences one step to the right to create a projectively equivalent sequence The second to last index goes to the last spot, the last index and last signature/jordanblock are thrown out. As new first signature/jordanblock, the inverse of the thrown out signature/jordanblock gets used. As new first index the ambient dimension minus the new last entry gets used. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple signatures for signature sequence jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple """ if len(indices) <= 1: return indices, signatures_jordanblocks, multiplicities # find new index to put in the back, # ambient dimension: indices[0] + indices[-1] index = indices[0] + indices[-1] - indices[-2] indices = (index,) + indices[:-1] # pluck signature of last root elem = signatures_jordanblocks[-1] # inverse last signature because of crossing infinity if isinstance(elem, Signature): element = Signature(elem.minus, elem.plus, elem.zero) # or inverse direction of last jordanblock elif isinstance(elem, tuple): element = () for tupl in elem: new_tuple = JordanBlock(tupl.size, -1 * tupl.epsilon) element += (new_tuple,) element = tuple(sorted(element, key=lambda block: block.epsilon)) element = tuple(sorted(element, reverse=True, key=lambda block: block.size)) else: raise NotImplementedError # rotate the tuples to the right signatures_jordanblocks = (element,) + signatures_jordanblocks[:-1] multiplicities = (multiplicities[-1],) + multiplicities[:-1] return indices, signatures_jordanblocks, multiplicities
[docs]def inverse_order(indices, signatures_jordanblocks, multiplicities): """ Inverses the order of a Signature and Index sequence to create a projectively equivalent sequence. Note that signatures stay unchanged while in the jordanblocks the epsilon changes exactly for blocks of odd size. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple signatures for signature sequence jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple """ indices = indices[::-1] signatures_jordanblocks = signatures_jordanblocks[::-1] new_signatures_jordanblocks = () for k in range(len(signatures_jordanblocks)): # inverse the plus and minus entries in signatures if isinstance(signatures_jordanblocks[k], Signature): new_signatures_jordanblocks += (signatures_jordanblocks[k],) # exchange the + and - in jordanblocks elif isinstance(signatures_jordanblocks[k], tuple): new_block = () for tupl in signatures_jordanblocks[k]: if tupl.size % 2 != 0: new_tuple = JordanBlock(tupl.size, -1 * tupl.epsilon) else: new_tuple = tupl new_block += (new_tuple,) new_block = tuple(sorted(new_block, key=lambda block: block.epsilon)) new_block = tuple( sorted(new_block, reverse=True, key=lambda block: block.size) ) new_signatures_jordanblocks += (new_block,) else: raise NotImplementedError multiplicities = multiplicities[::-1] return indices, new_signatures_jordanblocks, multiplicities
[docs]def find_complement(indices, signatures_jordanblocks, multiplicities): """ Inverse the orientation of a Signature or Index sequence to create a projectively equivalent sequence every index n gets turned into d- n, where d is the ambient dimension, every signature gets replaced by its inverse (plus and minus entry reversed) every jordanblock gets replaced by the same jordanblock with negative sign Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple - signatures for signature sequence - jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple """ dimension = indices[0] + indices[-1] new_indices = () new_signatures_jordanblocks = () for k in range(len(signatures_jordanblocks)): # find the complement of the indices new_indices += (dimension - indices[k],) # inverse the plus and minus entries in signatures if isinstance(signatures_jordanblocks[k], Signature): new_signatures_jordanblocks += ( Signature( signatures_jordanblocks[k].minus, signatures_jordanblocks[k].plus, signatures_jordanblocks[k].zero, ), ) # exchange the + and - in jordanblocks elif isinstance(signatures_jordanblocks[k], tuple): new_block = () for tupl in signatures_jordanblocks[k]: new_tuple = JordanBlock(tupl.size, -1 * tupl.epsilon) new_block += (new_tuple,) new_block = tuple(sorted(new_block, key=lambda block: block.epsilon)) new_block = tuple( sorted(new_block, reverse=True, key=lambda block: block.size) ) new_signatures_jordanblocks += (new_block,) else: raise NotImplementedError # indices is one longer: new_indices += (dimension - indices[-1],) return new_indices, new_signatures_jordanblocks, multiplicities
def _check_multiple_root(indices, signatures_jordanblocks, multiplicities): """ Check if there are multiple roots in the sequence and rotate them to the front if there is only one multiple root. This returns an equivalent signature/index sequence. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple - signatures for signature sequence - jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple with four entries - bool whether a singular multiple root was rotated to the front - three entries of type tuple See also -------- normalform """ # track whether we have already found a multiple root and rotated the sequence answ = False for k in range(len(multiplicities)): # find triple root if existing # (don't need to find 4x root as that one will be in the only place anyway) if multiplicities[k] == 3: # rotates triple root to the front, # one step is enough here bc triple root either first or second root if k > 0: indices, signatures_jordanblocks, multiplicities = rotate_left( indices, signatures_jordanblocks, multiplicities ) # save that we have found a triple root answ = True # find double root if existing elif multiplicities[k] == 2: # rotate to the front for j in range(k): indices, signatures_jordanblocks, multiplicities = rotate_left( indices, signatures_jordanblocks, multiplicities ) # if we have already found a 2 before that means we only have 2s # and don't need to care about which one is in front # otherwise, denote that one 2 has been found if answ: answ = False else: answ = True else: continue return answ, indices, signatures_jordanblocks, multiplicities def _search_for_zeros(indices, signatures_jordanblocks, multiplicities): """ searches a sequence for zeros and puts them in front Check if there are zeros among the indices of the sequence and rotate them to the front. This returns an equivalent signature/index sequence. In case there is a four instead of a zero, calculate the inverse of the sequence and rotate the four (now zero) to the front Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple - signatures for signature sequence - jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple with four entries - bool whether a zero was rotated to the front - three entries of type tuple See also -------- normalform """ # track whether we have already changed/found a 0 or 4 answ = False for k in range(len(indices)): # as long as we haven't found 0s or 4s yet: # if there is a zero, rotate it to the front if not answ and indices[k] == 0: answ = True for j in range(k): indices, signatures_jordanblocks, multiplicities = rotate_left( indices, signatures_jordanblocks, multiplicities ) # as long as we haven't found 0s or 4s yet: # if we find a 4, get the complement to have a 0 # and rotate that one to the front if not answ and indices[k] == 4: answ = True # gets the complement in the same equivalence class indices, signatures_jordanblocks, multiplicities = find_complement( indices, signatures_jordanblocks, multiplicities ) for j in range(k): indices, signatures_jordanblocks, multiplicities = rotate_left( indices, signatures_jordanblocks, multiplicities ) else: continue return answ, indices, signatures_jordanblocks, multiplicities def _change_orientation_not_order(indices, signatures_jordanblocks, multiplicities): """ Changes only the orientation of the sequence around the first zero This is in case the rest is immutable because there is a multiple root for example. The function returns an equivalent signature/index sequence. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple - signatures for signature sequence - jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple See also -------- normalform """ # Create the lowest possible values in the front, # if they sum up to >=4, the complement will return smaller values if indices[0] + indices[1] > 4: # calculate complement indices, signatures_jordanblocks, multiplicities = find_complement( indices, signatures_jordanblocks, multiplicities ) # is the smaller or equal value of the two in front? if indices[0] > indices[1]: # inverse order of first two entries by inversing the order # then rotating left (rotating right would change indices again) indices, signatures_jordanblocks, multiplicities = inverse_order( indices, signatures_jordanblocks, multiplicities ) for j in range(len(indices) - 2): indices, signatures_jordanblocks, multiplicities = rotate_left( indices, signatures_jordanblocks, multiplicities ) return indices, signatures_jordanblocks, multiplicities def _threes_into_ones_fixed_first_entries( indices, signatures_jordanblocks, multiplicities ): """ Turns indices of three into ones by getting the complement if the first two indices in the sequence are both twos The function returns an equivalent signature/index sequence. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple - signatures for signature sequence - jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple See also -------- normalform """ # dictionary to count 1s and 3s values_dict = {"ones": 0, "threes": 0} # go through the sequence and count 1s and 3s (no 0s and 4s possible) for k in range(len(indices)): if indices[k] == 1: values_dict["ones"] += 1 elif indices[k] == 3: values_dict["threes"] += 1 else: continue # make sure we have at least as many 1s as 3s if values_dict["ones"] < values_dict["threes"]: # by taking the complement if necessary indices, signatures_jordanblocks, multiplicities = find_complement( indices, signatures_jordanblocks, multiplicities ) return indices, signatures_jordanblocks, multiplicities def _threes_into_ones(indices, signatures_jordanblocks, multiplicities): """ Searches indices of one and three in the sequence, makes us have the highest possible amount of ones and puts them close together at the start Also handles the case of indices being a tuple of only twos. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple - signatures for signature sequence - jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple with four entries - bool whether a one was rotated to the front - three entries of type tuple See also -------- normalform """ # empty lists to save where the indices of one and three are ones = [] threes = [] # tracks whether we have already changed/found something answ = False for k in range(len(indices)): if indices[k] == 1: ones += [k] elif indices[k] == 3: threes += [k] else: continue # if we have less 1s than 3s we take the complement if len(ones) < len(threes): answ = True indices, signatures_jordanblocks, multiplicities = find_complement( indices, signatures_jordanblocks, multiplicities ) # also update where the 1s and 3s are bc that is inverse now ones, threes = threes, ones # if there are only indices of value 2, # that is (2], [2,2] or [2,2,2] # make first signature have the highest plus value possible for signature sequence # or accordingly make sure there are the most possible negative jordanblocks # for index sequence if len(ones) == 0: # if the list isn't empty, sort by types of signatures/jordanblocks if len(signatures_jordanblocks) >= 1: if isinstance(signatures_jordanblocks[0], Signature): indices, signatures_jordanblocks, multiplicities = sort_by_signature( indices, signatures_jordanblocks, multiplicities ) else: indices, signatures_jordanblocks, multiplicities = sort_by_jordanblock( indices, signatures_jordanblocks, multiplicities ) else: # only change signature if there isn't already a 1 at the front if ones[0] != 0: answ = True # rotate first one to the front for j in range(ones[0]): indices, signatures_jordanblocks, multiplicities = rotate_left( indices, signatures_jordanblocks, multiplicities ) # repeat in case rotating has lead to more threes than ones in the sequence # This will not lead to a loop since at some point we reach the canonical form # and stop changing the sequence if answ: indices, signatures_jordanblocks, multiplicities = _threes_into_ones( indices, signatures_jordanblocks, multiplicities ) return indices, signatures_jordanblocks, multiplicities
[docs]def sort_by_signature(indices, signatures, multiplicities): """ Makes first signature have the highest plus value possible Parameters ---------- indices: tuple indices of the sequence signatures: tuple signatures for signature sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple See also -------- threes_into_ones """ # if the first signature has more negative eigenvalues than positive # find the complement of the signature sequence if len(signatures) >= 1 and signatures[0].plus < signatures[0].minus: indices, signatures, multiplicities = find_complement( indices, signatures, multiplicities ) # if instead the first signature has as many positive eigenvalues as negative # and the second signature has more negative eigenvalues than positive # rotate to the right turning the second signature into the inversed first if ( len(signatures) >= 2 and signatures[0].plus >= signatures[0].minus and signatures[1].plus < signatures[1].minus ): indices, signatures, multiplicities = rotate_right( indices, signatures, multiplicities ) # there can not be more than two signatures for this case return indices, signatures, multiplicities
[docs]def sort_by_jordanblock(indices, jordanblocks, multiplicities): """ Makes the number of negative jordanblocks maximal Parameters ---------- indices: tuple indices of the sequence jordanblocks: tuple signatures for signature sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple See also -------- threes_into_ones """ # if the first block in the first tuple of jordanblocks is positive # find the complement of the sequence if len(jordanblocks) >= 1 and jordanblocks[0][0].epsilon == 1: indices, jordanblocks, multiplicities = find_complement( indices, jordanblocks, multiplicities ) # if now the first block in the second tuple of jordanblocks is positive, # rotate to the right # this will keep the first jordanblock negative and turn the second into # the inversed first (also negative) if len(jordanblocks) >= 2 and jordanblocks[1][0].epsilon == 1: indices, jordanblocks, multiplicities = rotate_right( indices, jordanblocks, multiplicities ) # there can not be more than two roots for this case return indices, jordanblocks, multiplicities
[docs]def normalform(indices, signatures_jordanblocks, multiplicities): """Brings sequence into the normalform Normalform is reached by the following rules in descending order of importance: - multiple roots to the front - smallest values to the front - smallest sum of values possible for indices - more positive than negative entries in signatures The function returns an equivalent signature/index sequence. Parameters ---------- indices: tuple indices of the sequence signatures_jordanblocks: tuple signatures for signature sequence jordanblocks for index sequence multiplicities: tuple multiplicities of sequence Returns ------- tuple of three entries of type tuple Raises ------ NotImplementedError if the dimension of the sequence isn't four """ if indices[0] + indices[-1] != 4: raise NotImplementedError( "Normalform only implemented for sequences of dimension 4." ) # if existing find the multiple root and bring it to the front answ, indices, signatures_jordanblocks, multiplicities = _check_multiple_root( indices, signatures_jordanblocks, multiplicities ) # if we only have one multiplicity: answ=false if not answ: # then we don't need to bring any multiple root to the front # and can get the most possible zeros answ, indices, signatures_jordanblocks, multiplicities = _search_for_zeros( indices, signatures_jordanblocks, multiplicities ) # and if there aren't any zeros, we get the most possible ones if not answ: indices, signatures_jordanblocks, multiplicities = _threes_into_ones( indices, signatures_jordanblocks, multiplicities ) else: # if we have a multiple root, # make sure the first two indices around the multiple root are minimal ( indices, signatures_jordanblocks, multiplicities, ) = _change_orientation_not_order( indices, signatures_jordanblocks, multiplicities ) # if the first two indices are identical now, # we then make sure we have the lowest possible entries everywhere else if len(indices) >= 2 and indices[0] == indices[1]: ( indices, signatures_jordanblocks, multiplicities, ) = _threes_into_ones_fixed_first_entries( indices, signatures_jordanblocks, multiplicities ) return indices, signatures_jordanblocks, multiplicities
[docs]def complete_types_dictionary(): """ Completes the ddg.geometry.signatures.types_to_index_and_segre dictionary with the corresponding ddg.geometry.SignatureSequence The resulting dictionary has entries of the form: "typeN": [ ddg.geometry.signatures.SignatureSequence, ddg.geometry.signatures.IndexSequence, ddg.geometry.signatures.SegreSymbol ] Returns ------- dict """ complete_dict = {} for elem in types_to_index_and_segre: index_seq = types_to_index_and_segre[elem][0] segre_symb = types_to_index_and_segre[elem][1] signat_seq = index_seq.signature_sequence() complete_dict[elem] = [signat_seq, index_seq, segre_symb] return complete_dict
signatures_to_types = { ( (1, 2, 1, 2, 3,), # fmt: skip ( Signature(1, 2, 1), Signature(1, 2, 1), Signature(1, 2, 1), Signature(2, 1, 1), ), (1, 1, 1, 1,), # fmt: skip ): [ "type1", "Type 1: The Quadric Surface Intersection Curve has " + "two null-homotopic components.", ], ( (0, 1, 2, 3, 4,), # fmt: skip ( Signature(0, 3, 1), Signature(1, 2, 1), Signature(2, 1, 1), Signature(3, 0, 1), ), (1, 1, 1, 1,), # fmt: skip ): [ "type2", "Type 2: The Quadric Surface Intersection Curve" + " is vacuous in RP³.", ], ((1, 2, 3,), (Signature(1, 2, 1), Signature(2, 1, 1),), (1, 1,),): [ "type3", "Type 3: The Quadric Surface Intersection Curve " + "has one null-homotopic component.", ], ((2,), (), ()): ["specialcase1"], ( (2, 2, 3, 2,), # fmt: skip (Signature(2, 1, 1), Signature(2, 1, 1), Signature(2, 1, 1),), (2, 1, 1,), # fmt: skip ): [ "type5a", "Type 5: The Quadric Surface Intersection Curve " + "has two null-homotopic components joining at a crunode.", ], ( (2, 2, 1, 2,), # fmt: skip (Signature(1, 2, 1), Signature(1, 2, 1), Signature(1, 2, 1),), (2, 1, 1,), # fmt: skip ): [ "type5b", "Type 5: The Quadric Surface Intersection Curve" + " has two null-homotopic components joining at a crunode.", ], ( (1, 1, 2, 3,), # fmt: skip (Signature(1, 2, 1), Signature(1, 2, 1), Signature(2, 1, 1),), (2, 1, 1,), # fmt: skip ): [ "type6", "Type 6: The Quadric Surface Intersection Curve" + " has one null-homotopic component and an acnode.", ], ( (1, 1, 2, 3,), # fmt: skip (Signature(0, 3, 1), Signature(1, 2, 1), Signature(2, 1, 1),), (2, 1, 1,), # fmt: skip ): [ "type7", "Type 7: The Quadric Surface Intersection Curve" + " has one real point, which is an acnode.", ], ((2, 2,), (Signature(2, 1, 1),), (2,)): [ "type8", "Type 8: The Quadric Surface Intersection Curve" + " has two non-null-homotopic components intersecting at a crunode.", ], ((2, 2, 2,), (Signature(2, 1, 1), Signature(2, 1, 1),), (2, 2,),): [ "type9", "Type 9: The Quadric Surface Intersection Curve has " + "one real line and one space cubic curve, intersecting at two " + "distinct real points.", ], ((1, 2, 3,), (Signature(1, 2, 1), Signature(2, 1, 1),), (3, 1,),): [ "type11", "Type 11: The Quadric Surface Intersection Curve " + "has one null-homotopic component with a cusp.", ], ((2, 2,), (Signature(2, 1, 1),), (4,)): [ "type12", "Type 12: The Quadric Surface Intersection Curve " + "has one real line and one real space cubic curve; the line is tangent to" + " the cubic at a real point.", ], ( (2, 2, 1, 2,), # fmt: skip (Signature(1, 1, 2), Signature(1, 2, 1), Signature(1, 2, 1),), (2, 1, 1,), # fmt: skip ): [ "type13", "Type 13: The Quadric Surface Intersection Curve is " + "planar with two real conics intersecting at two distinct real points.", ], ( (1, 3, 2, 3,), # fmt: skip (Signature(1, 1, 2), Signature(2, 1, 1), Signature(2, 1, 1),), (2, 1, 1,), # fmt: skip ): [ "type14", "Type 14: The Quadric Surface Intersection Curve " + "is planar with two real conics intersecting at two " + "complex conjugate points.", ], ( (1, 1, 2, 3,), # fmt: skip (Signature(0, 2, 2), Signature(1, 2, 1), Signature(2, 1, 1),), (2, 1, 1,), # fmt: skip ): [ "type15", "Type 15: The Quadric Surface Intersection Curve is planar " + "with two imaginary conics intersecting at two distinct real points.", ], ( (0, 2, 3, 4,), # fmt: skip (Signature(0, 2, 2), Signature(2, 1, 1), Signature(3, 0, 1),), (2, 1, 1,), # fmt: skip ): [ "type16a", "Type 16: The Quadric Surface Intersection Curve is planar" + " with two imaginary conics intersecting at two complex conjugate points.", ], ( (1, 3, 4, 3,), # fmt: skip (Signature(1, 1, 2), Signature(3, 0, 1), Signature(3, 0, 1),), (2, 1, 1,), # fmt: skip ): [ "type16b", "Type 16: The Quadric Surface Intersection Curve is planar" + " with two imaginary conics intersecting at two complex conjugate points.", ], ((1, 3,), (Signature(1, 1, 2),), (2,)): [ "type17", "Type 17: The Quadric Surface Intersection Curve " + "is planar with one real conic and one imaginary conic; " + "they intersect at two complex conjugate points.", ], ((2, 2,), (Signature(1, 1, 2),), (2,)): [ "type18", "Type 18: The Quadric Surface Intersection Curve " + "is planar with two real conics intersecting at two distinct real points. " + "The two conics cannot both be ellipses simultaneously in any " "affine realization of RP³. In other words, the QSIC is intersected by " + "every plane in RP³.", ], ((1, 2, 3,), (Signature(0, 1, 3), Signature(2, 1, 1),), (3, 1,),): [ "type19", "Type 19: The Quadric Surface Intersection Curve" + " is planar with one real conic counted twice.", ], ((0, 3, 4,), (Signature(0, 1, 3), Signature(3, 0, 1),), (3, 1,),): [ "type20", "Type 20: The Quadric Surface Intersection Curve " + "is planar with one imaginary conic counted twice.", ], ((1, 2, 3,), (Signature(1, 1, 2), Signature(2, 1, 1),), (3, 1,),): [ "type21", "Type 21: The Quadric Surface Intersection Curve" + " is planar with two real conics tangent to each other at one real point.", ], ((1, 2, 3,), (Signature(0, 2, 2), Signature(2, 1, 1),), (3, 1,),): [ "type22", "Type 22: The Quadric Surface Intersection Curve is planar with" + " two imaginary conics tangent to each other at one real point.", ], ((2, 2, 2,), (Signature(2, 1, 1), Signature(1, 1, 2),), (2, 2,),): [ "type23", "Type 23: The Quadric Surface Intersection Curve " + "is planar with one real conic and two real lines; they intersect pairwise " + "at three distinct real points.", ], ((1, 1, 3,), (Signature(1, 2, 1), Signature(1, 1, 2),), (2, 2,),): [ "type24", "Type 24: The Quadric Surface Intersection Curve " + "is planar with one real conic and a pair of complex conjugate lines. The " + "conic and the pair of lines intersect at two complex conjugate points.", ], ((1, 1, 3,), (Signature(0, 3, 1), Signature(1, 1, 2),), (2, 2,),): [ "type25", "Type 25: The Quadric Surface Intersection Curve " + "is planar with one imaginary conic and a pair of complex conjugate lines. " + "The conic and the pair of lines intersect at two complex conjugate points.", ], # fmt: off ((2, 2,), (Signature(1, 1, 2),), (4,),): ["specialcase2"], # fmt: on ((1, 3,), (Signature(1, 1, 2),), (4,),): [ "type27", "Type 27: The Quadric Surface Intersection Curve " + "is planar with one real conic and a pair of complex conjugate lines; " + "these three components intersect at a common real point.", ], ((2, 2, 2,), (Signature(1, 1, 2), Signature(1, 1, 2),), (2, 2,),): [ "type28", "Type 28: The Quadric Surface Intersection Curve " + "is planar with four real lines which form a quadrangle in RP³.", ], ((0, 2, 4,), (Signature(0, 2, 2), Signature(2, 0, 2),), (2, 2,),): [ "type29", "Type 29: The Quadric Surface Intersection Curve " + "is planar with four imaginary lines which form an imaginary quadrangle; each" + " of the four lines intersects two of the other three at imaginary points.", ], ((1, 1, 3,), (Signature(0, 2, 2), Signature(1, 1, 2),), (2, 2,),): [ "type30", "Type 30: The Quadric Surface Intersection Curve " + "is planar with two pairs of complex conjugate lines; one pair intersects " + "the other pair at two complex conjugate points.", ], ((2, 2,), (Signature(1, 0, 3),), (4,)): [ "type32", "Type 32: The Quadric Surface Intersection Curve " + "is planar with one pair of intersecting real lines, counted twice.", ], ((1, 3,), (Signature(1, 0, 3),), (4,)): [ "type33", "Type 33: The Quadric Surface Intersection Curve " + "is planar with one pair of complex conjugate lines, counted twice.", ], ((2, 2,), (Signature(2, 0, 2),), (4,)): [ "type34", "Type 34: The Quadric Surface Intersection Curve " + "is planar with one real double line and one pair of skew imaginary lines.", ], } types_to_index_and_segre = { "type1": [ IndexSequence( (1, 2, 1, 2, 3), ( (JordanBlock(1, 1),), (JordanBlock(1, -1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[1], [1], [1], [1]], reals=4), ], "type2": [ IndexSequence( (0, 1, 2, 3, 4), ( (JordanBlock(1, 1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[1], [1], [1], [1],], reals=4,), # fmt: skip ], "type3": [ IndexSequence( (1, 2, 3), ( (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[1], [1], [1], [1],], reals=2,), # fmt: skip ], "type4": [ IndexSequence((2,), (),), # fmt: skip SegreSymbol(roots=[[1], [1], [1], [1]], reals=0), # fmt: skip ], "type5a": [ IndexSequence( (2, 2, 1, 2), ( (JordanBlock(2, 1),), (JordanBlock(1, -1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[2], [1], [1]], reals=3), ], "type5b": [ IndexSequence( (2, 2, 1, 2), ( (JordanBlock(2, -1),), (JordanBlock(1, -1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[2], [1], [1]], reals=3), ], "type6": [ IndexSequence( (1, 1, 2, 3), ( (JordanBlock(2, -1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[2], [1], [1]], reals=3), ], "type7": [ # fmt: off IndexSequence( (1, 1, 2, 3,), # fmt: skip ( (JordanBlock(2, 1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[2], [1], [1]], reals=3), # fmt: on ], "type8": [ # fmt: off IndexSequence( (2, 2, ), ((JordanBlock(2, -1),),), ), SegreSymbol(roots=[[2], [1], [1]], reals=1), # fmt: on ], "type9": [ # fmt: off IndexSequence( (2, 2, 2,), ((JordanBlock(2, -1),), (JordanBlock(2, -1),),), ), SegreSymbol(roots=[[2], [2]], reals=2), # fmt: on ], "type10": [ IndexSequence((2,), (),), # fmt: skip SegreSymbol(roots=[[2], [2]], reals=0), # fmt: skip ], "type11": [ IndexSequence( (1, 2, 3,), # fmt: skip ((JordanBlock(3, 1),), (JordanBlock(1, 1),),), # fmt: skip ), SegreSymbol(roots=[[3], [1]], reals=2), ], "type12": [ # fmt: off IndexSequence((2, 2,), ((JordanBlock(4, -1),),)), SegreSymbol(roots=[[4]], reals=1), # fmt: on ], "type13": [ # fmt: off IndexSequence( (2, 2, 1, 2,), ( (JordanBlock(1, -1), JordanBlock(1, 1),), (JordanBlock(1, -1),), (JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1], [1]], reals=3), ], "type14": [ # fmt: off IndexSequence( (1, 3, 2, 3), ( (JordanBlock(1, 1), JordanBlock(1, 1),), (JordanBlock(1, -1),), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[1, 1], [1], [1]], reals=3), ], # fmt: on "type15": [ # fmt: off IndexSequence( (1, 1, 2, 3), ( (JordanBlock(1, -1), JordanBlock(1, 1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1], [1]], reals=3), ], "type16a": [ # fmt: off IndexSequence( (0, 2, 3, 4), ( (JordanBlock(1, 1), JordanBlock(1, 1),), (JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1], [1]], reals=3), ], "type16b": [ # fmt: off IndexSequence( (1, 3, 4, 3), ( (JordanBlock(1, 1), JordanBlock(1, 1),), (JordanBlock(1, 1),), (JordanBlock(1, -1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1], [1]], reals=3), ], "type17": [ # fmt: off IndexSequence( (1, 3,), ((JordanBlock(1, 1), JordanBlock(1, 1),),), ), # fmt: on SegreSymbol(roots=[[1, 1], [1], [1]], reals=1), ], "type18": [ # fmt: off IndexSequence( (2, 2,), ((JordanBlock(1, -1), JordanBlock(1, 1),),), ), # fmt: on SegreSymbol(roots=[[1, 1], [1], [1]], reals=1), ], "type19": [ # fmt: off IndexSequence( (1, 2, 3,), ( ( JordanBlock(1, -1), JordanBlock(1, 1), JordanBlock(1, 1), ), (JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1, 1], [1]], reals=2), ], "type20": [ IndexSequence( (0, 3, 4), ( ( JordanBlock(1, 1), JordanBlock(1, 1), JordanBlock(1, 1), ), (JordanBlock(1, 1),), ), ), SegreSymbol(roots=[[1, 1, 1], [1]], reals=2), ], "type21": [ # fmt: off IndexSequence( (1, 2, 3), ( (JordanBlock(2, -1), JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[2, 1], [1]], reals=2), ], "type22": [ # fmt: off IndexSequence( (1, 2, 3), ( (JordanBlock(2, 1), JordanBlock(1, 1),), (JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[2, 1], [1]], reals=2), ], "type23": [ # fmt: off IndexSequence( (2, 2, 2), ( (JordanBlock(2, -1),), (JordanBlock(1, -1), JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[2], [1, 1]], reals=2), ], "type24": [ # fmt: off IndexSequence( (1, 1, 3), ( (JordanBlock(2, -1),), (JordanBlock(1, 1), JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[2], [1, 1]], reals=2), ], "type25": [ # fmt: off IndexSequence( (1, 1, 3,), ( (JordanBlock(2, 1),), (JordanBlock(1, 1), JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[2], [1, 1]], reals=2), ], "type26": [ # fmt: off IndexSequence( (2, 2,), ((JordanBlock(3, -1), JordanBlock(1, 1),),), ), # fmt: on SegreSymbol(roots=[[3, 1]], reals=1), ], "type27": [ # fmt: off IndexSequence( (1, 3,), ((JordanBlock(3, 1), JordanBlock(1, 1),),), ), # fmt: on SegreSymbol(roots=[[3, 1]], reals=1), ], "type28": [ # fmt: off IndexSequence( (2, 2, 2,), ( (JordanBlock(1, -1), JordanBlock(1, 1),), (JordanBlock(1, -1), JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1, 1]], reals=2), ], "type29": [ # fmt: off IndexSequence( (0, 2, 4,), ( (JordanBlock(1, 1), JordanBlock(1, 1),), (JordanBlock(1, 1), JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1, 1]], reals=2), ], "type30": [ # fmt: off IndexSequence( (1, 1, 3,), ( (JordanBlock(1, -1), JordanBlock(1, 1),), (JordanBlock(1, 1), JordanBlock(1, 1),), ), ), # fmt: on SegreSymbol(roots=[[1, 1], [1, 1]], reals=2), ], "type31": [ # fmt: off IndexSequence((2,), (),), # fmt: on SegreSymbol(roots=[[1, 1], [1, 1]], reals=0), ], "type32": [ # fmt: off IndexSequence( (2, 2,), ( ( JordanBlock(2, -1), JordanBlock(1, -1), JordanBlock(1, 1), ), ), ), # fmt: on SegreSymbol(roots=[[2, 1, 1]], reals=1), ], "type33": [ # fmt: off IndexSequence( (1, 3,), ( ( JordanBlock(2, -1), JordanBlock(1, 1), JordanBlock(1, 1), ), ), ), # fmt: on SegreSymbol(roots=[[2, 1, 1]], reals=1), ], "type34": [ # fmt: off IndexSequence( (2, 2,), ((JordanBlock(2, -1), JordanBlock(2, -1),),), ), # fmt: on SegreSymbol(roots=[[2, 2]], reals=1), ], "type35": [ # fmt: off IndexSequence( (2, 2,), ((JordanBlock(2, -1), JordanBlock(2, 1),),), ), # fmt: on SegreSymbol(roots=[[2, 2]], reals=1), ], }