Skip to content

BUG: Deepcopy of interpolators not possible in SciPy 1.18.0 #25640

Description

@Wandersalamander

Describe your issue.

Test case for scipy interpolator deepcopy issue (scipy >= 1.18).

This test demonstrates that scipy >= 1.18 has issues with deepcopying
interpolator objects, particularly PchipInterpolator and Akima1DInterpolator.

The issue manifests as:

  1. Deepcopy fails

Reported in BLonD issue:
https://gitlab.cern.ch/blond/BLonD/-/work_items/339

To reproduce with scipy >= 1.18:
pip install 'scipy>=1.18'
python test_scipy_deepcopy_interpolator_bug.py

Expected behavior: All tests should pass.
Actual behavior with scipy >= 1.18: PchipInterpolator and Akima1DInterpolator
tests return NaN or raise exceptions after deepcopy.

Reproducing Code Example

from copy import deepcopy

import numpy as np
from scipy.interpolate import (
    Akima1DInterpolator,
    PchipInterpolator,
    interp1d,
)


def test_deepcopy_interp1d():
    """interp1d deepcopy works fine."""
    x = np.array([0.0, 1.0, 2.0, 3.0])
    y = np.array([0.0, 1.0, 4.0, 9.0])

    interp = interp1d(x, y, kind='cubic')
    interp_copy = deepcopy(interp)

    test_x = 0.5
    original_result = interp(test_x)
    copy_result = interp_copy(test_x)

    assert not np.isnan(original_result), f"Original returned NaN: {original_result}"
    assert not np.isnan(copy_result), f"Deepcopy returned NaN: {copy_result}"
    assert np.isclose(original_result, copy_result), (
        f"Results differ: original={original_result}, copy={copy_result}"
    )
    print("[PASS] interp1d deepcopy works correctly")


def test_deepcopy_pchip():
    """
    PchipInterpolator deepcopy broken with scipy >= 1.18.

    The deepcopied interpolator may:
    - Return NaN on evaluation
    - Raise exceptions
    - Produce incorrect results
    """
    x = np.array([0.0, 1.0, 2.0, 3.0])
    y = np.array([0.0, 1.0, 4.0, 9.0])

    interp = PchipInterpolator(x, y)
    interp_copy = deepcopy(interp)

    test_x = 0.5
    original_result = interp(test_x)
    copy_result = interp_copy(test_x)

    print(f"  Original result: {original_result}")
    print(f"  Deepcopy result: {copy_result}")

    assert not np.isnan(original_result), (
        f"Original returned NaN: {original_result} (scipy bug?)"
    )
    assert not np.isnan(copy_result), (
        f"Deepcopy returned NaN: {copy_result} (scipy >= 1.18 bug)"
    )
    assert np.isclose(original_result, copy_result), (
        f"Results differ: original={original_result}, copy={copy_result} "
        f"(scipy >= 1.18 deepcopy bug)"
    )
    print("[PASS] PchipInterpolator deepcopy works correctly")


def test_deepcopy_akima():
    """
    Akima1DInterpolator deepcopy broken with scipy >= 1.18.

    Similar issues as PchipInterpolator.
    """
    x = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
    y = np.array([0.0, 1.0, 4.0, 9.0, 16.0])

    interp = Akima1DInterpolator(x, y)
    interp_copy = deepcopy(interp)

    test_x = 0.5
    original_result = interp(test_x)
    copy_result = interp_copy(test_x)

    print(f"  Original result: {original_result}")
    print(f"  Deepcopy result: {copy_result}")

    assert not np.isnan(original_result), (
        f"Original returned NaN: {original_result} (scipy bug?)"
    )
    assert not np.isnan(copy_result), (
        f"Deepcopy returned NaN: {copy_result} (scipy >= 1.18 bug)"
    )
    assert np.isclose(original_result, copy_result), (
        f"Results differ: original={original_result}, copy={copy_result} "
        f"(scipy >= 1.18 deepcopy bug)"
    )
    print("[PASS] Akima1DInterpolator deepcopy works correctly")


def test_deepcopy_in_class():
    """
    Deepcopy interpolator as part of a class instance.

    This is the real-world scenario from BLonD's MagneticCycleByTime.
    """
    class Container:
        def __init__(self):
            x = np.array([0.0, 1.0, 2.0, 3.0])
            y = np.array([0.0, 1.0, 4.0, 9.0])
            self.interp = PchipInterpolator(x, y)

    original = Container()
    copy = deepcopy(original)

    test_x = 0.5
    original_result = original.interp(test_x)
    copy_result = copy.interp(test_x)

    print(f"  Original result: {original_result}")
    print(f"  Deepcopy result: {copy_result}")

    assert not np.isnan(original_result), (
        f"Original returned NaN: {original_result} (scipy bug?)"
    )
    assert not np.isnan(copy_result), (
        f"Deepcopy returned NaN: {copy_result} (scipy >= 1.18 bug)"
    )
    assert np.isclose(original_result, copy_result), (
        f"Results differ: original={original_result}, copy={copy_result} "
        f"(scipy >= 1.18 deepcopy bug)"
    )
    print("[PASS] PchipInterpolator deepcopy in class works correctly")


if __name__ == "__main__":
    import scipy
    print(f"scipy version: {scipy.__version__}\n")

    print("Testing deepcopy of scipy interpolators...")
    print()

    try:
        test_deepcopy_interp1d()
    except AssertionError as e:
        print(f"[FAIL] interp1d deepcopy FAILED: {e}\n")

    try:
        test_deepcopy_pchip()
    except AssertionError as e:
        print(f"[FAIL] PchipInterpolator deepcopy FAILED: {e}\n")

    try:
        test_deepcopy_akima()
    except AssertionError as e:
        print(f"[FAIL] Akima1DInterpolator deepcopy FAILED: {e}\n")

    try:
        test_deepcopy_in_class()
    except AssertionError as e:
        print(f"[FAIL] PchipInterpolator deepcopy in class FAILED: {e}\n")

    print("All tests passed!")

Error message

C:\workwork\python\xstuite_llm\venv\Scripts\python.exe "C:/Program Files/JetBrains/PyCharm Community Edition 2025.1.1.1/plugins/python-ce/helpers/pycharm/_jb_pytest_runner.py" --path C:\workwork\python\blond\test_scipy_deepcopy_interpolator_bug.py 
Testing started at 9:59 AM ...
Launching pytest with arguments C:\workwork\python\blond\test_scipy_deepcopy_interpolator_bug.py --no-header --no-summary -q in C:\workwork\python\blond

============================= test session starts =============================
collecting ... collected 4 items

test_scipy_deepcopy_interpolator_bug.py::test_deepcopy_pchip Set special to `cpp`
FAILED      [ 25%]
test_scipy_deepcopy_interpolator_bug.py:57 (test_deepcopy_pchip)
def test_deepcopy_pchip():
        """
        PchipInterpolator deepcopy broken with scipy >= 1.18.
    
        The deepcopied interpolator may:
        - Return NaN on evaluation
        - Raise exceptions
        - Produce incorrect results
        """
        x = np.array([0.0, 1.0, 2.0, 3.0])
        y = np.array([0.0, 1.0, 4.0, 9.0])
    
        interp = PchipInterpolator(x, y)
>       interp_copy = deepcopy(interp)
                      ^^^^^^^^^^^^^^^^

test_scipy_deepcopy_interpolator_bug.py:70: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:162: in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:259: in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:136: in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

x = <module 'scipy._external.array_api_compat.numpy' from 'C:\\workwork\\python\\xstuite_llm\\venv\\Lib\\site-packages\\scipy\\_external\\array_api_compat\\numpy\\__init__.py'>
memo = {1836576690432: <scipy.interpolate._cubic.PchipInterpolator object at 0x000001AB99DB1160>, 1836504991872: {'_delegate_....  ,  1.5 ,  3.75],
       [ 0.  ,  1.  ,  4.  ]]), 'x': array([0., 1., 2., 3.]), 'extrapolate': True, 'axis': 0}, ...}
_nil = []

    def deepcopy(x, memo=None, _nil=[]):
        """Deep copy operation on arbitrary Python objects.
    
        See the module's __doc__ string for more info.
        """
    
        if memo is None:
            memo = {}
    
        d = id(x)
        y = memo.get(d, _nil)
        if y is not _nil:
            return y
    
        cls = type(x)
    
        copier = _deepcopy_dispatch.get(cls)
        if copier is not None:
            y = copier(x, memo)
        else:
            if issubclass(cls, type):
                y = _deepcopy_atomic(x, memo)
            else:
                copier = getattr(x, "__deepcopy__", None)
                if copier is not None:
                    y = copier(memo)
                else:
                    reductor = dispatch_table.get(cls)
                    if reductor:
                        rv = reductor(x)
                    else:
                        reductor = getattr(x, "__reduce_ex__", None)
                        if reductor is not None:
>                           rv = reductor(4)
                                 ^^^^^^^^^^^
E                           TypeError: cannot pickle 'module' object

C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:151: TypeError

test_scipy_deepcopy_interpolator_bug.py::test_deepcopy_akima FAILED      [ 50%]
test_scipy_deepcopy_interpolator_bug.py:92 (test_deepcopy_akima)
def test_deepcopy_akima():
        """
        Akima1DInterpolator deepcopy broken with scipy >= 1.18.
    
        Similar issues as PchipInterpolator.
        """
        x = np.array([0.0, 1.0, 2.0, 3.0, 4.0])
        y = np.array([0.0, 1.0, 4.0, 9.0, 16.0])
    
        interp = Akima1DInterpolator(x, y)
>       interp_copy = deepcopy(interp)
                      ^^^^^^^^^^^^^^^^

test_scipy_deepcopy_interpolator_bug.py:102: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:162: in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:259: in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:136: in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

x = <module 'scipy._external.array_api_compat.numpy' from 'C:\\workwork\\python\\xstuite_llm\\venv\\Lib\\site-packages\\scipy\\_external\\array_api_compat\\numpy\\__init__.py'>
memo = {1836532307040: <scipy.interpolate._cubic.Akima1DInterpolator object at 0x000001AB99DB1220>, 1836532470464: {'_delegat...  [0., 2., 4., 6.],
       [0., 1., 4., 9.]]), 'x': array([0., 1., 2., 3., 4.]), 'extrapolate': False, 'axis': 0}, ...}
_nil = []

    def deepcopy(x, memo=None, _nil=[]):
        """Deep copy operation on arbitrary Python objects.
    
        See the module's __doc__ string for more info.
        """
    
        if memo is None:
            memo = {}
    
        d = id(x)
        y = memo.get(d, _nil)
        if y is not _nil:
            return y
    
        cls = type(x)
    
        copier = _deepcopy_dispatch.get(cls)
        if copier is not None:
            y = copier(x, memo)
        else:
            if issubclass(cls, type):
                y = _deepcopy_atomic(x, memo)
            else:
                copier = getattr(x, "__deepcopy__", None)
                if copier is not None:
                    y = copier(memo)
                else:
                    reductor = dispatch_table.get(cls)
                    if reductor:
                        rv = reductor(x)
                    else:
                        reductor = getattr(x, "__reduce_ex__", None)
                        if reductor is not None:
>                           rv = reductor(4)
                                 ^^^^^^^^^^^
E                           TypeError: cannot pickle 'module' object

C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:151: TypeError

test_scipy_deepcopy_interpolator_bug.py::test_deepcopy_interp1d PASSED   [ 75%][PASS] interp1d deepcopy works correctly

test_scipy_deepcopy_interpolator_bug.py::test_deepcopy_in_class FAILED   [100%]
test_scipy_deepcopy_interpolator_bug.py:124 (test_deepcopy_in_class)
def test_deepcopy_in_class():
        """
        Deepcopy interpolator as part of a class instance.
    
        This is the real-world scenario from BLonD's MagneticCycleByTime.
        """
        class Container:
            def __init__(self):
                x = np.array([0.0, 1.0, 2.0, 3.0])
                y = np.array([0.0, 1.0, 4.0, 9.0])
                self.interp = PchipInterpolator(x, y)
    
        original = Container()
>       copy = deepcopy(original)
               ^^^^^^^^^^^^^^^^^^

test_scipy_deepcopy_interpolator_bug.py:137: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:162: in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:259: in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:136: in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:162: in deepcopy
    y = _reconstruct(x, memo, *rv)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:259: in _reconstruct
    state = deepcopy(state, memo)
            ^^^^^^^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:136: in deepcopy
    y = copier(x, memo)
        ^^^^^^^^^^^^^^^
C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:221: in _deepcopy_dict
    y[deepcopy(key, memo)] = deepcopy(value, memo)
                             ^^^^^^^^^^^^^^^^^^^^^
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

x = <module 'scipy._external.array_api_compat.numpy' from 'C:\\workwork\\python\\xstuite_llm\\venv\\Lib\\site-packages\\scipy\\_external\\array_api_compat\\numpy\\__init__.py'>
memo = {1836549901872: <test_scipy_deepcopy_interpolator_bug.test_deepcopy_in_class.<locals>.Container object at 0x000001AB9A...B9AE78260>, 1836532472384: {'_delegate_to': <scipy.interpolate._interpolate._PPoly object at 0x000001AB9AEF3DE0>}, ...}
_nil = []

    def deepcopy(x, memo=None, _nil=[]):
        """Deep copy operation on arbitrary Python objects.
    
        See the module's __doc__ string for more info.
        """
    
        if memo is None:
            memo = {}
    
        d = id(x)
        y = memo.get(d, _nil)
        if y is not _nil:
            return y
    
        cls = type(x)
    
        copier = _deepcopy_dispatch.get(cls)
        if copier is not None:
            y = copier(x, memo)
        else:
            if issubclass(cls, type):
                y = _deepcopy_atomic(x, memo)
            else:
                copier = getattr(x, "__deepcopy__", None)
                if copier is not None:
                    y = copier(memo)
                else:
                    reductor = dispatch_table.get(cls)
                    if reductor:
                        rv = reductor(x)
                    else:
                        reductor = getattr(x, "__reduce_ex__", None)
                        if reductor is not None:
>                           rv = reductor(4)
                                 ^^^^^^^^^^^
E                           TypeError: cannot pickle 'module' object

C:\Users\slauber\AppData\Local\Programs\Python\Python312\Lib\copy.py:151: TypeError


=================== 3 failed, 1 passed, 1 warning in 0.97s ====================

Process finished with exit code 1

SciPy/NumPy/Python version and system information

1.18.0 2.4.6 sys.version_info(major=3, minor=12, micro=10, releaselevel='final', serial=0)
Build Dependencies:
  blas:
    cython blas ilp64: false
    detection method: pkgconfig
    found: true
    has ilp64: false
    include directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-sm43q9rt/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/include
    lib directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-sm43q9rt/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/lib
    name: scipy-openblas
    openblas configuration: OpenBLAS 0.3.31.dev DYNAMIC_ARCH NO_AFFINITY SkylakeX
      MAX_THREADS=24
    pc file directory: D:/a/scipy-release/scipy-release/.openblas
    version: 0.3.31.dev
  lapack:
    detection method: pkgconfig
    found: true
    has ilp64: false
    include directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-sm43q9rt/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/include
    lib directory: C:/Users/runneradmin/AppData/Local/Temp/cibw-run-sm43q9rt/cp312-win_amd64/build/venv/Lib/site-packages/scipy_openblas32/lib
    name: scipy-openblas
    openblas configuration: OpenBLAS 0.3.31.dev DYNAMIC_ARCH NO_AFFINITY SkylakeX
      MAX_THREADS=24
    pc file directory: D:/a/scipy-release/scipy-release/.openblas
    version: 0.3.31.dev
  pybind11:
    detection method: config-tool
    include directory: unknown
    name: pybind11
    version: 3.0.4
Compilers:
  c:
    commands: C:\Strawberry\c\bin\ccache.EXE, cc
    linker: ld.bfd
    name: gcc
    version: 15.2.0
  c++:
    commands: C:\Strawberry\c\bin\ccache.EXE, c++
    linker: ld.bfd
    name: gcc
    version: 15.2.0
  cython:
    commands: cython
    linker: cython
    name: cython
    version: 3.2.5
  fortran:
    commands: gfortran
    linker: ld.bfd
    name: gcc
    version: 15.2.0
  pythran:
    include directory: C:/Users/runneradmin/AppData/Local/Temp/build-env-i3hfaws3/Lib/site-packages/pythran
    version: 0.18.1
Machine Information:
  build:
    cpu: x86_64
    endian: little
    family: x86_64
    system: windows
  cross-compiled: false
  host:
    cpu: x86_64
    endian: little
    family: x86_64
    system: windows
Python Information:
  path: C:/Users/runneradmin/AppData/Local/Temp/build-env-i3hfaws3/Scripts/python.exe
  version: '3.12'

Metadata

Metadata

Assignees

No one assigned

    Labels

    defectA clear bug or issue that prevents SciPy from being installed or used as expected

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions