Repository: sagemath/sage

Issue:
Quaternion algebra: bugs in `maximal_order` and `normalize_basis_at_p`
### Steps To Reproduce

1. On any sage terminal run: 
```
a,b = -292, -732
A = QuaternionAlgebra(QQ,a,b)
A.maximal_order()
```

2. On any sage terminal run: 
```
from sage.algebras.quatalg.quaternion_algebra import normalize_basis_at_p
A.<i,j,k> = QuaternionAlgebra(-1,-7)
e = [A(1), k, j, 1/2 + 1/2*i + 1/2*j + 1/2*k]
e_norm = normalize_basis_at_p(e, 2)
V = QQ**4
V.span([V(x.coefficient_tuple()) for (x,_) in e_norm]).dimension()
```

### Expected Behavior

1. I expect the code to return a maximal order of the quaternion algebra. 
2. I expect a basis to be of rank 4

### Actual Behavior

1. It fails. 
2. Return a list of rank 3

### Additional Information

The error can be traced to a supposedly bug for the function `normalize_basis_at_p` when $p = 2$. In fact in the faulty execution it returns a supposed basis of rank $3$ instead of $4$. 

Trying to debug the function I discovered that actually in some situations it return a list of $4$ vectors with the last two being equal, clearly not a basis! This strange behaviour can be noted also in the examples inserted for the function:
```
sage: A.<i,j,k> = QuaternionAlgebra(-1,-7)
sage: e = [A(1), k, j, 1/2 + 1/2*i + 1/2*j + 1/2*k]
sage: normalize_basis_at_p(e, 2)
[(1, 0), (1/2 + 1/2*i + 1/2*j + 1/2*k, 0), (-34/105*i - 463/735*j + 71/105*k, 1),
         (-34/105*i - 463/735*j + 71/105*k, 1)]
```

I already tried to solve the issue myself so I insert here some possibly ***useful notes:***
- it is easy to find more errors (just take random $a,b$ until maximal order crashes), but here 20 of them:
`[(-292, -732), (-48, -564), (-436, -768), (-752, -708), (885, 545), (411, -710), (-411, 593), (805, -591), (-921, 353), (409, 96), (394, 873), (353, -722), (730, 830), (-466, -427), (-213, -630), (-511, 608), (493, 880), (105, -709), (-213, 530), (97, 745)]`,
- the two functions are *Algorithms 7.10, 7.9 and 3.12* from https://arxiv.org/abs/1004.0994 
- `maximal_order` to me seem to be implemented correctly and I believe the error is only traceable to `normalize_basis_at_p` (that in fact contains some differences from the original algorithm)
- trying to remove the previous differences I noted creates several crashes for even more quaternion algebras, so I suppose they where necessaries. 

I'll still try to solve the issue, so add here eventual updates.

### Environment

```markdown
- **OS**: MacOS
- **Sage Version**: Version: 9.2
```


### Checklist

- [X] I have searched the existing issues for a bug report that matches the one I want to file, without success.
- [X] I have read the documentation and troubleshoot guide


Discussion:
When I am running your second code it is giving me an error like below 

```python
sage: from sage.algebras.quatalg.quaternion_algebra import normalize_basis_at_p
....: A.<i,j,k> = QuaternionAlgebra(-1,-7)
....: e = [A(1), k, j, 1/2 + 1/2*i + 1/2*j + 1/2*k]
....: e_norm = normalize_basis_at_p(e, 2)
....: V = QQ**4
....: V.span([V(x.coefficient_tuple()) for (x,_) in basis]).dimension()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 6
      4 e_norm = normalize_basis_at_p(e, Integer(2))
      5 V = QQ**Integer(4)
----> 6 V.span([V(x.coefficient_tuple()) for (x,_) in basis]).dimension()

TypeError: 'function' object is not iterable

```
> When I am running your second code it is giving me an error like below
> 
> ```python
> sage: from sage.algebras.quatalg.quaternion_algebra import normalize_basis_at_p
> ....: A.<i,j,k> = QuaternionAlgebra(-1,-7)
> ....: e = [A(1), k, j, 1/2 + 1/2*i + 1/2*j + 1/2*k]
> ....: e_norm = normalize_basis_at_p(e, 2)
> ....: V = QQ**4
> ....: V.span([V(x.coefficient_tuple()) for (x,_) in basis]).dimension()
> ---------------------------------------------------------------------------
> TypeError                                 Traceback (most recent call last)
> Cell In[1], line 6
>       4 e_norm = normalize_basis_at_p(e, Integer(2))
>       5 V = QQ**Integer(4)
> ----> 6 V.span([V(x.coefficient_tuple()) for (x,_) in basis]).dimension()
> 
> TypeError: 'function' object is not iterable
> ```

Sorry, copied wrong line from another test. Should be `e_norm` instead of `basis`. I corrected it in the initial comment. Thanks for noting it!
> normalize_basis_at_p (that in fact contains some differences from the original algorithm)

I made a PR and then I realized that it was failing for some cases. So I think, I did something wrong, but to confirm that first I have to confirm that the algo which I am understanding is correct, only. So can you please provide me a link to that algorithm ?
Sure! The basis is evaluated via Algorithms 3.12 from https://arxiv.org/abs/1004.0994, is the only reference I know. 

Resolving patch:
diff --git a/src/sage/algebras/quatalg/quaternion_algebra.py b/src/sage/algebras/quatalg/quaternion_algebra.py
--- a/src/sage/algebras/quatalg/quaternion_algebra.py
+++ b/src/sage/algebras/quatalg/quaternion_algebra.py
@@ -756,8 +756,7 @@ def maximal_order(self, take_shortcuts=True):
 
             sage: for d in ( m for m in range(1, 750) if is_squarefree(m) ):        # long time (3s)
             ....:     A = QuaternionAlgebra(d)
-            ....:     R = A.maximal_order(take_shortcuts=False)
-            ....:     assert A.discriminant() == R.discriminant()
+            ....:     assert A.maximal_order(take_shortcuts=False).is_maximal()
 
         We do not support number fields other than the rationals yet::
 
@@ -767,9 +766,24 @@ def maximal_order(self, take_shortcuts=True):
             ...
             NotImplementedError: maximal order only implemented
             for rational quaternion algebras
+
+        TESTS:
+
+        Check that :issue:`37417` and the first part of :issue:`37217` are fixed::
+
+            sage: invars = [(-4, -28), (-292, -732), (-48, -564), (-436, -768),
+            ....:           (-752, -708), (885, 545), (411, -710), (-411, 593),
+            ....:           (805, -591), (-921, 353), (409, 96), (394, 873),
+            ....:           (353, -722), (730, 830), (-466, -427), (-213, -630),
+            ....:           (-511, 608), (493, 880), (105, -709), (-213, 530),
+            ....:           (97, 745)]
+            sage: all(QuaternionAlgebra(a, b).maximal_order().is_maximal()
+            ....:           for (a, b) in invars)
+            True
         """
         if self.base_ring() != QQ:
-            raise NotImplementedError("maximal order only implemented for rational quaternion algebras")
+            raise NotImplementedError("maximal order only implemented for "
+                                      "rational quaternion algebras")
 
         d_A = self.discriminant()
 
@@ -812,15 +826,17 @@ def maximal_order(self, take_shortcuts=True):
 
         # The following code should always work (over QQ)
         # Start with <1,i,j,k>
-        R = self.quaternion_order((1,) + self.gens())
+        order_basis = (self.one(),) + self.gens()
+        R = self.quaternion_order(order_basis)
         d_R = R.discriminant()
 
         e_new_gens = []
 
         # For each prime at which R is not yet maximal, make it bigger
         for p, _ in d_R.factor():
-            e = R.basis()
-            while self.quaternion_order(e).discriminant().valuation(p) > d_A.valuation(p):
+            e = order_basis
+            disc = d_R
+            while disc.valuation(p) > d_A.valuation(p):
                 # Compute a normalized basis at p
                 f = normalize_basis_at_p(list(e), p)
 
@@ -893,13 +909,18 @@ def maximal_order(self, take_shortcuts=True):
                             e_n = basis_for_quaternion_lattice(list(e) + e_n[1:], reverse=True)
 
                 # e_n now contains elements that locally at p give a bigger order,
-                # but the basis may be messed up at other primes (it might not even
-                # be an order). We will join them all together at the end
+                # but the basis may be messed up at other primes (it might not
+                # even be an order). We will join them all together at the end
                 e = e_n
 
+                # Since e might not define an order at this point, we need to
+                # manually calculate the updated discriminant
+                L = [[x.pair(y) for y in e] for x in e]
+                disc = matrix(QQ, 4, 4, L).determinant().sqrt()
+
             e_new_gens.extend(e[1:])
 
-        e_new = basis_for_quaternion_lattice(list(R.basis()) + e_new_gens, reverse=True)
+        e_new = basis_for_quaternion_lattice(list(order_basis) + e_new_gens, reverse=True)
         return self.quaternion_order(e_new)
 
     def order_with_level(self, level):
@@ -1894,8 +1915,9 @@ def discriminant(self):
             sage: type(S.discriminant())
             <... 'sage.rings.rational.Rational'>
         """
-        L = [[d.pair(e) for e in self.basis()] for d in self.basis()]
-        return (MatrixSpace(QQ, 4, 4)(L)).determinant().sqrt()
+        e = self.basis()
+        L = [[x.pair(y) for y in e] for x in e]
+        return matrix(QQ, 4, 4, L).determinant().sqrt()
 
     def is_maximal(self) -> bool:
         r"""
@@ -3909,7 +3931,18 @@ def normalize_basis_at_p(e, p, B=QuaternionAlgebraElement_abstract.pair):
         sage: e = [A(1), k, j, 1/2 + 1/2*i + 1/2*j + 1/2*k]
         sage: normalize_basis_at_p(e, 2)
         [(1, 0), (1/2 + 1/2*i + 1/2*j + 1/2*k, 0), (-34/105*i - 463/735*j + 71/105*k, 1),
-         (-34/105*i - 463/735*j + 71/105*k, 1)]
+         (1/7*i - 8/49*j + 1/7*k, 1)]
+
+    TESTS:
+
+    We check that the second part of :issue:`37217` is fixed::
+
+        sage: A.<i,j,k> = QuaternionAlgebra(-1,-7)
+        sage: e = [A(1), k, j, 1/2 + 1/2*i + 1/2*j + 1/2*k]
+        sage: e_norm = normalize_basis_at_p(e, 2)
+        sage: V = QQ**4
+        sage: V.span([V(x.coefficient_tuple()) for (x,_) in e_norm]).dimension()
+        4
     """
 
     N = len(e)
@@ -3957,8 +3990,7 @@ def normalize_basis_at_p(e, p, B=QuaternionAlgebraElement_abstract.pair):
 
             # Ensures that (B(f0,f0)/2).valuation(p) <= B(f0,f1).valuation(p)
             if B(f0, f1).valuation(p) + 1 < B(f0, f0).valuation(p):
-                f0 += f1
-                f1 = f0
+                f0, f1 = f0 + f1, f0
 
             # Make remaining vectors orthogonal to span of f0, f1
             e[min_m] = e[0]
@@ -3971,7 +4003,7 @@ def normalize_basis_at_p(e, p, B=QuaternionAlgebraElement_abstract.pair):
             tu = [(B01 * B(f1, e[l]) - B11 * B(f0, e[l]),
                    B01 * B(f0, e[l]) - B00 * B(f1, e[l])) for l in range(2, N)]
 
-            e[2:n] = [e[l] + tu[l-2][0]/d * f0 + tu[l-2][1]/d * f1 for l in range(2, N)]
+            e[2:N] = [e[l] + tu[l-2][0]/d * f0 + tu[l-2][1]/d * f1 for l in range(2, N)]
 
             # Recursively normalize remaining vectors
             f = normalize_basis_at_p(e[2:N], p)


Test patch: