flintlib / python-flint

Python bindings for Flint and Arb
MIT License
132 stars 27 forks source link

Initial bindings for generic rings #218

Closed oscarbenjamin closed 2 months ago

oscarbenjamin commented 2 months ago

This needs a lot more thought about how to design things but I thought it would be useful to put this up and maybe just merge as is on the basis that it is unstable/private.

The changes here are:

What I haven't added are polynomials and matrices (gr_mat, gr_poly, gr_mpoly) and also series (gr_series). I also haven't added many functions beyond arithmetic.

I don't think that the design here is really exactly what we would want because it uses the same gr type at the Python level for everything (fmpz, arb, ...) whereas I think that we want these to be different types at the Python level with different methods/attributes. More work is needed to figure out the right design but I think it might be like this:

Introducing a context based interface provides an opportunity to change the interface of many things and to solve many problems simultaneously:

  1. Can fix the non-prime modulus problem for nmod.
  2. Make a uniform interface for constructing objects of all types.
  3. Have a well designed coercion system.
  4. Add many new types easily
  5. Use better generic tests for more economic use of writing test code.
  6. Avoid global variables like the precision in arb or cap in series.

And of course the big one is that the generic rings code allows constructing new rings like a ring of matrices of multivariate polynomials etc.

I haven't added tests etc because I expect the design here to change a lot but I think it is worth just adding this as a private/unstable module so that people can test it out and we can work out the full design over time. I think that it would be worth fleshing this out with matrices, polynomials, special functions etc, figuring all the types, conversions and coercions and so on before making it "public". It can also be fine for people to use this as long as they understand that the interfaces are subject to change in future.

These are the scalar types that can be created through the generic system with this PR:

In [1]: from flint.types import _gr

In [2]: [name for name in dir(_gr) if not name.startswith('_')]
Out[2]: 
['gr',
 'gr_complex_acb_ctx',
 'gr_complex_algebraic_ca_ctx',
 'gr_complex_ca_ctx',
 'gr_complex_extended_ca_ctx',
 'gr_complex_float_acf_ctx',
 'gr_complex_qqbar_ctx',
 'gr_ctx',
 'gr_fexpr_ctx',
 'gr_fmpq_ctx',
 'gr_fmpz_ctx',
 'gr_fmpz_mod_ctx',
 'gr_fmpzi_ctx',
 'gr_fq_ctx',
 'gr_fq_nmod_ctx',
 'gr_fq_zech_ctx',
 'gr_matrix_domain_ctx',
 'gr_matrix_ring_ctx',
 'gr_matrix_space_ctx',
 'gr_mpoly_ctx',
 'gr_nf_ctx',
 'gr_nf_fmpz_poly_ctx',
 'gr_nmod_ctx',
 'gr_poly_ctx',
 'gr_real_algebraic_ca_ctx',
 'gr_real_arb_ctx',
 'gr_real_ca_ctx',
 'gr_real_float_arf_ctx',
 'gr_real_qqbar_ctx',
 'gr_scalar_ctx']

Note that some of these are just abstract classes e.g. there are no polynomials or matrices here. Some of these duplicate things that already exist like fmpz, nmod etc.

Some examples of things this allows that are not currently accessible through python-flint are:

  1. Gaussian integers:
    
    In [3]: R = _gr.gr_fmpzi_ctx

In [4]: R Out[4]: gr_fmpzi_ctx

In [5]: R.i() Out[5]: I

In [6]: R.i() + 2 Out[6]: (2+I)

In [7]: _*2 Out[7]: (3+4I)

In [10]: a = 1 + R.i()

In [11]: a Out[11]: (1+I)

In [13]: (2a).gcd(4a) Out[13]: (2+2*I)

Having contexts for all objects opens the possibility that it could be `R.gcd(2*a, 4*a)` or even that it could be `gcd(2*a, 4*a)` where `gcd` is a function that can find the context.

2. Algebraic number fields:
```python
In [20]: x = fmpq_poly([0, 1])

In [21]: R = _gr.gr_nf_ctx.new(x**2 - 2)

In [22]: R
Out[22]: gr_nf_ctx(x^2 + (-2))

In [24]: R.gen()
Out[24]: a

In [25]: a = R.gen()

In [26]: 1 + a + a**2
Out[26]: a+3
  1. QQbar:
    
    In [33]: R = _gr.gr_real_qqbar_ctx.new()

In [36]: R Out[36]: gr_real_qqbar_ctx(-1, -1)

In [34]: R(2).sqrt() Out[34]: Root a = 1.41421 of a^2-2

In [35]: R(2).sqrt() ** 3 Out[35]: Root a = 2.82843 of a^2-8


4. Calcium
```python
In [37]: R = _gr.gr_complex_ca_ctx.new()

In [38]: R
Out[38]: gr_complex_ca_ctx({})

In [39]: R(123).sqrt()
Out[39]: 11.0905 {a where a = 11.0905 [a^2-123=0]}
  1. Symbolic expressions
    
    In [40]: R = _gr.gr_fexpr_ctx.new()

In [41]: R Out[41]: gr_fexpr_ctx

In [42]: R(123)/2 + 1 Out[42]: Add(Div(123, 2), 1)


6. We already have arb, arf etc but currently they use global precision making them unsuitable for SymPy/mpmath. Here we can have per-context precision and no global variables:
```python
In [44]: R1 = _gr.gr_real_arb_ctx.new(10)

In [45]: R2 = _gr.gr_real_arb_ctx.new(20)

In [46]: R1(2).sqrt()
Out[46]: [1.41 +/- 6.02e-3]

In [47]: R2(2).sqrt()
Out[47]: [1.41421 +/- 5.09e-6]
oscarbenjamin commented 2 months ago

I've just added the gr poly, mpoly and series types. This gives e.g. multivariate polynomials over the Gaussian integers:

In [4]: from flint.types import _gr

In [5]: R = _gr.gr_gr_mpoly_ctx.new(_gr.gr_fmpzi_ctx, 2)

In [6]: R
Out[6]: gr_gr_mpoly_ctx(gr_fmpzi_ctx, 2, 0)

In [7]: R.gens_recursive()
Out[7]: [I, x1, x2]

In [8]: i, x1, x2 = R.gens_recursive()

In [9]: (x1 + x2 + i)**3
Out[9]: x1^3 + 3*x1^2*x2 + (3*I)*x1^2 + 3*x1*x2^2 + (6*I)*x1*x2 - 3*x1 + x2^3 + (3*I)*x2^2 - 3*x2 - I