Skip to content

Copy DoubleVector and IntVector data to numpy arrays in C++ - #3524

Open
FFroehlich wants to merge 1 commit into
claude/issue-3521-cache-edatasfrom
claude/issue-3510-fix-43b6f4
Open

FFroehlich wants to merge 1 commit into
claude/issue-3521-cache-edatasfrom
claude/issue-3510-fix-43b6f4

Conversation

@FFroehlich

@FFroehlich FFroehlich commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

What does this pull request do, and why?

Makes numpy.array/numpy.asarray of a DoubleVector or IntVector copy the data in C++, instead of building the array element by element from the vector proxy's Python iterator. That per-element conversion is what ReturnDataView does for every field it returns (np.array(attr).reshape(field_dim) in _field_as_numpy), so it was paid after every simulation for x, w, y, sx, FIM, and so on.

  • DoubleVector and IntVector get a __array__(dtype=None, copy=None) method. It calls a new _to_ndarray method, implemented in C++ in %extend, which allocates a 1D array and copies the data with memcpy (float64) or std::copy (int64, the same dtype np.array(IntVector) gave before).
  • _to_ndarray creates Python objects, so its wrapper must keep the GIL. The extension is built with -threads, which releases the GIL around every wrapped call, so _to_ndarray gets %feature("nothreadallow"). I checked in the generated wrapper that its call has no SWIG_PYTHON_THREAD_BEGIN_ALLOW (unlike, e.g., DoubleVector.__len__ and stdVec2ndarray, see below).
  • The result is always a new array, not a view: a view would not keep the owning ReturnData alive. Therefore np.asarray(vec, copy=False) raises ValueError, as the numpy protocol requires when a copy can't be avoided. NumPy 2 passes copy=True to __array__ from np.array and then doesn't copy again (checked), so _field_as_numpy is unchanged.

Timings on this Mac (Python 3.13, numpy 2.5.4), this branch vs. a build of #3520's head:

before this PR
np.array(DoubleVector), 123,000 elements (w of 82 expressions at 1500 time points, from the issue) 29 ms 0.017 ms
np.array(IntVector), 123,000 elements 29 ms 0.016 ms
_field_as_numpy of a 46×46 FIM, 18,338 times (the PEtab v2 _aggregate_s2llh case from the issue comment) 26 s 0.07 s

Not run on the Froehlich_CellSystems2018 problem or in sbmlsim.

Not addressed here:

This is stacked on #3522, but doesn't depend on its changes.

Testing:

  • New test_vector_to_ndarray in python/tests/test_swig_interface.py: replaces __iter__ of DoubleVector and IntVector with a function that raises, then checks values, dtypes, shapes, that the arrays are copies, dtype=, copy=False, empty vectors, repr, and the x, sx, y, and numsteps fields of a ReturnDataView against the element-wise conversion. Fails on Make PEtab v2 ExperimentManager lookups constant-time #3520's head ("converted via the Python iterator"), passes here.
  • test_swig_interface.py, test_rdata.py, test_edata.py, test_hdf5.py, test_sbml_import.py, and petab_/test_petab_v2.py: 115 passed, 7 skipped (not checked why). The 7 JAX tests in test_petab_v2.py didn't run, since JAX/diffrax isn't installed in this environment. test_fiddy_derivative_check_leaves_solver_unchanged needs fiddy from git (--group python/sdist/pyproject.toml:fiddy); it passes with it.

Related issues

Fixes #3510

Use of AI tools

Claude Code (Claude Opus 5.5) wrote the implementation, the test, the changelog entry, and this description, and ran the tests and timings described above.

Checklist

  • I have run the relevant tests locally
  • I have added a changelog entry in changelog.d/ (or it is not needed)

🤖 Generated with Claude Code

numpy has no buffer or array interface for the SWIG proxies of
std::vector, so `np.array(vec)` built the array from the proxy's Python
iterator, one element at a time. `ReturnDataView` converts every field
this way, which took tens of milliseconds for `x` or `w` of a medium
model and, e.g., more time than the simulations themselves for the FIMs
of a PEtab problem with many experiments.

Give `DoubleVector` and `IntVector` an `__array__` method which copies
the data into a new array in C++. The wrapper keeps the GIL, since the
extension is built with `-threads`.

Fixes #3510.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@FFroehlich
FFroehlich added this pull request to stack #3523 November 7, 2026 22:09
@FFroehlich
FFroehlich marked this pull request as ready for review November 7, 2026 22:12
@FFroehlich
FFroehlich requested a review from a team as a code owner November 7, 2026 22:12
@codecov

codecov Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.24%. Comparing base (e3c8beb) to head (b317ee2).

Additional details and impacted files

Impacted file tree graph

@@                        Coverage Diff                         @@
##           claude/issue-3521-cache-edatas    #3524      +/-   ##
==================================================================
- Coverage                           80.30%   80.24%   -0.07%     
==================================================================
  Files                                 331      331              
  Lines                               23434    23434              
  Branches                             1631     1631              
==================================================================
- Hits                                18819    18804      -15     
- Misses                               4609     4624      +15     
  Partials                                6        6              
Flag Coverage Δ
cpp 74.74% <ø> (-0.07%) ⬇️
cpp_python 34.64% <ø> (ø)
petab 46.92% <ø> (ø)
petab_sciml 16.60% <ø> (ø)
petab_sciml_benchmarks 15.19% <ø> (ø)
python 72.53% <ø> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.
see 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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.

ReturnDataView converts the fields of a ReturnData element by element in Python: tens of milliseconds for x and w of a medium model

1 participant