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

[WIP] Fix ndarray constructor #305

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

CallumJHays
Copy link
Contributor

@CallumJHays CallumJHays commented Feb 3, 2021

WIP fix for #301

  • Still missing constructor args other than 'shape'
  • Not yet implemented for circuitpython

Hey @v923z could you give this a look and let me know if there's a better way to write this?

code/ndarray.c Outdated
@@ -935,7 +935,33 @@ mp_obj_t ndarray_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw,
mp_arg_check_num(n_args, n_kw, 1, 2, true);
mp_map_t kw_args;
mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);
return ndarray_make_new_core(type, n_args, n_kw, args, &kw_args);
uint8_t dtype = ndarray_init_helper(n_args, args, &kw_args);
Copy link
Owner

Choose a reason for hiding this comment

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

Wouldn't it be simpler to move this whole block to make_new_core? That would solve the circuitpytthon problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds like a plan 🏂

@v923z
Copy link
Owner

v923z commented Feb 4, 2021

@CallumJHays Cal, I am struggling to understand, what this PR is trying to fix: while

a = array(5)

is perfectly valid in numpy, this seems to create a strange sort of array. E.g., I can't even read out the value of the element:

a[0]

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-81-544b26bc25e9> in <module>
      1 a = array(5)
----> 2 a[0]

IndexError: too many indices for array: array is 0-dimensional, but 1 were indexed

And while the initialisation doesn't raise an exception, this mode is not mentioned in the official documentation: https://numpy.org/doc/stable/reference/generated/numpy.array.html#numpy.array. The first argument should be something that looks like an array, walks like an array, and quacks like an array.

I am a bit flabbergasted to say the least. Not by the PR, but by the fact that there seems to be a bit of an inconsistency in numpy itself.

@CallumJHays
Copy link
Contributor Author

CallumJHays commented Feb 4, 2021

@v923z The main point of the PR will only affect ndarray.__init__(), following the numpy behaviour:

>>> import numpy as np
>>> np.ndarray([1, 2, 3])
array([[[4.63760270e-310, 0.00000000e+000, 4.63760245e-310],
        [6.91622088e-310, 1.92343089e-313, 6.91625378e-310]]])
>>> np.array([1, 2, 3])
array([1, 2, 3])

I believe currently both np.ndarray([1, 2, 3]) and np.array([1, 2, 3]) result in the same output.

As for accepting an int rather than either an int or iterable this was just the behaviour I noticed when testing numpy locally so I replicated it. I'm not well-versed on the nuances of numpy's API, but I would have to assume it could have benefits when using scalar arithmetic in numpy with the data already being in C memory. I can definitely see it having benefits in streamlining some usage of the numpy API in a pythonic duck-typing way. I also agree the numpy documentation is inconsistent.

Something I wanted to bring up btw is supporting kwargs that match the position of a positional arg, ie let this work:

>>> np.array(object=[1, 2, 3])
array([1, 2, 3])
>>> np.ndarray(shape=[1, 2, 3])
array([[[4.63760270e-310, 0.00000000e+000, 4.63760245e-310],
        [6.91622088e-310, 1.92343089e-313, 6.91625378e-310]]])

Last time I checked it threw an error similar to "required positional argument not included". Is there a way we can fix this without firmware bloat?

@v923z
Copy link
Owner

v923z commented Feb 6, 2021

@v923z The main point of the PR will only affect ndarray.__init__(), following the numpy behaviour:

>>> import numpy as np
>>> np.ndarray([1, 2, 3])
array([[[4.63760270e-310, 0.00000000e+000, 4.63760245e-310],
        [6.91622088e-310, 1.92343089e-313, 6.91625378e-310]]])
>>> np.array([1, 2, 3])
array([1, 2, 3])

I believe currently both np.ndarray([1, 2, 3]) and np.array([1, 2, 3]) result in the same output.

Thanks for the explanation! If you move the code block to make_new_core, I can merge the PR.

As for accepting an int rather than either an int or iterable this was just the behaviour I noticed when testing numpy locally so I replicated it. I'm not well-versed on the nuances of numpy's API, but I would have to assume it could have benefits when using scalar arithmetic in numpy with the data already being in C memory. I can definitely see it having benefits in streamlining some usage of the numpy API in a pythonic duck-typing way. I also agree the numpy documentation is inconsistent.

Something I wanted to bring up btw is supporting kwargs that match the position of a positional arg, ie let this work:

>>> np.array(object=[1, 2, 3])
array([1, 2, 3])
>>> np.ndarray(shape=[1, 2, 3])
array([[[4.63760270e-310, 0.00000000e+000, 4.63760245e-310],
        [6.91622088e-310, 1.92343089e-313, 6.91625378e-310]]])

Last time I checked it threw an error similar to "required positional argument not included". Is there a way we can fix this without firmware bloat?

I am not against this option, if you deem this to be useful. The keyword argument will add approx. 200-400 bytes to the firmware size. If you find that to be too much, you could make that configurable via a ULAB_NDARRAY_ACCEPTS_KEYWORDS or something similar pre-processor switch.

implmenetation to make_new_core
@CallumJHays
Copy link
Contributor Author

@v923z that last commit has the basic functionality of passing shape in, and the tests have been corrected. Sorry for dragging my feet but work and social life has been hectic this past week.

Do you think we should handle strides as well? I'm not sure if an alternative already exists in ulab other than setting the property after instantiation.

Also, do you know of a standard way to populate a C arr from a micropython iterator?, like a:

size_t len = 3;
uint8_t arr[len];
MP_ITER_FILL_ARR(mp_iter, uint8_t,  arr, len);

@v923z
Copy link
Owner

v923z commented Feb 12, 2021

@v923z that last commit has the basic functionality of passing shape in, and the tests have been corrected. Sorry for dragging my feet but work and social life has been hectic this past week.

Oh, don't worry, this is a project run by enthusiasts. I am not going to hold you to any specific deadline, and I am grateful for any contribution. By the way, thanks for patching up the doc stubs!

Do you think we should handle strides as well? I'm not sure if an alternative already exists in ulab other than setting the property after instantiation.

If you think it is useful, I don't see any reason not to. But I don't see pressing reasons for, either. I leave it up to you.

Also, do you know of a standard way to populate a C arr from a micropython iterator?, like a:

size_t len = 3;
uint8_t arr[len];
MP_ITER_FILL_ARR(mp_iter, uint8_t,  arr, len);

Do you mean something like ndarray_assign_elements?

void ndarray_assign_elements(ndarray_obj_t *ndarray, mp_obj_t iterable, uint8_t dtype, size_t *idx) {
This is not a macro, but you get the gist.

Copy link
Owner

@v923z v923z left a comment

Choose a reason for hiding this comment

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

@CallumJHays Cal, sorry for the belated reply. I only had time now to look at your code. Could you, please, address the two issues? Otherwise, I think this is a nice addition. Thanks for the undertaking!

In addition, don't forget to increment the version number

#define ULAB_VERSION 2.3.5
(I usually change the minor, when a new function is added, so this would probably be a sub-minor, though, I am not very particular about it.), and add a short comment in the change log https://github.com/v923z/micropython-ulab/blob/master/docs/ulab-change-log.md

mp_int_t ndim;
size_t shape[ULAB_MAX_DIMS];

if (mp_ndim_maybe == MP_OBJ_NULL) {
Copy link
Owner

Choose a reason for hiding this comment

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

This will fail with ndarray(-12.3). You can make it safe by explicitly checking for the input type:

if(MP_OBJ_IS_INT(oshape)) {

ndim = 1;
shape[ULAB_MAX_DIMS - 1] = MP_OBJ_SMALL_INT_VALUE(mp_shape);
} else {
mp_obj_get_int_maybe(mp_ndim_maybe, &ndim);
Copy link
Owner

Choose a reason for hiding this comment

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

Here you have to bail out, if the tuple is longer then ULAB_MAX_DIMS. The loop below is safe, but the user would still be left wondering, why the array is not what they expected.

@v923z
Copy link
Owner

v923z commented Apr 1, 2021

@CallumJHays do you want to go ahead with this PR? I could take over, if you have no time. I would just like to close the issue, because I think this would be a valuable addition.

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.

2 participants