From 3581c804cf25155fc80ecc5f0547e84ab252218b Mon Sep 17 00:00:00 2001 From: queue-miscreant Date: Tue, 5 Aug 2025 03:17:36 -0500 Subject: [PATCH] haskellify finite-field.4 --- posts/finite-field/2/Previous.hs | 23 +- posts/finite-field/4/Previous.hs | 1 + posts/finite-field/4/index.qmd | 727 +++++++++++++++++-------------- 3 files changed, 407 insertions(+), 344 deletions(-) create mode 120000 posts/finite-field/4/Previous.hs diff --git a/posts/finite-field/2/Previous.hs b/posts/finite-field/2/Previous.hs index d8db7dc..4eca892 100644 --- a/posts/finite-field/2/Previous.hs +++ b/posts/finite-field/2/Previous.hs @@ -19,7 +19,7 @@ zipAdd (c:cs) (d:ds) = (c + d):zipAdd cs ds ------------------------------------------------------------------------------} -- A polynomial is its ascending list of coefficients (of type a) -newtype Polynomial a = Poly { coeffs :: [a] } deriving Functor +newtype Polynomial a = Poly { coeffs :: [a] } deriving (Functor) instance (Eq a, Num a) => Num (Polynomial a) where (+) x@(Poly as) y@(Poly bs) = Poly $ zipAdd as bs @@ -47,9 +47,9 @@ instance (Eq a, Num a) => Eq (Polynomial a) where -- Interpret a number's base-b expansion as a polynomial -asPoly :: Int -> Int -> Polynomial Int -- Build a list with f, which returns either Nothing -- or Just (next element of list, next argument to f) +asPoly :: Int -> Int -> Polynomial Int asPoly b = Poly . unfoldr f where -- Divide x by b. Emit the remainder and recurse with the quotient. f x | x /= 0 = Just $ swap $ divMod x b @@ -57,10 +57,9 @@ asPoly b = Poly . unfoldr f where | otherwise = Nothing -- Horner evaluation of a polynomial at the integer b -evalPoly :: Int -> Polynomial Int -> Int -- Start with the highest coefficient -- Multiply by b at each step and add the coefficient of the next term -evalPoly b (Poly p) = foldr (\y acc -> acc*b + y) 0 p +evalPoly b (Poly p) = foldr (\y acc -> b*acc + y) 0 p -- Divide the polynomial ps by qs (coefficients in descending degree order) synthDiv' :: (Eq a, Num a) => [a] -> [a] -> ([a], [a]) @@ -122,7 +121,7 @@ irreducibles n = go [] $ allMonics n where - ------------------------------------------------------------------------------} -newtype Matrix a = Mat { unMat :: Array (Int, Int) a } deriving (Functor, Eq) +newtype Matrix a = Mat { unMat :: Array (Int, Int) a } deriving (Functor) asScalar = Mat . listArray ((0,0),(0,0)) . pure @@ -148,12 +147,22 @@ zipWithArr f a b mapRange :: Ix i => (i -> e) -> (i, i) -> [(i, e)] mapRange g r = map (\x -> (x, g x)) $ range r +instance (Num a, Eq a) => Eq (Matrix a) where + (==) (Mat x) (Mat y) + | (0,0) == (snd $ bounds x) = and [x!(0,0) == if m == n then u else 0 | ((m,n), u) <- assocs y] + | (0,0) == (snd $ bounds y) = and [x!(0,0) == if m == n then u else 0 | ((m,n), u) <- assocs x] + | otherwise = x == y + + instance Num a => Num (Matrix a) where (+) (Mat x) (Mat y) - | (0,0) == (snd $ bounds x) = Mat $ fmap ((x!(0,0))+) y --adding scalars + | (0,0) == (snd $ bounds x) && (0,0) == (snd $ bounds y) = Mat $ zipWithArr (+) x y + | (0,0) == (snd $ bounds x) = Mat y + (((x!(0,0)) *) <$> eye ((+1) $ snd $ snd $ bounds y)) --adding scalars + | (0,0) == (snd $ bounds y) = Mat x + (((y!(0,0)) *) <$> eye ((+1) $ snd $ snd $ bounds x)) --adding scalars | otherwise = Mat $ zipWithArr (+) x y (*) x y | (0,0) == (snd $ bounds $ unMat x) = fmap (((unMat x)!(0,0))*) y --multiplying scalars + | (0,0) == (snd $ bounds $ unMat y) = fmap (((unMat y)!(0,0))*) x --multiplying scalars | otherwise = toMatrix $ matMul' (fromMatrix x) (fromMatrix $ transposeM y) abs = fmap abs negate = fmap negate @@ -183,7 +192,7 @@ eye :: Num a => Int -> Matrix a eye n = Mat $ unMat (zero n) // take n [((i,i), 1) | i <- [0..]] -- Companion matrix -companion :: Polynomial Int -> Matrix Int +companion :: (Eq a, Num a) => Polynomial a -> Matrix a companion (Poly ps) | last ps' /= 1 = error "Cannot find companion matrix of non-monic polynomial" | otherwise = Mat $ array ((0,0), (n-1,n-1)) $ lastRow ++ shiftI where diff --git a/posts/finite-field/4/Previous.hs b/posts/finite-field/4/Previous.hs new file mode 120000 index 0000000..fbcffd3 --- /dev/null +++ b/posts/finite-field/4/Previous.hs @@ -0,0 +1 @@ +../2/Previous.hs \ No newline at end of file diff --git a/posts/finite-field/4/index.qmd b/posts/finite-field/4/index.qmd index 49fa85c..6d24616 100644 --- a/posts/finite-field/4/index.qmd +++ b/posts/finite-field/4/index.qmd @@ -5,15 +5,102 @@ description: | format: html: html-math-method: katex -jupyter: python3 date: "2024-02-03" -date-modified: "2025-07-20" +date-modified: "2025-08-05" categories: - algebra - finite field - haskell --- +```{haskell} +--| echo: false + +:l Previous.hs + +import Data.Array (ixmap, bounds, (!), array, range) +import Data.List (intercalate) +import IHaskell.Display (markdown) + +import Previous ( + Matrix(Mat, unMat), Polynomial(Poly, coeffs), + asPoly, evalPoly, synthDiv, + companion, zero, eye, fromMatrix, toMatrix + ) + +-- Explicit Matrix operations +(|+|) :: Num a => Matrix a -> Matrix a -> Matrix a +(|+|) = (+) + +(|*|) :: Num a => Matrix a -> Matrix a -> Matrix a +(|*|) = (*) + +-- Original determinant implementation, instead of the imported one based on Faddeev-Leverrier +determinant :: (Num a, Eq a) => Matrix a -> a +determinant (Mat xs) = determinant' xs where + -- Evaluate (-1)^i without repeated multiplication + parity i = if even i then 1 else -1 + -- Map old array addresses to new ones when eliminating row 0, column i + rowMap i (x,y) = (x+1, if y >= i then y+1 else y) + -- Recursive determinant Array + determinant' xs + -- Base case: 1x1 matrix + | n == 0 = xs!(0,0) + -- Sum of cofactor expansions + | otherwise = sum $ map cofactor [0..n] where + -- Produce the cofactor of row 0, column i + cofactor i + | xs!(0,i) == 0 = 0 + | otherwise = (parity i) * xs!(0,i) * determinant' (minor i) + -- Furthest extent of the bounds, i.e., the size of the matrix + (_,(n,_)) = bounds xs + -- Build a new Array by eliminating row 0 and column i + minor i = ixmap ((0,0),(n-1,n-1)) (rowMap i) xs + +-- Characteristic Polynomial +charpoly :: (Num a, Eq a) => Matrix a -> Polynomial a +charpoly xs = determinant $ eyeLambda |+| negPolyXs where + -- Furthest extent of the bounds, i.e., the size of the matrix + (_,(n,_)) = bounds $ unMat xs + -- Negative of input matrix, after being converted to polynomials + negPolyXs = fmap (\x -> Poly [-x]) xs + -- Identity matrix times lambda (encoded as Poly [0, 1]) + eyeLambda = (\x -> Poly [x] * Poly [0, 1]) <$> eye (n+1) + +-- Convert Polynomial to LaTeX string +texifyPoly' :: (Num a, Eq a) => String -> (a -> String) -> Polynomial a -> String +texifyPoly' var f (Poly xs) = texify' $ zip xs [0..] where + texify' [] = "0" + texify' ((c, n):xs) + | all ((==0) . fst) xs = showPow c n + | c == 0 = texify' xs + | otherwise = showPow c n ++ " + " ++ texify' xs + showPow c 0 = f c + showPow 1 1 = var + showPow c 1 = f c ++ showPow 1 1 + showPow 1 n = var ++ "^{" ++ show n ++ "}" + showPow c n = f c ++ showPow 1 n + +-- Convert Polynomial to LaTeX string +texifyPoly :: (Num a, Eq a, Show a) => Polynomial a -> String +texifyPoly = texifyPoly' "x" show + +texPolyAsPositional' x (Poly xs) = (++ "_{" ++ x ++ "}") $ + reverse xs >>= (\x -> if x < 0 then "\\bar{" ++ show (-x) ++ "}" else show x) + +texPolyAsPositional = texPolyAsPositional' "x" + +-- Convert matrix to LaTeX string +texifyMatrix' :: (a -> String) -> Matrix a -> String +texifyMatrix' f mat = surround mat' where + mat' = intercalate " \\\\ " $ map (intercalate " & " . map f) $ + fromMatrix mat + surround = ("\\left( \\begin{matrix}" ++) . (++ "\\end{matrix} \\right)") + +texifyMatrix :: Show a => Matrix a -> String +texifyMatrix = texifyMatrix' show +``` + The [last post](../3) in this series focused on understanding some small linear groups and implementing them on the computer over both a prime field and prime power field. @@ -45,6 +132,53 @@ $$ \end{gather*} $$ +```{haskell} +--| code-fold: true +--| code-summary: "Equivalent Haskell" + +data F4 = ZeroF4 | OneF4 | AlphaF4 | Alpha2F4 deriving Eq +field4 = [ZeroF4, OneF4, AlphaF4, Alpha2F4] + +instance Show F4 where + show ZeroF4 = "0" + show OneF4 = "1" + show AlphaF4 = "α" + show Alpha2F4 = "α^2" + +-- Addition and multiplication over F4 +instance Num F4 where + (+) ZeroF4 x = x + (+) OneF4 AlphaF4 = Alpha2F4 + (+) OneF4 Alpha2F4 = AlphaF4 + (+) AlphaF4 Alpha2F4 = OneF4 + (+) x y = if x == y then ZeroF4 else y + x + + (*) ZeroF4 x = ZeroF4 + (*) x ZeroF4 = ZeroF4 + (*) OneF4 x = x + (*) AlphaF4 AlphaF4 = Alpha2F4 + (*) Alpha2F4 Alpha2F4 = AlphaF4 + (*) AlphaF4 Alpha2F4 = OneF4 + (*) x y = y * x + + abs = id + negate = id + signum = id + fromInteger = (cycle field4 !!) . fromInteger + + +-- Companion matrix of `p`, an irreducible polynomial of degree 2 over GF(2) +cP :: (Num a, Eq a, Integral a) => Matrix a +cP = companion $ Poly [1, 1, 1] + +f ZeroF4 = zero 2 +f OneF4 = eye 2 +f AlphaF4 = cP +f Alpha2F4 = (`mod` 2) <$> cP |*| cP + +field4M = map f field4 +``` + Finally, we constructed GL(2, 4) using matrices of matrices -- not [block matrices](https://en.wikipedia.org/wiki/Block_matrix)! This post will focus on studying this method in slightly more detail. @@ -69,31 +203,34 @@ More explicitly, we looked at a matrix *B* in SL(2, 4) which had the property that it was cyclic of order five. Then, to work with it without relying on symbols, we simply applied *f* over the contents of the matrix. -$$ -\begin{gather*} - f^* : \mathbb{F}_4 {}^{2 \times 2} - \longrightarrow - (\mathbb{F}_2 {}^{2 \times 2})^{2 \times 2} - \\[10pt] - B = \left(\begin{matrix} - 0 & \alpha \\ - \alpha^2 & \alpha^2 - \end{matrix} \right) - \\ - B^* = f^*(B) - = \left(\begin{matrix} - f(0) & f(\alpha) \\ - f(\alpha^2) & f(\alpha^2) - \end{matrix} \right) - = \left(\begin{matrix} - \left(\begin{matrix} 0 & 0 \\ 0 & 0 \end{matrix} \right) - & \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) - \\ - \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - & \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - \end{matrix} \right) -\end{gather*} -$$ +```{haskell} +--| code-fold: true + +-- Starred maps are instances of fmap composed with modding out +-- by the characteristic + +fStar :: (Eq a, Num a, Integral a) => Matrix F4 -> Matrix (Matrix a) +fStar = fmap (fmap (`mod` 2) . f) + +mBOrig = toMatrix [[ZeroF4, AlphaF4], [Alpha2F4, Alpha2F4]] +mBStar = fStar mBOrig + +markdown $ "$$\\begin{gather*}" ++ concat [ + -- First row, type of fStar + "f^* : \\mathbb{F}_4 {}^{2 \\times 2}" ++ + "\\longrightarrow" ++ + "(\\mathbb{F}_2 {}^{2 \\times 2})^{2 \\times 2}" ++ + "\\\\[10pt]", + -- Second row, B + "B = " ++ texifyMatrix' show mBOrig ++ + "\\\\", + -- Third row, B* + "B^* = f^*(B) = " ++ + texifyMatrix' (\x -> "f(" ++ show x ++ ")") mBOrig ++ " = " ++ + texifyMatrix' (texifyMatrix' show) mBStar + ] ++ + "\\end{gather*}$$" +``` We can do this because a matrix contains values in the domain of *f*, thus uniquely determining a way to change the internal structure (what Haskell calls @@ -121,31 +258,27 @@ $$ It should be noted that the determinant strips off the *outer* matrix. We could also consider the map **det**\* , where we apply the determinant - to the internal matrices (in Haskell terms, `fmap det`). + to the internal matrices (in Haskell terms, `fmap determinant`). This map isn't as nice though, since: -$$ -\begin{align*} - \det {}^*(B^*) - &= \left(\begin{matrix} - \det \left(\begin{matrix} 0 & 0 \\ 0 & 0 \end{matrix} \right) - & \det \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) - \\ - \det \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - & \det \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - \end{matrix} \right) - = \left(\begin{matrix} - 0 & 1 \\ - 1 & 1 - \end{matrix} \right) - \\ \\ - &\neq \left(\begin{matrix} - 1 & 0 \\ - 0 & 1 - \end{matrix} \right) - = \det(B^*) -\end{align*} -$$ +```{haskell} +--| code-fold: true + +markdown $ "$$\\begin{align*}" ++ concat [ + -- First row, det* of B + "\\det {}^*(B^*) &= " ++ + texifyMatrix' (("\\det" ++) . texifyMatrix' show) mBStar ++ " = " ++ + texifyMatrix ((`mod` 2) . determinant <$> mBStar) ++ + "\\\\ \\\\", + -- Second row, determinant of B* + -- Note how the commutation between `determinant` and <$> fails + "&\\neq" ++ + texifyMatrix ((`mod` 2) <$> determinant mBStar) ++ " = " ++ + "\\det(B^*)", + "" + ] ++ + "\\end{align*}$$" +``` ### Polynomial Map @@ -155,6 +288,15 @@ For the purposes of demonstration, we'll work with $b = \lambda^2 + \alpha^2 \la the characteristic polynomial of *B*, since it has coefficients in the domain of *f*. We define the extended map $f^\bullet$ as: +```{haskell} +--| code-fold: true + +-- Bulleted maps are also just instances of fmap, like the starred maps + +fBullet :: (Eq a, Num a, Integral a) => Polynomial F4 -> Polynomial (Matrix a) +fBullet = fmap (fmap (`mod` 2) . f) +``` + $$ \begin{gather*} f^{\bullet} : \mathbb{F}_4[\lambda] \longrightarrow @@ -184,38 +326,39 @@ We already looked at the determinant of this matrix, which is the constant term Therefore, it's probably not surprising that $f^\bullet$ and the characteristic polynomial commute in a similar fashion to the determinant. +```{haskell} +--| code-fold: true + +bStar = fmap (fmap (`mod` 2)) $ charpoly $ fStar mBOrig +bBullet = fmap (fmap (`mod` 2)) $ fBullet $ charpoly mBOrig + +if bStar /= bBullet then + markdown "$b^\\star$ and $b^\\bullet$ are not equal!" + else + markdown $ "$$\\begin{align*}" ++ concat [ + "b^* &= \\text{charpoly}(f^*(B)) = \\text{charpoly} " ++ + texifyMatrix' (texifyMatrix' show) mBStar ++ + "\\\\", + "&= " ++ + texifyPoly' "\\Lambda" (texifyMatrix' show) bStar ++ " = " ++ + "f^\\bullet(\\text{charpoly}(B)) = b^\\bullet", + "" + ] ++ + "\\end{align*}$$" +``` + $$ -\begin{gather*} - \begin{align*} - b^* - &= \text{charpoly}(f^*(B)) - = \text{charpoly} - \left(\begin{matrix} - \left(\begin{matrix} 0 & 0 \\ 0 & 0 \end{matrix} \right) & - \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) \\ - \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) & - \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - \end{matrix} \right) - \\ - &= \Lambda^2 + - \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) \Lambda + - \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - = f^{\bullet}(\text{charpoly}(B)) - = b^\bullet - \end{align*} - \\ \\ - \begin{CD} - \mathbb{F}_4 {}^{2 \times 2} - @>{\text{charpoly}}>> - \mathbb{F}_4[\lambda] - \\ - @V{f^*}VV ~ @VV{f^\bullet}V - \\ - (\mathbb{F}_2 {}^{2 \times 2})^{2 \times 2} - @>>{\text{charpoly}}> - (\mathbb{F}_2 {}^{2 \times 2})[\Lambda] - \end{CD} -\end{gather*} +\begin{CD} + \mathbb{F}_4 {}^{2 \times 2} + @>{\text{charpoly}}>> + \mathbb{F}_4[\lambda] + \\ + @V{f^*}VV ~ @VV{f^\bullet}V + \\ + (\mathbb{F}_2 {}^{2 \times 2})^{2 \times 2} + @>>{\text{charpoly}}> + (\mathbb{F}_2 {}^{2 \times 2})[\Lambda] +\end{CD} $$ It should also be mentioned that **charpoly**\*, taking the characteristic polynomials @@ -228,29 +371,30 @@ There *does* happen to be an isomorphism between the two structures But even by converting to the proper type, we already have a counterexample in the constant term from taking **det**\* earlier. -$$ -\begin{align*} - \text{charpoly}^*(B^*) - &= \left(\begin{matrix} - \text{charpoly} \left(\begin{matrix} 0 & 0 \\ 0 & 0 \end{matrix} \right) & - \text{charpoly} \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) \\ - \text{charpoly} \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) & - \text{charpoly} \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - \end{matrix} \right) - \\ - &= \left(\begin{matrix} - \lambda^2 & \lambda^2 + \lambda + 1 \\ - \lambda^2 + \lambda + 1 & \lambda^2 + \lambda + 1 - \end{matrix} \right) - \\ - &\cong - \left(\begin{matrix} 1 & 1 \\ 1 & 1 \end{matrix} \right) \Lambda^2 - + \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) \Lambda - + \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) - \\ \\ - &\neq f^{\bullet}(\text{charpoly}(B)) -\end{align*} -$$ +```{haskell} +--| code-fold: true + +markdown $ "$$\\begin{align*}" ++ concat [ + "\\text{charpoly}^*(B^*) &= " ++ + texifyMatrix' (("\\text{charpoly}" ++) . texifyMatrix' show) mBStar ++ + "\\\\", + "&= " ++ + texifyMatrix' (texifyPoly' "\\lambda" show) + (fmap (fmap (`mod` 2) . charpoly) mBStar) ++ + "\\\\", + "&\\cong " ++ + -- Not constructing this by isomorphism yet + texifyPoly' "\\Lambda" texifyMatrix + (Poly [ + toMatrix [[0,1], [1,1]], + toMatrix [[0,1], [1,1]], + toMatrix [[1,1], [1,1]] + ]) ++ + "\\\\ \\\\", + "&\\neq f^\\bullet(\\text{charpoly}(B))" + ] ++ + "\\end{align*}$$" +``` Forgetting @@ -259,56 +403,45 @@ Forgetting Clearly, layering matrices has several advantages over how we usually interpret block matrices. But what happens if we *do* "forget" about the internal structure? -$$ -\begin{gather*} - \text{forget} : (\mathbb{F}_2 {}^{2 \times 2})^{2 \times 2} - \longrightarrow \mathbb{F}_2 {}^{4 \times 4} - \\ \\ - \hat B = \text{forget}(B^*) - = \text{forget}\left(\begin{matrix} - \left(\begin{matrix} 0 & 0 \\ 0 & 0 \end{matrix} \right) - & \left(\begin{matrix} 0 & 1 \\ 1 & 1 \end{matrix} \right) - \\ - \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - & \left(\begin{matrix} 1 & 1 \\ 1 & 0 \end{matrix} \right) - \end{matrix} \right) - = \left(\begin{matrix} - 0 & 0 & 0 & 1 \\ - 0 & 0 & 1 & 1 \\ - 1 & 1 & 1 & 1 \\ - 1 & 0 & 1 & 0 - \end{matrix} \right) -\end{gather*} -$$ +```{haskell} +--| code-fold: true +--| code-summary: "Haskell implementation of `forget`" -
- -Haskell implementation of `forget` - +import Data.List (transpose) - -```{.haskell} -forget :: Matrix (Matrix a) -> Matrix a --- Massively complicated point-free way to forget matrices: +-- Massively complicated point-free way to forget double matrices: -- 1. Convert internal matrices to lists of lists -- 2. Convert the external matrix to a list of lists -- 3. There are now four layers of lists. Transpose the second and third. -- 4. Concat the new third and fourth layers together -- 5. Concat the first and second layers together -- 6. Convert the list of lists back to a matrix -forget = toMatrix . concat . fmap (fmap concat . transpose) . +forget :: Matrix (Matrix a) -> Matrix a +forget = toMatrix . concatMap (fmap concat . transpose) . fromMatrix . fmap fromMatrix -``` -To see why this is the structure, remember that we need to work with rows - of the external matrix at the same time. -We'd like to read across the whole row, but this involves descending into two matrices. -The `fmap transpose` allows us to collect rows in the way we expect. -For example, for the above matrix, We get `[[[0,0],[0,1]], [[0,0],[1,1]]]` after the transposition, - which are the first two rows, grouped by the matrix they belonged to. -Then, we can finally get the desired row by `fmap (fmap concat)`ing the rows together. -Finally, we `concat` once more to undo the column grouping. -
+-- To see why this is the structure, remember that we need to work with rows +-- of the external matrix at the same time. +-- We'd like to read across the whole row, but this involves descending into two matrices. +-- The `fmap transpose` allows us to collect rows in the way we expect. +-- For example, for the above matrix, We get `[[[0,0],[0,1]], [[0,0],[1,1]]]` after the transposition, +-- which are the first two rows, grouped by the matrix they belonged to. +-- Then, we can finally get the desired row by `fmap (fmap concat)`ing the rows together. +-- Finally, we `concat` once more to undo the column grouping. + +mBHat = forget mBStar + +markdown $ "$$\\begin{gather*}" ++ concat [ + "\\text{forget} : (\\mathbb{F}_2 {}^{2 \\times 2})^{2 \\times 2}" ++ + "\\longrightarrow \\mathbb{F}_2 {}^{4 \\times 4}" ++ + "\\\\[10pt]", + "\\hat B = \\text{forget}(B^*) = \\text{forget}" ++ + texifyMatrix' (texifyMatrix' show) mBStar ++ " = " ++ + texifyMatrix mBHat, + "" + ] ++ + "\\end{gather*}$$" +``` Like *f*, `forget` preserves addition and multiplication, a fact already appreciated by block matrices. Further, by *f*, the internal matrices multiply the same as elements of GF(4). @@ -317,26 +450,28 @@ Hence, this shows us directly that GL(2, 4) is a subgroup of GL(4, 2). However, an obvious difference between layered and "forgotten" matrices is the determinant and characteristic polynomial: -$$ -\begin{align*} - \det {B^*} &= \left(\begin{matrix}1 & 0 \\ 0 & 1\end{matrix}\right) - \\ \\ - \det {\hat B} &= 1 -\end{align*} -\qquad -\begin{align*} - \text{charpoly}(B^*) - &= \Lambda^2 + - \left(\begin{matrix}1 & 1 \\ 1 & 0 \end{matrix}\right)\Lambda + - \left(\begin{matrix}1 & 0 \\ 0 & 1\end{matrix}\right) - \\ \\ - \text{charpoly}(\hat B) - &= \lambda^4 + \lambda^3 + \lambda^2 + \lambda + 1\\ -\end{align*} -$$ +```{haskell} +--| code-fold: true + +markdown $ "$$\\begin{align*}" ++ intercalate " \\\\ \\\\ " ( + map (intercalate " & ") [ + [ + "\\det B^* &= " ++ + texifyMatrix ((`mod` 2) <$> determinant mBStar), + "\\text{charpoly} B^* &= " ++ + texifyPoly' "\\Lambda" texifyMatrix (fmap (`mod` 2) <$> charpoly mBStar) + ], [ + "\\det \\hat B &= " ++ + show ((`mod` 2) $ determinant mBHat), + "\\text{charpoly} \\hat B &= " ++ + texifyPoly' "\\lambda" show ((`mod` 2) <$> charpoly mBHat) + ] + ]) ++ + "\\end{align*}$$" +``` -### Another Path to the Forgotten +### Another Forgotten Path It's a relatively simple matter to move between determinants, since it's straightforward to identify 1 and the identity matrix. @@ -346,7 +481,8 @@ However, a natural question to ask is whether there's a way to reconcile or coer First, let's formally establish a path from matrix polynomials to a matrix of polynomials. We need only use our friend from the [second post](../2) -- polynomial evaluation. -Simply evaluating a matrix polynomial at *λI* converts our matrix indeterminate (*Λ*) into a scalar one (*λ*). +Simply evaluating a matrix polynomial *r* at *λI* converts our matrix indeterminate (*Λ*) + into a scalar one (*λ*). $$ \begin{align*} @@ -356,32 +492,51 @@ $$ \\ &:: \quad r(\Lambda) \mapsto r(\lambda I) - \\ \\ - \text{eval}_{\Lambda \mapsto \lambda I}(\text{charpoly}(B^*)) - &= (\lambda I)^2 - + \left(\begin{matrix}1 & 1 \\ 1 & 0 \end{matrix}\right)(\lambda I) - + \left(\begin{matrix}1 & 0 \\ 0 & 1\end{matrix}\right) - \\ - &= \left(\begin{matrix} - \lambda^2 + \lambda + 1 & \lambda \\ - \lambda & \lambda^2 + 1 - \end{matrix}\right) \end{align*} $$ +```{haskell} +--| code-fold: true + +-- Function following from the evaluation definition above +-- Note that `Poly . pure` is used to transform matrices of `a` +-- into matrices of polynomials. + +toMatrixPolynomial :: (Eq a, Num a) => + Polynomial (Matrix a) -> Matrix (Polynomial a) +toMatrixPolynomial xs = evalPoly eyeLambda $ fmap (fmap (Poly . pure)) xs where + -- First dimensions of the coefficients + (is, _) = unzip $ map (snd . bounds . unMat) $ coeffs xs + -- Properly-sized identity matrix times a scalar lambda + eyeLambda = eye (1 + maximum is) * toMatrix [[Poly [0, 1]]] + + +markdown $ "$$\\begin{align*}" ++ + "\\text{eval}_{\\Lambda \\mapsto \\lambda I}(\\text{charpoly}(B^*)) &=" ++ + texifyPoly' "(\\lambda I)" texifyMatrix + (fmap (`mod` 2) <$> charpoly mBStar) ++ + "\\\\ &= " ++ + texifyMatrix' (texifyPoly' "\\lambda" show) + (toMatrixPolynomial $ fmap (`mod` 2) <$> charpoly mBStar) ++ + "\\end{align*}$$" +``` + Since a matrix containing polynomials is still a matrix, we can then take its determinant. What pops out is exactly what we were after... -$$ -\begin{align*} - \det(\text{eval}_{\Lambda \mapsto \lambda I}(\text{charpoly}(B^*))) - &= (\lambda^2 + \lambda + 1)(\lambda^2 + 1) - \lambda^2 - \\ - &= \lambda^4 + \lambda^3 + \lambda^2 + \lambda + 1 - \\ - &= \text{charpoly}(\hat B) -\end{align*} -$$ +```{haskell} +--| code-fold: true + +markdown $ "$$\\begin{align*}" ++ + "\\det(\\text{eval}_{\\Lambda \\mapsto \\lambda I}(" ++ + "\\text{charpoly}(B^*))) &=" ++ + "(1 + \\lambda + \\lambda^2)(1 + \\lambda^2) - \\lambda^2" ++ + "\\\\ &=" ++ + texifyPoly' "\\lambda" show + (fmap (`mod` 2) <$> determinant $ toMatrixPolynomial $ charpoly mBStar) ++ + "\\\\ &= \\text{charpoly}{\\hat B}" ++ + "\\end{align*}$$" +``` ...and we can arrange our maps into another diagram: @@ -409,66 +564,24 @@ $$ \end{gather*} $$ -
- -Haskell demonstration of this commutation - -Fortunately, the implementation of `charpoly` using Laplace expansion already works with numeric matrices. -Therefore, we need only define the special eval: - -```{.haskell} -toMatrixPolynomial :: Num a => Polynomial (Matrix a) -> Matrix (Polynomial a) --- Collect our coefficient matrices into a single matrix of polynomials -toMatrixPolynomial (Poly ps) = Mat $ array rs values where - -- Technically, we're always working with square matrices, but we should - -- always use the largest bounds available. - (is,js) = unzip $ map mDims ps - rs = ((0,0),(maximum is - 1,maximum js - 1)) - -- Address a matrix. This needs defaulting to zero to be fully correct - -- with respect to the range given by `rs` - access b (Mat m) = m!b - -- Build the value at an address by addressing over the coefficients - -- ps is already in rising coefficient order, so our values are too. - values = map (\r -> (r, Poly $ map (access r) $ ps)) (range rs) -``` - -Now we can simply observe: - - -```{.haskell} -field4 = [zero 2, eye 2, toMatrix [[0,1],[1,1]], toMatrix [[1,1],[1,0]]] - -mB = toMatrix $ [[field4!!0, field4!!2], [field4!!3, field4!!3]] - --- >>> mapM_ print $ fromMatrix $ forget mB --- -- [0,0,0,1] --- -- [0,0,1,1] --- -- [1,1,1,1] --- -- [1,0,1,0] - --- >>> fmap (`mod` 2) $ charpoly $ forget mB --- -- 1x^4 + 1x^3 + 1x^2 + 1x + 1 --- >>> fmap (`mod` 2) $ determinant $ toMatrixPolynomial $ charpoly mB --- -- 1x^4 + 1x^3 + 1x^2 + 1x + 1 -``` -
- It should be noted that we do *not* get the same results by taking the determinant after applying **charpoly**\*, indicating that the above method is "correct". -$$ -\begin{align*} - \text{charpoly}^*(B^*) &= \left(\begin{matrix} - \lambda^2 & \lambda^2 + \lambda + 1 \\ - \lambda^2 + \lambda + 1 & \lambda^2 + \lambda + 1 - \end{matrix}\right) - \\ ~ \\ - \det( \text{charpoly}^*(B^*)) - &= \lambda^2(\lambda^2 + \lambda + 1) - (\lambda^2 + \lambda + 1)^2 - \\ - &= \lambda^3 + 1 \mod 2 -\end{align*} -$$ +```{haskell} +--| code-fold: true + +markdown $ "$$\\begin{align*}" ++ + "\\text{charpoly}^*(B^*) &=" ++ + texifyMatrix' (texifyPoly' "\\lambda" show) + (fmap (`mod` 2) <$> fmap charpoly mBStar) ++ + "\\\\ \\\\" ++ + "\\det(\\text{charpoly}^*(B^*)) &=" ++ + "\\lambda^2(1 + \\lambda + \\lambda^2) - (1 + \\lambda + \\lambda^2)^2" ++ + "\\\\ &= " ++ + texifyPoly' "\\lambda" show + (fmap (`mod` 2) <$> determinant $ fmap charpoly mBStar) ++ + "\\end{align*}$$" +``` ### Cycles and Cycles @@ -484,7 +597,6 @@ However, the reason we see it is more obvious if we look at the powers of scalar First, recall that *f*\* takes us from a matrix over GF(4) to a matrix of matrices of GF(2). Then define a map *g* that gives us degree 4 polynomials: -::: {layout="[[1],[1,1,1]]"} $$ \begin{gather*} g : \mathbb{F}_4^{2 \times 2} \rightarrow \mathbb{F}_2[\lambda] @@ -493,78 +605,29 @@ $$ \end{gather*} $$ -$$ -\begin{array}{} - & \scriptsize \left(\begin{matrix} - 0 & \alpha \\ - \alpha^2 & \alpha^2 - \end{matrix}\right) - \\ - B & \overset{g}{\mapsto} - & 11111_\lambda - \\ - B^2 & \overset{g}{\mapsto} - & 11111_\lambda - \\ - B^3 & \overset{g}{\mapsto} - & 11111_\lambda - \\ - B^4 & \overset{g}{\mapsto} - & 11111_\lambda - \\ - B^5 & \overset{g}{\mapsto} - & 10001_\lambda -\end{array} -$$ +```{haskell} +--| code-fold: true +--| layout-ncol: 3 -$$ -\begin{array}{} - & \scriptsize \left(\begin{matrix} - 0 & \alpha^2 \\ - 1 & 1 - \end{matrix}\right) - \\ - \alpha B & \overset{g}{\mapsto} - & 10011_\lambda - \\ - (\alpha B)^2 & \overset{g}{\mapsto} - & 10011_\lambda - \\ - (\alpha B)^3 & \overset{g}{\mapsto} - & 11111_\lambda - \\ - (\alpha B)^4 & \overset{g}{\mapsto} - & 10011_\lambda - \\ - (\alpha B)^5 & \overset{g}{\mapsto} - & 10101_\lambda -\end{array} -$$ +g = fmap (`mod` 2) . charpoly . forget . fStar -$$ -\begin{array}{} - & \scriptsize \left(\begin{matrix} - 0 & 1 \\ - \alpha & \alpha - \end{matrix}\right) - \\ - \alpha^2 B & \overset{g}{\mapsto} - & 11001_\lambda - \\ - (\alpha^2 B)^2 & \overset{g}{\mapsto} - & 11001_\lambda - \\ - (\alpha^2 B)^3 & \overset{g}{\mapsto} - & 11111_\lambda - \\ - (\alpha^2 B)^4 & \overset{g}{\mapsto} - & 11001_\lambda - \\ - (\alpha^2 B)^5 & \overset{g}{\mapsto} - & 10101_\lambda -\end{array} -$$ -::: +showSeries varName var = "$$\\begin{array}{}" ++ + " & \\scriptsize " ++ + texifyMatrix var ++ + "\\\\" ++ + intercalate " \\\\ " [ + (if n == 1 then varName' else varName' ++ "^{" ++ show n ++ "}") ++ + "& \\overset{g}{\\mapsto} &" ++ + texPolyAsPositional' "\\lambda" (g $ var^n) + | n <- [1..5] + ] ++ + "\\end{array}$$" where + varName' = if length varName == 1 then varName else "(" ++ varName ++ ")" + +markdown $ showSeries "B" mBOrig +markdown $ showSeries "αB" (fmap (AlphaF4*) mBOrig) +markdown $ showSeries "α^2 B" (fmap (Alpha2F4*) mBOrig) +``` The matrices in the middle and rightmost columns both have order 15 inside GL(2, 4). Correspondingly, both 10011~λ~ = ~2~19 and 11001~λ~ = ~2~25 are primitive, @@ -586,33 +649,23 @@ Haskell demonstration of the field-like-ness of these matrices All we really need to do is test additive closure, since the powers trivially commute and include the identity matrix. - -```{.haskell} -hasAdditiveClosure :: Integral a => Int -> a -> [Matrix a] -> bool +```{haskell} -- Check whether n x n matrices (mod p) have additive closure -- Supplement the identity, even if it is not already present +hasAdditiveClosure :: Integral a => Int -> a -> [Matrix a] -> Bool hasAdditiveClosure n p xs = all (`elem` xs') sums where -- Add in the zero matrix xs' = zero n:xs -- Calculate all possible sums of pairs (mod p) sums = map (fmap (`mod` p)) $ (+) <$> xs' <*> xs' - -generatesField :: Integral a => Int -> a -> Matrix a -> bool -- Generate the powers of x, then test if they form a field (mod p) +generatesField :: Integral a => Int -> a -> Matrix a -> Bool generatesField n p x = hasAdditiveClosure n p xs where xs = map (fmap (`mod` p) . (x^)) [1..p^n-1] -alphaB = toMatrix [[zero 2, field4!!3],[eye 2, eye 2]] --- >>> mapM_ $ print $ fromMatrix $ forget alphaB --- -- [0,0,1,1] --- -- [0,0,1,0] --- -- [1,0,1,0] --- -- [0,1,0,1] --- --- >>> generatesField 4 2 $ forget $ alphaB --- -- True +print $ generatesField 4 2 $ forget $ fStar $ fmap (AlphaF4*) mBOrig ``` @@ -693,8 +746,8 @@ This concern about prime dimensions is unique to characteristic 2. For any other prime *p*, *p*^*m*^ - 1 is composite since it is at the very least even. All other remarks about the above diagram should still hold for any other prime *p*. -In addition, our earlier diagram where we correspond the order of an element in GL(2, 2^2^) - with the order of an element in GF(2^2×2^) via the characteristic polynomial also generalizes. +In addition, the diagram where we found a correspondence between the orders of elements in + GL(2, 2^2^) and GF(2^2×2^) via the characteristic polynomial also generalizes. Though I have not proven it, I strongly suspect the following diagram commutes, at least in the case where *K* is a finite field: @@ -727,12 +780,12 @@ If the above diagram is true, then the prior statement follows. The action of forgetting the internal structure may sound somewhat familiar if you know your Haskell. Remember that for lists, we can do something similar -- converting `[[1,2,3],[4,5,6]]` to `[1,2,3,4,5,6]` is just a matter of applying `concat`. -But this is an instance in which we know lists to behave like a [monad](https://wiki.haskell.org/Monad). +This is an instance in which we know lists to behave like a [monad](https://wiki.haskell.org/Monad). Despite being an indecipherable bit of jargon to newcomers, it just means we: 1. can apply functions inside the structure (for example, to the elements of a list), 2. have a sensible injection into the structure (creating singleton lists, called `return`), and -3. can reduce two layers to one (concat, or join for monads in general). +3. can reduce two layers to one (`concat`, or `join` for monads in general). - Monads are traditionally defined using the operator `>>=`, but `join = (>>= id)` Just comparing the types of `join :: Monad m => m (m a) -> m a` @@ -742,9 +795,9 @@ Of course, **this is only true when our internal matrices are all the same size* In the above diagrams, this restriction has applied, but should be stated explicitly since no dimension is specified by `Matrix a`. -However, we run into difficulty at condition 2. -For one, only "numbers" (elements of a ring) can go inside matrices. -This restricts where monadicity can hold. +Condition 2 gives us some trouble, though. +For one, only "numbers" (elements of a ring) can go inside matrices, which restricts + where monadicity can hold. More importantly, we have a *lot* of freedom in what dimension we choose to inject into. For example, we might pick a `return` that uses 1×1 matrices (which add no additional structure). We might also pick `return2`, which scalar-multiplies its argument to a 2×2 identity matrix instead. @@ -799,7 +852,7 @@ $$ & \begin{matrix} | \\ \downarrow \end{matrix} & \searrow & & \swarrow & \begin{matrix} | \\ \downarrow \end{matrix} - & f_2^* + & \small f_2^* \\ \\ & (K^{r \times r})^{n \times n} & \underset{\text{forget}} {\longrightarrow} @@ -809,7 +862,7 @@ $$ \end{matrix} $$ -Or in English, whether "rebracketing" certain *nr*×*nr* matrices can be traced back to +Or in English, whether "rebracketing" certain *nr* × *nr* matrices can be traced back to not only a degree *r* field extension, but also one of degree *n*. The mathematician in me tells me to believe in well-defined structures. @@ -819,7 +872,7 @@ However, the computer scientist in me laments that the application of these stru There is clear utility and interest in doing so, otherwise the diagrams shown above would not exist. Of course, there's plenty of reason *not* to go down this route. -For one, it's plainly inefficient -- GPUs are *built* on matrix operations being as efficient as possible, - i.e., without the layering. +For one, it's plainly inefficient -- GPUs are *built* on matrix operations being + as efficient as possible, i.e., without the layering. It's also inefficient to learn for people *just* learning matrices. -I'd still argue that the method is efficient for learning about more complex topics, like field extensions. +I'd still argue that the method is useful for learning about more complex topics, like field extensions.