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

MAINT: interpolate: move fpknot wrap from Cython to manual #14

Open
wants to merge 697 commits into
base: bspl_no_fused
Choose a base branch
from

Conversation

ev-br
Copy link
Owner

@ev-br ev-br commented Jul 21, 2024

Reference issue

What does this implement/fix?

Additional information

Copy link

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

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

Overall seems fine for the most part, hope the comments are helpful.

py_fpknot(PyObject* self, PyObject *args)
{
PyObject *py_x=NULL, *py_t=NULL, *py_residuals=NULL;
int k; // XXX: python int => "i" in ParseTuple?

Choose a reason for hiding this comment

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

This is fine as long as you never expect reasonable values of k to vall outside the values of a C int. You also need to check the return value of PyArg_ParseTuple for the error case to catch the OverflowError, but you do that.

return NULL;
}

// XXX: Do not need to DECREF py_x etc from PyArg_ParseTuple?

Choose a reason for hiding this comment

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

Since it's a borrowed reference, you don't if you never create an owned reference.

That said, borrowed references are really hard to reason about, and you're creating objects based on py_x and py_t below, so I'd personally INCREF here and then DECREF at the end when you're done with them.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Thanks! Point taken about borrowed vs owned references. Could use an advise though: If the whole trampoline function is literally

  • PyArg_ParseTuple to get python arguments
  • check_array
  • grab pointers from arrays and delegate to numpy-oblivious C code

(this py_fpknot is what I've in mind) keeping all references borrowed looks simpler. How does it fare in the nogil context / under free-threading though, and does switching to owned references change the equation? From reading the free-threading docs (which you wrote?), I've an impression that owned references are generally safer but not always. Don't have a good gut feeling of what is safe and what is not.



/*
// XXX: overkill? Mimic cython's `double[::1] x` etc --- replaced by check_array stanza below

Choose a reason for hiding this comment

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

It doesn't make a ton of difference if you don't want the API to be flexible and cast from other dtypes if you don't hand it a double array or don't hand it a C-contiguous array.

If you already control all that then what you have with check_array is fine.

PyArrayObject *a_t = (PyArrayObject *)py_t;
PyArrayObject *a_residuals = (PyArrayObject *)py_residuals;

// XXX: npy_intp vs Py_ssize_t vs ssize_t (== ptrdiff_t)?

Choose a reason for hiding this comment

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

In NumPy 2.0, npy_intp and Py_ssize_t are the same on all platforms. Before NumPy 2.0, npy_intp was 32 bit on windows. It's not quite the same as ptrdiff_t because it's a sized int (to make error checking less awful - negative values always indicate an error).

Choose a reason for hiding this comment

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

PY_SSIZE_T_MAX (which is the same as sys.maxsize in python) is also often used as a sigil value for things that are typed as Py_ssize_t.

Copy link
Owner Author

@ev-br ev-br Jul 23, 2024

Choose a reason for hiding this comment

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

Ugh. I full well realize it's asking for more and more, so if you've bandwidth to spare for me, I could use an advice.

For context:

  • The goal is to communicate between python and the C++ extension which does actual computations (https://github.com/ev-br/scipy/blob/bspl_no_fused/scipy/interpolate/src/__fitpack.cc);
  • The extension does not use anything from python or numpy; Also does not do any memory allocations and relies on the caller to send in everything including temp buffers.
  • The extension uses ssize_t for all indexing where the classic C++ would use size_t, to explicitly avoid any signed/unsigned mess.
  • Under MSVC on Windows ssize_t is not defined (apparently), so I just #define ssize_t ptrdiff_t
  • Have to support Windows, and int32 overflows are not impossible
  • I control both C++ and python side.

So when I need to return to python an array of integer indices (offsets: for a 2D array A, nonzeros in row j start at A[ j, offset[j]]), my thinking was

  • allocate np.empty(..., dtype=np.intp) on the python side
  • use npy_intp in the python/C glue
  • keep using ssize_t in C++

Does this sound reasonable or am I making things hard for myself?

Choose a reason for hiding this comment

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

Ah, it turns out that Py_ssize_t is a typedef for ssize_t where available, so I think using Py_ssize_t on the python side and then ssize_t on the C++ side should be sufficient.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Which leaves a question of the matching numpy dtype/typecode still?

Copy link
Owner Author

Choose a reason for hiding this comment

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

So the resolution (as discussed offline) is:

  • under numpy 2.x, npy_intp is interchangeable with Py_ssize_t (== ssize_t == ptrdiff_t).
  • under numpy 1.x and on Windows, npy_intp is 32-bit, but indexing in numpy itself is problematic if indices overflow 32-bit sized ints.

So in the end, the best one can do here is to detect numpy 1.x and Windows, check that the offsets are too large and bail out if so.

static_cast<const double *>(PyArray_DATA(a_residuals))
);

// XXX: need to DECREF a_* variables? Apparently not unless PyArray_ContiguousFromObject

Choose a reason for hiding this comment

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

ContiguousFromObject returns a new reference to an array object. That array might just be a view onto the original array, but it's a new PyObject and so needs to be DECREF'd. If you don't go through that function you're creating a borrowed reference, which doesn't need to be DECREF'd. That said, borrowed references are hard to reason about (as you saw!) since most APIs return new references.

Py_ssize_t nc;
Py_ssize_t startrow=1; // XXX: optional, keeps the value intact if not given?

const char *kwlist[] = {"a", "offset", "nc", "y", "startrow", NULL};

Choose a reason for hiding this comment

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

just an FYI, depending on what you're doing, if this function is called a lot there's a surprising amount of overhead to create python strings from these strings each time the function is called. For internal-only functions implemented in C, I would look at only using arguments positionally. NumPy goes out of its way to avoid using ParseTupleAndKeywords in hot code paths and also has a whole keyword argument name caching system.

}

if (!(check_array(py_a, 2, NPY_DOUBLE) &&
check_array(py_offs, 1, NPY_LONG) && // XXX: typecode for ssize_t (==ptrdiff_t)?

Choose a reason for hiding this comment

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

If you need to support windows and NumPy 1.x, this might be annoying to deal with if you need specifically a Py_ssize_t, since it may or may not be the same depending on architecture. You could always do a conversion, which will be a no-op if it really is the type you want. Also you probably want NPY_DEFAULT_INT instead of NPY_LONG, which is wrong on NumPy 2.x.

);

//Py_DECREF(a_offs);
// XXX: a & y modified in-place: need to incref?

Choose a reason for hiding this comment

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

Nope, not with a borrowed reference. You can always temporarily take ownership for the length of the function to make it easier to reason about.

/*
XXX:
1. how to check for refleaks / refcounting errors?
2. goto fail & crosses initialization errors: a better pattern or copy-paste?

Choose a reason for hiding this comment

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

You either declare all your variables at the top of the function, or you create a new scope with {} and define the temporary variables in the new scope.


/*
XXX:
1. how to check for refleaks / refcounting errors?

Choose a reason for hiding this comment

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

Think really hard. There's also https://pypi.org/project/pytest-leaks/ but it's bitrotting pretty hard at this point. You can make a quick and dirty version with a debug python build using sys.gettotalrefcount() though.

ollybritton and others added 11 commits September 10, 2024 11:51
Removes an extra `xp.asarray` inside `xp.concat` which was causing
errors due to setting an array with a sequence.
- Use `scipy._lib.array_api_compat.numpy` rather than `numpy` as a
  default when no array specified
- Remove use of in-place item assignment in `_cartesian_product`
- Remove use of `xp.newaxis` since Torch doesn't support this
- Switch to using positional arguments for `xp.linspace` since Torch
  uses different keywords
- Modify slow tests
- Use same style of typing as `_array_api` for `Array`
- Use `np_compat` from `_lib.array_api` rather than
  `_lib.array_api_compat.numpy`
- Default to `np.linalg` when `xp.linalg` doesn't exist
- Add `xp` argument to test functions rather than calling
  `array_namespace` every time
- Skip testing `array_like` inputs when the backend is not numpy
- Change how `FixedRule`, `NestedRule` and `NestedFixedRule` determine
  the array namespace
- use a `_concat` helper function to pick whether to use `xp.concat` or
  `xp.concatenate`
- use `xp_test = array_namespace(a)` for using `atan` since `atan` isn't
  supported in np < 2.
- compute subregion coordinates and cartesian products without using
  `np`-specific code
The failing test is testing np.matrix inputs to RGI. This is
allowed for backwards compatibility but does not work with
xp_assert_* infrastructure. Thus, convert to arrays for assertions.

An alternative is to skip the test when running with SCIPY_ARRAY_API
env variable (since then we're not bound by backwards compat). But
converting to ndarray seems simpler.
The computational complexity is O(1), so there is no reason
for this helper to stay in Cython or move to C++.
lucascolley and others added 9 commits September 10, 2024 16:29
[skip cirrus] [skip circle]
- Removes an unnecessary `isinstance(a, list)` check since by default
  `array_namespace(a_list)` is `np_compat`
- Deletes the function `_concat` which was used to fall back to
  `np.concatenate` if `np.concat` wasn't available, and instead make
  sure `np_compat` is being used
- Determine the compatible version of the array namespace in each rule
* BUG: special.logsumexp: fix type promotion

[skip cirrus] [skip circle]

* MAINT: adjust dtype determination logic in xp_broadcast_promote

* TST: special.logsumexp: add dtype tests

* TST: special.logsumexp: fix tests

* MAINT: define mixed-type promotion rule if underlying library doesn't

* TST: special.logsumexp: update tests

* STY: special.logsumexp: use is_array_api_strict where appropriate

[skip ci]

---------

Co-authored-by: Matt Haberland <[email protected]>
MAINT: interpolate: move an internal utility from cython to python
…ic_filter`, etc.) (scipy#20811)

* add axes support to gradient_magnitude and laplace functions

update rank filter axes docstring to match more detailed style of other functions

* axes should not be cast using xp.asarray in test case

* np.empty_like -> xp.empty_like

* use concatenation instead of in-place array modification in test suite for JAX compatibility
* ENH: interpolate: add Floater Hormann weights

Reviewed at scipy#21497

Co-authored-by: Pauli Virtanen <[email protected]>
dschult and others added 29 commits November 3, 2024 22:48
…21784)

* add tests of basic matmul involving 1D arrays

* fix the bug in A@v
…cipy#21792)

* BUG: sparse: fix setdiag for matrices with missing diagonal entries

We need to first set existing diagonal entries before adding
the missing ones, as inserting new values invalidates the computed
offsets.

* add tests for fix of csr.setdiag

---------

Co-authored-by: Dan Schult <[email protected]>
From python, it's find_interval, not py_find_interval
Otherwise, 32-bit build on linux in CI fails on
./tools/check_pyext_symbol_hiding.sh
…trix

Reuse _dierckx.data_matrix, which constructs the same object in a "packed" format,
with per-row offsets of non-zero elements instead of per-element CSR indices/indptr
arrays.

Add `extrapolate=False` parameter to data_matrix to account for design_marix usages.
use vectors not unique_ptr(malloc) for work arrays
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.