Repository: quantumlib/Qualtran

Issue:
`GF2MultiplyByConstantMod ` is incompatible with registers of the type `QGF(2, m)` where user doesn't explicitly specify an irreducible_poly
This is because https://github.com/quantumlib/Qualtran/pull/1533 update the `QGF` type to include `irreducible_poly` as an optional field which defaults to None. `GF2MultiplyByConstantMod` always specifies an irreducible polynomial. two instances of `QGF` where one has `irreducible_poly = None` and other has an `irreducible_poly` polynomial explicitly specified (albeit the default one) are not equal and thus fail the dtype equality checks when wiring up bloqs together.

See the error below for more details 

```python
In [1]: from qualtran import QGF

In [2]: qgf_one = QGF(2, 4)

In [3]: qgf_two = QGF(2, 4, irreducible_poly=qgf_one.gf_type.irreducible_poly)

In [4]: qgf_one == qgf_two
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[4], line 1
----> 1 qgf_one == qgf_two

File <attrs generated eq qualtran._infra.data_types.QGF>:7, in __eq__(self, other)
      2 if other.__class__ is not self.__class__:
      3     return NotImplemented
      4 return  (
      5     self.characteristic == other.characteristic and
      6     self.degree == other.degree and
----> 7     self.irreducible_poly == other.irreducible_poly and
      8     self.element_repr == other.element_repr
      9 )

File ~/opt/anaconda3/envs/dqi/lib/python3.12/site-packages/galois/_polys/_poly.py:1149, in Poly.__eq__(self, other)
   1147 else:
   1148     field = None
-> 1149 other = Poly._PolyLike(other, field=field)
   1151 return (
   1152     self.field is other.field
   1153     and np.array_equal(self.nonzero_degrees, other.nonzero_degrees)
   1154     and np.array_equal(self.nonzero_coeffs, other.nonzero_coeffs)
   1155 )

File ~/opt/anaconda3/envs/dqi/lib/python3.12/site-packages/galois/_polys/_poly.py:143, in Poly._PolyLike(cls, poly_like, field)
    141     poly = poly_like
    142 else:
--> 143     raise TypeError(
    144         f"A 'poly-like' object must be an int, str, tuple, list, np.ndarray, or galois.Poly, "
    145         f"not {type(poly_like)}."
    146     )
    148 return poly

TypeError: A 'poly-like' object must be an int, str, tuple, list, np.ndarray, or galois.Poly, not <class 'NoneType'>.
```

Discussion:
@tanujkhattar: Adding a default value should fix this issue: 

```python
@attrs.frozen
class QGF(QDType):
    characteristic: SymbolicInt
    degree: SymbolicInt
    irreducible_poly: 'galois.Poly' = attrs.field()
    element_repr: Literal["int", "poly", "power"] = 'int'

    @irreducible_poly.default
    def irreducible_poly_default(self):
        from galois import GF

        return GF(  # type: ignore[call-overload]
            int(self.characteristic), int(self.degree), compile='python-calculate'
        ).irreducible_poly
```

Solution patch:
diff --git a/qualtran/_infra/data_types.py b/qualtran/_infra/data_types.py
index 8c541bfc..bc3e80e2 100644
--- a/qualtran/_infra/data_types.py
+++ b/qualtran/_infra/data_types.py
@@ -51,7 +51,7 @@ respectively.
 import abc
 from enum import Enum
 from functools import cached_property
-from typing import Any, Iterable, List, Optional, Sequence, TYPE_CHECKING, Union
+from typing import Any, Iterable, List, Literal, Optional, Sequence, TYPE_CHECKING, Union
 
 import attrs
 import numpy as np
@@ -906,8 +906,19 @@ class QGF(QDType):
 
     characteristic: SymbolicInt
     degree: SymbolicInt
-    irreducible_poly: Optional['galois.Poly'] = None
-    element_repr: str = 'int'
+    irreducible_poly: Optional['galois.Poly'] = attrs.field()
+    element_repr: Literal["int", "poly", "power"] = attrs.field(default='int')
+
+    @irreducible_poly.default
+    def _irreducible_poly_default(self):
+        if is_symbolic(self.characteristic, self.degree):
+            return None
+
+        from galois import GF
+
+        return GF(  # type: ignore[call-overload]
+            int(self.characteristic), int(self.degree), compile='python-calculate'
+        ).irreducible_poly
 
     @cached_property
     def order(self) -> SymbolicInt:
@@ -936,10 +947,12 @@ class QGF(QDType):
     def gf_type(self):
         from galois import GF
 
+        poly = self.irreducible_poly if self.degree > 1 else None
+
         return GF(  # type: ignore[call-overload]
             int(self.characteristic),
             int(self.degree),
-            irreducible_poly=self.irreducible_poly,
+            irreducible_poly=poly,
             repr=self.element_repr,
             compile='python-calculate',
         )


Test patch:
diff --git a/qualtran/_infra/data_types_test.py b/qualtran/_infra/data_types_test.py
index ff08b7df..0c889cdc 100644
--- a/qualtran/_infra/data_types_test.py
+++ b/qualtran/_infra/data_types_test.py
@@ -540,3 +540,11 @@ def test_montgomery_bit_conversion(bitsize):
     dtype = QMontgomeryUInt(bitsize)
     for v in range(1 << bitsize):
         assert v == dtype.from_bits(dtype.to_bits(v))
+
+
+def test_qgf_with_default_poly_is_compatible():
+    qgf_one = QGF(2, 4)
+
+    qgf_two = QGF(2, 4, irreducible_poly=qgf_one.gf_type.irreducible_poly)
+
+    assert qgf_one == qgf_two