Bug 2339539 - gnofract4d fails to build with Python 3.14: AssertionError: 'ct_sin6 = (0.841471,-0)' != 'ct_sin6 = (0.841471,0)'
Summary: gnofract4d fails to build with Python 3.14: AssertionError: 'ct_sin6 = (0.841...
Keywords:
Status: CLOSED RAWHIDE
Alias: None
Product: Fedora
Classification: Fedora
Component: gnofract4d
Version: rawhide
Hardware: Unspecified
OS: Unspecified
unspecified
unspecified
Target Milestone: ---
Assignee: Yaakov Selkowitz
QA Contact: Fedora Extras Quality Assurance
URL:
Whiteboard:
: 2371720 (view as bug list)
Depends On:
Blocks: PYTHON3.14 F43FTBFS, RAWHIDEFTBFS F43FailsToInstall, RAWHIDEFailsToInstall
TreeView+ depends on / blocked
 
Reported: 2025-01-22 15:55 UTC by Karolina Surma
Modified: 2025-06-11 19:09 UTC (History)
4 users (show)

Fixed In Version: gnofract4d-4.3-21.fc43
Clone Of:
Environment:
Last Closed: 2025-06-11 19:09:16 UTC
Type: Bug
Embargoed:


Attachments (Terms of Use)
Possible patch (2.20 KB, patch)
2025-06-06 06:06 UTC, Yaakov Selkowitz
no flags Details | Diff


Links
System ID Private Priority Status Summary Last Updated
Github fract4d gnofract4d issues 267 0 None open Tests fail on Python 3.14 2025-06-11 18:28:48 UTC

Description Karolina Surma 2025-01-22 15:55:11 UTC
gnofract4d fails to build with Python 3.14.0a4.

_______________________________ Test.test_stdlib _______________________________

self = <fract4d_compiler.tests.test_codegen.Test testMethod=test_stdlib>

    def test_stdlib(self):
        '''This is the slowest test, due to how much compilation it does.
        Calls standard functions with a variety
        of values, checking that they produce the right answers'''
    
        # additions to python math stdlib
        def myfcotan(x):
            return math.cos(x) / math.sin(x)
    
        def myfcotanh(x):
            return math.cosh(x) / math.sinh(x)
    
        def mycotanh(z):
            return cmath.cosh(z) / cmath.sinh(z)
    
        def myasinh(z):
            return cmath.log(z + cmath.sqrt(z * z + 1))
    
        def myacosh(z):
            return cmath.log(z + cmath.sqrt(z - 1) * cmath.sqrt(z + 1))
    
        def myctrunc(z):
            return complex(int(z.real), int(z.imag))
    
        def mycfloor(z):
            return complex(math.floor(z.real), math.floor(z.imag))
    
        def mycround(z):
            return complex(int(z.real + 0.5), int(z.imag + 0.5))
    
        def mycceil(z):
            x = complex(math.ceil(z.real), math.ceil(z.imag))
            return x
    
        def mycosxx(z):
            # python 2.6's cmath cos produces opposite sign from 2.5 &
            # my implementation. odd but hopefully harmless
            if z == (0 - 1j):
                return cmath.cos(z)
    
            cosz = cmath.cos(z)
            i = -cosz.imag
    
            return complex(cosz.real, i)
    
        def myczero(z):
            return complex(0, 0)
    
        tests = [
            # code to run, var to inspect, result
            ["fm = (3.0 % 2.0, 3.1 % 1.5)", "fm", "(1,0.1)"],
            ["cj = conj(y)", "cj", "(1,-2)"],
            ["fl = flip(y)", "fl", "(2,1)"],
            ["ri = (imag(y),real(y))", "ri", "(2,1)"],
            ["m = |y|", "m", "(5,0)"],
            ["t = (4,2) * (2,-1)", "t", "(10,0)"],
            ["d1 = y/(1,0)", "d1", "(1,2)"],
            ["d2 = y/y", "d2", "(1,0)"],
            ["d3 = (4,2)/y", "d3", "(1.6,-1.2)"],
            ["d4 = (2,1)/2", "d4", "(1,0.5)"],
            ["recip1 = recip((4,0))/recip(4)", "recip1", "(1,0)"],
            ["i = ident(y)", "i", "(1,2)"],
            ["a = (abs(4),abs(-4))", "a", "(4,4)"],
            ["a2 = abs((4,-4))", "a2", "(4,4)"],
            ["cab = (cabs((0,0)), cabs((3,4)))", "cab", "(0,5)"],
            ["sq = (sqrt(4),sqrt(2))", "sq", self.predict(math.sqrt, 4, 2)],
            ["l = (log(1),log(3))", "l", self.predict(math.log, 1, 3)],
            ["ex = (exp(1),exp(2))", "ex", self.predict(math.exp, 1, 2)],
            ["pow0a = (2^2, 9^0.5)", "pow0a", "(4,3)"],
            ["pow0b = ((-1)^0.5,(-1)^2)", "pow0b", "(nan,1)"],
            ["pow1 = (1,0)^2", "pow1", "(1,0)"],
            ["pow2 = (-2,-3)^7.5", "pow2", "(-13320.5,6986.17)"],
            ["pow3 = (-2,-3)^(1.5,-3.1)", "pow3", "(0.00507248,-0.00681128)"],
            ["pow4 = (0,0)^(1.5,-3.1)", "pow4", "(0,0)"],
            ["manh1 = (manhattanish(2.0,-1.0),manhattanish(0.1,-0.1))",
             "manh1", "(1,0)"],
            ["manh2 = (manhattan(2.0,-1.5),manhattan(-2,1.7))",
             "manh2", "(3.5,3.7)"],
            ["manh3 = (manhattanish2(2.0,-1.0),manhattanish2(0.1,-0.1))",
             "manh3", "(25,0.0004)"],
            ["mx2 = (max2(2,-3),max2(-3,0))", "mx2", "(9,9)"],
            ["mn2 = (min2(-1,-2),min2(7,4))", "mn2", "(1,16)"],
            ["r2 = (real2(3,1),real2(-2.5,2))", "r2", "(9,6.25)"],
            ["i2 = (imag2(3,2),imag2(2,-0))", "i2", "(4,0)"],
            ["ftrunc1 = (trunc(0.5), trunc(0.4))", "ftrunc1", "(0,0)"],
            ["ftrunc2 = (trunc(-0.5), trunc(-0.4))", "ftrunc2", "(0,0)"],
            ["frnd1 = (round(0.5), round(0.4))", "frnd1", "(1,0)"],
            ["frnd2 = (round(-0.5), round(-0.4))", "frnd2", "(0,0)"],
            ["fceil1 = (ceil(0.5), ceil(0.4))", "fceil1", "(1,1)"],
            ["fceil2 = (ceil(-0.5), ceil(-0.4))", "fceil2", "(0,0)"],
            ["ffloor1 = (floor(0.5), floor(0.4))", "ffloor1", "(0,0)"],
            ["ffloor2 = (floor(-0.5), floor(-0.4))", "ffloor2", "(-1,-1)"],
            ["fzero = (zero(77),zero(-41.2))", "fzero", "(0,0)"],
            ["fmin = (min(-2,-3), min(2,3))", "fmin", "(-3,2)"],
            ["fmax = (max(-2,-3), max(2,3))", "fmax", "(-2,3)"],
    
            # trig functions
            ["t_sin = (sin(0),sin(1))", "t_sin", self.predict(math.sin)],
            ["t_cos = (cos(0),cos(1))", "t_cos", self.predict(math.cos)],
            ["t_tan = (tan(0),tan(1))", "t_tan", self.predict(math.tan)],
            ["t_cotan = (cotan(0),cotan(1))", "t_cotan",
             self.predict(myfcotan)],
            ["t_sinh = (sinh(0),sinh(1))", "t_sinh", self.predict(math.sinh)],
            ["t_cosh = (cosh(0),cosh(1))", "t_cosh", self.predict(math.cosh)],
            ["t_tanh = (tanh(0),tanh(1))", "t_tanh", self.predict(math.tanh)],
            ["t_cotanh = (cotanh(0),cotanh(1))", "t_cotanh",
             self.predict(myfcotanh)],
    
            # inverse trig functions
            ["t_asin = (asin(0),asin(1))", "t_asin", self.predict(math.asin)],
            ["t_acos = (acos(0),acos(1))", "t_acos", self.predict(math.acos)],
            ["t_atan = (atan(0),atan(1))", "t_atan", self.predict(math.atan)],
            ["t_atan2 = (atan2((1,1)),atan2((-1,-1)))",
             "t_atan2", "(0.785398,-2.35619)"],
            # these aren't in python stdlib, need to hard-code results
            ["t_asinh = (asinh(0),asinh(1))", "t_asinh", "(0,0.881374)"],
            ["t_acosh = (acosh(10),acosh(1))", "t_acosh", "(2.99322,0)"],
            ["t_atanh = (atanh(0),atanh(0.5))", "t_atanh", "(0,0.549306)"],
        ]
        tests += self.manufacture_tests("sin", cmath.sin)
        tests += self.manufacture_tests("cos", cmath.cos)
        tests += self.manufacture_tests("cosxx", mycosxx)
        tests += self.manufacture_tests("tan", cmath.tan)
        tests += self.manufacture_tests("sinh", cmath.sinh)
        tests += self.manufacture_tests("cosh", cmath.cosh)
        tests += self.manufacture_tests("tanh", cmath.tanh)
        tests += self.manufacture_tests("exp", cmath.exp)
        tests += self.manufacture_tests("sqrt", cmath.sqrt)
        tests += self.manufacture_tests("round", mycround)
        tests += self.manufacture_tests("ceil", mycceil)
        tests += self.manufacture_tests("floor", mycfloor)
        tests += self.manufacture_tests("trunc", myctrunc)
        tests += self.manufacture_tests("zero", myczero)
        tests += self.cotantests()
        tests += self.cotanhtests()
        tests += self.logtests()
    
        tests += self.asintests()
        tests += self.acostests()
        tests += self.atantests()
        tests += self.manufacture_tests("asinh", myasinh)
        tests += self.manufacture_tests("acosh", myacosh)
        tests += self.atanhtests()
    
        # construct a formula calculating all of the above,
        # run it and compare results with expected values
        src = 't_c6{\ninit: y = (1,2)\n' + \
              "\n".join([x[0] for x in tests]) + "\n}"
    
        check = "\n".join([self.inspect_complex(x[1]) for x in tests])
        exp = ["%s = %s" % (x[1], x[2]) for x in tests]
>       self.assertCSays(src, "init", check, exp)

fract4d_compiler/tests/test_codegen.py:1995: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
fract4d_compiler/tests/test_codegen.py:2305: in assertCSays
    self.assertEqual(exp, res)
E   AssertionError: 'ct_sin6 = (0.841471,-0)' != 'ct_sin6 = (0.841471,0)'
E   - ct_sin6 = (0.841471,-0)
E   ?                     -
E   + ct_sin6 = (0.841471,0)

https://docs.python.org/3.14/whatsnew/3.14.html

For the build logs, see:
https://copr-be.cloud.fedoraproject.org/results/@python/python3.14/fedora-rawhide-x86_64/08558686-gnofract4d/

For all our attempts to build gnofract4d with Python 3.14, see:
https://copr.fedorainfracloud.org/coprs/g/python/python3.14/package/gnofract4d/

Testing and mass rebuild of packages is happening in copr.
You can follow these instructions to test locally in mock if your package builds with Python 3.14:
https://copr.fedorainfracloud.org/coprs/g/python/python3.14/

Let us know here if you have any questions.

Python 3.14 is planned to be included in Fedora 43.
To make that update smoother, we're building Fedora packages with all pre-releases of Python 3.14.
A build failure prevents us from testing all dependent packages (transitive [Build]Requires),
so if this package is required a lot, it's important for us to get it fixed soon.

We'd appreciate help from the people who know this package best,
but if you don't want to work on this now, let us know so we can try to work around it on our side.

Comment 1 Yaakov Selkowitz 2025-01-22 18:45:13 UTC
-0 vs. 0, just lovely... Any mathematicians around to explain that?

I'm afraid I could use some help with that.  FWIW, this just successfully rebuilt for the F42 mass rebuild.

Comment 2 Miro Hrončok 2025-01-25 20:42:46 UTC
$ python3.13 -c 'from cmath import sin; print(sin(1-0j))'
(0.8414709848078965+0j)

$ python3.14 -c 'from cmath import sin; print(sin(1-0j))'
(0.8414709848078965-0j)


I am no math expert, but I think both results are de facto correct.

We can bisect Python to see where this happened if it helps.

Comment 3 Miro Hrončok 2025-01-26 11:20:10 UTC
commit 987311d42e3ec838de8ff27f9f0575aa791a6bde
Author: Sergey B Kirpichev <skirpichev>
Date:   Tue Nov 26 18:57:39 2024 +0300

    gh-69639: Add mixed-mode rules for complex arithmetic (C-like) (GH-124829)
    
    "Generally, mixed-mode arithmetic combining real and complex variables should
    be performed directly, not by first coercing the real to complex, lest the sign
    of zero be rendered uninformative; the same goes for combinations of pure
    imaginary quantities with complex variables." (c) Kahan, W: Branch cuts for
    complex elementary functions.
    
    This patch implements mixed-mode arithmetic rules, combining real and
    complex variables as specified by C standards since C99 (in particular,
    there is no special version for the true division with real lhs
    operand).  Most C compilers implementing C99+ Annex G have only these
    special rules (without support for imaginary type, which is going to be
    deprecated in C2y).

https://github.com/python/cpython/commit/987311d42e3ec838de8ff27f9f0575aa791a6bde
https://github.com/python/cpython/pull/124829
https://github.com/python/cpython/issues/69639

----

I don't know if the -0 thing is a desired result of this or an accidental one.

Comment 5 Aoife Moloney 2025-02-26 13:25:25 UTC
This bug appears to have been reported against 'rawhide' during the Fedora Linux 42 development cycle.
Changing version to 42.

Comment 6 Yaakov Selkowitz 2025-06-06 06:06:39 UTC
Created attachment 2093177 [details]
Possible patch

The relevant code appears to be:

```
    def cpredict(self, f, arg=(1 + 0j)):
        try:
            z = f(arg)
            return "(%.6g,%.6g)" % (z.real, z.imag)
        except OverflowError:
            return "(inf,inf)"
        except ZeroDivisionError:
            return "(nan,nan)"
        except ValueError:
            return "(inf,inf)"

    def make_test(self, myfunc, pyfunc, val, n):
        codefrag = "ct_%s%d = %s((%d,%d))" % (
            myfunc, n, myfunc, val.real, val.imag)
        lookat = "ct_%s%d" % (myfunc, n)
        result = self.cpredict(pyfunc, val)
        return [codefrag, lookat, result]

    def manufacture_tests(self, myfunc, pyfunc):
        vals = [0 + 0j, 0 + 1j, 1 + 0j, 1 + 1j, 3 + 2j,
                1 - 0j, 0 - 1j, -3 + 2j, -2 - 2j, -1 + 0j]
        return [self.make_test(myfunc, pyfunc, x_y[0], x_y[1])
                for x_y in zip(vals, list(range(1, len(vals))))]
```
It's the `1 - 0j` iteration that's failing.

So I think what's happening here is that, while the sign is being preserved by Python initially (as of 3.14), it's not being preserved elsewhere:

1) it is not preserved when passed through the "%d" formatting of `make_test`, e.g.:

>>> val = 1 - 0j
>>> val
(1-0j)
>>> val.imag
-0.0
>>> print("%d" % val.imag)
0

2) it is not being preserved by C++ and/or gf4d; note other places marked `CONSIDER` in the sources which need to be fixed due to mismatches.

The simplest workaround appears to be to skip the 1-0j iteration, per the attached patch which I tested in the side tag:

https://koji.fedoraproject.org/koji/taskinfo?taskID=133610276

I'm open to suggestions though.

Comment 7 Karolina Surma 2025-06-11 15:53:44 UTC
*** Bug 2371720 has been marked as a duplicate of this bug. ***


Note You need to log in before you can comment on or make changes to this bug.