Skip to content

BUG: Create complex scalars from real and imaginary parts - #23780

Merged
seberg merged 3 commits into
numpy:mainfrom
ngoldbaum:fix-complex-scalar-creation
Jul 5, 2023
Merged

seberg merged 3 commits into
numpy:mainfrom
ngoldbaum:fix-complex-scalar-creation

Conversation

@ngoldbaum

@ngoldbaum ngoldbaum commented May 18, 2023 •

Copy link
Copy Markdown
Member

fixes gh-19125

If anyone has an idea for how to generalize this for np.clongdouble I'm all ears. I couldn't come up with a straightforward way to do that. I can't go through python complex numbers for clongdouble because the cast from clongdouble to complex is unsafe. Proposing this PR as-is without that support because extended precision types like clongdouble are already kind of an odd duck.

@seberg seberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I am fine with adding this, this code needs more love, but its tricky and supporting this for complex64 and complex128 seems like a safe thing to do (not hard to keep supporting if there is a refactor).

if (PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwnames, &real_obj, &imag_obj)) {
if (imag_obj == NULL) {
obj = real_obj;
Py_XINCREF(obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The refcounting for this is off below, hard to fix without a goto finish or so probably. Might be easier to just include the scalar creation/assignment in the branch directly (duplicating some code)...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Can you explain a bit more what the refcounting issue is? I think with the latest push everything is correct.

We INCREF obj if it gets assigned to real_obj, but then a decref happens after obj gets used in PyArray_FromAny. There's an early return before that in the obj == NULL case, but then in that case we can't have any refcounting errors because obj is NULL.

There was a refcounting issue in the original version of this PR if getting the typecode failed, but as you pointed out in another comment, that's impossible given the way it's written. I deleted the error case and left a comment that it can't fail to make that clearer.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ah, yes you are right, I thought there were more error paths, but there is mainly that obj == NULL block.

Py_XINCREF(obj);
}
else if (PyNumber_Check(real_obj) && PyNumber_Check(imag_obj)) {
if (PyComplex_Check(real_obj) || PyComplex_Check(imag_obj)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, without this we would allow np.complex128(numpy_complex, numpy_complex), although it would give a warning. I don't have a strong opinion on the need, I agree, this is mainly about accepting the trivial case of np.complex128(3., 4) with python scalars anyway.

Comment thread numpy/core/src/multiarray/scalartypes.c.src
obj = PyComplex_Type.tp_new(&PyComplex_Type, args, NULL);
if (obj == NULL) {
return NULL;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

While annoying, duplicating the scalar creation may be easier here? Something like this (but I am not sure the cast is actually valid, may have to cast real/imag separately.

npy_@name@ val = PyComplex_AsCComplex(obj);  // does that work, since its struct-like
// Need to fetch the typecode.  Note that this can't actually fail
// (unlike the code below makes you believe).
PyObject *robj = PyArray_Scalar(&val, typecode, NULL);
Py_DECREF(py_complex);
return robj;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unfortunately Numpy at the moment allows any array-like to be passed to the scalar initializers (and there are tests for it), which means I'd first need to re-implement the array-like coercion logic in PyArray_FromAny to get this to work.

In [2]: np.complex128([1, 2, 3, 4])
Out[2]: array([1.+0.j, 2.+0.j, 3.+0.j, 4.+0.j])

In [3]: np.complex128(b"1+4j")
Out[3]: (1+4j)

I think it's better to just avoid the early return and re-use the logic in the rest of this function that goes through PyArray_FromAny.

@ngoldbaum
ngoldbaum force-pushed the fix-complex-scalar-creation branch from d3ff38c to 6d3bed8 Compare June 6, 2023 18:52
@seberg

seberg commented Jul 5, 2023

Copy link
Copy Markdown
Member

I will put this in, thanks @ngoldbaum. I will not that I think this chunk of code needs a heavy-handed rewrite while being open to just break some stuff and keeping these fallbacks probably only where it's too difficult to decide how to just delete (i.e. possibly strings).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cdouble does not take 2 arguments

2 participants