Skip to content
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

Scipy: Fix recursive functions #4822

Merged
merged 11 commits into from
Jun 24, 2024

Conversation

hoodmane
Copy link
Member

@hoodmane hoodmane commented May 30, 2024

Non recursive functions declare all their locals as static, ones marked "recursive" need them to be proper local variables. Not sure if non recursive funcs need static locals, let's try making all locals non-static first. If this doesn't work, we can locate functions marked recursive and only modify them.

Fixes #4818.

@lesteve can you check the status of the scipy tests here?

Non recursive functions declare all their locals as static, ones marked
"recursive" need them to be proper local variables. Not sure if non
recursive funcs need static locals, let's try making all locals non-static
first.
@hoodmane hoodmane added this to the 0.26.1 milestone May 30, 2024
@lesteve
Copy link
Contributor

lesteve commented May 31, 2024

Looks like I get Pyodide fatal errors on this PR, for example:

node scipy-pytest.js --pyargs scipy.integrate.tests.test_quadrature -v

End of the output:

::TestQMCQuad::test_basic[8-256] Exit: Program terminated with exit(0)
    at ensureCaughtObjectIsError (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:994:693)
    at API.fatal_error (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:998:925)
    at API.maybe_fatal_error (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:998:1751)
    at callPyObjectKwargs (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:884)
    at Module.callPyObject (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:2115)
    at Function.apply (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:17630)
    at Object.apply (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:14926)
    at main (/home/lesteve/dev/scipy-tests-pyodide/scipy-pytest.js:72:24) {
  status: 0,
  pyodide_fatal_error: true
}

@hoodmane
Copy link
Member Author

The problem is that if you say static int x; it gets automatically zero-initialized. When I remove the static it becomes an uninitialized variable hence accesses are undefined behavior. Have to change it to int x = {0}.

@lesteve
Copy link
Contributor

lesteve commented Jun 6, 2024

Still the same issue as #4822 (comment) in the last iteration of the PR. Quicky looking it seems like scipy.special.stdtrit is the one creating the Pyodide fatal error.

Here is a snippet that reproduces the issue in this PR (simplified from scipy/integrate/tests/test_quadrature.py TestQMCQuad::test_basic[8-256])

import numpy as np
from numpy import cos, sin, pi
from numpy.testing import (assert_equal, assert_almost_equal, assert_allclose,
                           assert_, suppress_warnings)
from scipy.integrate import qmc_quad
from scipy import stats, special

def func(x):
    return stats.multivariate_normal.pdf(x.T, mean, cov)

n_points=2**8
n_estimates=8
signs=np.ones(2)
ndim = 2
mean = np.zeros(ndim)
cov = np.eye(ndim)
rng = np.random.default_rng(2879434385674690281)
qrng = stats.qmc.Sobol(ndim, seed=rng)
a = np.zeros(ndim)
b = np.ones(ndim) * signs
print('before qmc_quad', flush=True)
res = qmc_quad(func, a, b, n_points=n_points,
                n_estimates=n_estimates, qrng=qrng)
print('after qmc_quad', flush=True)

ref = stats.multivariate_normal.cdf(b, mean, cov, lower_limit=a)
print('before stdtdrit', flush=True)
atol = special.stdtrit(n_estimates-1, 0.995) * res.standard_error  # 99% CI
print('after stdtdrit', flush=True)
assert_allclose(res.integral, ref, atol=atol)
assert np.prod(signs)*res.integral > 0

rng = np.random.default_rng(2879434385674690281)
qrng = stats.qmc.Sobol(ndim, seed=rng)
print('before qmc_quad', flush=True)
logres = qmc_quad(lambda *args: np.log(func(*args)), a, b,
                    n_points=n_points, n_estimates=n_estimates,
                    log=True, qrng=qrng)
print('after qmc_quad', flush=True)
assert_allclose(np.exp(logres.integral), res.integral, rtol=1e-14)
assert np.imag(logres.integral) == (np.pi if np.prod(signs) < 0 else 0)
assert_allclose(np.exp(logres.standard_error),
                res.standard_error, rtol=1e-14, atol=1e-16)

I get this kind of info STOP SMALL, X, BIG not monotone in INVR statement executed. Here is the stack-trace I get when running with node:

../../mnt/test_quadrature.py::TestQMCQuad::test_basic[8-256] before qmc_quad
after qmc_quad
before stdtdrit
STOP  SMALL, X, BIG not monotone in INVR statement executed
Exit: Program terminated with exit(0)
    at ensureCaughtObjectIsError (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:994:693)
    at API.fatal_error (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:998:925)
    at API.maybe_fatal_error (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:998:1751)
    at callPyObjectKwargs (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:884)
    at Module.callPyObject (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:2115)
    at Function.apply (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:17630)
    at Object.apply (/home/lesteve/dev/scipy-tests-pyodide/node_modules/pyodide/pyodide.asm.js:1000:14926)
    at main (/home/lesteve/dev/scipy-tests-pyodide/scipy-pytest.js:72:24) {
  status: 0,
  pyodide_fatal_error: true
}

@hoodmane
Copy link
Member Author

hoodmane commented Jun 6, 2024

Thanks for the reproduction @lesteve.

@hoodmane
Copy link
Member Author

hoodmane commented Jun 7, 2024

Okay I think I got it.

@lesteve
Copy link
Contributor

lesteve commented Jun 7, 2024

The dblquad test does not seem to pass with your latest fixes 😅 ...

For example, node build log:

FAILED scipy/test_scipy.py::dblquad[node] - AssertionError: Unit square area calculated using scipy.integrate.dblquad of -1.7763568394002363e-15 (+- -1.7763568394002363e-15) is too far from 1.0
assert 1.0000000000000018 < 1.1102230246251565e-14

There is this warning as well just before that is likely relevant:

/lib/python3.12/site-packages/scipy/integrate/_quadpack_py.py:1272: IntegrationWarning: The integral is probably divergent, or slowly convergent.
  quad_r = quad(f, low, high, args=args, full_output=self.full_output,

@hoodmane
Copy link
Member Author

hoodmane commented Jun 7, 2024

Hmm still doesn't work. It's just a matter of futzing with it at this point I think, some of the commits make the dblquad test pass so I definitely have the right approach.

@hoodmane hoodmane removed this from the 0.26.1 milestone Jun 7, 2024
@hoodmane hoodmane merged commit bd28205 into pyodide:main Jun 24, 2024
39 of 41 checks passed
@hoodmane hoodmane added this to the 0.26.2 milestone Jun 24, 2024
Copy link
Member

@ryanking13 ryanking13 left a comment

Choose a reason for hiding this comment

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

Thanks for handling fortran issues @hoodmane! Let me port the f2c fixes to pyodide/pyodide-build.

@lesteve
Copy link
Contributor

lesteve commented Jun 26, 2024

FYI looks like this fixed quite a number of issues in the Scipy test suite, see lesteve/scipy-tests-pyodide#37. In particular:

  • there does not seem to be "memory corruption"-like issue anymore, i.e. non-deterministic errors that can be anything between Pyodide fatal errors, Python error that does not make any sense or wrong results
  • a lot less tests need to be xfailed

ryanking13 added a commit to pyodide/pyodide-build that referenced this pull request Jun 28, 2024
Ports pyodide/pyodide#4822

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
hoodmane added a commit to hoodmane/pyodide that referenced this pull request Jul 24, 2024
Non recursive functions declare all their locals as static, ones marked
"recursive" need them to be proper local variables. The Fortran files
each contain exactly one function. If the one function is recursive, we
label the source file with a comment. Then when processing the C file,
if it contains the recursive marker comment, we remove the `static`
modifiers from the variables.

At this point, we really ought to consider modifying f2c itself...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

scipy.integrate.dblquad gives very wrong result even for the simplest integrations
3 participants