Skip to content

Commit 2a94125

Browse files
authored
meshgrid (#740) (#746)
* feat: `meshgrid` (#740) • Default implementation equivalent to NumPy's with `copy=True` and `sparse=False` • Add 2D, 3D, 4D test cases • Update docs * docs: update version to 6.12.0
1 parent a8b25ef commit 2a94125

14 files changed

Lines changed: 464 additions & 5 deletions

File tree

code/numpy/create.c

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,143 @@ mp_obj_t create_logspace(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_a
747747
MP_DEFINE_CONST_FUN_OBJ_KW(create_logspace_obj, 2, create_logspace);
748748
#endif
749749

750+
#if ULAB_NUMPY_HAS_MESHGRID
751+
//| def meshgrid(*xi, indexing="xy"):
752+
//| """
753+
//| .. param: xi
754+
//| 1-D arrays representing the coordinates of a grid
755+
//| .. param: indexing
756+
//| "xy" (Cartesian) or "ij" (matrix) indexing of output
757+
//|
758+
//| Return a tuple of coordinate matrices from coordinate vectors.
759+
//| Implements the NumPy equivalent with copy=True and sparse=False."""
760+
//| ...
761+
762+
mp_obj_t create_meshgrid(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
763+
static const mp_arg_t allowed_args[] = {
764+
{ MP_QSTR_indexing, MP_ARG_KW_ONLY | MP_ARG_OBJ, { .u_rom_obj = MP_ROM_QSTR(MP_QSTR_xy) } },
765+
};
766+
767+
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
768+
mp_arg_parse_all(0, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
769+
770+
if(!mp_obj_is_str(args[0].u_obj)) {
771+
mp_raise_TypeError(MP_ERROR_TEXT("indexing should be 'xy' or 'ij'"));
772+
}
773+
bool cartesian = true;
774+
const char *_indexing = mp_obj_str_get_str(args[0].u_obj);
775+
if(strcmp(_indexing, "ij") == 0) {
776+
cartesian = false;
777+
} else if (strcmp(_indexing, "xy") != 0) {
778+
mp_raise_ValueError(MP_ERROR_TEXT("indexing must be 'xy' or 'ij'"));
779+
}
780+
781+
if(n_args == 0) {
782+
return mp_const_empty_tuple;
783+
}
784+
if(n_args > ULAB_MAX_DIMS) {
785+
mp_raise_ValueError(MP_ERROR_TEXT("too many input arrays"));
786+
}
787+
788+
size_t *shape = m_new0(size_t, ULAB_MAX_DIMS);
789+
size_t shape_offset = ULAB_MAX_DIMS - n_args;
790+
for(size_t i = 0; i < n_args; i++) {
791+
if(!mp_obj_is_type(pos_args[i], &ulab_ndarray_type)) {
792+
mp_raise_TypeError(MP_ERROR_TEXT("arguments must be ndarrays"));
793+
}
794+
ndarray_obj_t *in = MP_OBJ_TO_PTR(pos_args[i]);
795+
if(in->ndim != 1) {
796+
mp_raise_ValueError(MP_ERROR_TEXT("arguments must be 1D arrays"));
797+
}
798+
shape[shape_offset + i] = in->shape[ULAB_MAX_DIMS - 1];
799+
}
800+
if(cartesian && n_args >= 2) {
801+
SWAP(size_t, shape[shape_offset], shape[shape_offset + 1]);
802+
}
803+
804+
mp_obj_tuple_t *tuple = MP_OBJ_TO_PTR(mp_obj_new_tuple(n_args, NULL));
805+
for(size_t p = 0; p < n_args; p++) {
806+
ndarray_obj_t *source = MP_OBJ_TO_PTR(pos_args[p]);
807+
ndarray_obj_t *target = ndarray_new_dense_ndarray(n_args, shape, source->dtype);
808+
uint8_t *tarray = (uint8_t *)target->array;
809+
uint8_t *sarray = (uint8_t *)source->array;
810+
size_t itemsize = source->itemsize;
811+
int32_t source_stride = source->strides[ULAB_MAX_DIMS - 1];
812+
813+
size_t active_axis_index = p;
814+
if(cartesian && n_args >= 2) {
815+
if(p == 0) {
816+
active_axis_index = 1;
817+
} else if(p == 1) {
818+
active_axis_index = 0;
819+
}
820+
}
821+
size_t active_dim_ulab = shape_offset + active_axis_index;
822+
823+
#if ULAB_MAX_DIMS > 3
824+
size_t i = 0;
825+
do {
826+
#endif
827+
#if ULAB_MAX_DIMS > 2
828+
size_t j = 0;
829+
do {
830+
#endif
831+
#if ULAB_MAX_DIMS > 1
832+
size_t k = 0;
833+
do {
834+
#endif
835+
size_t l = 0;
836+
do {
837+
// Determine the index in the source array based on
838+
// which loop corresponds to the active dimension
839+
size_t source_index = 0;
840+
#if ULAB_MAX_DIMS > 3
841+
if(active_dim_ulab == ULAB_MAX_DIMS - 4) {
842+
source_index = i;
843+
}
844+
#endif
845+
#if ULAB_MAX_DIMS > 2
846+
if(active_dim_ulab == ULAB_MAX_DIMS - 3) {
847+
source_index = j;
848+
}
849+
#endif
850+
#if ULAB_MAX_DIMS > 1
851+
if(active_dim_ulab == ULAB_MAX_DIMS - 2) {
852+
source_index = k;
853+
}
854+
#endif
855+
if(active_dim_ulab == ULAB_MAX_DIMS - 1) {
856+
source_index = l;
857+
}
858+
859+
memcpy(tarray, sarray + (source_index * source_stride), itemsize);
860+
tarray += itemsize;
861+
862+
l++;
863+
} while(l < target->shape[ULAB_MAX_DIMS - 1]);
864+
#if ULAB_MAX_DIMS > 1
865+
k++;
866+
} while(k < target->shape[ULAB_MAX_DIMS - 2]);
867+
#endif
868+
#if ULAB_MAX_DIMS > 2
869+
j++;
870+
} while(j < target->shape[ULAB_MAX_DIMS - 3]);
871+
#endif
872+
#if ULAB_MAX_DIMS > 3
873+
i++;
874+
} while(i < target->shape[ULAB_MAX_DIMS - 4]);
875+
#endif
876+
877+
tuple->items[p] = MP_OBJ_FROM_PTR(target);
878+
}
879+
880+
m_del(size_t, shape, ULAB_MAX_DIMS);
881+
return MP_OBJ_FROM_PTR(tuple);
882+
}
883+
884+
MP_DEFINE_CONST_FUN_OBJ_KW(create_meshgrid_obj, 0, create_meshgrid);
885+
#endif
886+
750887
#if ULAB_NUMPY_HAS_ONES
751888
//| def ones(shape: Union[int, Tuple[int, ...]], *, dtype: _DType = ulab.numpy.float) -> ulab.numpy.ndarray:
752889
//| """

code/numpy/create.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ mp_obj_t create_logspace(size_t , const mp_obj_t *, mp_map_t *);
5757
MP_DECLARE_CONST_FUN_OBJ_KW(create_logspace_obj);
5858
#endif
5959

60+
#if ULAB_NUMPY_HAS_MESHGRID
61+
mp_obj_t create_meshgrid(size_t , const mp_obj_t *, mp_map_t *);
62+
MP_DECLARE_CONST_FUN_OBJ_KW(create_meshgrid_obj);
63+
#endif
64+
6065
#if ULAB_NUMPY_HAS_ONES
6166
mp_obj_t create_ones(size_t , const mp_obj_t *, mp_map_t *);
6267
MP_DECLARE_CONST_FUN_OBJ_KW(create_ones_obj);

code/numpy/numpy.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ static const mp_rom_map_elem_t ulab_numpy_globals_table[] = {
163163
#if ULAB_NUMPY_HAS_LOGSPACE
164164
{ MP_ROM_QSTR(MP_QSTR_logspace), MP_ROM_PTR(&create_logspace_obj) },
165165
#endif
166+
#if ULAB_NUMPY_HAS_MESHGRID
167+
{ MP_ROM_QSTR(MP_QSTR_meshgrid), MP_ROM_PTR(&create_meshgrid_obj) },
168+
#endif
166169
#if ULAB_NUMPY_HAS_ONES
167170
{ MP_ROM_QSTR(MP_QSTR_ones), MP_ROM_PTR(&create_ones_obj) },
168171
#endif

code/ulab.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
#include "user/user.h"
3434
#include "utils/utils.h"
3535

36-
#define ULAB_VERSION 6.11.0
36+
#define ULAB_VERSION 6.12.0
3737
#define xstr(s) str(s)
3838
#define str(s) #s
3939

code/ulab.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,10 @@
365365
#define ULAB_NUMPY_HAS_LOGSPACE (1)
366366
#endif
367367

368+
#ifndef ULAB_NUMPY_HAS_MESHGRID
369+
#define ULAB_NUMPY_HAS_MESHGRID (1)
370+
#endif
371+
368372
#ifndef ULAB_NUMPY_HAS_ONES
369373
#define ULAB_NUMPY_HAS_ONES (1)
370374
#endif

docs/manual/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
author = 'Zoltán Vörös'
2828

2929
# The full version, including alpha/beta/rc tags
30-
release = '6.11.0'
30+
release = '6.12.0'
3131

3232

3333
# -- General configuration ---------------------------------------------------

docs/ulab-convert.ipynb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
{
1616
"cell_type": "code",
17-
"execution_count": 1,
17+
"execution_count": null,
1818
"metadata": {
1919
"ExecuteTime": {
2020
"end_time": "2022-02-09T06:27:15.118699Z",
@@ -61,7 +61,7 @@
6161
"author = 'Zoltán Vörös'\n",
6262
"\n",
6363
"# The full version, including alpha/beta/rc tags\n",
64-
"release = '6.11.0'\n",
64+
"release = '6.12.0'\n",
6565
"\n",
6666
"\n",
6767
"# -- General configuration ---------------------------------------------------\n",

docs/ulab-ndarray.ipynb

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@
507507
"source": [
508508
"# Array initialisation functions\n",
509509
"\n",
510-
"There are nine functions that can be used for initialising an array. Starred functions accept `complex` as the value of the `dtype`, if the firmware was compiled with complex support.\n",
510+
"There are several functions that can be used for initialising an array. Starred functions accept `complex` as the value of the `dtype`, if the firmware was compiled with complex support.\n",
511511
"\n",
512512
"1. [numpy.arange](#arange)\n",
513513
"1. [numpy.concatenate](#concatenate)\n",
@@ -518,6 +518,7 @@
518518
"1. [numpy.full*](#full)\n",
519519
"1. [numpy.linspace*](#linspace)\n",
520520
"1. [numpy.logspace](#logspace)\n",
521+
"1. [numpy.meshgrid](#meshgrid)\n",
521522
"1. [numpy.ones*](#ones)\n",
522523
"1. [numpy.zeros*](#zeros)"
523524
]
@@ -1152,6 +1153,59 @@
11521153
"print('num=5:\\t\\t\\t', np.logspace(1, 10, num=5, endpoint=False, base=2))"
11531154
]
11541155
},
1156+
{
1157+
"cell_type": "markdown",
1158+
"metadata": {},
1159+
"source": [
1160+
"## meshgrid\n",
1161+
"\n",
1162+
"`numpy`: https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html\n",
1163+
"\n",
1164+
"The `meshgrid` function returns coordinate matrices from coordinate vectors. Given *N* one-dimensional arrays, it returns a tuple of *N* N-dimensional arrays.\n",
1165+
"\n",
1166+
"Make sure *N* is not larger than the maximum dimension of your firmware.\n",
1167+
"\n",
1168+
"The `indexing` keyword argument determines the arrangement of the output. The default is `'xy'` (Cartesian indexing), where the first two dimensions are swapped. This is commonly used for generating grids for 2D plotting. If `indexing` is set to `'ij'` (matrix indexing), the dimensions of the output arrays correspond exactly to the order of the input arrays.\n",
1169+
"\n",
1170+
"Note that unlike `numpy`, `ulab`'s implementation always behaves as if `sparse=False` and `copy=True` i.e. it always returns fully allocated dense arrays."
1171+
]
1172+
},
1173+
{
1174+
"cell_type": "code",
1175+
"execution_count": 12,
1176+
"metadata": {},
1177+
"outputs": [
1178+
{
1179+
"name": "stdout",
1180+
"output_type": "stream",
1181+
"text": [
1182+
"X:\n",
1183+
"array([[1, 2, 3],\n",
1184+
" [1, 2, 3]], dtype=uint8)\n",
1185+
"Y:\n",
1186+
"array([[10, 10, 10],\n",
1187+
" [20, 20, 20]], dtype=uint8)\n",
1188+
"\n",
1189+
"\n"
1190+
]
1191+
}
1192+
],
1193+
"source": [
1194+
"%%micropython -unix 1\n",
1195+
"\n",
1196+
"from ulab import numpy as np\n",
1197+
"\n",
1198+
"x = np.array([1, 2, 3], dtype=np.uint8)\n",
1199+
"y = np.array([10, 20], dtype=np.uint8)\n",
1200+
"\n",
1201+
"X, Y = np.meshgrid(x, y)\n",
1202+
"\n",
1203+
"print(\"X:\")\n",
1204+
"print(X)\n",
1205+
"print(\"Y:\")\n",
1206+
"print(Y)"
1207+
]
1208+
},
11551209
{
11561210
"cell_type": "markdown",
11571211
"metadata": {},

tests/2d/numpy/meshgrid.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
try:
2+
from ulab import numpy as np
3+
except:
4+
import numpy as np
5+
6+
# 1. Standard 2D Case - XY Indexing (Default)
7+
# Expected Output: x repeated in rows, y repeated in cols. Shape (len(y), len(x))
8+
print("--- 2D xy ---")
9+
x = np.array([1, 2, 3], dtype=np.uint8)
10+
y = np.array([10, 20], dtype=np.uint8)
11+
12+
xv, yv = np.meshgrid(x, y, indexing='xy')
13+
print(xv)
14+
print(yv)
15+
16+
# 2. Standard 2D Case - IJ Indexing
17+
# Expected Output: x repeated in cols, y repeated in rows. Shape (len(x), len(y))
18+
print("\n--- 2D ij ---")
19+
xi, yi = np.meshgrid(x, y, indexing='ij')
20+
print(xi)
21+
print(yi)
22+
23+
print("\n=== 2D Strides ===")
24+
# x: [0, 1, 2, 3] -> [::2] -> [0, 2] (len 2)
25+
x = np.array([0, 1, 2, 3], dtype=np.uint8)[::2]
26+
# y: [10, 20, 30] -> [::-1] -> [30, 20, 10] (len 3)
27+
y = np.array([10, 20, 30], dtype=np.uint8)[::-1]
28+
29+
print("--- XY ---")
30+
# Shape: (3, 2)
31+
mx, my = np.meshgrid(x, y, indexing='xy')
32+
print(mx)
33+
print(my)
34+
35+
print("--- IJ ---")
36+
# Shape: (2, 3)
37+
mx, my = np.meshgrid(x, y, indexing='ij')
38+
print(mx)
39+
print(my)

tests/2d/numpy/meshgrid.py.exp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--- 2D xy ---
2+
array([[1, 2, 3],
3+
[1, 2, 3]], dtype=uint8)
4+
array([[10, 10, 10],
5+
[20, 20, 20]], dtype=uint8)
6+
7+
--- 2D ij ---
8+
array([[1, 1],
9+
[2, 2],
10+
[3, 3]], dtype=uint8)
11+
array([[10, 20],
12+
[10, 20],
13+
[10, 20]], dtype=uint8)
14+
15+
=== 2D Strides ===
16+
--- XY ---
17+
array([[0, 2],
18+
[0, 2],
19+
[0, 2]], dtype=uint8)
20+
array([[30, 30],
21+
[20, 20],
22+
[10, 10]], dtype=uint8)
23+
--- IJ ---
24+
array([[0, 0, 0],
25+
[2, 2, 2]], dtype=uint8)
26+
array([[30, 20, 10],
27+
[30, 20, 10]], dtype=uint8)

0 commit comments

Comments
 (0)