Skip to content

Add SageMath-FLINT bridge for number type conversion #39898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/doc/en/reference/interfaces/flint_bridge.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.. nodoctest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why nodoctest?

FLINT Bridge Interface
======================

This module provides functions to convert from SageMath's numerical types
to Python-FLINT's arbitrary precision types. It solves the issue of direct
conversion failing with code like:

.. code-block:: python

flint.arb(RR(1))
flint.acb(CC(1+I))

The bridge uses string-based conversion through mpmath to preserve precision,
with fallback to direct conversion when necessary.

Real Number Conversion
---------------------

.. autofunction:: sage.interfaces.flint.bridge.sage_to_flint_arb

Complex Number Conversion
------------------------

.. autofunction:: sage.interfaces.flint.bridge.sage_to_flint_acb

Matrix Conversion
---------------

.. autofunction:: sage.interfaces.flint.bridge.sage_matrix_to_flint

Module Reference
--------------

.. automodule:: sage.interfaces.flint.bridge
:members:
:undoc-members:
:show-inheritance:
1 change: 1 addition & 0 deletions src/doc/en/reference/interfaces/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ and testing to make sure nothing funny is going on).
sage/interfaces/abc
sage/interfaces/axiom
sage/interfaces/ecm
flint_bridge
sage/interfaces/four_ti_2
sage/interfaces/fricas
sage/interfaces/frobby
Expand Down
8 changes: 8 additions & 0 deletions src/sage/interfaces/flint/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
SageMath-FLINT bridge interface.

This module provides conversion functions from SageMath's numerical types
to Python-FLINT's arbitrary precision types.
"""

from .bridge import sage_to_flint_arb, sage_to_flint_acb, sage_matrix_to_flint
150 changes: 150 additions & 0 deletions src/sage/interfaces/flint/bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
"""
flint_sage_bridge.py - Bridge between SageMath and Python-FLINT
Solves GitHub issue #39836
This module provides one-way conversion from SageMath numerical types
to Python-FLINT arbitrary precision types.
"""
import flint
import mpmath
from sage.all import RR, CC

def sage_to_flint_arb(sage_real):
"""
Convert SageMath RealNumber to FLINT arb type.

This preserves more precision than a simple float conversion by using
mpmath as an intermediary.

Parameters:
-----------
sage_real : sage.rings.real_mpfr.RealNumber
A SageMath real number

Returns:
--------
flint.arb
The corresponding FLINT arb number

Examples:
---------
>>> from sage.all import RR, pi
>>> sage_to_flint_arb(RR(pi))
[3.1415926535897932384626433832795028842 +/- ...]
"""
try:
# Get the precision from the SageMath object if available
prec = sage_real.prec() if hasattr(sage_real, 'prec') else 53

# Get exact string representation from SageMath
if hasattr(sage_real, 'str'):
# Use the exact string representation if available
str_val = sage_real.str(no_sci=True)
else:
# Fall back to regular string representation
str_val = str(sage_real).replace('[', '').replace(']', '')

# Convert using mpmath for precision preservation
with mpmath.workdps(prec):
mp_val = mpmath.mpf(str_val)
result = flint.arb(mp_val, prec=prec)

return result
except (TypeError, ValueError):
# Fallback method
return flint.arb(float(sage_real))

def sage_to_flint_acb(sage_complex):
"""
Convert SageMath ComplexNumber to FLINT acb type.

This preserves more precision than a simple complex conversion by using
mpmath as an intermediary.

Parameters:
-----------
sage_complex : sage.rings.complex_mpfr.ComplexNumber
A SageMath complex number

Returns:
--------
flint.acb
The corresponding FLINT acb complex number

Examples:
---------
>>> from sage.all import CC, pi, I
>>> sage_to_flint_acb(CC(pi + I*pi))
([3.1415926535897932384626433832795028842 +/- ...] + [3.1415926535897932384626433832795028842 +/- ...]*I)
"""
try:
# Get the precision from the SageMath object if available
prec = sage_complex.prec() if hasattr(sage_complex, 'prec') else 53

# Get string representations of real and imaginary parts
real_part = sage_complex.real()
imag_part = sage_complex.imag()

if hasattr(real_part, 'str'):
real_str = real_part.str(no_sci=True)
imag_str = imag_part.str(no_sci=True)
else:
real_str = str(real_part).replace('[', '').replace(']', '')
imag_str = str(imag_part).replace('[', '').replace(']', '')

# Convert using mpmath with appropriate precision
with mpmath.workdps(prec):
mp_val = mpmath.mpc(real_str, imag_str)
result = flint.acb(mp_val, prec=prec)

return result
except (TypeError, ValueError):
# Fallback method
return flint.acb(complex(sage_complex))

def sage_matrix_to_flint(sage_matrix):
"""
Convert a SageMath matrix with RR/CC entries to FLINT matrix.

Parameters:
-----------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use INPUT: in our code base.

(Some AI not familiar with our code base may use whatever convention it learns. Not a problem per se, I'm happy if some AI solves my problem as long as the code is high quality, but not disclosing AI usage is dishonest.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're correct that I used AI assistance when drafting portions of this PR, particularly for structuring the documentation format. I should have disclosed this upfront, and I apologize for the oversight.

Copy link
Author

@Grimreaper00025 Grimreaper00025 Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll soon update the code to use SageMath's standard "INPUT:" convention instead of "Parameters:"

sage_matrix : sage.matrix
A SageMath matrix with real or complex entries

Returns:
--------
flint.arb_mat or flint.acb_mat
A FLINT matrix with corresponding entries

Examples:
---------
>>> from sage.all import matrix, RR
>>> M = matrix(RR, 2, 2, [1.1, 2.2, 3.3, 4.4])
>>> sage_matrix_to_flint(M)
Copy link
Contributor

@user202729 user202729 Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversion must work out of the box with existing interface, unless there's very strong evidence it is impossible. (For RR and CC it should be possible, see my comment on the issue.)

Also need a # optional - python_flint and the infrastructure behind it to add a new feature flag.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, I attempted to create a monkey-patching solution that would modify Python-FLINT's initialization methods to directly handle SageMath types. However, I encountered a limitation: the flint.types.arb.arb class has an immutable __init__ attribute that can't be directly replaced or modified.

Due to this constraint, I pivoted to creating explicit conversion functions instead. This approach works reliably but requires users to call the conversion functions rather than working automatically.

Copy link
Contributor

@user202729 user202729 Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I explained in the original issue

implement magic method _mpf_ and _mpc_ on RR and CC object respectively

This should work at least for ℝ and ℂ, as you can figure by reading the source code. Unless I misread, then explain why.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did attempt implementing mpf and mpc as you suggested in the original issue, but ran into difficulties with the Cython compilation of these methods in SageMath's core files.

[1.10000000000000 2.20000000000000]
[3.30000000000000 4.40000000000000]
"""
rows, cols = sage_matrix.nrows(), sage_matrix.ncols()

# Detect if we're working with real or complex entries
is_complex = False
for i in range(rows):
for j in range(cols):
if sage_matrix[i,j].parent() == CC or (hasattr(sage_matrix[i,j], 'imag') and not sage_matrix[i,j].imag().is_zero()):
is_complex = True
break
if is_complex:
break

# Create the appropriate matrix type
if is_complex:
result = flint.acb_mat(rows, cols)
for i in range(rows):
for j in range(cols):
result[i,j] = sage_to_flint_acb(sage_matrix[i,j])
else:
result = flint.arb_mat(rows, cols)
for i in range(rows):
for j in range(cols):
result[i,j] = sage_to_flint_arb(sage_matrix[i,j])

return result
110 changes: 110 additions & 0 deletions src/sage/interfaces/flint/test_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""
Test module for SageMath-FLINT bridge (one-way conversion)
"""
import unittest
import flint
import mpmath
from sage.all import RR, CC, matrix, pi, I, RealField, ComplexField
from sage.interfaces.flint.bridge import (
sage_to_flint_arb,
sage_to_flint_acb,
sage_matrix_to_flint
)

class TestFlintSageBridge(unittest.TestCase):
"""
Test suite for the SageMath-to-FLINT bridge functions.
"""

def setUp(self):
"""Set up test cases."""
# Real number examples at different precisions
self.real_std = RR(1.234)
self.real_high = RealField(200)(pi)
self.real_exact = RR(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use doctest almost everywhere here instead of pytest.

Do read the existing code yourself and try to fit your contribution in before writing code.


# Complex number examples
self.complex_std = CC(1.234 + 5.678*I)
self.complex_high = ComplexField(200)(pi + I*pi)
self.complex_exact = CC(1 + I)

# Matrix examples
self.matrix_real = matrix(RR, 2, 2, [1.1, 2.2, 3.3, 4.4])
self.matrix_complex = matrix(CC, 2, 2, [1+I, 2-I, 3+2*I, 4])

def test_real_conversion_basic(self):
"""Test basic conversion of SageMath RealNumber to FLINT arb."""
arb_num = sage_to_flint_arb(self.real_std)
self.assertIsInstance(arb_num, flint.arb)
self.assertAlmostEqual(float(arb_num), float(self.real_std))

def test_real_conversion_exact(self):
"""Test exact number conversion of SageMath RealNumber to FLINT arb."""
arb_num = sage_to_flint_arb(self.real_exact)
self.assertIsInstance(arb_num, flint.arb)
self.assertEqual(float(arb_num), float(self.real_exact))

def test_real_conversion_high_precision(self):
"""Test high precision conversion of SageMath RealNumber to FLINT arb."""
arb_num = sage_to_flint_arb(self.real_high)
self.assertEqual(str(arb_num).replace('[', '').replace(']', '')[:15], str(self.real_high)[:15])

def test_complex_conversion_basic(self):
"""Test basic conversion of SageMath ComplexNumber to FLINT acb."""
acb_num = sage_to_flint_acb(self.complex_std)
self.assertIsInstance(acb_num, flint.acb)
self.assertAlmostEqual(complex(acb_num), complex(self.complex_std))

def test_complex_conversion_high_precision(self):
"""Test high precision conversion of SageMath ComplexNumber to FLINT acb."""
acb_num = sage_to_flint_acb(self.complex_high)

flint_real_str = str(acb_num.real).replace('[', '').replace(']', '')
flint_imag_str = str(acb_num.imag).replace('[', '').replace(']', '')

self.assertEqual(flint_real_str[:15], str(self.complex_high.real())[:15])
self.assertEqual(flint_imag_str[:15], str(self.complex_high.imag())[:15])

def test_complex_conversion_exact(self):
"""Test exact number conversion of SageMath ComplexNumber to FLINT acb."""
acb_num = sage_to_flint_acb(self.complex_exact)
self.assertIsInstance(acb_num, flint.acb)
self.assertEqual(complex(acb_num), complex(self.complex_exact))

def test_matrix_conversion_real(self):
"""Test conversion of SageMath real matrix to FLINT arb_mat."""
arb_mat = sage_matrix_to_flint(self.matrix_real)
self.assertIsInstance(arb_mat, flint.arb_mat)
self.assertEqual(arb_mat.nrows(), self.matrix_real.nrows())
self.assertEqual(arb_mat.ncols(), self.matrix_real.ncols())
# Check elements
for i in range(self.matrix_real.nrows()):
for j in range(self.matrix_real.ncols()):
self.assertAlmostEqual(float(arb_mat[i,j]), float(self.matrix_real[i,j]))

def test_matrix_conversion_complex(self):
"""Test conversion of SageMath complex matrix to FLINT acb_mat."""
acb_mat = sage_matrix_to_flint(self.matrix_complex)
self.assertIsInstance(acb_mat, flint.acb_mat)
self.assertEqual(acb_mat.nrows(), self.matrix_complex.nrows())
self.assertEqual(acb_mat.ncols(), self.matrix_complex.ncols())
# Check elements
for i in range(self.matrix_complex.nrows()):
for j in range(self.matrix_complex.ncols()):
self.assertAlmostEqual(
complex(acb_mat[i,j]),
complex(self.matrix_complex[i,j])
)

def test_error_handling(self):
"""Test error handling for invalid input types."""
# Test with invalid input for sage_to_flint_arb
with self.assertRaises(Exception):
sage_to_flint_arb("not a number")

# Test with invalid input for sage_to_flint_acb
with self.assertRaises(Exception):
sage_to_flint_acb("not a number")

if __name__ == "__main__":
unittest.main()