Repository: mhostetter/galois

Issue:
Allow serialization of Galois field
I recently tried playing around with multithreading and noticed, that I can not pass the Galois field object to a thread, since the Galois class can not be pickled. An example error would be
PicklingError: Can't pickle <class 'galois.GF(47)'>: attribute lookup GF(47) on galois failed

Discussion:

Patch:
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 73e0f1216..4fa65a650 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,5 +1,5 @@
 sphinx==5
-git+https://github.com/jbms/sphinx-immaterial@5e70669e8f42bdd1dfa87c2a5f1879b7ba73d25a
+git+https://github.com/jbms/sphinx-immaterial@bf169751fba59362bcd06da96bfa65b2728cbbae
 myst-parser
 sphinx-design
 sphinxcontrib-details-directive
diff --git a/galois/_fields/_array.py b/galois/_fields/_array.py
index d8fb46cee..7dda7c9ae 100644
--- a/galois/_fields/_array.py
+++ b/galois/_fields/_array.py
@@ -47,6 +47,13 @@ class FieldArrayMeta(ArrayMeta):
         # Construct the irreducible polynomial from its integer representation
         cls._irreducible_poly = Poly.Int(cls._irreducible_poly_int, field=cls._prime_subfield)
 
+    def __repr__(cls) -> str:
+        # When FieldArray instances are created they are added to the `galois._fields._factory` module with a name
+        # like `FieldArray_<p>_<primitive_element>` or `FieldArray_<p>_<m>_<primitive_element>_<irreducible_poly>`.
+        # This is visually unappealing. So here we override the repr() to be more succinct and indicate how the class
+        # was created. So galois._fields._factory.FieldArray_31_3 is converted to galois.GF(31).
+        return f"<class 'galois.{cls.name}'>"
+
     ###############################################################################
     # Class properties
     ###############################################################################
diff --git a/galois/_fields/_factory.py b/galois/_fields/_factory.py
index ef4dc113e..18f7579a3 100644
--- a/galois/_fields/_factory.py
+++ b/galois/_fields/_factory.py
@@ -3,6 +3,7 @@ A module to implement the Galois field class factory `GF()`.
 """
 from __future__ import annotations
 
+import sys
 import types
 from typing import Union, Optional, Type
 from typing_extensions import Literal
@@ -251,8 +252,6 @@ def _GF_prime(
     """
     Class factory for prime fields GF(p).
     """
-    name = f"GF({p})"
-
     # Get default primitive element
     if alpha is None:
         alpha = primitive_root(p)
@@ -262,6 +261,7 @@ def _GF_prime(
         raise ValueError(f"Argument `primitive_element` must be non-zero in the field 0 < x < {p}, not {alpha}.")
 
     # If the requested field has already been constructed, return it
+    name = f"FieldArray_{p}_{alpha}"
     key = (p, alpha)
     if key in _GF_prime._classes:
         field = _GF_prime._classes[key]
@@ -287,8 +287,9 @@ def _GF_prime(
             "primitive_element": alpha,
         })
 
-    # Add the class to the "galois" namespace
-    field.__module__ = "galois"
+    # Add the class to this module's namespace
+    field.__module__ = __name__
+    setattr(sys.modules[__name__], name, field)
 
     # Since this is a new class, compile the ufuncs and set the display mode
     field.compile("auto" if compile is None else compile)
@@ -317,7 +318,6 @@ def _GF_extension(
     Class factory for extension fields GF(p^m).
     """
     # pylint: disable=too-many-statements
-    name = f"GF({p}^{m})"
     prime_subfield = _GF_prime(p)
     is_primitive_poly = None
     verify_poly = verify
@@ -352,6 +352,7 @@ def _GF_extension(
         raise ValueError(f"Argument `primitive_element` must have degree strictly less than {m}, not {alpha.degree}.")
 
     # If the requested field has already been constructed, return it
+    name = f"FieldArray_{p}_{m}_{int(alpha)}_{int(irreducible_poly_)}"
     key = (p, m, int(alpha), int(irreducible_poly_))
     if key in _GF_extension._classes:
         field = _GF_extension._classes[key]
@@ -379,8 +380,9 @@ def _GF_extension(
         "prime_subfield": prime_subfield,
     })
 
-    # Add the class to the "galois" namespace
-    field.__module__ = "galois"
+    # Add the class to this module's namespace
+    field.__module__ = __name__
+    setattr(sys.modules[__name__], name, field)
 
     # Since this is a new class, compile the ufuncs and set the display mode
     field.compile("auto" if compile is None else compile)


Test patch:
diff --git a/tests/fields/test_classes.py b/tests/fields/test_classes.py
index 740bb5ba3..f62ba0a0d 100644
--- a/tests/fields/test_classes.py
+++ b/tests/fields/test_classes.py
@@ -1,6 +1,8 @@
 """
 A pytest module to test the class attributes of FieldArray subclasses.
 """
+import pickle
+
 import pytest
 import numpy as np
 
@@ -97,3 +99,37 @@ def test_is_primitive_poly():
     poly = galois.conway_poly(3, 101)
     GF = galois.GF(3**101, irreducible_poly=poly, primitive_element="x", verify=False)
     assert GF.is_primitive_poly == True
+
+
+def test_pickle_class(tmp_path):
+    GF = galois.GF(13)
+    with open(tmp_path / "class.pkl", "wb") as f:
+        pickle.dump(GF, f)
+    with open(tmp_path / "class.pkl", "rb") as f:
+        GF_loaded = pickle.load(f)
+    assert GF is GF_loaded
+
+    GF = galois.GF(3**5)
+    with open(tmp_path / "class.pkl", "wb") as f:
+        pickle.dump(GF, f)
+    with open(tmp_path / "class.pkl", "rb") as f:
+        GF_loaded = pickle.load(f)
+    assert GF is GF_loaded
+
+
+def test_pickle_array(tmp_path):
+    GF = galois.GF(13)
+    x = GF.Random(10)
+    with open(tmp_path / "array.pkl", "wb") as f:
+        pickle.dump(x, f)
+    with open(tmp_path / "array.pkl", "rb") as f:
+        x_loaded = pickle.load(f)
+    assert np.array_equal(x, x_loaded)
+
+    GF = galois.GF(3**5)
+    x = GF.Random(10)
+    with open(tmp_path / "array.pkl", "wb") as f:
+        pickle.dump(x, f)
+    with open(tmp_path / "array.pkl", "rb") as f:
+        x_loaded = pickle.load(f)
+    assert np.array_equal(x, x_loaded)